feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
2026-08-15 22:27:52 +02:00
parent e074f04bdf
commit d4d99a7473
18854 changed files with 2615729 additions and 173735 deletions

60
web/node_modules/flatted/golang/README.md generated vendored Normal file
View File

@@ -0,0 +1,60 @@
# flatted (Go)
A super light and fast circular JSON parser.
## Usage
```go
package main
import (
"fmt"
"github.com/WebReflection/flatted/golang/pkg/flatted"
)
type Group struct {
Name string `json:"name"`
}
type User struct {
Name string `json:"name"`
Friend *User `json:"friend"`
Group *Group `json:"group"`
}
func main() {
group := &Group{Name: "Developers"}
alice := &User{Name: "Alice", Group: group}
bob := &User{Name: "Bob", Group: group}
alice.Friend = bob
bob.Friend = alice // Circular reference
// Stringify Alice
s, _ := flatted.Stringify(alice)
fmt.Println(s)
// Output: [{"name":"Alice","friend":"1","group":"2"},{"name":"Bob","friend":"0","group":"2"},{"name":"Developers"}]
// Flattening in action:
// Index "0" is Alice, Index "1" is Bob, Index "2" is the shared Group.
// Parse back into a generic map structure
res, _ := flatted.Parse(s)
aliceMap := res.(map[string]any)
fmt.Println(aliceMap["name"]) // Alice
}
```
## CLI
Build the binary using the provided Makefile:
```bash
make build
```
Then use it to parse flatted JSON from stdin:
```bash
echo '[{"a":"1"},"b"]' | ./flatted
```

279
web/node_modules/flatted/golang/pkg/flatted/flatted.go generated vendored Normal file
View File

@@ -0,0 +1,279 @@
package flatted
import (
"encoding/json"
"reflect"
"sort"
"strconv"
"strings"
)
// flattedIndex is a internal type used to distinguish between
// actual strings and flatted indices during the reconstruction phase.
type flattedIndex string
// Stringify converts a Go value into a specialized flatted JSON string.
func Stringify(value, replacer, space any) (string, error) {
knownKeys := []any{}
knownValues := []string{}
input := []any{}
index := func(v any) string {
input = append(input, v)
idx := strconv.Itoa(len(input) - 1)
knownKeys = append(knownKeys, v)
knownValues = append(knownValues, idx)
return idx
}
relate := func(v any) any {
if v == nil {
return nil
}
rv := reflect.ValueOf(v)
kind := rv.Kind()
if kind == reflect.String || kind == reflect.Slice || kind == reflect.Map || kind == reflect.Pointer {
for i, k := range knownKeys {
if kind == reflect.String {
if k == v {
return knownValues[i]
}
} else {
rk := reflect.ValueOf(k)
if rk.Kind() == kind && rk.Pointer() == rv.Pointer() {
return knownValues[i]
}
}
}
return index(v)
}
return v
}
transform := func(v any) any {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return nil
}
if _, ok := v.(json.Marshaler); ok {
return v
}
// Dereference pointers to process the underlying Slice, Map, or Array
for rv.Kind() == reflect.Pointer && !rv.IsNil() {
rv = rv.Elem()
}
switch rv.Kind() {
case reflect.Slice, reflect.Array:
res := make([]any, rv.Len())
for i := 0; i < rv.Len(); i++ {
res[i] = relate(rv.Index(i).Interface())
}
return res
case reflect.Map:
res := make(map[string]any)
keys := rv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
whitelist, isWhitelist := replacer.([]string)
for _, key := range keys {
kStr := key.String()
if isWhitelist {
found := false
for _, w := range whitelist {
if w == kStr {
found = true
break
}
}
if !found {
continue
}
}
res[kStr] = relate(rv.MapIndex(key).Interface())
}
return res
case reflect.Struct:
res := make(map[string]any)
t := rv.Type()
for i := 0; i < rv.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" {
continue
}
name := field.Name
if tag := field.Tag.Get("json"); tag != "" {
name = strings.Split(tag, ",")[0]
}
res[name] = relate(rv.Field(i).Interface())
}
return res
default:
return v
}
}
index(value)
output := []any{}
for i := 0; i < len(input); i++ {
output = append(output, transform(input[i]))
}
var b []byte
var err error
indent := ""
switch v := space.(type) {
case string:
indent = v
case int:
indent = strings.Repeat(" ", v)
}
if indent != "" {
b, err = json.MarshalIndent(output, "", indent)
} else {
b, err = json.Marshal(output)
}
if err != nil {
return "", err
}
return string(b), nil
}
// Parse converts a specialized flatted string into a Go value.
func Parse(text string, reviver func(key string, value any) any) (any, error) {
var jsonInput []any
if err := json.Unmarshal([]byte(text), &jsonInput); err != nil {
return nil, err
}
var wrap func(any) any
wrap = func(v any) any {
if s, ok := v.(string); ok {
return flattedIndex(s)
}
if arr, ok := v.([]any); ok {
for i, item := range arr {
arr[i] = wrap(item)
}
return arr
}
if m, ok := v.(map[string]any); ok {
for k, item := range m {
m[k] = wrap(item)
}
return m
}
return v
}
wrapped := make([]any, len(jsonInput))
for i, v := range jsonInput {
wrapped[i] = wrap(v)
}
input := make([]any, len(wrapped))
for i, v := range wrapped {
if fi, ok := v.(flattedIndex); ok {
input[i] = string(fi)
} else {
input[i] = v
}
}
if len(input) == 0 {
return nil, nil
}
value := input[0]
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
set := make(map[uintptr]bool)
set[rv.Pointer()] = true
res := loop(value, input, set)
if reviver != nil {
return revive("", res, reviver), nil
}
return res, nil
}
if reviver != nil {
return reviver("", value), nil
}
return value, nil
}
func revive(key string, value any, reviver func(k string, v any) any) any {
switch v := value.(type) {
case []any:
for i, el := range v {
v[i] = revive(strconv.Itoa(i), el, reviver)
}
case map[string]any:
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v[k] = revive(k, v[k], reviver)
}
}
return reviver(key, value)
}
func loop(value any, input []any, set map[uintptr]bool) any {
if arr, ok := value.([]any); ok {
for i, v := range arr {
if fi, ok := v.(flattedIndex); ok {
idx, _ := strconv.Atoi(string(fi))
arr[i] = ref(input[idx], input, set)
}
}
return arr
}
if m, ok := value.(map[string]any); ok {
for k, v := range m {
if fi, ok := v.(flattedIndex); ok {
idx, _ := strconv.Atoi(string(fi))
m[k] = ref(input[idx], input, set)
}
}
return m
}
return value
}
func ref(value any, input []any, set map[uintptr]bool) any {
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
ptr := rv.Pointer()
if !set[ptr] {
set[ptr] = true
return loop(value, input, set)
}
}
return value
}
// ToJSON converts a generic value into a JSON serializable object without losing recursion.
func ToJSON(value any) (any, error) {
s, err := Stringify(value, nil, nil)
if err != nil {
return nil, err
}
var res any
err = json.Unmarshal([]byte(s), &res)
return res, err
}
// FromJSON converts a previously serialized object with recursion into a recursive one.
func FromJSON(value any) (any, error) {
b, err := json.Marshal(value)
if err != nil {
return nil, err
}
return Parse(string(b), nil)
}