From bf508c9e6bd3cae821192cf419d303a6475da6b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 13:57:28 +0000 Subject: [PATCH] chore(client): remove abandoned SolidJS beachhead (D6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA --- Client/tauri-client/package-lock.json | 266 ---------------- Client/tauri-client/package.json | 3 - .../src/components/solid/Badge.test.tsx | 27 -- .../src/components/solid/Badge.tsx | 46 --- .../src/components/solid/ChannelListItem.tsx | 52 ---- .../src/components/solid/PluginContainer.tsx | 29 -- .../src/components/solid/README.md | 59 ---- Client/tauri-client/src/lib/solidAdapter.ts | 52 ---- Client/tauri-client/src/lib/solidMount.ts | 39 --- .../tauri-client/tests/setup-solid.test.tsx | 75 ----- Client/tauri-client/tests/setup-solid.ts | 22 -- Client/tauri-client/tsconfig.json | 37 +-- Client/tauri-client/vite.config.ts | 17 +- Client/tauri-client/vitest.config.ts | 24 +- README.md | 4 +- docs/audit-2026-07-19.md | 2 +- docs/client-architecture.md | 287 +----------------- docs/plans/audit-2026-07-19-decisions.md | 2 +- 18 files changed, 28 insertions(+), 1015 deletions(-) delete mode 100644 Client/tauri-client/src/components/solid/Badge.test.tsx delete mode 100644 Client/tauri-client/src/components/solid/Badge.tsx delete mode 100644 Client/tauri-client/src/components/solid/ChannelListItem.tsx delete mode 100644 Client/tauri-client/src/components/solid/PluginContainer.tsx delete mode 100644 Client/tauri-client/src/components/solid/README.md delete mode 100644 Client/tauri-client/src/lib/solidAdapter.ts delete mode 100644 Client/tauri-client/src/lib/solidMount.ts delete mode 100644 Client/tauri-client/tests/setup-solid.test.tsx delete mode 100644 Client/tauri-client/tests/setup-solid.ts diff --git a/Client/tauri-client/package-lock.json b/Client/tauri-client/package-lock.json index cbd8c8c7..70f45976 100644 --- a/Client/tauri-client/package-lock.json +++ b/Client/tauri-client/package-lock.json @@ -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", diff --git a/Client/tauri-client/package.json b/Client/tauri-client/package.json index 20e0e5b4..d92acbef 100644 --- a/Client/tauri-client/package.json +++ b/Client/tauri-client/package.json @@ -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" } } diff --git a/Client/tauri-client/src/components/solid/Badge.test.tsx b/Client/tauri-client/src/components/solid/Badge.test.tsx deleted file mode 100644 index cb4e5397..00000000 --- a/Client/tauri-client/src/components/solid/Badge.test.tsx +++ /dev/null @@ -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(() => ); - expect(getByText("online")).toBeTruthy(); - }); - - it("invokes onClick when activated", () => { - let clicked = 0; - const { getByText } = render(() => ( - clicked++} /> - )); - getByText("press me").click(); - expect(clicked).toBe(1); - }); -}); diff --git a/Client/tauri-client/src/components/solid/Badge.tsx b/Client/tauri-client/src/components/solid/Badge.tsx deleted file mode 100644 index 32570aef..00000000 --- a/Client/tauri-client/src/components/solid/Badge.tsx +++ /dev/null @@ -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, 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 ( - { - if (props.onClick && (e.key === "Enter" || e.key === " ")) { - e.preventDefault(); - props.onClick(); - } - }} - > - {props.label} - - ); -} diff --git a/Client/tauri-client/src/components/solid/ChannelListItem.tsx b/Client/tauri-client/src/components/solid/ChannelListItem.tsx deleted file mode 100644 index 60bf4b4c..00000000 --- a/Client/tauri-client/src/components/solid/ChannelListItem.tsx +++ /dev/null @@ -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( - channelsStore, - (s) => s.channels.get(props.channelId), - ); - const isActive = fromStoreSlice( - channelsStore, - (s) => s.activeChannelId === props.channelId, - ); - - return ( - - {(ch) => ( -
  • props.onSelect(props.channelId)} - > - # - {ch().name} - 0}> - - -
  • - )} -
    - ); -} diff --git a/Client/tauri-client/src/components/solid/PluginContainer.tsx b/Client/tauri-client/src/components/solid/PluginContainer.tsx deleted file mode 100644 index 0901d433..00000000 --- a/Client/tauri-client/src/components/solid/PluginContainer.tsx +++ /dev/null @@ -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
    ; -} diff --git a/Client/tauri-client/src/components/solid/README.md b/Client/tauri-client/src/components/solid/README.md deleted file mode 100644 index 4bf52631..00000000 --- a/Client/tauri-client/src/components/solid/README.md +++ /dev/null @@ -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. diff --git a/Client/tauri-client/src/lib/solidAdapter.ts b/Client/tauri-client/src/lib/solidAdapter.ts deleted file mode 100644 index e99bc1ac..00000000 --- a/Client/tauri-client/src/lib/solidAdapter.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Phase B Step 6 — Solid.js adapter. - * - * Bridges the existing custom reactive `Store` (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
    {auth().username}
    ; - * - * 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(store: Store): Accessor { - const [value, setValue] = createSignal(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( - store: Store, - selector: (state: T) => S, - isEqual?: (a: S, b: S) => boolean, -): Accessor { - const initial = selector(store.getState()); - const [value, setValue] = createSignal(initial, { equals: false }); - const unsub = store.subscribeSelector(selector, (next) => setValue(() => next), isEqual); - onCleanup(unsub); - return value; -} diff --git a/Client/tauri-client/src/lib/solidMount.ts b/Client/tauri-client/src/lib/solidMount.ts deleted file mode 100644 index a4c1ad79..00000000 --- a/Client/tauri-client/src/lib/solidMount.ts +++ /dev/null @@ -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(); - }, - }; -} diff --git a/Client/tauri-client/tests/setup-solid.test.tsx b/Client/tauri-client/tests/setup-solid.test.tsx deleted file mode 100644 index ea365fb9..00000000 --- a/Client/tauri-client/tests/setup-solid.test.tsx +++ /dev/null @@ -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(() => test, 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(() => cleanup-check, 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(() => a, parent); - const b = mountSolid(() => b, 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(() => el-check, parent); - - expect(handle.el).toBeInstanceOf(HTMLElement); - expect(handle.el).toBe(parent.firstElementChild); - - handle.destroy(); - parent.remove(); - }); -}); diff --git a/Client/tauri-client/tests/setup-solid.ts b/Client/tauri-client/tests/setup-solid.ts deleted file mode 100644 index e78791f5..00000000 --- a/Client/tauri-client/tests/setup-solid.ts +++ /dev/null @@ -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); diff --git a/Client/tauri-client/tsconfig.json b/Client/tauri-client/tsconfig.json index 45b5dc20..85aa443d 100644 --- a/Client/tauri-client/tsconfig.json +++ b/Client/tauri-client/tsconfig.json @@ -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"] } diff --git a/Client/tauri-client/vite.config.ts b/Client/tauri-client/vite.config.ts index 8f5a5449..7e65c900 100644 --- a/Client/tauri-client/vite.config.ts +++ b/Client/tauri-client/vite.config.ts @@ -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, }, }); diff --git a/Client/tauri-client/vitest.config.ts b/Client/tauri-client/vitest.config.ts index fbc987c4..cfa111de 100644 --- a/Client/tauri-client/vitest.config.ts +++ b/Client/tauri-client/vitest.config.ts @@ -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"], diff --git a/README.md b/README.md index 61cf97f5..1e169bf3 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/audit-2026-07-19.md b/docs/audit-2026-07-19.md index 0e02ebc1..5b3285c3 100644 --- a/docs/audit-2026-07-19.md +++ b/docs/audit-2026-07-19.md @@ -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 | diff --git a/docs/client-architecture.md b/docs/client-architecture.md index 5dd144a7..9e03a44a 100644 --- a/docs/client-architecture.md +++ b/docs/client-architecture.md @@ -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. diff --git a/docs/plans/audit-2026-07-19-decisions.md b/docs/plans/audit-2026-07-19-decisions.md index c0dd9d83..1c4e3dbc 100644 --- a/docs/plans/audit-2026-07-19-decisions.md +++ b/docs/plans/audit-2026-07-19-decisions.md @@ -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 001–015, 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. |