fix(ci): unblock the client pipeline; isolate the known-red test job

- 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 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-18 11:46:52 +02:00
co-authored by Claude Fable 5
parent 77ec4f1466
commit 2e6bce154e
5 changed files with 28 additions and 588 deletions
+24 -1
View File
@@ -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
+4 -10
View File
@@ -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<UnlistenFn> {
export async function onStatusChange(handler: (payload: string) => void): Promise<UnlistenFn> {
return listen<string>("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<UnlistenFn> {
export async function onWsState(handler: (payload: string) => void): Promise<UnlistenFn> {
return listen<string>("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<UnlistenFn> {
export async function onCertTofu(handler: (payload: types.Value) => void): Promise<UnlistenFn> {
return listen<types.Value>("cert-tofu", (event) => {
handler(event.payload);
});
-290
View File
@@ -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).
-148
View File
@@ -1,148 +0,0 @@
# OwnCord — Phase B: Acceleration
**Steps 68 | Weeks 614 | 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) | 46 weeks | — | Yes (client-side) |
| 7 | Event persistence layer | 12 weeks | Steps 1+3 | Yes (with 6, 8) |
| 8 | Add OpenTelemetry | 12 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 (46 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 (12 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 1015 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 (12 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 | |
-139
View File
@@ -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 | 46 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 (46 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 (~25x) |
| 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 17)
| Step | Task | Duration | Depends On | Parallel? |
|------|------|----------|------------|-----------|
| 1 | Extract service/domain layer + permission cache | 23 weeks | — | No |
| 2 | Adopt sqlc | 1 week | — | Yes (with 1) |
| 3 | Abstract DB + PostgreSQL target | 23 weeks | Steps 1+2 | No |
| 4 | Consolidate logging | 12 days | — | Yes (anytime) |
| 5 | Refactor hub to pub/sub + global rate limits | 23 weeks | Step 1 | After Step 1 |
### Phase B: Acceleration (Weeks 614)
| Step | Task | Duration | Depends On | Parallel? |
|------|------|----------|------------|-----------|
| 6 | Adopt Solid.js (incremental) | 46 weeks | — | Yes (client-side) |
| 7 | Event persistence layer | 12 weeks | Steps 1+3 | Yes (with 6, 8) |
| 8 | Add OpenTelemetry | 12 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 | 46 weeks | Step 1 | When ready |
### Total
**1418 weeks** for Steps 18. 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.