From 2e6bce154e76ab4245bc2cbaafc87a8d1c38ab1d Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:46:52 +0200 Subject: [PATCH] fix(ci): unblock the client pipeline; isolate the known-red test job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the unused 'type Event' named import from the committed generated events.ts and teach the CI patch step to strip it on future regenerations — the previous line-anchored patch deliberately skipped imports, so ESLint failed on every run. - Split vitest into its own client-tests job so the known-red suite (P2 triage pending) is exactly one visible failing check instead of masking the audit/lint/typecheck/prettier gates, which are now green. - Drop the three stale roadmap files (PHASE_BC_LOCAL_TODO.md, phase-b-acceleration.md, phase-c-differentiation.md) — referenced nowhere since the CHANGELOG cleanup; already deleted on the security-hardening branch. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 25 +- Client/tauri-client/src/generated/events.ts | 14 +- PHASE_BC_LOCAL_TODO.md | 290 -------------------- phase-b-acceleration.md | 148 ---------- phase-c-differentiation.md | 139 ---------- 5 files changed, 28 insertions(+), 588 deletions(-) delete mode 100644 PHASE_BC_LOCAL_TODO.md delete mode 100644 phase-b-acceleration.md delete mode 100644 phase-c-differentiation.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39a15908..5015e1b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: working-directory: Server/ client-check: - name: Client Typecheck & Test + name: Client Static Checks runs-on: windows-latest defaults: run: @@ -107,6 +107,7 @@ jobs: if (fs.existsSync(p)) { let c = fs.readFileSync(p, 'utf8'); c = c.replace(/^type Event\b/gm, 'type _Event').replace(/^interface Event\b/gm, 'interface _Event'); + c = c.replace(/,\s*type Event\s*(?=\})/g, ' '); // unused named import from @tauri-apps/api/event fs.writeFileSync(p, c); console.log('Patched: renamed Event -> _Event in generated/events.ts'); } else { @@ -132,6 +133,28 @@ jobs: - name: Knip (unused code & deps) run: npx knip || true + # Unit tests live in their own job so a red suite is visible as exactly one + # failing check instead of masking the static gates above. The suite is + # KNOWN RED pending the reboot-plan P2 triage — do not "fix" tests here by + # editing assertions; see docs/plans and the P2 triage rules. + client-tests: + name: Client Unit Tests + runs-on: windows-latest + defaults: + run: + working-directory: Client/tauri-client/ + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: Client/tauri-client/package-lock.json + + - name: Install npm dependencies + run: npm ci + - name: Run unit tests with coverage run: npx vitest run --coverage --reporter=default diff --git a/Client/tauri-client/src/generated/events.ts b/Client/tauri-client/src/generated/events.ts index bce17bae..a97f7d62 100644 --- a/Client/tauri-client/src/generated/events.ts +++ b/Client/tauri-client/src/generated/events.ts @@ -11,7 +11,7 @@ * Event Listeners * Type-safe event listener helpers for Tauri events */ -import { listen, type UnlistenFn, type Event } from "@tauri-apps/api/event"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import * as types from "./types"; /** @@ -19,9 +19,7 @@ import * as types from "./types"; * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onStatusChange( - handler: (payload: string) => void, -): Promise { +export async function onStatusChange(handler: (payload: string) => void): Promise { return listen("status-change", (event) => { handler(event.payload); }); @@ -32,9 +30,7 @@ export async function onStatusChange( * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onWsState( - handler: (payload: string) => void, -): Promise { +export async function onWsState(handler: (payload: string) => void): Promise { return listen("ws-state", (event) => { handler(event.payload); }); @@ -45,9 +41,7 @@ export async function onWsState( * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onCertTofu( - handler: (payload: types.Value) => void, -): Promise { +export async function onCertTofu(handler: (payload: types.Value) => void): Promise { return listen("cert-tofu", (event) => { handler(event.payload); }); diff --git a/PHASE_BC_LOCAL_TODO.md b/PHASE_BC_LOCAL_TODO.md deleted file mode 100644 index addbc667..00000000 --- a/PHASE_BC_LOCAL_TODO.md +++ /dev/null @@ -1,290 +0,0 @@ -# Phase B + C — Local Follow-up TODO - -This file enumerates everything from `phase-b-acceleration.md` and -`phase-c-differentiation.md` that **could not be completed inside the -sandboxed Claude session** because the work requires: - -- network access to fetch new modules / npm packages, -- a Go toolchain matching `go.mod`'s `go 1.25.0` directive, -- a WASM toolchain (TinyGo / Rust / AssemblyScript), -- a real machine that can run `npm install`, `cargo`, `tauri`, etc. - -The session **branch is `claude/plan-phases-b-c-bGpoS`**. Everything below -must be run on a developer machine (or CI) before the branch is mergeable. - -The session-resident plan that was actually executed lives in -`/root/.claude/plans/woolly-wiggling-wolf.md` (not in this repo). - ---- - -## Verification (do first — confirms the in-session work compiles) - -- [x] `cd Server && go build ./...` — passes on the dev machine with Go 1.24.x. -- [x] `cd Server && go test ./store/... ./ws/... ./plugin/... ./telemetry/...` - — all pass; full suite `go test ./...` green. -- [x] `cd Server && go vet ./...` — clean. -- [x] `cd Client/tauri-client && npm install && npm run lint && npm run build` - — Pulls in `solid-js`, `vite-plugin-solid`, and - `@solidjs/testing-library` (added to `package.json`); confirms the - Solid pipeline compiles inside the existing Vite + TS setup. -- [x] `cd Client/tauri-client && npm run test` — runs the new - `Badge.test.tsx` smoke test. All 111 test files / 3186 tests pass. - ---- - -## Phase B Step 6 — Solid.js migration (rest of the components) - -The session landed: -- Vite + TS toolchain wiring (`vite.config.ts`, `tsconfig.json`) -- `solid-js` + `vite-plugin-solid` + `@solidjs/testing-library` in - `package.json` -- `src/lib/solidAdapter.ts` (wraps custom stores as Solid signals) -- `src/lib/solidMount.ts` (`{mount, destroy}` adapter for Solid roots) -- `src/components/solid/Badge.tsx` — first leaf -- `src/components/solid/ChannelListItem.tsx` — store-subscribed leaf -- `src/components/solid/Badge.test.tsx` — pipeline smoke test -- `src/components/solid/README.md` — migration recipe - -Still TODO locally: - -- [x] Run `npm install` and verify the build passes (sandbox had no - network). -- [ ] Migrate the remaining leaf components in - `src/components/` one PR at a time, following the recipe in - `src/components/solid/README.md`. Suggested order: presence pills, - typing indicators, message attachments, voice volume meters, then - containers (channel list, member list, message list). -- [ ] Once every leaf is migrated, replace the manual `mountSolid` calls - in containers with native Solid components and delete the old - vanilla DOM utilities (`createComponent`, factory shells) referenced - from `src/components/`. -- [x] Add Vitest config preset: vite-plugin-solid added to vitest.config.ts, - include expanded to pick up src/components/solid/**/*.test.tsx. - Badge.test.tsx now runs automatically (112 files, 3188 tests pass). - ---- - -## Phase B Step 7 — Event persistence - -The session landed: -- `Server/migrations/014_events_table.sql` (SQLite) -- `events` table appended to `Server/migrations/postgres/001_initial_schema.sql` -- `Server/db/queries/sqlite/events.sql`, `Server/db/queries/postgres/events.sql` -- `Server/db/persisted_event.go` — domain type -- `EventStore` sub-interface added to `Server/store/store.go` -- SQLite implementation in `Server/store/sqlite_events.go` (raw SQL via - `*sql.DB`, no `dbgen` dependency) -- MemStore implementation in `Server/store/memstore_events.go` -- Postgres stubs returning `ErrPostgresNotImplemented` -- `Server/ws/event_persister.go` — batched async writer -- `Server/ws/event_pruner.go` — retention pruner goroutine -- Three `replayBuf.Push` call sites in `Server/ws/hub.go` now also call - `h.persistEvent(...)` -- Tiered reconnect replay in `Server/ws/serve.go` (buffer → DB → full) -- Reconnect-tier metrics in the hub + telemetry counter -- `EventPersistenceConfig` added to `Server/config/config.go` with - defaults `{enabled: true, retention_hours: 24, batch_size: 50, - batch_flush_ms: 100, pruner_interval_minutes: 60}` -- `Server/main.go` wires the persister + pruner -- `Server/ws/event_persister_test.go` — batching, drop, drain tests - -Still TODO locally: - -- [x] Run `make sqlc-generate` — done; `db/pgdbgen/events.sql.go` and - `db/pgdbgen/plugins.sql.go` generated; `//go:build postgres` tag - prepended to all 19 pgdbgen files to gate pgx/v5 import. -- [x] Replace the postgres EventStore stubs in `Server/store/postgres.go` - with real implementations using PostgreSQL SQL syntax - (`$1/$2` params, `RETURNING id`, native `bool`/`time.Time`). -- [x] Add an integration test that pushes more than 1000 events through a - real hub with a 1000-slot buffer, disconnects at seq=500, and asserts - the DB tier returns the missing events. Landed in - `Server/ws/reconnect_db_test.go` (`TestReconnect_BufferMiss_FallsBackToDBTier`). -- [x] Add a `replay_source` field to the auth_ok payload — landed in - Pass 4. `buildAuthOK` takes the tier as a parameter, "none" on - fresh connect, "buffer" or "db" on resume. -- [x] Document the new `event_persistence` block in `defaultYAML` inside - `Server/config/config.go` — landed in Pass 3. - ---- - -## Phase B Step 8 — OpenTelemetry - -The session landed: -- `Server/telemetry/telemetry.go` — public API + no-op provider -- `Server/telemetry/telemetry_default.go` — default-build `Init` -- `Server/telemetry/telemetry_otel.go` — wazero/postgres-style build-tag - skeleton (build with `-tags otel`); compiles only when the OTel modules - are in `go.mod` and is currently a structural placeholder -- `Server/telemetry/metrics.go` — `AppMetrics` bundle -- `Server/telemetry/middleware.go` — `HTTPMiddleware` + `PrometheusHandler` -- `Server/telemetry/telemetry_test.go` -- `Server/api/router.go` mounts `telemetry.HTTPMiddleware()` - unconditionally and the Prometheus exporter when non-nil -- `Server/main.go` calls `telemetry.Init` early and defers `Shutdown` -- `TelemetryConfig` added to `Server/config/config.go` -- Spans added to `MessageService.SendMessage`, - `PermissionService.HasChannelPerm`, - `ChannelService.ListVisibleChannels` -- Reconnect-tier counter wired into `WSReconnectTierTotal` from - `Server/ws/serve.go` - -Still TODO locally: - -- [x] Add the OTel modules to `go.mod`: otel v1.43.0, sdk v1.43.0, - exporters/prometheus v0.65.0, - exporters/otlp/otlptrace/otlptracegrpc v1.43.0, and - `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` - v0.67.0 (otelhttp replaces the unmaintained otelchi wrapper - referenced by the original plan; otelhttp is upstream-supported - and wraps any `http.Handler` including a Chi router). -- [x] Replace the placeholder body of `telemetry/telemetry_otel.go`'s - `Init` with the real tracer + meter provider construction. The - tagged build wires an OTel Prometheus exporter (pull), an OTLP/gRPC - trace exporter when `exporter=otlp` (with `OTLPInsecure` opt-in for - plaintext gRPC), `otelhttp.NewHandler` as the HTTP middleware, and - a real provider that re-binds `AppMetrics` instruments via - `resetAppMetricsForInit`. Tests in - `Server/telemetry/telemetry_otel_test.go` - (`TestOtelInitPrometheusExporter`, `TestOtelTracerRecordsSpan`, - `TestOtelHistogramRecordsSeconds`, `TestOtelShutdownIdempotent`, - `TestOtelConvertAttrsHandlesUnsignedInts`, - `TestOtelConvertAttrsUint64OverflowFallsBackToString`, - `TestOtelAppMetricsRebindsAfterInit`) run under - `go test -tags otel ./telemetry/...`. -- [x] Build with `-tags otel` — passes. Full 4-tag matrix green - (default, otel, wazero, otel+wazero) against Go 1.25.1. -- [ ] Add a CI job that exercises `go build -tags otel ./...` and - `go test -tags otel ./telemetry/...`. -- [x] Add spans to the remaining service-layer entry points - (`DMService`, `VoiceService`, `InviteService`, `ModerationService`, - `BlockService`, `UserService`) — landed in Pass 3, one entrypoint - per service. Add additional spans on demand. -- [x] Document the new `telemetry` block in `defaultYAML` inside - `Server/config/config.go` — landed in Pass 3. -- [x] Add a `make otel-up` target that spins up Jaeger via - docker-compose for local tracing development. Landed in - `Server/Makefile` (`otel-up` / `otel-down`); overlay file at - `Server/docker-compose.otel.yml`; Prometheus config at - `Server/prometheus.dev.yml`. - ---- - -## Phase C Step 9 — Wazero plugin runtime - -The session landed: -- `Server/plugin/manifest.go` — JSON manifest parser + capability checks -- `Server/plugin/loader.go` — directory scan + entrypoint validation -- `Server/plugin/registry.go` — registry + lifecycle (install/enable/uninstall) -- `Server/plugin/host_commands.go`, `host_storage.go`, `host_events.go`, - `host_http.go`, `host_ui.go` — capability surfaces -- `Server/plugin/sandbox_default.go` — no-op runtime (default build) -- `Server/plugin/sandbox_wazero.go` — `-tags wazero` skeleton -- `Server/plugin/errors.go` -- `Server/plugin/plugin_test.go` -- `Server/plugin/examples/hello/plugin.json` + `README.md` -- `Server/migrations/015_plugins.sql` (SQLite) -- `plugins` + `plugin_kv` tables appended to the postgres schema -- `Server/db/queries/sqlite/plugins.sql`, - `Server/db/queries/postgres/plugins.sql` -- `PluginStore` sub-interface in `Server/store/store.go` with SQLite, - MemStore, and postgres-stub implementations -- `Server/api/plugins_handler.go` — admin REST surface -- `Server/api/router.go` mounts the admin plugin handler -- `Server/main.go` constructs and starts the registry when - `cfg.Plugins.Enabled` -- `PluginsConfig` added to `Server/config/config.go` -- `Client/tauri-client/src/lib/pluginBridge.ts` — iframe + postMessage host -- `Client/tauri-client/src/components/solid/PluginContainer.tsx` — Solid - host component for plugin tabs - -Still TODO locally: - -- [x] Add `github.com/tetratelabs/wazero v1.11.0` to `go.mod`. -- [x] Replace the placeholder body in `Server/plugin/sandbox_wazero.go` - with real wazero runtime construction. The tagged build owns a - shared `wazero.Runtime` created in `platformInit` (with the - configured `MaxMemoryMB` translated to `WithMemoryLimitPages` and - WASI preview-1 imports pre-instantiated), compiles + instantiates - each plugin's `.wasm` entrypoint in `activateWithRuntime`, and - tears modules + runtime down in `platformDeactivate` / `Close`. - The host-guest command ABI is JSON-over-linear-memory: - `allocate(size)` / `command_dispatch(ptr,len) → (ptr,len)` / - `deallocate(ptr,len)`, with optional `list_commands` for command - auto-registration. Tests in `Server/plugin/sandbox_wazero_test.go` - (`TestWazeroRegistryCreatesRuntime`, - `TestWazeroActivateCompilesModule`, - `TestWazeroDispatchCommandMissingExport`, - `TestWazeroCloseTearsDownRuntime`, - `TestWazeroInvalidWASMFailsActivation`, - `TestWazeroDisablePluginFreesModule`) run under - `go test -tags wazero ./plugin/...` using a 41-byte embedded WASM - fixture for the smoke tests. -- [x] Add a precompiled `Server/plugin/examples/hello/hello.wasm` - (925 KiB) built with TinyGo 0.40.1 + Go 1.25.3 + Binaryen - wasm-opt 129. Source in `examples/hello/main.go`; exports: - `allocate`, `deallocate`, `list_commands`, `command_dispatch`, - `on_event`. -- [x] Replace JSON-only manifest parsing with TOML support behind the - `wazero` build tag. Added `github.com/BurntSushi/toml` v1.6.0, - `manifest_toml.go` (wazero) + `manifest_nottoml.go` (!wazero); - `loader.go` prefers `plugin.toml` and falls back to - `plugin.json`. -- [x] Wire `Server/plugin/host_events.go` into the WS pub/sub hub. - Landed: `EventSink.SetBroadcaster`/`Emit` added; hub gains - `SetPluginEventSink`; `deliverBroadcast` calls `sink.Dispatch` - on each sequenced broadcast; wired in `api/router.go`. -- [x] Wire `Server/plugin/host_commands.go` into the WS slash-command - dispatcher. Landed: `chat_command` V1 handler in - `Server/ws/handlers_command.go`; hub gains `SetPluginRegistry`; - wired in `api/router.go`. Tests in `handlers_command_test.go`. -- [x] Pass the live `*plugin.Registry` from `Server/main.go` into - `NewPluginAdminHandler` — landed in Pass 2. The router now accepts - a `*plugin.Registry` parameter and the handler is also wrapped in - `admin.RequireAdminAuth` (Pass 2 closed the auth bypass too). -- [x] Add precompiled `Server/plugin/examples/hello/hello.wasm` (925 KiB). - Built with TinyGo 0.40.1 + Go 1.25.3 + Binaryen wasm-opt 129. - Source in main.go; exports: allocate, deallocate, list_commands, - command_dispatch, on_event. -- [x] Implement plugin marketplace install path - (`POST /api/v1/admin/plugins/install` with multipart zip) — landed - in Pass 4. `Registry.InstallFromZip` does zip-slip validation, no - symlinks, 16 MiB compressed cap, 64 MiB uncompressed cap, then - atomic rename into the plugin directory. -- [x] Replace plugin postgres stubs in `Server/store/postgres.go` with - real SQL implementations (same session as EventStore stubs). -- [ ] Build the first real plugin: game detection. Pulls Steam API, - tracks playtime, exposes `/playtime` slash command. This is the - acceptance criterion in `phase-c-differentiation.md`. - ---- - -## Build-tag matrix the user should set up in CI - -| Tag set | What it builds | Why | -|---|---|---| -| (none) | Default sqlite-only server, no OTel SDK, no wazero | Existing path | -| `otel` | Above + OpenTelemetry SDK + Prometheus exporter | Phase B Step 8 | -| `wazero` | Above + plugin runtime executes WASM modules | Phase C Step 9 | -| `postgres` | Replaces sqlite with postgres backend | Phase A pending | -| `otel,wazero,postgres` | Full community-hub build | Production target | - -Each tag is independently selectable; CI should test every combination at -least minimally so the build-tag boundaries don't drift. - ---- - -## Things explicitly **out of scope** for this branch - -(Documenting so reviewers don't expect them.) - -- Migration of the entire vanilla TypeScript component tree to Solid. Two - proof-of-concept components landed; the rest is mechanical PRs. -- Full OTel SDK wiring (only the public API + no-op default + structural - build-tag skeleton landed). -- Real Wazero `.wasm` execution (only the registry, host APIs, and a - build-tag skeleton landed). -- Real game-detection plugin (the manifest fields and host APIs needed to - build it are in place). -- A reverse postgres → sqlite migration (Phase A documented this is - deliberately unavailable; nothing changed here). diff --git a/phase-b-acceleration.md b/phase-b-acceleration.md deleted file mode 100644 index 4cfd9045..00000000 --- a/phase-b-acceleration.md +++ /dev/null @@ -1,148 +0,0 @@ -# OwnCord — Phase B: Acceleration - -**Steps 6–8 | Weeks 6–14 | Community Scale Edition** - -*Solid.js Frontend • Event Persistence • OpenTelemetry* - ---- - -## Phase Overview - -Phase B runs after the server foundation is in place. It tackles the three remaining infrastructure gaps: a proper frontend framework to accelerate UI development, event persistence for reliable reconnection at scale, and pluggable observability for community hub operators. These steps overlap with the tail end of Phase A since Step 6 is entirely client-side work. - -The Solid.js migration (Step 6) is the highest-effort, highest-payoff change in the entire plan. Event persistence (Step 7) and OpenTelemetry (Step 8) are shorter server-side tasks that run in parallel. All three are required before the community hub milestone can ship. - -### Timeline - -| Step | Task | Duration | Depends On | Parallel? | -|------|------|----------|------------|-----------| -| 6 | Adopt Solid.js (incremental) | 4–6 weeks | — | Yes (client-side) | -| 7 | Event persistence layer | 1–2 weeks | Steps 1+3 | Yes (with 6, 8) | -| 8 | Add OpenTelemetry | 1–2 weeks | Step 1 | Yes (with 6, 7) | - -### Milestone Gate - -**After Phase B:** The client has a proper framework, reconnection is reliable at scale, and operators have pluggable monitoring. The community hub milestone (Milestone 2 from the parity plan) is shippable. - ---- - -## Step 6: Adopt Solid.js Frontend Framework - -**Effort:** High (4–6 weeks incremental) | **Impact:** Critical | **Phase:** B — Acceleration - -### Problem - -The vanilla TypeScript frontend has effectively become a custom framework: reactive stores, a component model, a dispatcher, virtual scrolling, manual DOM lifecycle management. Every new UI feature requires hand-wiring DOM creation, updates, and teardown. Community-scale features (role permission editors with tri-state per-channel overrides, audit log viewers with filtering, moderation dashboards, member lists grouped by role with presence indicators) are complex stateful UIs that are painful to build and maintain without a framework. - -### Options - -| Framework | Reactivity Model | Migration Path | Tauri Ecosystem | Verdict | -|-----------|-----------------|----------------|-----------------|---------| -| **Solid.js** | Signals, effects, memos — maps directly to existing store pattern | Incremental: wrap stores as signals, convert components one by one | Growing. Solid + Tauri examples available. Vite integration native. | **RECOMMENDED** | -| Svelte 5 | Runes (compile-time reactivity). Simple component model, no virtual DOM. | Requires rewriting components in .svelte files. Larger upfront cost. | Good. Tauri + Svelte is well-documented. | VIABLE | -| React | Virtual DOM reconciliation. useState/useEffect hooks. Largest ecosystem. | Full rewrite into JSX components. Hooks model differs from current stores. | Excellent. Most Tauri projects use React. Huge community. | VIABLE | -| Vue 3 | Composition API with ref/reactive. Template-based. Good DX. | Moderate. Composition API maps reasonably to stores. | Good. Tauri + Vue works well. | VIABLE | -| Stay vanilla | Custom reactive stores + manual DOM. Full control, full burden. | No migration. But every new feature carries the full cost. | N/A | AVOID | - -### Why Solid.js - -Solid is the recommended choice because it has the lowest migration cost and the closest philosophical match to what OwnCord already has. Your existing reactive stores are conceptually identical to Solid signals. Solid compiles to direct DOM operations with no virtual DOM overhead, so performance stays where it is now. The migration is incremental: wrap existing stores as Solid signals, convert one component at a time, keep the WebSocket dispatcher and LiveKit facade untouched since they are framework-agnostic. - -### Pros & Cons (Solid.js) - -| Pros | Cons | -|------|------| -| Signals map 1:1 to existing store pattern | Smaller community than React or Vue | -| No virtual DOM — same performance as vanilla | Fewer off-the-shelf UI component libraries | -| Incremental migration (component by component) | Team must learn Solid's reactivity rules | -| Proper component lifecycle and error boundaries | Some vanilla patterns don't translate directly | -| Strong TypeScript support (first-class) | Migration is still weeks of work even incrementally | -| Small bundle size (~7KB gzipped) | | -| Vite integration is native (no config changes) | | - -### Migration Strategy - -1. Install Solid.js and configure Vite for JSX/TSX. Keep all existing code working — Solid and vanilla coexist. -2. Wrap existing stores as Solid signals using a thin adapter. This lets new Solid components read from existing state immediately. -3. Convert one leaf component (a simple, self-contained UI element) to Solid as a proof of concept. Validate that it works in the Tauri WebView. -4. Migrate components bottom-up: leaf components first, then containers. Each converted component is a self-contained PR. -5. Leave framework-agnostic code (dispatcher, LiveKit facade, API client, crypto) untouched. These don't need to change. -6. Once all components are migrated, remove the old DOM manipulation utilities and the custom component model. - ---- - -## Step 7: Event Persistence Layer - -> **NEW IN V2** — This step was a deferred TODO in v1. At community scale the 1000-event ring buffer is insufficient — 100 active users can burn through it in minutes. - -**Effort:** Medium (1–2 weeks) | **Impact:** High | **Phase:** B — Acceleration - -### Problem - -The current reconnection protocol uses a 1000-event in-memory ring buffer. When a client reconnects, the server replays missed events from the buffer. At 100+ active users, a busy server generates 1000 events in minutes. Any user who goes offline for 10–15 minutes will find their last_seq is no longer in the buffer, forcing a full ready re-sync. That re-sync serializes all channels, permissions, member lists, and presence state — an expensive operation that gets more expensive with more users and channels. - -### Solution - -Implement a tiered event persistence model. Keep the in-memory ring buffer for hot events (last ~60 seconds). Write events to the database (via the Store interface from Step 3) for cold replay. On reconnect, the server first checks the ring buffer. If last_seq is too old, it queries the database for events since last_seq up to a reasonable limit. Only if both are exhausted does it fall back to a full ready re-sync. Events in the database are pruned after a configurable retention period (default: 24 hours). - -### Options - -| Option | Details | Verdict | -|--------|---------|---------| -| **Tiered (buffer + DB)** | Ring buffer for hot path, database for cold replay. Best balance of performance and reliability. Uses existing Store interface. | **RECOMMENDED** | -| Larger ring buffer | Increase from 1000 to 50000 events in memory. Simple but uses significant RAM and is still lossy. | VIABLE | -| External event log | Use NATS JetStream or Redis Streams. Robust but adds an external dependency to self-hosted deployments. | AVOID | -| Status quo | Keep 1000-event buffer. Works for friend groups, fails at community scale. | AVOID | - -### Pros & Cons (Tiered) - -| Pros | Cons | -|------|------| -| Hot path stays fast (in-memory ring buffer) | Event writes add DB load (mitigated by batching) | -| Cold replay from DB handles longer disconnections | Must handle event serialization/deserialization | -| Full re-sync becomes rare instead of common | Retention pruning adds a background job | -| Uses existing Store interface — no new dependencies | Query performance matters — needs indexed seq column | -| Configurable retention (24h default) keeps DB size bounded | Two code paths for replay (buffer vs DB) add complexity | -| Architecture was already designed to be persistence-ready | | - -### Implementation Plan - -1. Add an events table to the schema (both SQLite and PostgreSQL migrations): seq INTEGER PRIMARY KEY, event_type TEXT, payload BLOB, channel_id TEXT, created_at TIMESTAMP. Index on (channel_id, seq). -2. Add PersistEvent and GetEventsSince methods to the Store interface. The SQLite implementation uses INSERT with write batching (flush every 100ms or 50 events, whichever comes first). -3. Modify the reconnection handler: check ring buffer first, then query DB, then fall back to full re-sync. Each tier returns the events it can plus a "complete" flag. -4. Add a background goroutine that prunes events older than the retention period (configurable, default 24h). Runs every hour. -5. Add metrics for reconnection tier hits (buffer/db/full) to feed into OpenTelemetry (Step 8). - ---- - -## Step 8: Add OpenTelemetry for Observability - -> **MOVED UP FROM PHASE C** — This step was Phase C in v1. Community hub operators expect pluggable monitoring from day one. - -**Effort:** Medium (1–2 weeks) | **Impact:** High | **Phase:** B — Acceleration - -### Problem - -The current observability is custom: a /metrics endpoint with hand-picked counters, structured logging across two libraries, and client-side JSONL logs. Community hub operators running servers for 100+ people need plug-and-play compatibility with their existing monitoring stack (Prometheus, Grafana, Datadog). They also need distributed tracing to diagnose slow requests when users report latency. - -### Solution - -Integrate the OpenTelemetry Go SDK. Instrument the Chi router middleware for automatic HTTP tracing, add spans to service layer methods, and export metrics in Prometheus format via OTLP. Self-hosters get plug-and-play compatibility with whatever monitoring they already run. This replaces the custom /metrics endpoint with an industry-standard exporter. - -### What to Instrument - -- **HTTP layer:** Chi middleware for automatic request tracing — method, path, status code, duration. This is a single middleware addition. -- **Service layer:** Spans around key service methods (CreateMessage, EditMessage, CheckPermission, etc.). This shows where time is spent in business logic. -- **Database layer:** Query timing and connection pool metrics. Shows when the DB is the bottleneck. -- **WebSocket layer:** Custom metrics for messages/second, active connections, broadcast latency, reconnection rates (by tier from Step 7). -- **LiveKit:** Voice session metrics — active sessions, participant count, connection quality distribution. - -### Pros & Cons - -| Pros | Cons | -|------|------| -| Self-hosters plug into existing monitoring (Grafana, Datadog, etc.) | OTel SDK adds ~5MB to binary size | -| Distributed tracing across REST → Service → DB | Tracing overhead (small but nonzero) on every request | -| Replaces custom /metrics with industry-standard export | Configuration complexity for exporters | -| Chi and database drivers have OTel middleware available | Additional Docker Compose services (collector) for full setup | -| Essential for community hub operators to diagnose issues | | diff --git a/phase-c-differentiation.md b/phase-c-differentiation.md deleted file mode 100644 index bf99e844..00000000 --- a/phase-c-differentiation.md +++ /dev/null @@ -1,139 +0,0 @@ -# OwnCord — Phase C: Differentiation - -**Step 9 | When Phase 6 Features Start | Community Scale Edition** - -*Wazero Plugin Runtime • Game Detection • Server Browser • Stats* - ---- - -## Phase Overview - -Phase C is about what makes OwnCord different from every other Discord alternative. The plugin system is the core differentiator — game detection, server browser, stats, leaderboards, screenshots. All of these are implemented as plugins, not core features, which keeps the server lean while enabling an ecosystem. - -This phase has a single step: implementing the plugin runtime using Wazero. It waits until the plugin features from the product roadmap (Phase 6) are actively being built. There is no urgency to implement the runtime before the features that use it are in scope. The server foundation from Phase A and the client framework from Phase B must be in place first. - -### Timeline - -| Step | Task | Duration | Depends On | Parallel? | -|------|------|----------|------------|-----------| -| 9 | Wazero plugin runtime | 4–6 weeks | Step 1 | When Phase 6 starts | - -### Milestone Gate - -**After Phase C:** The plugin runtime enables the gaming features that differentiate OwnCord from every other Discord alternative. Game detection, rich presence, playtime tracking, server browser, stats, and leaderboards all run as sandboxed WASM plugins. Third-party developers can build and distribute plugins through the plugin marketplace. - -### Prerequisites - -- **Phase A complete:** The service layer (Step 1) provides the host API surface that plugins call into. The Store interface (Step 3) provides plugin-scoped storage. The pub/sub hub (Step 5) enables plugins to emit events to subscribed clients. -- **Phase B complete:** The Solid.js frontend (Step 6) enables plugin UI tabs and widgets. OpenTelemetry (Step 8) provides plugin performance monitoring. - ---- - -## Step 9: Use Wazero for Plugin Runtime - -**Effort:** High (4–6 weeks) | **Impact:** High | **Phase:** C — Differentiation - -### Problem - -The plugin system is OwnCord's core differentiator — game detection, server browser, stats, leaderboards — but the runtime is only designed, not implemented. The design mentions WASM or shared library loading. Shared libraries (.so/.dll) offer no sandboxing, crash the host process on failure, and are platform-specific. At community scale, a misbehaving plugin taking down a 100-user server is unacceptable. - -### Options - -| Option | Details | Verdict | -|--------|---------|---------| -| **Wazero** | Pure-Go WebAssembly runtime. No CGO. Memory-sandboxed, resource-limited, language-agnostic plugin authoring (Rust, Go, C, AssemblyScript). | **RECOMMENDED** | -| Extism | Plugin framework built on Wazero. Higher-level API, easier plugin development. Adds a dependency layer on top of Wazero. | VIABLE | -| Shared libs | Native .so/.dll loading via Go plugin package. No sandboxing, platform-specific, crashes take down the server. | AVOID | -| gRPC sidecar | Plugins as separate processes communicating via gRPC. Strong isolation but heavy: each plugin is a full process with its own lifecycle. | VIABLE | - -### Why Wazero - -Wazero is the recommended choice because it is pure Go (no CGO, matching the existing constraint), provides real memory sandboxing, supports resource limits per plugin (CPU and memory caps), and enables language-agnostic plugin development. Plugins compile to WASM from Rust, Go, C, or AssemblyScript. A crashing plugin cannot take down the server. At community scale this isolation is non-negotiable — you cannot allow a third-party plugin to crash a server serving 100+ users. - -Extism is a viable alternative if you want a higher-level abstraction over Wazero. It simplifies plugin authoring with PDKs (Plugin Development Kits) for multiple languages and handles memory management between host and guest. The tradeoff is an additional dependency and less control over the low-level WASM runtime. For OwnCord's plugin use cases (game detection, server queries, stats computation), the raw Wazero API is sufficient and more transparent. - -### Pros & Cons (Wazero) - -| Pros | Cons | -|------|------| -| Pure Go — no CGO, matches existing constraint | WASM has limited I/O capabilities by design | -| Real memory sandboxing (plugins can't crash server) | Plugin performance is slower than native (~2–5x) | -| Resource limits per plugin (CPU, memory) | Debugging WASM plugins is harder than native code | -| Language-agnostic: Rust, Go, C, AssemblyScript → WASM | Plugin authors must learn WASM toolchain | -| Well-defined host API for plugin ↔ server communication | Complex host API design for game detection, UI tabs, etc. | -| Critical for community scale — isolation is non-negotiable | | - ---- - -## Architecture - -The plugin runtime has four components: the loader, the host API, the sandbox, and the client bridge. - -### Plugin Loader - -Reads plugin.toml manifests, validates declared permissions, loads the .wasm binary into a Wazero runtime instance. Each plugin gets its own isolated module with a dedicated memory space. The loader handles plugin lifecycle: install, enable, disable, uninstall, update. - -### Host API - -The server exposes functions that plugins can call through WASM imports. These are the capabilities a plugin requests in its manifest: - -- **commands:** Register slash commands (/playtime, /serverstatus, /stats). The command dispatcher routes user input to the owning plugin. -- **events:** Subscribe to server events (message_send, user_join, voice_join). The pub/sub hub (Step 5) delivers subscribed events to the plugin. -- **storage:** Plugin-scoped key-value storage via the Store interface (Step 3). Each plugin gets its own namespace. No cross-plugin data access. -- **http:** Outbound HTTP requests (for querying game servers, APIs). Proxied through the server with configurable allowlists per plugin. -- **ui:** Register UI tabs and widgets that render in the Solid.js client (Step 6). Plugin declares HTML/JS assets, client renders them in an iframe sandbox. - -### Sandbox - -Each plugin instance runs with enforced limits: maximum memory allocation (default 64MB), CPU time budget per invocation (default 100ms), and no direct filesystem or network access. The server monitors resource usage and kills plugins that exceed their budget. A crashed or killed plugin is automatically disabled and the admin is notified via the mod log channel. - -### Client Bridge - -Plugins that declare UI capabilities get a rendering surface in the Solid.js client. Plugin UI runs in a sandboxed iframe with postMessage communication to the host client. The host provides a theme-aware CSS injection so plugin UIs match OwnCord's look and feel. The client bridge also handles plugin-specific settings panels. - ---- - -## Implementation Plan - -1. Define the plugin.toml manifest format: name, version, author, permissions (commands, events, storage, http, ui), resource limits. Validate against a JSON schema. -2. Implement the Wazero runtime wrapper: module loading, memory allocation, function imports/exports, lifecycle management (start, stop, restart). -3. Implement the host API functions one capability at a time. Start with commands (simplest — input/output only), then events (requires pub/sub integration), then storage, then HTTP. -4. Build the first plugin: game detection. This exercises commands (user queries playtime), events (presence updates), storage (playtime database), and HTTP (Steam API queries). If game detection works, the architecture is validated. -5. Add the UI capability: client-side iframe sandbox, postMessage bridge, theme injection. Build the server browser plugin to validate the UI integration. -6. Implement plugin marketplace: browse available plugins, install/update/remove from within the admin panel. Plugin packages are .wasm + assets in a zip archive hosted on a registry (GitHub Releases initially). - ---- - -## Complete Timeline — All Phases - -For reference, here is the complete execution timeline across all three phases. - -### Phase A: Foundation (Weeks 1–7) - -| Step | Task | Duration | Depends On | Parallel? | -|------|------|----------|------------|-----------| -| 1 | Extract service/domain layer + permission cache | 2–3 weeks | — | No | -| 2 | Adopt sqlc | 1 week | — | Yes (with 1) | -| 3 | Abstract DB + PostgreSQL target | 2–3 weeks | Steps 1+2 | No | -| 4 | Consolidate logging | 1–2 days | — | Yes (anytime) | -| 5 | Refactor hub to pub/sub + global rate limits | 2–3 weeks | Step 1 | After Step 1 | - -### Phase B: Acceleration (Weeks 6–14) - -| Step | Task | Duration | Depends On | Parallel? | -|------|------|----------|------------|-----------| -| 6 | Adopt Solid.js (incremental) | 4–6 weeks | — | Yes (client-side) | -| 7 | Event persistence layer | 1–2 weeks | Steps 1+3 | Yes (with 6, 8) | -| 8 | Add OpenTelemetry | 1–2 weeks | Step 1 | Yes (with 6, 7) | - -### Phase C: Differentiation (When Phase 6 Features Start) - -| Step | Task | Duration | Depends On | Parallel? | -|------|------|----------|------------|-----------| -| 9 | Wazero plugin runtime | 4–6 weeks | Step 1 | When ready | - -### Total - -**14–18 weeks** for Steps 1–8. Step 9 is deferred until plugin features are in scope. Phases overlap, so calendar time is shorter than the sum of estimates. - -The central principle: stop building infrastructure, start using infrastructure. Every hour spent maintaining a custom query layer, a custom component model, a custom broadcast loop, or a custom event buffer is an hour not spent on the 146 features that make OwnCord compete with Discord.