chore(client): remove abandoned SolidJS beachhead (D6)

The Solid.js migration was abandoned (per CHANGELOG); the 154-LOC
beachhead and its scaffolding remained in-tree, leaving two UI paradigms
for contributors. Removed:

- src/components/solid/ (Badge, ChannelListItem, PluginContainer — none
  imported by production code)
- src/lib/solidMount.ts and src/lib/solidAdapter.ts
- tests/setup-solid.ts and tests/setup-solid.test.tsx
- vite-plugin-solid from vite.config.ts and vitest.config.ts (and the
  now-unneeded tsx test include + setupFiles)
- jsx/jsxImportSource from tsconfig.json
- solid-js, @solidjs/testing-library, vite-plugin-solid from package.json

docs/client-architecture.md (which described the SolidJS design) is
retired to a pointer at docs/architecture/client.md; README links
updated. Audit A-2026-07-12 and decision D6 marked closed.

Verified: tsc --noEmit clean (previous 3 test-file errors were caused by
the Solid jsx config and are gone); oxlint/eslint error counts identical
to HEAD (pre-existing); vitest runner healthy on a sample suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
This commit is contained in:
Claude
2026-07-19 13:57:28 +00:00
parent ca3b0fee3e
commit bf508c9e6b
18 changed files with 28 additions and 1015 deletions
-266
View File
@@ -20,13 +20,11 @@
"@tauri-apps/plugin-store": "^2",
"@tauri-apps/plugin-updater": "^2.10.0",
"livekit-client": "^2.18.0",
"solid-js": "^1.9.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@playwright/test": "^1",
"@solidjs/testing-library": "^0.8.10",
"@stryker-mutator/core": "^9.6.0",
"@stryker-mutator/typescript-checker": "^9.6.0",
"@stryker-mutator/vitest-runner": "^9.6.0",
@@ -41,7 +39,6 @@
"typescript": "^5.7",
"typescript-eslint": "^8.58.0",
"vite": "^6",
"vite-plugin-solid": "^2.11.0",
"vitest": "^3"
}
},
@@ -3461,28 +3458,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@solidjs/testing-library": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@solidjs/testing-library/-/testing-library-0.8.10.tgz",
"integrity": "sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@testing-library/dom": "^10.4.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@solidjs/router": ">=0.9.0",
"solid-js": ">=1.0.0"
},
"peerDependenciesMeta": {
"@solidjs/router": {
"optional": true
}
}
},
"node_modules/@stryker-mutator/api": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.0.tgz",
@@ -4013,51 +3988,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -4640,68 +4570,6 @@
"js-tokens": "^10.0.0"
}
},
"node_modules/babel-plugin-jsx-dom-expressions": {
"version": "0.40.6",
"resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.6.tgz",
"integrity": "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "7.18.6",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/types": "^7.20.7",
"html-entities": "2.3.3",
"parse5": "^7.1.2"
},
"peerDependencies": {
"@babel/core": "^7.20.12"
}
},
"node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/babel-plugin-jsx-dom-expressions/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/babel-preset-solid": {
"version": "1.9.12",
"resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz",
"integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"babel-plugin-jsx-dom-expressions": "^0.40.6"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
"solid-js": "^1.9.12"
},
"peerDependenciesMeta": {
"solid-js": {
"optional": true
}
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -5017,12 +4885,6 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@@ -6002,13 +5864,6 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/html-entities": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
"dev": true,
"license": "MIT"
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -6176,19 +6031,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -6647,22 +6489,6 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/merge-anything": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
"integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -7560,27 +7386,6 @@
"node": ">=10"
}
},
"node_modules/seroval": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz",
"integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.2.tgz",
"integrity": "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"seroval": "^1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7728,32 +7533,6 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/solid-js": {
"version": "1.9.12",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz",
"integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.0",
"seroval": "~1.5.0",
"seroval-plugins": "~1.5.0"
}
},
"node_modules/solid-refresh": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
"integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.23.6",
"@babel/helper-module-imports": "^7.22.15",
"@babel/types": "^7.23.6"
},
"peerDependencies": {
"solid-js": "^1.3"
}
},
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@@ -8408,51 +8187,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-plugin-solid": {
"version": "2.11.12",
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz",
"integrity": "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.3",
"@types/babel__core": "^7.20.4",
"babel-preset-solid": "^1.8.4",
"merge-anything": "^5.1.7",
"solid-refresh": "^0.6.3",
"vitefu": "^1.0.4"
},
"peerDependencies": {
"@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*",
"solid-js": "^1.7.2",
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@testing-library/jest-dom": {
"optional": true
}
}
},
"node_modules/vitefu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
"integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
"dev": true,
"license": "MIT",
"workspaces": [
"tests/deps/*",
"tests/projects/*",
"tests/projects/workspace/packages/*"
],
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vitest": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
-3
View File
@@ -32,7 +32,6 @@
"devDependencies": {
"@eslint/js": "^9.39.4",
"@playwright/test": "^1",
"@solidjs/testing-library": "^0.8.10",
"@stryker-mutator/core": "^9.6.0",
"@stryker-mutator/typescript-checker": "^9.6.0",
"@stryker-mutator/vitest-runner": "^9.6.0",
@@ -47,7 +46,6 @@
"typescript": "^5.7",
"typescript-eslint": "^8.58.0",
"vite": "^6",
"vite-plugin-solid": "^2.11.0",
"vitest": "^3"
},
"prettier": {
@@ -72,7 +70,6 @@
"@tauri-apps/plugin-store": "^2",
"@tauri-apps/plugin-updater": "^2.10.0",
"livekit-client": "^2.18.0",
"solid-js": "^1.9.3",
"zod": "^4.3.6"
}
}
@@ -1,27 +0,0 @@
/**
* Phase B Step 6 — Solid pipeline smoke test.
*
* Verifies that the Vite + Solid + Vitest configuration actually compiles
* and renders a component end-to-end. This test only exists to prove the
* pipeline; the test scope expands as more components migrate.
*/
import { describe, it, expect } from "vitest";
import { render } from "@solidjs/testing-library";
import { Badge } from "./Badge";
describe("Badge (solid)", () => {
it("renders the label", () => {
const { getByText } = render(() => <Badge label="online" variant="online" />);
expect(getByText("online")).toBeTruthy();
});
it("invokes onClick when activated", () => {
let clicked = 0;
const { getByText } = render(() => (
<Badge label="press me" onClick={() => clicked++} />
));
getByText("press me").click();
expect(clicked).toBe(1);
});
});
@@ -1,46 +0,0 @@
/**
* Phase B Step 6 — first Solid.js leaf component.
*
* Trivial proof-of-concept badge used by other Solid components and the
* mount helper. Self-contained: no store subscriptions, no async work, just
* a presentational element. Use it as the canonical example when migrating
* vanilla badge/pill components in the rest of the tree.
*/
import type { JSX } from "solid-js";
export interface BadgeProps {
label: string;
/** Visual variant; defaults to "neutral". */
variant?: "neutral" | "online" | "idle" | "dnd" | "offline";
/** Optional click handler. When set the badge gains role="button". */
onClick?: () => void;
}
const variantClass: Record<NonNullable<BadgeProps["variant"]>, string> = {
neutral: "badge",
online: "badge badge--online",
idle: "badge badge--idle",
dnd: "badge badge--dnd",
offline: "badge badge--offline",
};
export function Badge(props: BadgeProps): JSX.Element {
const cls = () => variantClass[props.variant ?? "neutral"];
return (
<span
class={cls()}
role={props.onClick ? "button" : undefined}
tabIndex={props.onClick ? 0 : undefined}
onClick={props.onClick}
onKeyDown={(e) => {
if (props.onClick && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
props.onClick();
}
}}
>
{props.label}
</span>
);
}
@@ -1,52 +0,0 @@
/**
* Phase B Step 6 — second Solid.js leaf component.
*
* Renders a single row in the channel list. Reads from the existing
* `channelsStore` via the Solid adapter so it stays in sync with whatever
* the vanilla dispatcher writes into the store.
*
* This is the canonical example for migrating list-item components: pure
* presentation, fed by an accessor, with a click handler delegated up to the
* parent so the component knows nothing about the dispatcher.
*/
import type { JSX } from "solid-js";
import { Show } from "solid-js";
import { fromStoreSlice } from "@lib/solidAdapter";
import { channelsStore, type Channel, type ChannelsState } from "@stores/channels.store";
import { Badge } from "./Badge";
export interface ChannelListItemProps {
channelId: number;
onSelect: (id: number) => void;
}
export function ChannelListItem(props: ChannelListItemProps): JSX.Element {
// Subscribe to just this row's channel object so unrelated changes don't
// re-render. Falsy → row is hidden until the channel arrives.
const channel = fromStoreSlice<ChannelsState, Channel | undefined>(
channelsStore,
(s) => s.channels.get(props.channelId),
);
const isActive = fromStoreSlice<ChannelsState, boolean>(
channelsStore,
(s) => s.activeChannelId === props.channelId,
);
return (
<Show when={channel()}>
{(ch) => (
<li
class={"channel-list-item" + (isActive() ? " channel-list-item--active" : "")}
onClick={() => props.onSelect(props.channelId)}
>
<span class="channel-list-item__hash">#</span>
<span class="channel-list-item__name">{ch().name}</span>
<Show when={ch().unreadCount > 0}>
<Badge label={String(ch().unreadCount)} variant="dnd" />
</Show>
</li>
)}
</Show>
);
}
@@ -1,29 +0,0 @@
/**
* Phase C Step 9 — Solid component that hosts a plugin tab.
*
* Mounts the plugin's iframe via `pluginBridge.mount(...)` and tears it down
* when the component is disposed. The container itself is intentionally tiny:
* the bridge owns the iframe lifecycle and the postMessage protocol.
*/
import { onMount, onCleanup, type JSX } from "solid-js";
import { pluginBridge, type PluginTabBinding } from "@lib/pluginBridge";
export interface PluginContainerProps {
binding: PluginTabBinding;
}
export function PluginContainer(props: PluginContainerProps): JSX.Element {
// oxlint-disable-next-line no-unassigned-vars -- Solid ref assigned by JSX ref={host}
let host!: HTMLDivElement;
let dispose: (() => void) | undefined;
onMount(() => {
dispose = pluginBridge.mount(props.binding, host);
});
onCleanup(() => {
dispose?.();
});
return <div class="plugin-container" data-plugin-id={props.binding.pluginId} ref={host} />;
}
@@ -1,59 +0,0 @@
# Solid.js components
Phase B Step 6 lives here. This directory holds the incremental Solid.js
migration of the OwnCord client. Vanilla TypeScript components and Solid
components coexist throughout the migration; new UI work goes in this
directory, and existing leaf components are ported one PR at a time.
## Migration recipe
1. **Pick a leaf component.** Start with components that have no children of
their own and read at most one or two stores. Avoid container components
until every leaf inside them is Solid-native.
2. **Read state via the adapter.** Import `fromStore` or `fromStoreSlice` from
`@lib/solidAdapter` and pass the existing custom store. Do **not** rewrite
the store — Solid components and vanilla components share the same source
of truth.
3. **Mount via `solidMount`.** Containers that aren't yet Solid-native should
call `mountSolid(component, parentEl)` from `@lib/solidMount`. The returned
handle has the same `{ destroy }` shape that the rest of the codebase uses.
4. **Test the pipeline.** New components get a `*.test.tsx` next to them
using `@solidjs/testing-library`. The tests run under the existing Vitest
configuration without any extra setup. Do **not** call `cleanup()` manually
in test files — `tests/setup-solid.ts` registers `afterEach(cleanup)` globally
via Vitest's `setupFiles` (added in T-500).
5. **Delete vanilla DOM code.** Once a component is fully migrated, remove
the old factory function and update its callers to import from
`@components/solid/...`.
## Allowed reactivity
- `createSignal`, `createMemo`, `createEffect`, `createResource`
- `Show`, `For`, `Switch`/`Match`, `Index`
- `onMount`, `onCleanup`
## Forbidden patterns
- Direct DOM manipulation inside Solid components — use Solid's bindings or a
ref. The point of the migration is to delete manual DOM lifecycle code.
- Re-implementing existing stores in Solid's `createStore`. Wrap the existing
custom store via `fromStore` instead.
- Touching framework-agnostic code (`lib/ws.ts`, `lib/dispatcher.ts`,
`lib/livekitSession.ts`, `lib/api.ts`). These never need to know about Solid.
- Using `innerHTML`, `insertAdjacentHTML`, or `dangerouslySetInnerHTML` in any
Solid component. JSX interpolation (`{value}`) auto-escapes user content and
is the correct approach. Components rendering user-supplied text (e.g. message
attachments, toast messages) must never bypass this escaping.
- Placing `.tsx` files outside `src/components/solid/`. The `vite-plugin-solid`
transform is scoped to this directory in both `vite.config.ts` and
`vitest.config.ts`. A `.tsx` file elsewhere will fail with cryptic JSX parse
errors at build time.
## Existing components
- `Badge.tsx` — presentational badge / pill (no store dependency)
- `ChannelListItem.tsx` — single channel row (subscribes to channels.store)
The plugin client bridge introduced by Phase C also adds a `PluginContainer`
component to this directory; see `Server/plugin/host_ui.go` for the host
side of that contract.
@@ -1,52 +0,0 @@
/**
* Phase B Step 6 — Solid.js adapter.
*
* Bridges the existing custom reactive `Store<T>` (lib/store.ts) into Solid's
* signal model so migrated components can read from existing stores without
* touching them. Vanilla and Solid components coexist throughout the
* migration: a vanilla component can update a store, and any Solid component
* subscribed via this adapter sees the new value through its accessor.
*
* Usage from a Solid component:
*
* import { fromStore } from "@lib/solidAdapter";
* import { authStore } from "@stores/auth";
*
* const auth = fromStore(authStore);
* return <div>{auth().username}</div>;
*
* The accessor returned by fromStore is a Solid signal getter, so it triggers
* fine-grained reactivity in any computation, JSX expression, or `createMemo`.
*/
import { createSignal, onCleanup, type Accessor } from "solid-js";
import type { Store } from "./store";
/**
* Wrap a custom Store as a Solid signal accessor. The signal updates whenever
* the underlying store fires, and the subscription is torn down when the
* Solid owner is disposed (so leaf components don't leak listeners).
*/
export function fromStore<T>(store: Store<T>): Accessor<T> {
const [value, setValue] = createSignal<T>(store.getState(), { equals: false });
const unsub = store.subscribe((next) => setValue(() => next));
onCleanup(unsub);
return value;
}
/**
* Wrap a derived slice of a store. Equivalent to fromStore(store).map(selector)
* but uses the store's native subscribeSelector so changes are gated by the
* existing equality comparator.
*/
export function fromStoreSlice<T, S>(
store: Store<T>,
selector: (state: T) => S,
isEqual?: (a: S, b: S) => boolean,
): Accessor<S> {
const initial = selector(store.getState());
const [value, setValue] = createSignal<S>(initial, { equals: false });
const unsub = store.subscribeSelector(selector, (next) => setValue(() => next), isEqual);
onCleanup(unsub);
return value;
}
-39
View File
@@ -1,39 +0,0 @@
/**
* Phase B Step 6 — Solid.js mount helper.
*
* Wraps Solid's `render(...)` so a Solid component conforms to the
* `{ mount, destroy }` factory contract used everywhere else in the vanilla
* codebase. Existing container components can host a Solid leaf without
* being aware of Solid at all:
*
* import { mountSolid } from "@lib/solidMount";
* import { Badge } from "@components/solid/Badge";
*
* const handle = mountSolid(() => Badge({ label: "online" }), parentEl);
* // …later
* handle.destroy();
*/
import { render } from "solid-js/web";
import type { JSX } from "solid-js";
export interface SolidMount {
/** The DOM element the Solid root is rendered into. */
el: HTMLElement;
/** Tear the Solid root down and remove it from the DOM. */
destroy(): void;
}
export function mountSolid(component: () => JSX.Element, parent: HTMLElement): SolidMount {
const host = document.createElement("div");
host.dataset.solidRoot = "true";
parent.appendChild(host);
const dispose = render(component, host);
return {
el: host,
destroy() {
dispose();
host.remove();
},
};
}
@@ -1,75 +0,0 @@
/**
* Phase B Step 6 — mountSolid adapter lifecycle smoke tests (T-500).
*
* Verifies that the mountSolid adapter in @lib/solidMount correctly inserts a
* Solid reactive root into the DOM and that destroy() disposes the root and
* removes the host element, preventing memory leaks across test suites.
*
* These tests complement Badge.test.tsx (which validates the Solid rendering
* pipeline end-to-end) by proving the *adapter* contract used by vanilla-DOM
* container components.
*/
import { describe, it, expect } from "vitest";
import { mountSolid } from "@lib/solidMount";
describe("mountSolid adapter", () => {
it("appends a data-solid-root host element to the parent on mount", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>test</span>, parent);
expect(parent.querySelector("[data-solid-root]")).not.toBeNull();
expect(handle.el.dataset.solidRoot).toBe("true");
expect(handle.el.parentElement).toBe(parent);
handle.destroy();
parent.remove();
});
it("removes the host element from the DOM after destroy()", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>cleanup-check</span>, parent);
expect(parent.children).toHaveLength(1);
handle.destroy();
expect(parent.querySelector("[data-solid-root]")).toBeNull();
expect(parent.children).toHaveLength(0);
parent.remove();
});
it("supports multiple independent mounts under the same parent", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const a = mountSolid(() => <span>a</span>, parent);
const b = mountSolid(() => <span>b</span>, parent);
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(2);
a.destroy();
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(1);
b.destroy();
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(0);
parent.remove();
});
it("exposes the host element via handle.el", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>el-check</span>, parent);
expect(handle.el).toBeInstanceOf(HTMLElement);
expect(handle.el).toBe(parent.firstElementChild);
handle.destroy();
parent.remove();
});
});
-22
View File
@@ -1,22 +0,0 @@
/**
* Vitest global setup for Solid.js component tests (T-500).
*
* Loaded via `test.setupFiles` in vitest.config.ts so every test suite
* automatically gets Solid's afterEach cleanup without having to import
* or call it manually.
*
* Add future Solid testing helpers here (custom matchers, query extensions,
* aria-query configuration, etc.). Do NOT import application code here —
* this file executes once before every test suite, including non-Solid suites.
*
* Security note: Solid JSX auto-escapes interpolated values ({expr}), so
* user-controlled strings passed through JSX are safe. Never use innerHTML,
* insertAdjacentHTML, or dangerouslySetInnerHTML in Solid components.
*/
import { cleanup } from "@solidjs/testing-library";
import { afterEach } from "vitest";
// Register cleanup after every test so Solid reactive roots are disposed
// and host DOM nodes are removed. Without this, roots accumulate across tests
// and can cause state leakage between test cases.
afterEach(cleanup);
+8 -29
View File
@@ -11,36 +11,15 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"lib": [
"ES2023",
"DOM",
"DOM.Iterable"
],
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"paths": {
"@lib/*": [
"./src/lib/*"
],
"@stores/*": [
"./src/stores/*"
],
"@components/*": [
"./src/components/*"
],
"@pages/*": [
"./src/pages/*"
],
"@styles/*": [
"./src/styles/*"
]
"@lib/*": ["./src/lib/*"],
"@stores/*": ["./src/stores/*"],
"@components/*": ["./src/components/*"],
"@pages/*": ["./src/pages/*"],
"@styles/*": ["./src/styles/*"]
}
},
"include": [
"src",
"tests"
],
"exclude": [
"tests/e2e"
]
"include": ["src", "tests"],
"exclude": ["tests/e2e"]
}
+2 -15
View File
@@ -1,10 +1,5 @@
import { defineConfig, type Plugin } from "vite";
import { resolve } from "path";
// Phase B Step 6 — Solid.js incremental migration. The plugin compiles
// JSX/TSX files anywhere under src/components/solid/ to direct DOM ops, while
// the rest of the vanilla codebase keeps building unchanged. The plugin is a
// no-op for files that don't contain Solid syntax.
import solidPlugin from "vite-plugin-solid";
const host = process.env.TAURI_DEV_HOST;
@@ -19,13 +14,7 @@ function stripCrossOrigin(): Plugin {
}
export default defineConfig({
plugins: [
// Solid first so its JSX transform runs before any other transforms.
solidPlugin({
include: ["src/components/solid/**/*.{ts,tsx,js,jsx}"],
}),
stripCrossOrigin(),
],
plugins: [stripCrossOrigin()],
build: {
modulePreload: { polyfill: false },
cssCodeSplit: false,
@@ -44,8 +33,6 @@ export default defineConfig({
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? { protocol: "ws", host, port: 1421 }
: undefined,
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
},
});
+3 -21
View File
@@ -1,19 +1,7 @@
import { defineConfig } from "vitest/config";
import { resolve } from "path";
import solidPlugin from "vite-plugin-solid";
export default defineConfig({
// The Solid plugin must be applied here in addition to vite.config.ts so
// Vitest can transform `.tsx` test files under src/components/solid/.
// Without it, JSX in component tests is parsed as TypeScript and fails on
// the angle brackets.
plugins: [
solidPlugin({
// Phase B Step 6: cover both the component directory and test files
// under tests/ that use JSX (e.g. setup-solid.test.tsx, T-500).
include: ["src/components/solid/**/*.{ts,tsx,js,jsx}", "tests/**/*.tsx"],
}),
],
resolve: {
alias: {
"@lib": resolve(__dirname, "src/lib"),
@@ -25,15 +13,9 @@ export default defineConfig({
},
test: {
environment: "jsdom",
// Both the legacy `tests/**/*.test.ts` suite and component-local
// `src/**/*.test.{ts,tsx}` files are picked up. The latter is required
// for Phase B Step 6 Solid components, whose tests live alongside the
// component file (see src/components/solid/README.md).
// T-500: also pick up .tsx test files under tests/ (e.g. setup-solid.test.tsx).
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"],
// T-500: global Solid.js test setup — registers afterEach(cleanup) so
// individual *.test.tsx files do not need to call cleanup() manually.
setupFiles: ["./tests/setup-solid.ts"],
// Both the `tests/**/*.test.ts` suite and component-local
// `src/**/*.test.ts` files are picked up.
include: ["tests/**/*.test.ts", "src/**/*.test.ts"],
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
+2 -2
View File
@@ -96,7 +96,7 @@ The client uses TOFU (Trust On First Use) for self-signed certificates: it promp
- TOTP 2FA support and API rate limiting
- Desktop client auto-update with signature verification
See deeper feature and architecture docs in [docs/client-architecture.md](docs/client-architecture.md) and [docs/protocol.md](docs/protocol.md).
See deeper feature and architecture docs in [docs/architecture/](docs/architecture/README.md) and [docs/protocol.md](docs/protocol.md).
## Architecture
@@ -212,7 +212,7 @@ When rotating the server updater key, update [Server/updater/server_update_publi
- [docs/api.md](docs/api.md)
- [docs/protocol.md](docs/protocol.md)
- [docs/schema.md](docs/schema.md)
- [docs/client-architecture.md](docs/client-architecture.md)
- [docs/architecture/client.md](docs/architecture/client.md) — client architecture (replaces client-architecture.md)
- [docs/contributing.md](docs/contributing.md)
- [docs/security.md](docs/security.md)
+1 -1
View File
@@ -25,7 +25,7 @@ accepted-risk note before the beta gate. MEDIUMs are folded into the backlog
| A-2026-07-09 | MEDIUM | Dual V1+V2 WS dispatch (strangler-fig) still live; two parsers/registries to keep in sync | OPEN |
| A-2026-07-10 | MEDIUM | `api.NewRouter` god-constructor: builds services, hub, LiveKit, updater, admin, plugins; spawns goroutines; mounts everything | OPEN |
| A-2026-07-11 | MEDIUM | `ws.Hub` mega-object with post-construction `Set*` wiring ("must be called before Run") | OPEN |
| A-2026-07-12 | MEDIUM | Abandoned SolidJS beachhead still in-tree; `docs/client-architecture.md` describes the abandoned architecture | DECIDED 2026-07-19 — delete beachhead + retire the stale doc (D6) (see [plans/audit-2026-07-19-decisions.md](plans/audit-2026-07-19-decisions.md)) |
| A-2026-07-12 | MEDIUM | Abandoned SolidJS beachhead still in-tree; `docs/client-architecture.md` describes the abandoned architecture | CLOSED 2026-07-19 — beachhead, adapters, build plugin, and Solid deps removed; client-architecture.md retired in favor of architecture/client.md |
| A-2026-07-13 | LOW | Dead schema: `sounds` table survives soundboard removal (correction 2026-07-19: `audit_log_v6` is only a transient rename inside migration 003, not a coexisting table) | OPEN |
| A-2026-07-14 | LOW | Scattered client constants (`#5865F2` ×18, `localhost:8443` ×3); 64 timer call sites with manual lifecycle | OPEN |
| A-2026-07-15 | LOW | `docs/plans/security-hardening-remediation.md` partly stale (references deleted `store/postgres.go`) | OPEN |
+11 -276
View File
@@ -1,280 +1,15 @@
# Client Architecture: Tauri v2
# Client Architecture (moved)
Comprehensive architecture reference for the OwnCord Tauri v2 desktop client. Covers project structure, data flow, component system, and subsystems.
This document previously described the client architecture, including the
planned SolidJS migration. That migration was **abandoned** (see CHANGELOG)
and the beachhead code has been removed from the tree — the client is vanilla
TypeScript with hand-rolled reactive stores and imperative DOM components.
## Why Tauri v2
The current, maintained client architecture document is:
Tauri v2 uses the OS webview (WebView2 on Windows) so the install is ~10-15 MB and RAM usage is ~30-50 MB. The HTML/CSS mockups become the actual UI code, with CSS handling hover effects, conditional visibility, theming, and animations.
**[docs/architecture/client.md](architecture/client.md)**
---
## Project Layout
```text
Client/tauri-client/
├── src-tauri/ # Rust backend
│ ├── Cargo.toml
│ ├── tauri.conf.json # Window size, title, plugins, CSP, updater
│ └── src/
│ ├── main.rs # Windows entry point
│ ├── lib.rs # Tauri Builder: plugins, commands, state
│ ├── credentials.rs # Win Credential Manager (DPAPI)
│ ├── commands.rs # Settings store, cert fingerprints, DevTools
│ ├── ws_proxy.rs # WSS proxy with TOFU cert pinning
│ ├── livekit_proxy.rs # TCP-to-TLS tunnel for LiveKit signaling
│ ├── ptt.rs # Push-to-talk via GetAsyncKeyState
│ ├── tray.rs # System tray icon and menu
│ ├── hotkeys.rs # Global shortcut registration
│ └── update_commands.rs # Auto-update check + install
├── src/ # TypeScript frontend
│ ├── index.html # Single HTML entry point
│ ├── main.ts # Bootstrap, router, service wiring
│ │
│ ├── styles/
│ │ ├── tokens.css # CSS custom properties
│ │ ├── base.css # Reset, scrollbar, typography
│ │ ├── login.css # ConnectPage styles
│ │ ├── app.css # MainPage + component styles
│ │ └── theme-neon-glow.css # Default theme overrides
│ │
│ ├── lib/ # Core services (no UI, no DOM)
│ │ ├── api.ts # REST client (Tauri plugin-http)
│ │ ├── ws.ts # WebSocket client (Tauri IPC proxy)
│ │ ├── types.ts # Protocol types (WS + REST + permissions)
│ │ ├── store.ts # Reactive store factory
│ │ ├── dispatcher.ts # WS message -> store action router
│ │ ├── router.ts # In-memory page router
│ │ ├── livekitSession.ts # LiveKit voice/video session
│ │ ├── connectionStats.ts # WebRTC stats poller
│ │ ├── rate-limiter.ts # Sliding-window rate limiter
│ │ ├── permissions.ts # Bitfield utilities
│ │ ├── profiles.ts # Server profile CRUD
│ │ ├── credentials.ts # Credential storage (Tauri IPC)
│ │ ├── disposable.ts # Component lifecycle cleanup
│ │ ├── dom.ts # XSS-safe DOM helpers
│ │ ├── safe-render.ts # Error boundary
│ │ ├── logger.ts # Structured logger
│ │ ├── notifications.ts # Desktop notifications
│ │ ├── tenor.ts # Tenor GIF API v2
│ │ ├── themes.ts # Theme manager
│ │ ├── updater.ts # Auto-update
│ │ ├── reconcile.ts # Keyed DOM list reconciliation
│ │ ├── icons.ts # Lucide SVG icon factory
│ │ └── ...
│ │
│ ├── stores/ # Reactive state stores
│ │ ├── auth.store.ts
│ │ ├── channels.store.ts
│ │ ├── dm.store.ts
│ │ ├── messages.store.ts
│ │ ├── members.store.ts
│ │ ├── voice.store.ts
│ │ └── ui.store.ts
│ │
│ ├── components/ # UI components
│ │ ├── MessageList.ts, MessageInput.ts, ...
│ │ ├── message-list/ # MessageList sub-modules
│ │ └── settings/ # Settings tab components
│ │
│ └── pages/
│ ├── ConnectPage.ts # Login/register page
│ ├── MainPage.ts # Main app layout
│ └── main-page/ # MainPage sub-controllers
│ ├── SidebarArea.ts
│ ├── ChatArea.ts
│ ├── ChannelController.ts
│ ├── MessageController.ts
│ └── ...
├── tests/
│ ├── unit/ # Vitest unit tests
│ ├── integration/ # Vitest with mocked WS
│ └── e2e/ # Playwright E2E tests
├── vite.config.ts
├── tsconfig.json
├── vitest.config.ts
└── playwright.config.ts
```
---
## Architecture Layers
```text
+===================================================================+
| UI Components |
| (HTML + CSS, vanilla TypeScript DOM manipulation) |
| Components are factory functions returning { mount, destroy } |
+===================================================================+
| | |
| subscribe() | actions | events
v v v
+===================================================================+
| Reactive Stores |
| auth | channels | dm | messages | members | voice | ui |
| Immutable state. Batched notifications via queueMicrotask. |
+===================================================================+
^ |
| WS events | send()
+===================================================================+
| Core Services |
| ws.ts api.ts dispatcher.ts rate-limiter.ts |
| livekitSession.ts notifications.ts ptt.ts tenor.ts |
+===================================================================+
| |
| invoke() | listen()
v v
+===================================================================+
| Tauri IPC Bridge |
+===================================================================+
| ^
v |
+===================================================================+
| Rust Backend |
| ws_proxy (WSS + TOFU) livekit_proxy (TCP-to-TLS tunnel) |
| credentials (Win32 DPAPI) ptt (GetAsyncKeyState polling) |
| commands (settings store) tray hotkeys update_commands |
+===================================================================+
```
Data flows DOWN through layers. Events flow UP via subscriptions. No component directly calls the WebSocket or REST API; they go through stores and controllers.
---
## Rust Backend Modules
### ws_proxy.rs -- WebSocket Proxy with TOFU
WebView2 rejects self-signed TLS certificates. All WebSocket traffic routes through Rust. The Rust proxy implements TOFU certificate pinning -- on first connect, the cert fingerprint is stored; on subsequent connects, it is verified.
### livekit_proxy.rs -- LiveKit TLS Tunnel
A local TCP listener proxies LiveKit SDK connections through TLS to the remote server, avoiding self-signed cert issues.
### credentials.rs -- Windows Credential Manager
Uses Win32 Credential Manager APIs. Credentials are stored as DPAPI-encrypted blobs tied to the Windows user account.
### ptt.rs -- Push-to-Talk
Uses `GetAsyncKeyState` for non-consuming key detection. 20ms polling loop on a background thread.
### tray.rs -- System Tray
System tray icon with Show/Hide, Status submenu, and Quit.
### update_commands.rs -- Auto-Update
Dynamic server URL updater endpoint. Update artifacts are verified via Ed25519 signature.
---
## Store System
The store factory (`createStore`) provides `getState`, `setState`, `subscribe`, `subscribeSelector`, `select`, and `flush`. State is always immutable. Notifications are batched via `queueMicrotask`.
### Store Responsibilities
| Store | Key State | WS Events Handled |
|-------|-----------|-------------------|
| **auth** | token, user, serverName, motd, isAuthenticated | `auth_ok`, `auth_error` |
| **channels** | channels (Map), activeChannelId | `ready`, `channel_create/update/delete` |
| **dm** | DM channel list | `dm_channel_open`, `dm_channel_close` |
| **messages** | per-channel messages, pending sends, hasMore | `chat_message`, `chat_edited`, `chat_deleted`, `chat_send_ok`, `reaction_update` |
| **members** | member Map, typing indicators | `ready`, `member_join/leave/update/ban`, `typing`, `presence` |
| **voice** | currentChannelId, voice users, local audio state | `voice_state`, `voice_leave`, `voice_config`, `voice_token` |
| **ui** | sidebar mode, modals, theme, connection status | `server_restart`, `error` |
Messages per channel are capped at 500. Typing indicators auto-clear after 5 seconds.
---
## Component System
Components are factory functions returning `{ mount, destroy }`. `mount()` appends elements to a container; `destroy()` removes DOM, unsubscribes listeners, and clears intervals.
### DOM List Reconciliation
For efficient list updates (member list, channel list), a keyed reconciliation algorithm reuses existing DOM elements, updates in place, and removes stale elements -- preserving hover states, focus, and scroll position.
---
## Sidebar Architecture
```text
+----------------------------------+
| SERVER HEADER |
+----------------------------------+
| DIRECT MESSAGES (3) [+] |
| Top 3 DMs with unread badges |
| View all messages link |
+----------------------------------+
| TEXT CHANNELS |
| Category-grouped, collapsible |
+----------------------------------+
| VOICE CHANNELS |
| User avatars in channel |
+----------------------------------+
| MEMBERS (collapsible) |
| Role-grouped, drag-to-resize |
+----------------------------------+
| VOICE WIDGET |
| Mute/deafen/camera/screen/leave |
+----------------------------------+
| USER BAR |
| Settings + quick-switch buttons |
+----------------------------------+
```
Two sidebar modes: **"channels"** (full server view) and **"dms"** (full DM conversations list).
---
## Chat Area Architecture
The chat area composes: chat header, message list, typing indicator, message input, video grid (overlays when cameras are active), pinned messages panel, and search overlay.
The `ChannelController` manages mounting/destroying per-channel components when the active channel changes.
---
## Voice and Video (Client Side)
### LiveKit Session
The `LiveKitSession` class manages the full voice/video lifecycle via LiveKit's `livekit-client` JS SDK.
**Stream Quality Presets:**
| Preset | Camera Resolution | Camera Bitrate | Screen Resolution | Screen Bitrate |
|--------|------------------|----------------|-------------------|----------------|
| low | 360p | 600 Kbps | 720p@5fps | 1.5 Mbps |
| medium | 720p | 1.7 Mbps | 1080p@15fps | 3 Mbps |
| high | 1080p | 4 Mbps | 1080p@30fps | 6 Mbps |
| source | 1080p | 8 Mbps | native | 10 Mbps |
### Connection Quality
A 2-second polling interval collects WebRTC stats from both publisher and subscriber PeerConnections. Quality is color-coded: green (<100ms), yellow (100-200ms), red (>200ms).
---
## REST API Client
Uses `@tauri-apps/plugin-http` fetch (not browser fetch) to bypass self-signed cert rejection. All requests include `danger: { acceptInvalidCerts: true }` for server URLs only. Third-party fetches use standard cert validation.
---
## Dispatcher
`wireDispatcher(ws)` attaches listeners to the WsClient, routing each server message type to the appropriate store actions. Key mappings:
- `ready` -> sets channels, members, voice states, DM channels
- `chat_message` -> adds message, increments unread, triggers notifications
- `voice_token` -> starts LiveKit session
- `presence` -> updates member status
- `server_restart` -> shows warning banner
See [protocol.md](protocol.md) for complete message type reference.
See also [docs/architecture/README.md](architecture/README.md) for the full
blueprint set and its maintenance rule, and decision D6 in
[docs/plans/audit-2026-07-19-decisions.md](plans/audit-2026-07-19-decisions.md)
for the removal rationale.
+1 -1
View File
@@ -19,7 +19,7 @@ here (and the audit's closure table) as items land.
| D3 | Fate of `Server/store/` (untested abstraction seam) | prior audit #6 | **Remove `store/`**: execute the prior audit's P4 "single data layer" direction. Services call the (sqlc-backed) `db` package directly; tests use in-memory SQLite instead of `MemStore`. | Planned (sequence with/after D2) |
| D4 | Protocol constants sync (`message_types.go` / `protocolTypes.ts` claim a nonexistent `docs/protocol-schema.json`) | A-2026-07-08 | **Create real codegen**: commit an actual `protocol-schema.json` plus a generator that emits the Go and TS constant files (and, ideally, protocol.md's message table), making the "single source of truth" comment true. | **Implemented 2026-07-19**: `docs/protocol-schema.json` + `Server/scripts/genprotocol` + `make protocol-generate`/`protocol-verify` + CI gate. protocol.md table generation deferred to D7. |
| D5 | Client HTTP TLS gap (`allowSelfSigned: true`, no TOFU pinning on the REST path) | A-2026-07-02 | **Next security work**: build the TOFU HTTP proxy in Rust (mirroring `ws_proxy.rs`) as the next security task — highest-priority security item. | Planned |
| D6 | Abandoned SolidJS beachhead + stale `docs/client-architecture.md` | A-2026-07-12 | **Delete it all**: remove `src/components/solid/`, `solidMount`/`solidAdapter`, `vite-plugin-solid`, and Solid test deps; retire `client-architecture.md` in favor of [docs/architecture/client.md](../architecture/client.md). | Planned |
| D6 | Abandoned SolidJS beachhead + stale `docs/client-architecture.md` | A-2026-07-12 | **Delete it all**: remove `src/components/solid/`, `solidMount`/`solidAdapter`, `vite-plugin-solid`, and Solid test deps; retire `client-architecture.md` in favor of [docs/architecture/client.md](../architecture/client.md). | **Implemented 2026-07-19** — solid/ dir, solidMount/solidAdapter, setup-solid tests, vite-plugin-solid, jsx tsconfig settings, and solid-js/@solidjs deps all removed; client-architecture.md is now a pointer. |
| D7 | Spec refresh strategy for api.md / protocol.md / schema.md | A-2026-07-03 | **One refresh PR first**, using the audit's §2 conformance matrix as the checklist; afterwards specs are kept current per-PR (see the maintenance rule in [docs/architecture/README.md](../architecture/README.md)). Announcement channels (D1) later update the *fresh* specs. | **Implemented 2026-07-19** — all three specs refreshed against the code (incl. E2EE protocol section, migrations 001015, profile/blocks/plugin-admin endpoints); reference tables now point at `protocol-schema.json`. |
| D8 | What to implement first | backlog §6 | **Greenlit now: Protocol codegen (D4) + the quick-wins batch**`LogAudit` error handling (`admin/handlers_backup.go`), contradictory upload `Cache-Control` (`upload_handler.go`), hub inline settings SQL through the data layer (`ws/hub.go`), Hub constructor cleanup (required collaborators into `NewHub`). | **Implemented 2026-07-19** (all four quick wins + D4). Hub cleanup shipped as: race fix — `eventPersister`/`eventStore`/`pluginSink` are now atomic (they were plain fields written by `main.go` after `NewRouter` had already started `Run`); remaining pre-Run setters now reject late calls with an error log instead of racing silently. Note discovered during the work: the discarded-`LogAudit` pattern is repo-wide (23 call sites) — the two tracker-flagged backup handlers are fixed; whether best-effort audit writes stay the convention elsewhere needs a policy decision. |