mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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:
Generated
-266
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user