mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
7a4e5dc357942ae858df8445fed67be54b69a467
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a37f386f9 |
B1-3: repository hygiene gates (RL-19 / L-13, S-05) (#1414)
* chore(format): one Prettier config at the repository root Every formatting rule in this repository lived under Client/ and covered exactly two globs: Client/src/**/*.ts and Client/tests/**/*.ts. Root Markdown, all of docs/, every YAML and JSON, all CSS, the root scripts and tools/mcp-introspect were formatted by nothing. There was no .editorconfig. The obvious fix -- a second Prettier config at the root for "everything else" -- gives two configs and two ignore files that can silently disagree about the same file. So the root takes ownership instead: config, ignore file and gate move up, and Client/ folds in. Client's inline "prettier" block, its .prettierignore, its format/format:check scripts and its now-unused prettier devDependency are all deleted; knip would have failed client-check on that last one. The .prettierrc.json values are lifted byte-for-byte from Client/package.json, which is what keeps the reformat commit free of client TypeScript churn: 87 tracked files need reformatting and not one of them is under Client/src or Client/tests. .prettierignore carries only what .gitignore does not. Prettier 3 reads the root .gitignore by default, so node_modules/, dist/, coverage/, Client/src/generated/ and docs/security-findings/ need no entry. It does NOT read nested .gitignore files, which is why .remember/ is listed explicitly -- 38 untracked per-machine scratch files were otherwise able to turn a shared gate red. graphify-out/ is listed because its seven files are tracked and .graphify_labels.json is signed byte-for-byte by its .sig, so formatting it would silently invalidate the signature. check:hygiene is registered in scripts/run.mjs and folded into check and release:preflight. It deliberately contains no `gofmt -l` step: gofmt -l prints offenders and still exits 0, so it cannot fail a build. Go formatting is enforced separately. shellcheck and actionlint take their file lists from `git ls-files`, never a filesystem glob -- .claude/worktrees/ holds a gitignored pre-flatten copy of the tree with three .sh files a glob would happily lint. This commit leaves the tree non-conformant on purpose. The reformat is the next commit, so the 87-file diff is reviewable separately from the rule that caused it. Not included: editorconfig-checker. .editorconfig is the editor baseline the audit asked for; Prettier, gofmt and rustfmt already fail CI on the same indentation and newline rules, so a fourth tool checking them again is a gate with no failure mode of its own. Verified: `npx prettier --check .` names 87 tracked files and zero untracked ones; the same command listed 38 .remember/ scratch files before the ignore entry and none after. `node scripts/run.mjs --list` resolves check:hygiene to 8 shell targets and 4 workflow targets. Both package.json files parse. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): reformat the tree to the repository Prettier rules Mechanical. This commit is `npx prettier --write .` and nothing else -- the rule that caused it landed in the previous commit so this diff can be reviewed as a transformation rather than as 84 files of hunks. 84 tracked files: 54 Markdown, 7 .mjs, 7 JSON, 6 YAML, 4 .js, 3 CSS, 2 TypeScript (the two Playwright configs at Client's root, which the old Client/src + Client/tests globs never covered). No file under Client/src or Client/tests moves, because .prettierrc.json carries Client's former inline values byte-for-byte. Prettier rewrote 87 files, not 84. The three in .github/ISSUE_TEMPLATE/ had CRLF on disk and differ only in line endings, which .gitattributes (`* text=auto eol=lf`) already normalises, so their committed blobs are unchanged. Worth knowing before someone reconciles the two numbers. The largest single diff is .superpowers/findings-ledger.json at 7976 lines rewritten. That is safe to format: nothing writes the ledger programmatically -- render-ledger.mjs reads it and writes only FINDINGS.md -- so no tool will fight Prettier over its style on the next hunt. FINDINGS.md itself is ignored as generated. Verified: `npx prettier --check .` reports "All matched files use Prettier code style", so the pass is both complete and idempotent. All 7 reformatted JSON files were parsed before and after and compared as values: semantically identical, zero content changes. `node .superpowers/render-ledger.mjs --check` still reports 348 valid findings and leaves FINDINGS.md untouched. `node scripts/check-doc-counts.mjs` still passes its selftest and still agrees on 27 claims across 9 watched documents -- table realignment did not break the patterns it matches on. `node scripts/run.mjs --list` still parses. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): enforce Go formatting in the Server linter S-05: repository-wide Go formatting was not a required gate. The only gofmt enforcement anywhere was .githooks/pre-commit, which is opt-in per clone (`npm run hooks:install`), only sees staged files, and warns-and-skips when gofmt is off PATH. The obvious fix -- a `gofmt -l` step in CI -- does not work: `gofmt -l` prints its offenders and still exits 0, so the step passes no matter what it finds. scripts/run.mjs has the same problem, which is why check:hygiene has no Go step either. So gofmt goes where it can actually fail something: Server/.golangci.yml. The file was already `version: "2"` but had no `formatters:` block at all, so the 19 enabled linters ran with zero formatters. In v2 gofmt/gofumpt/goimports moved out of `linters.enable` into their own section with its own exclusions. Adding it there means the gate reports through the Lint step of "Server Build & Test", which is already pinned as required on dev -- no new job and no new pin. Every tracked .go file is under Server/ (551 of them, one go.mod), so Server-scoped is repository-wide here. One file was genuinely misformatted: a one-space struct field alignment in Server/admin/handlers_users_broadcast_test.go, fixed in the same commit because a single line does not need its own reformat commit. Trap worth recording: `gofmt -l .` on a Windows working tree lists every file that has CRLF on disk, because gofmt normalises line endings. That reported 18 offenders here, 17 of them ghosts. The blobs are all LF -- .gitattributes forces `eol=lf` -- so CI never saw them, and the honest test is to run gofmt over `git show HEAD:<file>` rather than the working copy. Doing that across all 551 tracked Go files found exactly the one real offender above. Verified both directions with golangci-lint v2 locally: `golangci-lint run ./...` reports 0 issues on the formatted tree; appending a misformatted function to Server/auth/constants.go produces 2 gofmt findings; appending the same misformatted function to Server/db/dbgen/admin.sql.go produces 0, so the exclusion holds. Both files restored and verified clean afterwards. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): escape the NUL separator instead of embedding one The `tracked()` helper added earlier in this branch splits `git ls-files -z` output on NUL. The separator was written as a literal NUL byte rather than the two-character JavaScript escape, so scripts/run.mjs became a binary file: `git diff` refused to show it, `grep` reported "Binary file matches" instead of the line, and `* text=auto` in .gitattributes stops normalising line endings for a blob it detects as binary. The code worked -- splitting on a raw NUL and splitting on "\0" are the same operation -- which is exactly why this is worth fixing before it is inherited. A source file that tooling classifies as binary is a file nobody can review. Verified: zero NUL bytes remain, `grep -n "split("` now prints line 50 instead of "Binary file matches", `node scripts/run.mjs --list` still resolves the same 8 shell and 4 workflow targets, and prettier still reports the file clean. * chore(lint): enforce Rust formatting Rust had no formatting gate of any kind: no rustfmt.toml, no `cargo fmt` anywhere in CI, in scripts/run.mjs, in the Makefile or in the git hooks. Clippy was the only Rust gate, and clippy does not check layout. `cargo fmt --all -- --check` now runs in the rust-tests job, ahead of clippy: a formatting failure is cheap to produce and cheap to fix, and there is no reason to spend a clippy pass to surface one. The stable toolchain in that job requested `components: clippy` only, so rustfmt is added there. Only that job. ci.yml has a second, byte-identical `Install Rust` block in tauri-build; it stays clippy-only, because a full desktop build is the wrong place to discover a misplaced brace. No rustfmt.toml. The default profile is the point of a baseline -- a config file here would be a second opinion about style with nothing to say. Client/src-tauri is a single `[package]`, not a workspace, so `--all` is a safeguard against a future member rather than a fan-out today. Verified: `node scripts/run.mjs --list` resolves check:rust to three steps with `cargo fmt --all -- --check` first, `npm run format` now also runs `cargo fmt --all`, and prettier reports ci.yml, run.mjs and the ci-check skill clean. `cargo fmt --all -- --check` currently fails on 13 files -- that is the reformat, and it is the next commit. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): reformat the Rust crate to rustfmt defaults Mechanical. This commit is `cargo fmt --all` and nothing else; the gate that demands it landed in the previous commit so this diff is reviewable on its own. 13 of the 17 tracked .rs files, +343/-164. The crate had never been formatted, so the changes are the usual first-run set: aligned trailing comments collapsed to single spaces, single-element slice literals folded onto one line, long method chains broken across lines, closure bodies expanded into blocks. Verified: `cargo fmt --all -- --check` is clean, so the pass is complete and idempotent. `cargo clippy --all-targets -- -D warnings` finishes with no warnings, and `cargo test --lib` reports 115 passed / 0 failed -- identical to before the reformat, which is what "mechanical" has to mean for a commit that touches this much of the crate. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): make the root facade actually run on Windows Adding the first gate that a contributor would run from the repository root exposed two bugs in the facade, both of which made it silently wrong on the platform this project is developed on. 1. Every npm and npx step failed. bin() appends `.cmd` on Windows, but Node refuses to spawn a .cmd or .bat with shell:false -- the CVE-2024-27980 mitigation -- and fails with EINVAL and a *null* exit status. run.mjs only special-cased ENOENT, so the result was `FAILED: npx prettier --check . exited null` with nothing to explain it. check:client has three npm steps and has never been able to run here. Fixed by spawning only the npm shims through a shell. They are concatenated into a single command string rather than passed as an args array, because shell:true plus a separate array is deprecated (DEP0190) and prints a warning on every invocation; no argument in this file contains a space. 2. Every optional() step was skipped, always. onPath() shelled out to `where` on Windows, but where.exe lives in C:\WINDOWS\System32, which a Git Bash PATH does not necessarily contain -- on this machine PATH carries System32\Wbem, System32\WindowsPowerShell\v1.0 and System32\OpenSSH but not System32 itself. The probe could not start, `probe.status === 0` was false, and golangci-lint and sqlc reported as "not installed" while installed. Fixed by resolving against PATH and PATHEXT directly. No subprocess, and no dependency on which directories happen to be on PATH. A spawn error other than ENOENT now reports its code instead of surfacing as a null exit status. Verified: before, `node scripts/run.mjs check:hygiene` died with "exited null" and both optional steps printed SKIP with the tools present on PATH. After, the same command runs prettier, shellcheck and actionlint and prints "check:hygiene: passed", with no deprecation warning. `golangci-lint` is detected by the new onPath where the old one missed it. Refs RL-20 / L-14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): ignore build output that nested gitignores hide Prettier honours the root .gitignore and no other. Every build and scratch directory in this repository is ignored by a *nested* one -- Client/.gitignore, .serena/.gitignore, .superpowers/sdd/.gitignore -- so none of them were excluded from the new repository-wide gate. The effect is not subtle. Running `cargo test` once drops roughly 850 formattable files into Client/src-tauri/target/, and the hygiene gate goes from clean to "Code style issues found in 939 files". CI never sees it, because a fresh checkout has no build output; every contributor sees it on their second command. Mirrors the three nested files rather than inventing a list: dist, coverage, playwright-report, test-results, .vite, src-tauri/target and src-tauri/gen from Client/.gitignore, plus .serena/ and .superpowers/sdd/. node_modules needs no entry -- Prettier ignores it by default. Verified: `npx prettier --check .` reports "All matched files use Prettier code style" with a fully populated Client/src-tauri/target/ present on disk, and still names README.md when a misformatted table is appended to it. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(ci): shellcheck, actionlint, and a repository hygiene job The last two gates RL-19 asks for. Neither existed: the shell scripts were never linted, the workflows were never syntax-checked, and .githooks/pre-commit carried hand-written `# shellcheck disable=` directives that nothing had ever read. New `hygiene` job, ubuntu-only and root-scoped, modelled on docs-consistency for the same reason: every gate in it is platform-independent text analysis, and .gitattributes pins eol=lf so a second OS would only re-prove line endings. It runs `npm run check:hygiene` -- the same entry point a contributor runs, not a parallel copy of the commands. shellcheck ships in the runner image. actionlint does not, so it is pinned by version and verified by sha256: an installer script piped from a branch would be the one unverified download in a workflow file that pins every action by commit SHA. Prettier's step moves here from client-check, where it no longer belongs. Both linters found real defects. shellcheck, 3 findings in 8 scripts. Two are SC1125 errors in .githooks/pre-commit: `# shellcheck disable=SC2086 - repo paths contain no spaces` is not a valid directive. Trailing prose makes shellcheck discard the rest of the line, so neither suppression was ever in effect -- and one of the two was written earlier in this same branch, which is a fair demonstration of why the gate is worth having. The prose moves to its own line above. The third is SC2015 in start-server.sh, rewritten as an explicit if. actionlint, 5 findings, all inside `run:` blocks it shellchecks once shellcheck is on PATH. Three SC2015 in load-baseline.yml, rewritten as explicit ifs. Two SC2035 in release.yml, where `sha256sum *` should not become `sha256sum ./*`: the comment four lines above records that ParseChecksumFile exact-matches the last field, so a "./" prefix would strand every deployed server exactly as a "windows/" prefix would. `sha256sum -- *` satisfies the linter and leaves the output bytes identical. Verified all three gates in both directions with shellcheck 0.10.0 and actionlint 1.7.7 on PATH. Passing: `node scripts/run.mjs check:hygiene` prints "check:hygiene: passed" with all three steps run, not skipped. Failing: appending `bait_fn() { cat $1; }` to Server/scripts/voice-test.sh fails on SC2086; changing a runs-on to `ubunt-latest` fails on runner-label; appending a misformatted table to README.md fails prettier. All three files restored and confirmed clean afterwards. actionlint validates the new job in ci.yml itself. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(plans): record B1 progress through B1-3 The header still read "B1-0 is complete; B1-1 is the next step" three merged phases later. A plan that misstates where it is costs a reader the same confusion whether it is stale by one phase or three. B1-0 (#1410), B1-1 (#1411), B1-2 (#1412) and B1-3 (this branch) are done; B1-4, dependency automation, is next. Verified: `node scripts/check-doc-counts.mjs` still agrees on 27 claims across 9 watched documents -- this file is one of them -- and prettier reports it clean. * chore(ci): pin Repository Hygiene as a required check on dev The second half of S-05. Its acceptance criterion is "tree is formatted AND a fast required gate fails future drift" -- a check that runs but is not pinned lets a formatting regression merge, so the gate is not a gate until this lands. The name was read off PR #1414 with `gh pr checks` after the job reported `pass` in 26s, not copied out of ci.yml. That order matters: the B0 script records that three pinned names exist in no workflow file at all, and that a required check which never reports blocks every PR forever. Extends the existing script rather than adding a second one, per the B1 plan. Also records, in the "deliberately NOT pinned" list, that Docs & Ledger Consistency reports and passes on a dev PR yet is unpinned. That reads as an oversight from the 2026-08-25 pass rather than a decision, but it belongs to G-04, so it is documented here and not changed. NOT APPLIED YET. Running this script now would pin a check that PR #1413 cannot report -- its branch predates the hygiene job, so the job does not exist in its workflow file and the check would never arrive. Run it after #1414 merges; #1413 needs a rebase onto dev regardless. Verified: shellcheck clean, the embedded JSON parses, and `check:hygiene` passes with prettier, shellcheck and actionlint all running. Refs S-05, RL-14 / G-03. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
70473e8e10 |
B1-2: truth, entry points, and contributor path (#1412)
* fix(hooks): guard on the command the hook actually runs
pre-commit probed one binary and invoked another. The protocol block
guarded on `command -v go` and then ran `make protocol-verify`; the sqlc
block guarded on `command -v sqlc` and ran `make sqlc-verify`. `make` is
not on PATH on a stock Windows box, so a contributor with Go installed
but no make had their commit rejected with
pre-commit: FAIL: protocol constants are stale — run 'make
protocol-generate' in Server/ and stage the result
when nothing had been generated and nothing compared. The real cause was
`make: not found`, and the advice the message gives fails the same way.
Rather than add a `command -v make` guard, inline what the two Makefile
targets reduce to — `sqlc generate` / `go run ./scripts/genprotocol`
followed by `git diff --exit-code`. Same semantics, one less prerequisite,
and it doubles as the make-free equivalent B1-2 asks for. The Makefile
targets stay for anyone who prefers them.
Also: the protocol block was the only one with no `else`, so a
contributor without Go got no check and no notice. It now warns like its
two siblings. And gofmt is a separate binary from go, so it is probed
separately.
Verified both directions with the hook body replayed verbatim:
- Go present, make absent -> passes, no staleness claimed.
- schema edited without regenerating -> fails, as it must.
Refs RL-20 / L-14.
* docs: state one branch and PR model
Active documents contradicted each other head-on. README.md and
docs/contributing.md said branch from `dev` and target `dev`; CLAUDE.md
said branch from `main`, PR to `main`. That is R-02, and the 2026-08-19
audit had already recorded it as D-06 without it being resolved.
`dev` is the answer, and the repository already behaves that way: B0 made
`dev` PR-only with ten required checks enforced on admins, and #1409,
#1410 and #1411 all landed there. `main` carries releases.
docs/contributing.md becomes the single source of truth. It now states the
model, what protection is actually applied, and the two consequences a
contributor meets on their first PR — that a self-mergeable PR still cannot
merge red, and that Docker and Tauri Full Build report as skipped against
`dev` rather than failing. Everywhere else summarises and links here.
- CLAUDE.md: corrected, with a link rather than a second copy.
- CONTRIBUTING.md: new. GitHub's contributing-guidelines affordance only
resolves the root, .github/ or docs/ — `docs/contributing.md` is not a
path it finds, so the link never appeared on issues or PRs.
- PULL_REQUEST_TEMPLATE.md: names the base branch, which it did not.
- bughunt-run skill: reviewed the branch against `origin/main`, which is
the wrong base once every PR targets `dev`.
README.md already said `dev` and is left as the short summary it should be.
Dated audits and the historical remediation plan keep their `main`-era
wording — they are records.
Refs R-02.
* fix(hooks): pick the pre-push base from the nearest integration branch
pre-push decided which side's gates to run from
`git diff --name-only origin/main...HEAD`. That was right when everything
targeted `main`. Once `dev` became the integration branch it stopped being
right: a branch cut from `dev` diffed against `main` counts everything on
`dev` and not yet on `main` as "changed".
Measured on this branch: the old base reported 609 changed files, the new
one reports 6. So in practice the hook was running the full server build
matrix and the client typecheck plus eslint on every push, whatever the
change touched — the file-based narrowing it exists for never engaged.
Now it picks whichever of origin/dev, origin/main is nearest, by commits
between merge-base and HEAD, skipping a candidate that scores 0. Verified:
a branch cut from dev picks origin/dev (2 ahead); dev itself scores 0
against dev and picks origin/main (8 ahead), which is what a dev -> main
release PR wants. With no candidate resolvable it falls back to the
existing `__all__`, so an unfetched or shallow clone still runs everything.
Refs RL-20 / L-14, R-02.
* chore(node): one Node source of truth
`.nvmrc` and all ten `actions/setup-node` pins said 24; five active
documents and the repo's only `engines` block still said 20. A contributor
following the docs installed a version CI does not run.
Node 24 wins — it is what CI already runs. Every manifest now declares
`engines`, and `engine-strict=true` turns a wrong major into a failed
install rather than an `EBADENGINE` warning nobody reads. `>=24` rather
than `^24` so a Node 26 box keeps working; `Client/.nvmrc` stays the
human-facing pin and the docs point at it instead of restating a number.
The `.npmrc` is per package root, not one at the top. npm reads the
project `.npmrc` from the package directory and does not walk parents —
verified with a throwaway package requiring node >=99: with only a parent
`.npmrc` npm warned and exited 0; with one in the package directory it
failed `notsup`. A single root file would have left `Client/`, the package
that matters most, on warnings.
Five docs, not the four previously identified — `docs/mcp-introspect.md`
also said 20. And `docs/contributing.md` claimed "`.nvmrc` + CI both say
Node 20", which was wrong about both.
Verified both directions in all three package roots: Node 22 fails
`notsup`; Node 24 installs clean and `npm ci` passes in Client/.
Refs RL-17 / C-01, ENV-01.
* docs: add the documentation landing page
`docs/` had 24 top-level files and no index. The root README carried a
flat list of 22 links that had drifted: six documents were reachable from
nowhere at all — including both 2026-08-23 audits and the test audit — and
two entries were labelled "latest" while newer unlinked audits existed.
docs/README.md is the index RL-12 asked for. It groups by what a document
*is*, because that is what decides whether to trust it: guidance tells you
how to do something, reference describes a contract the code implements,
audits are dated snapshots nobody updates, plans record intent. Every
tracked file under docs/ now appears exactly once, and the audit table says
plainly that audit-2026-08-19.md still claims "0 open findings" when the
ledger has 38.
The root README keeps a short curated list and defers to the index, rather
than maintaining a second copy that drifts again. Two fixes while there:
`docs/plans/` was linked as a bare directory, unlike its two sibling
directory entries, and was annotated "each carries a verified status
header" — which docs/plans/README.md:7-9 explicitly contradicts, since a
plan's header is exactly the thing that drifts and the index is the
authority.
Verified: 78 relative links across the new and edited files resolve, and
no tracked docs/ file is unreachable from the index.
Refs RL-12 / R-06.
* feat(scripts): root command facade
Entry points existed only inside Server/ (a Makefile) and Client/ (npm
scripts). Nothing at the root told a new contributor where to start, and
the root package.json had three scripts, none of which built or tested
anything.
`npm run check` from the root now runs what CI gates on, and
check:server / check:client / check:rust run one stack. scripts/run.mjs
is dependency-free Node — the shape render-ledger.mjs already uses — so
`npm run check` works before `npm install` has.
Cross-platform by construction: every step is spawned with an explicit cwd
and no shell, so there is nothing to quote and no `cd &&` to behave
differently on Windows. npm and npx get their .cmd suffix there. No step
shells out to make.
The facade orchestrates; it is not a new required path. Each step prints
the command and the directory before running it, and those are exactly the
commands documented per-stack — so a server contributor can read the output
and type them instead, and still never needs Node. Tools CI installs but a
contributor may not have (golangci-lint, which has no wrapper in this repo
at all; sqlc, pinned by Server/sqlc.version) are skipped with a printed
reason rather than failing.
Three corrections to the ci-check skill while aligning it:
- `make sqlc-verify protocol-verify` replaced by what those targets reduce
to, so the documented path does not require make either.
- `cargo test` -> `cargo test --lib`, which is what ci.yml actually runs.
- "NODE_OPTIONS=--no-experimental-webstorage is mandatory on Node 22+" was
false. tests/setup.ts installs the shim, CI runs Node 24 without the
flag, and the suite was measured passing without it — 192 files / 5257
tests, identical to the flagged run.
Also documents the third RL-20 problem, which needed no code: core.hooksPath
is exclusive, so `npm run hooks:install` silently disables any
.git/hooks/post-commit — including the one `graphify hook install` writes,
which CLAUDE.md tells agents to install. Nothing warned about that.
Verified: check:client 5257/192 green, check:rust 123 tests + clippy green,
--list prints every command, and the optional-tool skip path reports rather
than fails.
Refs RL-04 / L-04, RL-20 / L-14.
* feat(ci): fail on a document that contradicts the findings ledger
G-04's remaining half. The ledger is the source of truth for defect counts,
but nothing stopped a planning document from stating a different number and
nothing noticed when one did. `render-ledger.mjs --check` cannot help: it
validates the JSON schema and returns before rendering, so it never reads
FINDINGS.md and cannot see drift at all — and no workflow ran it anyway.
scripts/check-doc-counts.mjs counts ledger statuses and compares them to
what an allow-list of active documents claims, failing with file, line,
claimed value and actual. Wired into ci.yml as a job with no npm ci, since
the script imports nothing outside node:, and into the facade as
`npm run check:docs` — first in `check`, so a contradicted count does not
wait behind ten minutes of -race.
The patterns are narrow on purpose. A first attempt matched any
"<number> <status>" and flagged nineteen things, all false: "the 45 open P1
rows" (issue-register rows, not ledger findings), "All 8 findings F1-F8"
(a different register), "G-05 **refuted**" (an identifier), `">=20"` and
`CGO_ENABLED=0` (not counts at all). A check that cries wolf gets ignored,
which is the failure G-04 already describes. So a number is only read as a
claim in three shapes that cannot mean anything else: an enumeration of two
or more "<n> <status>" pairs, a status table row in a table that totals
itself, and "<n> records/findings" where the ledger is named within three
lines. Fifteen selftest assertions pin both directions, and the job runs
them before it runs the check.
It reads findings-ledger.json directly rather than importing
render-ledger.mjs for `validate`/`render`: that module ends in a bare
top-level `await main()` with no import.meta.main guard, so importing it
rewrites FINDINGS.md as a side effect.
Dated docs/audit-*.md are reported, never failed — they are snapshots
nobody maintains. audit-2026-08-19.md does claim zero open findings against
38 open, so b0-baseline's "No plan was found claiming '0 open findings'"
holds for docs/plans/ but not for docs/.
Not included: a real FINDINGS.md render-drift check. That is RL-07 and
belongs with the generated-artifact work, not here.
Verified: 27 claims across 9 documents agree; corrupting one count in
docs/plans/README.md fails the check naming that line, for both the status
and the total.
Refs G-04.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
7365a31b45 |
refactor: flatten Client/tauri-client into Client (B1-1) (#1411)
* refactor: move Client/tauri-client to Client (pure move, no content change) * refactor: re-point paths after the Client flatten (mechanical, no behaviour change) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fb4329dd94 |
release: v1.2.0-alpha.3 (#1377)
Bump the client version in package.json (+lock), tauri.conf.json and Cargo.toml (+lock) so release.yml's verify-versions gate passes and deployed clients see the update; refresh the literal version in the README and docs build examples; add the curated CHANGELOG entry covering the 199 verified defects fixed since v1.2.0-alpha.2 (#1366-#1375), the observability/backup/deployment hardening in #1376, migration 031, and the new config keys. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d3526968bb |
release: v1.2.0-alpha.2 (#1333)
* docs: add bug-detection improvements plan
Plan for mechanical bug detection alongside the agentic hunt: activate the 14
unused Go fuzz harnesses, the configured-but-never-run Stryker setup, and
browser-mode vitest; encode recurring bug classes as semgrep rules; add
model-based and fault-injected ordering tests; add a persistent seen-ledger
and sibling-sweep lens to the hunt.
All local-only and on demand - fuzz crashers are working reproducers, and this
repo is public.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* build: add make fuzz target and ignore mutation-test output
`go test ./...` runs each Fuzz* function against its committed seed corpus
only - one pass per seed, zero generated inputs - so the 17 fuzz harnesses in
Server/ have never actually fuzzed. `make fuzz` enumerates every target and
runs each with a time budget (Go fuzzes one target per package per
invocation, hence the loop). Local-only by design: a crasher is a working
reproducer and this repo is public.
Also gitignore Client/tauri-client/.stryker-tmp/ and reports/ - a Stryker run
left 200+ untracked files, and a surviving-mutant report maps exactly which
behaviour nothing tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(client): pin reconnect auth-frame and replay-dedup arming
Stryker found 14 surviving mutants across ws.ts:413/422/428 - the auth frame
built on reconnect. Every condition there could be flipped with all 4777
tests still green: the replay-dedup arming guard, the resume-vs-fresh-connect
ternary, and the conditional active_channel_id spread.
Seven tests through the public send/isReplaying surface, no new exports. Two
isolate each half of the `reconnectAttempt > 0 && lastSeq > 0` AND condition -
the combination no existing test reached, and the one an && -> || mutant
walked straight through.
Verified by flipping the line 413 guard to `if (true)`: 3 of 7 fail, revert
restores green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: record two fuzz corpus traps
Interrupting a fuzz run manufactures a false crasher: Go cannot distinguish a
worker that crashed on an input from one killed externally, so it saves the
in-flight input to testdata/fuzz/ as a suspect. It looks exactly like a real
security finding. Replay before believing it.
And committed seed corpus shares the testdata/fuzz/<Target>/ directory with
any false crasher, so clearing one by removing the directory deletes the
seeds too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(client): enforce three prose invariants as ESLint rules
CLAUDE.md documents the voice-supersession, E2EE staleness and dispatcher
invariants in English. English fails no build, and bug hunts keep rediscovering
the same classes. Five rules encode them as an inline flat-config plugin - no
new dependency, and `npx eslint src/` is already a blocking CI gate.
- no-leave-voice-when-superseded: a global leaveVoice() inside a branch that
already confirmed supersession tears down the newer live session
- e2ee-epoch-needs-keypair-check: a non-key-holder never bumps the epoch, so
an epoch-only staleness guard cannot see a restarted session
- e2ee-verified-status-literal: keeps "verified" tied to a hand-written call
site that earned it, never a computed status
- no-identity-scope-fallback: a `?? 0` placeholder scope mints a keypair under
the wrong account
- no-store-write-in-ws-on: page-local ws.on handlers may read stores, not
write them
Each rule proven to fire by reintroducing the historical bug shape and
reverting; RuleTester cases cover both the real shapes that must stay clean
and the bug shapes that must not.
A fourth candidate - await-then-stale-snapshot - was declined as not
AST-expressible: whether an await needs a guard, and whether the guard is
sufficient, is intent rather than shape, and the rule would flag most of the
already-correct guard code in livekitSession.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct dispatcher invariant, record Tier 2 as shipped
The client CLAUDE.md claimed ws.on(...) appears only in dispatcher.ts. Eight
handlers across main.ts, MainPage.ts and ChannelController.ts say otherwise -
page-local UI (ringing, overlays, slow-mode timers) legitimately subscribes.
The real invariant is narrower: dispatcher is the single path by which server
events WRITE to domain stores. That is what local/no-store-write-in-ws-on
enforces, and the doc now matches the code.
Also record that Tier 2 shipped as ESLint rules rather than semgrep, and why
the fourth candidate was declined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(client): move the status-picker dot onto the avatar corner
The corner dot on the user bar avatar was a static hardcoded-green div —
never reflected real status and did nothing on click. Removed it and
relocated the actual StatusPicker trigger dot (real color, opens the
status dropdown) to that same corner instead of its own row. The
"Online"/"Idle"/... text label under the username is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(client): return the saved password over IPC again
The remember-password box saved a password the client could never read
back. Hardening had put #[serde(skip)] on CredentialData::password, so
load_credential returned a record whose password was always absent and
the login form could not prefill it — the box appeared to work and
silently did nothing.
Drop the skip and carry the field through the TS wrapper, which now maps
a non-string password to undefined rather than trusting the payload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(client): add an auto-connect checkbox to the login form
Auto-connect already existed end to end — ServerProfile.autoConnect,
setAutoLogin(), and the boot auto-login block with its cancel overlay —
but was only reachable through the zap button on a server card. This
surfaces the same state as a checkbox under Remember password, where
users look for it.
Ticking it forces Remember password on and disables it: boot auto-login
replays the stored token, which saveCredential only writes when the
password is remembered, so the two cannot be set independently without
producing a setting that silently does nothing.
Unticking is guarded. setAutoLogin(null) clears autoConnect on every
profile, so a bare toggle-off would wipe another server's setting; the
clear now only fires when this profile is the current holder. The guard
lives in ensureProfileExists, which all four auth paths already route
through.
Also consume the password restored in the previous commit, so selecting
a saved server prefills it instead of leaving the field blank.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore(release): bump client to 1.2.0-alpha.2
The client version is not derived from the tag — release.yml's
verify-versions job compares the tag against package.json and
tauri.conf.json and fails the release if they drift, so all five
manifests (both lockfiles included) move together.
Also refreshes the literal version in the README and docs build
examples, and closes the Unreleased changelog section as v1.2.0-alpha.2.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(changelog): record the three bug-hunt sweeps in v1.2.0-alpha.2
PRs #1328, #1331 and #1332 merged to main after v1.2.0-alpha.1 was tagged
and closed 233 verified defects between them, but none of the three left
an entry in the curated changelog — the generated list covers commits,
this file covers behaviour, and nothing bridged the two.
Verified unreleased by ancestry rather than by date (none of the three
merge commits is an ancestor of v1.2.0-alpha.1), so all of it ships for
the first time in alpha.2.
Nine entries grouped by subsystem, leading with the changes an operator
or user would actually notice: the 24h-retention desync, the avatar-
deleting orphan sweep, the zero-byte restore truncation, the six hot-mic
paths, and the TOFU re-pin that would have warned every install at once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(client): drop the e2e assertion for the removed user-bar status dot (#1334)
|
||
|
|
4ff199e14f |
fix: resolve 107 verified defects across ws hub, voice/E2EE, db, and client (#1331)
* fix(client): style the user profile popup
The popup rendered unstyled: it appeared at the bottom of the page and
pushed the rest of the app up, with the avatar drawn as a full-width bar.
app.css carried a complete Discord-shaped card under `.user-popup` /
`.up-*`, but nothing in the codebase renders those classes — the
component emits `.upp-*`. The component had been rewritten with a new
prefix and the stylesheet was left pointing at a DOM that no longer
existed. With no rule matching, the card stayed `position: static`, so
the left/top it computes were discarded and both it and its overlay laid
out as ordinary blocks at the end of <body>.
Replace the orphaned block with rules for the classes actually rendered,
following the same anatomy: banner strip, avatar straddling the
banner/body seam inside a ring punched from the card background, panel
sections, action row. Everything routes through existing tokens, so the
card follows the theme contract.
Two latent bugs fixed while there:
- Placement guessed a 300px card height and clamped only the top edge,
so a member clicked low in the list opened a card that ran off the
bottom of the window. Measure the card and clamp both edges.
- The avatar has to hang off the body's top edge, but the body scrolls,
and `overflow-y: auto` clips horizontally too. Make it a child of the
card rather than the body.
The fade+scale moves from inline styles into CSS so a
`prefers-reduced-motion` override can drop it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(client): stop vite watching src-tauri
`npm run tauri dev` died on Windows partway through the cargo build:
Error: EBUSY: resource busy or locked, watch
'src-tauri\target\debug\deps\owncord_client_lib.dll'
Error The "beforeDevCommand" terminated with a non-zero status code.
Vite's watcher recursed into `src-tauri/target/`, and the moment cargo
wrote the output DLL, node's FSWatcher raised EBUSY as an unhandled
error event and killed the vite process. Vite is tauri's
`beforeDevCommand`, so its death aborted the whole dev session.
The config matched the upstream Tauri vite template in every respect
except the `server.watch.ignored` block that template ships with. Add
it. Tauri already watches `src-tauri` itself for rebuilds, so nothing
is lost.
Windows-specific — EBUSY on an open handle is a Windows filesystem
semantic, and CI only ever runs `tauri build`, never `tauri dev`, so
neither caught it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(security): add 2026-08-04 whole-codebase security review (#1326)
Read-only security review of the full tree (Go server, admin panel, WASM
plugin host, LiveKit voice, Tauri client). No code changes.
Three findings, all the same defect class — a security predicate enforced
at some members of a handler family but not all:
- A-2026-08-01 (HIGH) handleDeleteChannelPermission omits the hierarchy and
grantability guards its PUT twin carries, so a MANAGE_CHANNELS holder can
clear their own role's channel deny and read private channels.
- A-2026-08-02 (HIGH) the admin channel list/patch/delete handlers omit the
type == "dm" guard their sibling getPermChannel carries, so the same role
can enumerate and irreversibly cascade-delete arbitrary DMs and group DMs.
- A-2026-08-03 (MEDIUM) DMService.RingTargets omits the block check the five
other DM interaction sinks perform, so a blocked user can ring the person
who blocked them.
Also records one non-vulnerability observation (backup restore writes to a
hardcoded database path, silently no-opping when database.path is
customised), the candidates rejected during verification, the areas verified
clean, and the areas not examined.
Claude-Session: https://claude.ai/code/session_01Q7GUJtdsHHHGs4pSiLn6LJ
Co-authored-by: Claude <noreply@anthropic.com>
* Full audit: docs/spec refresh + remediation (security fixes, dead-code removal, test & CI gaps) (#1327)
* docs: fix server reference docs (api, protocol, server-configuration, deployment)
api.md:
- Correct the login rate limit: 5/min per IP (was documented as 60/min);
document the per-username lockout and lockout persistence
(Server/api/constants.go, Server/api/auth_handler.go)
- Complete the middleware list to the real 9-entry chain incl. the
opt-in Coraza WAF (Server/api/router.go)
- Add voice_sessions and broadcast_drops to the metrics sample
(Server/api/metrics_handler.go) and document the otel-only
Prometheus /metrics mount
- Add reference sections for the previously undocumented /admin/api
endpoints: setup, stats, users, audit-log, settings, tokens,
backups, updates, and the SSE log stream (Server/admin/api.go)
protocol.md:
- Fix type counts (client->server 26, server->client 37) and add the
missing rows: call_ring, call_decline, emoji_update, call_incoming,
call_declined
- Correct rate limits: voice join/leave 5/1s (was "None"), E2EE offer
64/1s (was 5/1s), and add the call-ring limit (1/3s)
- Document the plugin command wire types (chat_command, command_reply,
plugin_broadcast) and flag that they sit outside protocol-schema.json
server-configuration.md:
- Add missing keys: server.waf_* (3), database.type,
telemetry.otlp_insecure, and the whole logging section +
OWNCORD_LOGGING_LEVEL
- Correct plugin-disabled status code to 503 (was 501)
deployment.md:
- Drop the removed "version" field from the /health sample; add
broadcast_drops to the metrics sample; note the distroless non-root
image; refresh build version strings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDaeRAN79nVdgtNX2zJVrx
* docs(architecture): rewrite stale architecture pages against
|
||
|
|
49595e48d7 |
release: v1.1.0-alpha.4 — first-run setup wizard, LiveKit auto-download, WAF & CI fixes (#1292)
* fix(admin): accept same-origin first-run setup requests A freshly generated config.yaml leaves allowed_origins commented out, so the list is empty. The setup handler's CSRF guard assumed "no Origin header means same-origin", but browsers send Origin on same-origin POSTs too — Chrome and Edge always, Firefox since 70. The admin panel's own setup call is one of those POSTs, so every new install hit "cross-origin setup request blocked" and could never create an owner account. The guard now accepts a request whose Origin names the same host:port as the request's own Host header, falling back to the allowlist otherwise. That is what the original comment intended. CSRF protection is unaffected: a cross-site attacker cannot set Origin, the browser does, and a foreign origin still needs an explicit allowlist entry. Scheme is not compared. Nothing in this server derives the external scheme (no r.TLS or X-Forwarded-Proto handling exists anywhere), so a scheme check would reject legitimate requests behind a TLS-terminating proxy. Tests: isSameOrigin table covering port/host/suffix/schemeless/opaque-origin cases, plus two handler-level tests pinning both halves — same-origin succeeds against an empty allowlist, a foreign origin still 403s and creates no user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(identity): implement identity keypair caching and error handling * fix(client): use the real OS credential store, not keyring's mock (#1281) The `keyring` crate declares no `default` feature. Every platform arm in its lib.rs selects a backend only when that platform's feature is on and otherwise falls through to `pub use mock as default`, so the client's bare `keyring = "3"` compiled the in-memory mock store on Windows, macOS and Linux alike. The mock keeps its secret in the `Entry` object itself, and each command built its own `Entry`: save_identity_key -> Entry::new(..) -> set_password -> Ok(()) load_identity_key -> Entry::new(..) -> get_password -> NoEntry So a save reported success, the very next read in the same process returned nothing, `NoEntry` was mapped to `Ok(None)` so neither side logged anything, and no entry was ever written to Credential Manager on any machine. Downstream, the voice-E2EE identity keypair was regenerated on reconnect, the published identity key stopped matching the key that signed the announce, and peers correctly rejected it as a possible MITM. Name the platform backends explicitly, and stop trusting a store that reports a write it did not keep: - secret_store: read every write back and compare before reporting success. If the store returns a value we did not write, purge it so it cannot shadow the fallback on the next read. - On Windows only, fall back to a DPAPI-protected file in the app data dir, engaged solely after a proven round-trip failure and cleared as soon as the real store works again. The account name is mixed into the DPAPI entropy so a blob cannot be moved between entries and decrypt. macOS/Linux report an error instead of writing secrets to plaintext. - Log the compiled backend at startup and add `probe_credential_store` so an affected machine can be diagnosed from its own log file. - Guard the regression: `compiled_keyring_backend_is_persistent` fails the build if the features are ever dropped again. Verified to fail against `keyring = "3"`. The E2EE fail-closed posture is unchanged: a peer whose announce signature does not verify is still rejected. Linux builds now need `libdbus-1-dev` for the Secret Service backend. Claude-Session: https://claude.ai/code/session_016oUHtEUWWxC79eB88GvX58 Co-authored-by: Claude <noreply@anthropic.com> * fix(client, admin): make the settings panel, client, and admin panel do what they say (#1282) * fix(client): make the settings panel do what it says Functional review of every control in the settings overlay. Each fix below closes a gap between what a control promised and what it did. - Appearance: picking a theme no longer drops a saved accent colour. applyThemeByName strips every inline custom property from <body>, which includes the accent override; under neon-glow (whose body class sets --accent) the user's colour silently reverted until restart. - Overlay: reopening the panel rebuilds the active tab. The Voice & Audio mic meter and camera preview are torn down on close, so a reopened panel showed a dead meter and a black preview; tabs also now re-read prefs. The Logs tab's live listener is released when you switch away from it. - Status: the UserBar picker always started at "online" and never persisted, while the Account tab read a pref nobody else wrote — the two surfaces disagreed. Both now go through lib/userStatus, sync live via the pref-change event, and the saved status is re-asserted on connect. - Notifications: Do Not Disturb now suppresses the desktop notification and the chime, as its description in the panel claims. The taskbar flash, a passive cue, stays. - Keybinds: Ctrl+F, Ctrl+M, Ctrl+D, Ctrl+Shift+V and Ctrl+U were listed but unimplemented. They are wired now (voice ones only while in voice, all of them suspended while the settings panel is open). "Mark as Read" had no feature behind it at all and is replaced by the Escape behaviour that actually exists. - Account: backup codes now carry a "you won't see them again" warning and a copy button; the change-password form requires the current password before spending a server attempt and disables itself while in flight. - Advanced: removed the Hardware Acceleration toggle. Nothing read the preference it wrote — the webview decides GPU compositing before any JS runs, so honouring it needs a Rust startup change. - The settings sidebar name/avatar follow a rename instead of going stale, and settings/helpers no longer keeps a drifted copy of lib/preferences (the copy lacked the write guard, so a failed save could throw). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(client): close silent-failure gaps in the inline admin surface Continuation of the settings-panel review into the rest of the client. - Member context menu had no styling at all: AdminActions renders BEM class names (context-menu__item and friends) that appear nowhere in the CSS, so the menu had no hover, no danger colour, and the "Change Role" submenu pushed the menu open instead of flying out. Added the missing rules. - The submenu offered a hardcoded admin/moderator/member list. On a server with custom roles those roles were unreachable, and picking a name that didn't resolve to a role id silently did nothing. Roles now come from the server's ready payload (owner excluded), and an unresolvable role reports an error instead of dead-ending. - Kick / ban / delete-channel now show an in-flight state, and the two-click confirm disarms after a few seconds so a menu left open can't turn a stray click into a ban (docs/architecture/ux/settings-and-admin.md §3). - Ban collects a reason, which the server already stores and displays (adminBanMember has always accepted one; the menu never passed it). - Copying an invite code was silent: no confirmation, and a clipboard rejection looked identical to success. It now toasts either way. - Creating an invite double-click-minted two of them, and revoking — which kills a live link — had neither a confirm nor an in-flight guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(client): restore moderator message deletion and formatting - The delete affordance was offered only on your own messages, so a moderator could not moderate anything from the client. It now also appears when the signed-in user's role carries MANAGE_MESSAGES, derived from the role bitmasks the server already sends in `ready` (this is what docs/architecture/ux/messaging.md §4 specifies as "Delete (own / moderator)"). lib/permissions.ts existed for exactly this and had no callers at all. - Developer-mode "Copy ID" was silent on success and swallowed clipboard failures; it toasts either way now. - prettier --write on AdminActions.ts (Client Static Checks). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): stop the panel reporting success it didn't have Functional review of the server admin web panel. - An expired admin session left the panel on screen toasting "invalid or expired session" for every action, with no way back to the login form — only the log-stream code handled it. api() now handles 401 centrally: clear the token, return to login, and say why. - Deleting a backup called fetch() without looking at the response, so a failed delete reported "Backup deleted" and left the file in place. It now goes through api(), and — like every other destructive action here — asks for confirmation first. - A failed update check rendered as "Up to date. You're running the latest version", which is a lie that hides a broken update path. It now says the check failed and why. A failed apply no longer leaves the button stuck on "Applying...". - The Edit Channel modal could only rename. PATCH /channels/{id} accepts topic, slow_mode, position and archived, and the channel table has an Archived column — which was read-only state with no control behind it. All four are editable now. - Banned users showed "Yes" with no reason, even though the ban reason is collected on ban and returned by the API. It's now displayed. - Login and first-run setup had no in-flight guard, so a double-click spent two attempts against the login lockout / setup rate limit. Settings' Save stayed enabled after a successful save, implying unsaved changes. - Clipboard copies (invite code, new API token) had no rejection path: a refused clipboard looked exactly like a successful copy. - Backup names in inline onclick handlers go through jsq() like every other interpolated string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * feat(admin): add the plugin management UI the backend already had /api/v1/admin/plugins has exposed list/install/enable/disable/uninstall since Phase C Step 9 — its own header says it "exposes plugin lifecycle operations to the admin panel", and docs/architecture/ux/settings-and-admin.md tells operators plugin management lives in the web panel. The panel had no Plugins section at all, so installing a plugin meant hand-crafting a multipart POST. Panel: - Plugins section: installed table (name, manifest description and requested permissions, version, enabled state, install date), zip upload with the 16 MB server cap stated up front, enable/disable, and uninstall behind a confirm. One lifecycle call at a time. - The lifecycle API sits under a different prefix than the rest of the panel and answers errors as plain text (http.Error), not JSON, so it gets its own fetch helper — sharing api() would have surfaced "unexpected token" instead of the server's reason. 401 still routes back to login. Server: - PluginRow had no JSON tags, so the list marshalled Go field names and every column would have rendered empty. Now snake_case like the rest of the API. - GET /plugins returns X-Plugin-Runtime: enabled|disabled. An empty list means "nothing installed" on a live runtime and "you can't install anything" on a disabled one; the body can't tell them apart, so the panel's empty state had no way to be honest about it. The plugin-store test helper now hands back the database the registry writes to — the existing happy-path test wired a *different* in-memory DB into the handler, which is why nothing noticed the list was always empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * feat(client): gate the composer on slow mode instead of failing the send Verified the optimistic message lifecycle against docs/architecture/ux — pending → chat_send_ok → sent, failed rows with mapped reasons, retry and delete-draft all behave as documented. One thing did not: slow mode. The UX spec (§5) says slow mode should "disable send with a live countdown in the composer; do not drop the drafted message". In practice the composer knew nothing about it: you typed, sent, and got a red failed row back — the exact enabled-then-rejected pattern §6.2 forbids. The client never even received the channel's slow_mode value. - Server: channel payloads (ready, channel_create, channel_update) now carry slow_mode alongside can_send, for the same reason can_send is there — the client can express the limit as affordance. The server still enforces. - Client: after an accepted send the composer disables itself for the channel's cooldown with a per-second countdown, and a SLOW_MODE refusal restarts the full window (the server's limiter is the authority on when the next send is allowed). The draft stays in the textarea. Moderators, who bypass slow mode server-side, are not gated. - The MANAGE_MESSAGES lookup added for moderator deletes moves into lib/permissions as currentUserPermissions/currentUserHasPermission/ canManageMessages, so the composer and the message renderer share one definition instead of two. - WsErrorCode listed 9 of the server's 16 codes: SLOW_MODE, CONFLICT, BAD_REQUEST, INVALID_JSON, UNKNOWN_TYPE, BAD_PAYLOAD, NOT_KEY_HOLDER and ALREADY_JOINED were missing, so code switching on it could not name cases the server actually sends. Now mirrors Server/ws/errors.go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): make backup restore actually restart, and fail closed without a safety copy Verification pass over the remaining review items. Two real defects in restore, one duplicate resolved; cert TOFU and the replay path checked out as-is. Restore: - The handler closed the database, swapped the file underneath it, told the admin "database restored — server restarting", broadcast a 5-second restart countdown to every client... and then kept running. Nothing restarted it, so the server answered every subsequent request against a closed DB until an operator noticed. It now respawns for real, reusing the update-apply pattern (SpawnDetached → SIGTERM → os.Exit backstop) behind a test seam. - A failed pre-restore backup was a warning, and the irreversible overwrite went ahead anyway — removing the safety net the panel explicitly promises ("A pre-restore backup will be created"), precisely when it matters. It now aborts with the database untouched. - The safety copy was written to a cwd-relative "data/backups" while every other backup handler uses the absolute backupBaseDir, so a server started from another directory filed it somewhere the operator would never find. Both new tests were confirmed to fail against the previous behaviour. Client: - SidebarArea kept a private 140-line copy of the member-list wiring that SidebarMemberSection already provides (the extracted, tested one was never imported). Fixing the silent role-change failure earlier meant patching both; now there is one copy. Verified without changes: the optimistic send lifecycle (pending → chat_send_ok → sent, failed rows with mapped reasons, retry, delete-draft), reconnect replay (monotonic last_seq, dedup on reconnect, replay suppression of unread/notifications), and cert TOFU (first-use and mismatch modals, accept re-pins and reconnects, reject disconnects back to connect). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): remove the data race in the restart test hook CI (-race) failed identically on ubuntu and windows: TestHandleRestoreBackup_ Success polled a plain bool that the restore handler's goroutine wrote, and swapped the restartSelf package var from the test goroutine while that handler read it. The hook is now behind a mutex with an atomic flag in StubRestart. Production behaviour is unchanged — the race was entirely in the test seam I added. Verified with `go test -race -count=2 ./admin/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor + perf: split largest source files into modules; optimize hot paths (#1283) * refactor(updater): split updater.go into cohesive files Split the 1070-line updater.go into four files within the same package: updater.go (core types, release checking), download.go (download and tarball extraction), verify.go (signatures, checksums, staged binary), and assets.go (client assets, text-asset cache, HTTP fetching). Pure mechanical move — no behavior or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(ws): split hub.go into cohesive files Split the 1289-line hub.go into five files within the same package: hub.go (Hub struct, lifecycle, register/unregister), hub_broadcast.go (broadcast fan-out and per-user sends), hub_events.go (sequencing, replay, persistence), hub_sweep.go (stale client/session/voice sweepers), and hub_livekit.go (LiveKit accessors). Also optimizes wrapWithSeq on the hot broadcast path: build the seq prefix with a single preallocated append + strconv.AppendUint instead of fmt.Sprintf, halving allocations per broadcast message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(client): extract E2EEManager from livekitSession Move all client-side E2EE key-exchange logic (~550 lines) out of LiveKitSession into a new E2EEManager class in livekitE2EE.ts: ECDH keypair management, identity signing and TOFU pin verification, announce/offer handling, key-holder election, membership rekeying, and periodic key rotation. Dependencies are injected following the existing roomEventHandlers pattern. LiveKitSession keeps thin public delegates (handleE2EEAnnounce, handleE2EEOffer, handleParticipantLeft, rePinPeerIdentity) so the module-level bound exports and the public API are unchanged. livekitSession.ts shrinks from 1955 to 1409 lines. Adds focused unit tests for E2EEManager (key-holder setup, pending announce queue, offer resolution, clearState, rotation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(server): hot-path and query optimizations Logging (biggest win): rewrite the admin log RingBuffer as a true ring (fixed array + head/count) instead of allocating a fresh 2000-entry slice + full copy per log line; gate the ring handler on a configurable level instead of unconditional DEBUG capture; move the broadcast debug log out of the seqMu critical section; drop the per-message slog.With clone in the WS handler. Database: new migration 019 adds idx_attachments_message (message pages no longer scan the attachments table), a covering role-leading index on channel_overrides (replacing a duplicate of the UNIQUE auto-index), a partial index for pinned messages, and narrows the FTS trigger to content changes only; ANALYZE runs after migrations. Rewrite GetChannelUnreadCounts and GetUserDMChannels to correlated subqueries that range-scan idx_messages_channel — O(unread) instead of O(all messages) per WS connect. New GetUserDMChannelIDs replaces the full DM query where only IDs are needed. CreateMessage/EditMessageContent use RETURNING, removing the re-read after every send/edit. Write-path contention: TouchSession throttled to once per minute per session (was one UPDATE per authenticated request); EventPersister flushes its batch in a single transaction with per-row fallback; revoked-session and stale-voice sweeps run off the hub dispatch goroutine with an in-flight guard, and session checks are batched into one IN query; the rate limiter is sharded into 32 buckets with allocation-free strconv key building (auth.Key). WS structural: voice E2EE channel fan-out goes through the existing pubsub voice topic instead of scanning every connected client under h.mu; channelReadAudience memoizes role lookups per call; hasChannelAccess drops its redundant duplicate permission check; voice_join batches SPEAK/VIDEO/SCREENSHARE checks via HasChannelPermBatch. Also: pubsub topic builders and NewAppMetrics stop allocating via Sprintf/global mutex. Verified with go test -race across all packages, go vet, gofmt, and sqlc generate idempotency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(client): render-path, logging, and bundle optimizations Logging: the logger no longer runs permanently at debug — level is set from the environment at startup (debug in dev, info in prod), so every hot-path debug entry stops being serialized, buffered, consoled, and persisted to disk; per-URL debug logs in embed rendering removed. Render path: MessageList's store selector is scoped to the mounted channel, so messages in other channels no longer trigger re-renders, and a new incremental tail-append fast path appends rows instead of tearing down the whole window; Intl.DateTimeFormat instances are cached at module level; parseTimestamp memoizes epoch millis; media prefs (showEmbeds/inlineMedia/showLinkPreviews/animateGifs) are cached with pref-change invalidation; members store gains a roleRevision counter so MessageList stops rebuilding a role map on every presence/typing event. MemberList patches presence changes in place (status dot + offline class) via a row map instead of rebuilding every row, with single-pass role grouping. ChannelSidebar splits its voice subscription into a structural selector (excluding speaking) and a speaking-only patcher using a cached element map instead of per-user querySelector on every speaker event. Memory: GIF/media elements are unobserved before the message window discards them, fixing unbounded IntersectionObserver retention of detached DOM (including frozen-frame data URLs). Bundle: livekit-client (1.3 MB) moves to its own chunk via dynamic imports and manualChunks; the READY handler's stale-voice check reads the voice store instead of requiring the module synchronously. Adds 11 focused tests (different-channel no-rerender, append fast path, media release, presence patch, speaking patch). Full unit suite: 3606/3606 passing; typecheck, lint, and production build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(ci): skip alloc test under deadlock tag; cut bcrypt cost in tests (#1284) The deadlock-tag CI pass failed on TestRingBuffer_WriteDoesNotAllocate: under -tags deadlock, syncutil.Mutex is the go-deadlock mutex whose Lock allocates, so the steady-state ring write measures 1 alloc/call. Extend the build constraint to !race && !deadlock — the test's guarantee is about the ring buffer itself, which the -race-less default pass covers. Make bcryptCost a var with an exported SetCostForTesting hook that also resets the dummy timing pad, and call it with bcrypt.MinCost from the api, auth, and admin TestMains. Password hashing at production cost 12 dominated those suites (~264 hashes): with the race detector the api package alone took ~860s; it now runs in ~33s. Nothing under test depends on hash strength, and no test asserts the cost. Hygiene in the same pass: migration 020 drops idx_sessions_token and idx_invites_code (exact duplicates of their UNIQUE auto-indexes, pure write overhead) with updated db_test assertions; remove the dead tar.TypeRegA comparison in the updater (stdlib normalises it to TypeReg since Go 1.11); gofmt storage/storage.go comment alignment. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * perf(ws): route hot-path permission checks through the cached PermissionService (#1285) The ws package was the only major subsystem still doing live per-check permission queries (GetRoleForUser + GetChannelPermissions per check): a V2 voice join cost 9+ DB reads across its four gates, and every channel broadcast resolved one role query per connected client. Hub now holds svc.Permissions and the voice deps carry it (nil-safe: bare test fixtures fall back to the existing live path, fail-closed semantics preserved everywhere). Converted sites: the voice join and token-refresh permission gates, USE_VIDEO/SHARE_SCREEN controls, requireChannelAccess, channelReadAudience, and RefreshChannelVisibility. Caching these is revocation-correct: every permission-changing mutation already invalidates synchronously before hub fan-out (InvalidateUser on role change, InvalidateAll on override change), the 30s TTL is only a backstop, and the service's gen-counter guard prevents a populate that races an invalidation from caching stale data — the audience-resolution comments now document that invariant. The stale-voice sweeper's check deliberately stays live: it is the last-line backstop for revocations that might bypass an invalidation hook, runs once a minute for only in-voice clients, and its eviction test pins exactly that guarantee. requirePerm keeps its INTERNAL-vs-FORBIDDEN distinction by using the cache only for positive verdicts and falling through to the live path on denial. Adds perm_cache_test.go: role-change invalidation is immediate (no TTL wait), and a counting-store test proving the second check is served from cache. All pinning tests (authz, voice_perm_stale, channel visibility agreement, sweep eviction) pass unmodified. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * perf + refactor: SQLite reader pool, async audits, real lazy-livekit, test splits, eslint 10 (#1286) * perf(db): batch audit writes through an async writer Audit inserts ran synchronously on the request path — including one INSERT per WebSocket connect — each an implicit transaction on the single SQLite connection. WriteAudit keeps its exact signature and D8 policy (never fail the caller, never silently discard): it now upgrades to an async path when the passed Auditor also implements AsyncAuditor. *DB implements that via an atomic pointer that main.go populates at server startup with an AuditWriter modeled on the event persister (bounded queue, batched single-transaction flush with per-row fallback, drain-on-stop, atomic counters, non-blocking enqueue that error-logs drops without leaking the detail field). The token CLI and tests never install a writer, so they keep today's synchronous behavior with zero call-site changes. The writer's Stop defer registers after database.Close's so the LIFO unwind drains the queue before the DB shuts. Adds audit_writer_test.go: batch flush, D8 drop logging, drain-on-stop, flush-failure accounting, poison-row fallback, concurrent enqueue, and seam tests pinning sync-without-writer vs async-with-writer behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(client): actually defer livekit-client; honor saved log level at startup The manualChunks split was cosmetic: index.html modulepreloaded the 531 kB livekit chunk and the entry statically imported it. All four import chains from startup are now cut — auth.store's logout leaveVoice and ptt's setMuted go through dynamic imports, applyStoredAppearance moved to lib/appearance.ts so main.ts and ConnectPage stop pulling the settings tree (whose overlay now loads on first open), and MainPage itself is a dynamic import in renderPage, guarded against the destroy-before-mount race by a navigation-generation helper and pre-warmed once the socket connects. Entry chunk drops 387 kB -> 114 kB (gzip 36 kB); index.html has no modulepreload links; livekit/MainPage/SettingsOverlay/livekitSession load as lazy chunks. The logger now honors the Logs tab's saved minimum level at startup (applyStoredLogLevel with the legacy-key migration moved into lib/preferences.ts) and re-applies it live on pref changes. Dead code: remove unreachable VoiceChannel.ts (superseded by ChannelSidebar's renderer) and its test, plus all knip-flagged unused re-exports in message-list/renderers.ts and ConnectPage's unused form types — knip is now clean apart from pre-existing config hints. Tests: +12 (navigation guard incl. stale-mount discard; logger startup pref, migration, and live re-apply); ptt/stored-appearance updated for dynamic-import plumbing only. Full suite 3593 passing; typecheck, lint, and production build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(db): split SQLite into single-writer + multi-reader connection pools The entire server serialized on one SQLite connection: every read queued behind every other read and every write, throwing away WAL's concurrent-reader capability. File-backed databases now open two pools from a DSN that carries all seven PRAGMAs as per-connection _pragma parameters (an Exec'd PRAGMA only configures one arbitrary pooled connection — moving them into the DSN is what makes >1 connection safe, foreign_keys included): a single-connection writer with _txlock=immediate, and a reader pool sized max(4, NumCPU). In-memory databases keep the exact historical single-connection behavior, which preserves every :memory: test site and the connection-scoped PRAGMA-toggle tests untouched. Routing lives in a dbtx router implementing sqlc's DBTX: statements go to the reader only when provably read-only (leading SELECT/PRAGMA after skipping comments — necessary because sqlc routes INSERT/UPDATE/DELETE ... RETURNING through QueryRowContext/QueryContext, which must stay on the writer); Exec, transactions, migrations, ANALYZE, VACUUM INTO, and the SQLDb() escape hatch all pin to the writer. Every former sqlDB reference across the package was re-pointed deliberately. New pool_test.go pins the properties the split must preserve on a file-backed DB: foreign_keys=1 across many reader connections, WAL journal mode, FK enforcement through both write paths, 8x8 concurrent reader/writer hammering with exact row counts, and a read completing against the pre-tx snapshot while a write transaction is open — the property this change exists to unlock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * test(client)+chore: split the two largest test files; eslint 10; audit clean Split tests/unit/ws.test.ts (3340 lines) into ws-cert / ws-reconnect / ws-messaging / ws-lifecycle plus a shared helpers/ws-mocks.ts module, and tests/unit/audio-pipeline.test.ts (2547 lines) into core / gain / vad-worklet / vad-fallback files. Test bodies moved verbatim; the suite count is unchanged at 3593 passing. Upgrade eslint 9 -> 10 (with @eslint/js 10; typescript-eslint's peer range already covers v10, flat config unchanged, zero new findings) and pin test-exclude ^8 via the existing overrides block so the coverage chain picks up patched glob/minimatch/brace-expansion. npm audit: 8 high -> 0 vulnerabilities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(server): split remaining large files; dependency hygiene notes Split ws/coverage_boost_test.go (2856 lines) into coverage_helpers / chat / voice / voice_lifecycle / misc test files — bodies verbatim, 746 passing tests before and after. Split service/message.go (781) into message_crud / message_reactions / message_query / message_perms with types and the constructor staying put, and ws/serve.go (754) into serve / serve_pumps / serve_auth / serve_ready. Dependency findings (no changes needed): coraza-coreruleset's stale Feb-2024 pseudo-version is unreachable from our code — it enters the module graph only through coraza's own internal tests, and our WAF uses inline directives, never the CRS (fresher rules would require adopting the /v4 module and rewiring the WAF config — deliberate follow-up, not hygiene); gogo/protobuf is likewise graph-only via the livekit SDK and never built into our binaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * style: satisfy golangci-lint modernize/staticcheck in new pool and audit code CI's golangci-lint pass (not run locally until now) flagged the Phase 3/4 additions: range-over-int loops, interface{} -> any on the dbtx router, WaitGroup.Go in the pool tests, and a De Morgan simplification in isReadOnlySQL's identifier-boundary check. Pure style — verified against the same golangci-lint v2.11.3 binary CI uses (0 issues) and re-ran db/ws race + deadlock suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(waf) + fix(deps) + test(ws): OWASP CRS, Dependabot fixes, sleep-free ws tests (#1287) * fix(deps): clear quick-xml RUSTSEC advisories in Tauri lockfile cargo-audit identified the two Dependabot alerts on the default branch: quick-xml 0.37.5 and 0.38.4 both carry RUSTSEC-2026-0194 (quadratic runtime on duplicate-attribute checks) and RUSTSEC-2026-0195 (unbounded namespace allocation DoS), fixed in >=0.41. Both were transitive: plist 1.8.0 (via tauri) and tauri-winrt-notification 0.7.2 (via notify-rust). Semver-compatible updates fix both — plist 1.10.0 moves to quick-xml 0.41, and tauri-winrt-notification 0.7.3 drops quick-xml entirely. cargo-audit is now clean of vulnerabilities; the remaining 20 informational notices are the unmaintained GTK3-binding crates inherent to Tauri v2 on Linux. Verified plist compiles against quick-xml 0.41 (full Tauri build needs the GTK/WebKit system libs CI installs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * feat(waf): layer the maintained OWASP Core Rule Set onto the WAF The WAF previously ran six inline directives only — the CRS never loaded (the old coreruleset dep was a stale graph-only pseudo-version). A second Coraza engine now loads the embedded CRS from coraza-coreruleset/v4 (v4.25.0), layered on top of the inline rules, which stay byte-identical and keep blocking exactly as before. CRS ships in a new server.waf_crs_mode knob (off|detect|block), defaulting to detect: chat traffic is CRS-false-positive-prone (a new test pins that block mode rejects benign SQL-ish chat prose at the default threshold), so operators get rule-match visibility via structured logs first and opt into blocking after tuning. Setup mirrors the official connector: Host/Transfer-Encoding restored to the transaction (else 920280 fires on everything), phase 2 always runs so query-string attacks are scored, PUT/PATCH/DELETE added to the CRS method policy for this REST API, body limits matched to the app's 1 MiB cap with uploads excluded from body access and the content-type policy. Also fixes a latent middleware bug: the body was previously swapped for the buffered reader even when nothing was buffered, which would have handed body-access-off routes an empty body; now pinned by a test across all modes. Adds waf_crs_test.go (load, mode wiring, XSS/traversal detection without blocking, block-mode blocking + benign passthrough, upload body preservation); waf_test.go passes unmodified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * test(ws): replace fixed sleeps with condition-based waits The ws suite paced async hub effects with 537 fixed time.Sleep calls — slow at best, flaky under load at worst. They are now condition-based: a small waitFor/waitRegistered/waitClientCount/waitMsgOfType helper set (waitRegistered exploits the hub's in-order client-event processing), plus blocking decode-scans for the DM tests. The bulk deletion is grounded in verified production facts, unchanged by this commit: sendMsg is a synchronous buffered send (error replies are already buffered when the handler returns), the voice control / rollback / cleanup / sweep paths are synchronous, and serve.go registers the client before writing the ready frame. Absence assertions were deliberately NOT inverted into polling — they keep bounded windows, each commented. 20 sleeps remain, all justified in place: poll intervals inside condition loops, absence windows, clock-granularity pacing, and the event-pruner's inherently time-based no-prune-after-cancel assertion. Suite: 746 tests before and after; 62.6s -> 46.1s (30s of the remainder is GracefulStop's hard-coded production 5s drain, out of scope here); race flake check passes 3 consecutive iterations; deadlock pass and golangci-lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix: audit-driven fixes — client leaks/lazy-load, WAF detect logging, audit shutdown race (#1288) * fix(waf,db): aggregate CRS detect-mode logging; make audit Stop await goroutine exit WAF detect mode wired logCRSMatch as the engine-level error callback, which fires one slog.Warn per matched rule on the request goroutine. In the default detect mode ordinary chat prose trips several CRS SQLi/XSS rules plus anomaly scoring, so each request logged a burst of Warn lines in the hot path. Aggregate per request from per-transaction state instead of the shared global callback: in the default detect path leave the engine error callback nil and, in the existing crsTx defer, emit at most one Warn per request that had matches (count + highest-severity rule), demoting the full rule-id list to Debug. Block mode keeps per-rule logging (blocked requests are rare and their detail is wanted), and a caller-supplied onCRSMatch callback keeps per-rule delivery so existing tests stay unmodified. Detection, interruption, and body handling are unchanged — only the detect-path logging shape. The audit writer's Stop selected between <-done and <-ctx.Done(); on a slow flush the 5s ctx could win, returning while run() was still flushing. main.go's LIFO defers then closed the DB pool under a live flusher, losing audits. Stop now always waits on done (the goroutine has stopped touching the store) while ctx bounds only the drain inside run() via a published stopCtxDone channel, so a slow store delays shutdown by at most one in-flight flush and the pool is never closed under a live writer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * fix(client): plug listener leaks, guard lazy livekit load, honor saved log level Follow-up audit of the recently-landed lazy-livekit and session wiring found three real issues: - clearAuth unconditionally dynamic-imported livekitSession to call leaveVoice on every logout, pulling the ~531 kB livekit chunk into the logout path even when no voice session was ever active. Guard the import on an active voice session (currentChannelId set and status not idle) and add a .catch so a failed teardown import can't reject unhandled. - The onStateChange handler unsubscribed session listeners only on the ready transition, not on disconnected; user_update and ready listeners registered per session were never collected for cleanup. Collect them into a sessionUnsubs array cleaned up on both ready and disconnected, preventing duplicate handlers accumulating across reconnects. - The Logs tab min-level select ignored the persisted log level when no explicit dropdown preference was saved. Add logger.getLogLevel() and default the select to it so the UI reflects the level actually in effect. Also add .catch to the ptt setMuted dynamic import. New unit tests cover the clearAuth guard, getLogLevel, and the LogsTab default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(waf): load embedded OWASP CRS ruleset correctly on Windows (#1289) The CRS WAF engine failed to initialize on Windows, taking the whole api package's test suite red there. coraza's seclang parser resolves Include globs through path/filepath: for every match of `Include @owasp_crs/*.conf` it calls filepath.Join(currentDir, match), which on Windows rewrites the forward slashes to backslashes. It then feeds names like `@owasp_crs\REQUEST-901-INITIALIZATION.conf` back into the root fs.FS. That FS is the ruleset's embed.FS, which is always forward-slash and rejects a backslash name, so newCRSWAF returned "file does not exist" and no CRS rule under a subdirectory was ever loaded. Wrap coreruleset.FS in a small slash-normalizing fs.FS (Open/ReadFile/ReadDir/ Glob) that converts backslashes to forward slashes before delegating. This fixes CRS loading on Windows without patching coraza or the ruleset module and is a no-op where the separator is already "/". The Linux-only local verification for the CRS work missed this because coraza never emits backslashes there. The new test reproduces the failure mode on any OS by constructing the exact backslash name coraza produces on Windows: the raw ruleset FS fails to read it, the wrapper resolves it, and a forward-slash path still works. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * fix(e2e): repair the Playwright suite so the CI job stops timing out (#1291) The Client E2E CI job never completed: every run hit its 25-minute cap and was cancelled. ~229 of the 255 web tests were failing, all cascading from the shared login helper, and 255 tests x 3 attempts x 20-45s of timeout burn on 1 worker deterministically exceeds the cap. Root cause: the e2e Tauri mock predates the Rust HTTP TOFU proxy. api.ts now awaits invoke("start_http_proxy") and builds REST URLs as http://127.0.0.1:{port}/api/v1/..., but the mock's invoke returned null for the unstubbed command, so every URL got a literal "null" port and Request construction threw before the mocked plugin:http transport was consulted. Login rejected, [data-testid='app-layout'] never mounted, and every logged-in test burned its full timeout. Stubbing start_http_proxy with any numeric port fixes the cascade because route matching is substring-based. The tail of failures after that fix were tests asserting behavior the app intentionally changed: - The ready payload can no longer pre-connect the local user to voice: the dispatcher treats "self in ready.voice_states while idle" as stale state from a reload and immediately leaves. MOCK_VOICE_STATE now seeds remote users only (2, 3), and widget tests join through the real click path via a new joinVoiceChannelByName helper. - The mock's voice_join reply no longer includes a voice_token: a token starts a real LiveKit session that deterministically self-destructs in the browser mock (E2EE key exchange timeout ~15s / connect-refused retries), tearing the widget down mid-test. These web tests validate the WS/UI layer only; real LiveKit is covered by the native suite. The reply also gained the full VoiceStatePayload shape — the sidebar renders user.username directly, and the omitted field broke the whole voice-user list render. - Message-load failure now renders an inline region error + Retry instead of a toast (UX spec 2), so the toast specs assert the inline UI and get their auto-dismiss vehicle from the delete-confirmation toast. CI hardening so a future systemic breakage can never burn the full cap again: maxFailures 20 and a 20-minute globalTimeout in CI (Playwright now self-terminates with a usable report instead of being SIGKILLed), with the workflow's timeout-minutes 25 as the outer backstop. The job stays continue-on-error until it has proven stably green across a few pushes; the ci.yml comment documents that flip trigger. Full suite: 255/255 passing locally (~7.5 min at 1 worker, ~4 min at 2). Unit tests (3598), typecheck, and prettier all clean. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * feat(admin): first-run setup wizard with config.yaml write-back + LiveKit auto-download (#1290) * feat(admin): first-run setup wizard with config.yaml write-back Turn the single-screen owner-account setup into a guided multi-step wizard so non-technical operators never have to hand-edit YAML: - config: new comment-preserving config.Save (yaml.Node round-trip, atomic temp+rename write, verified loadable before replacing the file) plus a shared config.DefaultPath. Persists the runtime-generated LiveKit credentials so voice tokens survive restarts. - admin: POST /admin/api/setup accepts an optional "wizard" object (server name, MOTD, registration, port, TLS mode/domain, upload limit, voice quality). Values are validated before the account is created; DB settings and config.yaml are written after; failures downgrade to warnings so the created owner is never orphaned behind a 5xx. When a startup-only value changed the server restarts itself (reusing the backup/update restart machinery) and returns the new admin URL. - admin: GET /admin/api/setup/status now returns secret-free prefill defaults while setup is pending. - admin panel: six-step wizard UI (welcome, account, server basics, uploads & voice, access, review) with plain-language explanations, a restart/reconnect screen, and a "skip" path that keeps the legacy account-only flow byte-for-byte. - legacy payload {username,password} and all existing call sites keep working (SetupOptions is a trailing variadic parameter). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * fix(lint): satisfy modernize — any over interface{}, new(expr) over ptr helper Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * feat(voice): auto-download the LiveKit server binary Voice now works with zero manual setup: when voice.auto_download_livekit is enabled and no voice.livekit_binary is configured, the server fetches the pinned livekit-server release (v1.13.5, overridable via voice.livekit_version) from the official LiveKit GitHub releases in the background at startup, verifies it against the release's checksums.txt, extracts it into data/livekit/, and manages it as the existing companion process (crash recovery, health checks, graceful shutdown). - ws: new livekit_download.go — pinned version, per-platform asset mapping (linux/windows × amd64/arm64/armv7, matching LiveKit's goreleaser config), size-capped downloads, hash verification and extraction through one open handle (TOCTOU-safe), O_EXCL staging, atomic rename, stale-version cleanup. LiveKitProcess.Start resolves the binary asynchronously with retries so boot is never blocked. - config: voice.auto_download_livekit + voice.livekit_version; enabled in the generated default config so fresh installs get working voice out of the box, while the compiled-in default stays off for existing configs. config.Load now loads the default file it just wrote, so the first boot runs with exactly the configuration the file documents. - wizard: "Voice chat" toggle (on by default) in the Uploads & voice step; the choice is written to config.yaml and factored into the restart decision. - docs: livekit-setup, server-configuration, deployment, README. Verified end-to-end against the real v1.13.5 release: download, checksum match, extraction, and process spawn all succeed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * chore: remove stray server.log, ignore local run logs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e392939c52 |
chore(deps): batch-apply all open dependabot bumps (2026-07-23)
Applies all 22 open dependabot PRs in one pass (CI on those PRs never ran — Actions minutes exhausted). Verified locally via the ci-check mirror: builds (all tag variants), vet, golangci-lint, sqlc/protocol verify, vitest 3304/3304, npm audit clean, cargo check. Server (Go): wazero 1.12.0, x/mod 0.38.0, chi 5.3.1, otel 1.44.0, otel/trace 1.44.0, otel prometheus exporter 0.66.0, modernc sqlite 1.54.0, livekit/protocol 1.50.2, koanf/v2 2.3.5, x/sync 0.22.0. Also x/text 0.39.0 (fixes GO-2026-5970, flagged by govulncheck). livekit/protocol requires Go 1.26 → go.mod, CI pins, and docs bumped. Client (npm, lockfile-only): playwright/test 1.61.1, oxlint 1.75.0, eslint 9.39.5, knip 6.29.0, plugin-http 2.5.9, plugin-fs 2.5.1, plugin-opener 2.5.4, tauri-apps/api 2.11.1 + npm audit fix (brace-expansion, fast-uri transitive highs). Client (cargo, lockfile-only): futures-util 0.3.33, env_logger 0.11.11, serde 1.0.229, tauri-typegen 0.5.2. Closes #1204 #1205 #1206 #1207 #1209 #1210 #1211 #1212 #1213 #1214 Closes #1215 #1216 #1219 #1220 #1221 #1222 #1223 #1225 #1226 #1227 #1228 #1224 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a11dd7eab5 |
docs: record per-operator GIF key decision and surface setup
GIFs are off by default and each operator supplies their own Klipy key, but nothing user-facing said so — README and quick-start had zero mentions, so a fresh self-hoster had no way to learn the feature exists. Records the decision as D12 with the rejected alternatives, so the trade-off is not silently revisited later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
82893e036b |
fix(client): start reliably on Wayland; degrade PTT without X11
On Wayland sessions (notably GNOME + NVIDIA), WebKitGTK's DMABUF renderer can crash or render a blank window, so the client failed to start. Set WEBKIT_DISABLE_DMABUF_RENDERER=1 on Wayland unless the user has already set it themselves. device_query's global key state needs an X11/XWayland display and panicked per poll on pure-Wayland setups. Use DeviceState::checked_new() so push-to- talk degrades to inactive with a single warning instead. Fixes #96 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK |
||
|
|
c3a8aa477c |
fix: resolve 20 code review bugs across Rust, TypeScript, and Go
Critical/High Rust (Tauri client): - BUG-140: replace .run() with .build() + RunEvent::Exit handler; native error dialog on startup failure - BUG-141: eliminate PTT thread TOCTOU race with Mutex critical section; add AtomicBool shutdown and catch_unwind - BUG-144: fix TOFU cert store corruption — read-before-write rollback restores previous fingerprint on save failure (all 3 write sites) - BUG-145: add VK code range guard (1..=254) in is_key_down; fix cast to (state as i16) < 0 - BUG-147: replace bare spawns with JoinSet; abort_all + drain on exit; unconditional closed event - BUG-150: add CRLF guard in handle_connection before header rewriting - BUG-151: wrap header read loop in tokio::time::timeout(10s) - BUG-158: extract CERTS_STORE/SETTINGS_STORE to constants.rs (eliminate 3 duplicates) - HIGH-2: PTT thread self-cleanup uses unwrap_or_else defensive pattern - HIGH-4: ws_send distinguishes Full vs Closed errors; warn log on backpressure Critical/High TypeScript (Tauri client): - BUG-142: join-generation counter prevents stale connectAndSetup completions - BUG-143: replace 8 mutable LiveKit session fields with discriminated union SessionState - BUG-146: 60s token refresh deadline; cleared on reply or voice leave - BUG-148: ResizeObserver hoisted to outer scope; disconnect() in destroy() before ac.abort() - BUG-152: dismissSignal.aborted guard already present (no change needed) - BUG-153: measureRendered split into two-pass read-then-write; eliminates per-message reflow - BUG-154: WS dedup cache batch-evicts to 80% on overflow (amortised O(1)) - BUG-157: pendingUpdates replaced with coalesced function-composition slot (O(1) queue depth) Go server: - BUG-149: safe two-value type assertion in getOutboundIP with localhost fallback - BUG-155: broadcast buffer 256→1024; broadcastDrops atomic counter exposed in /api/v1/metrics - BUG-156: LiveKitHealthCheck and implementations accept ctx context.Context; all call sites pass r.Context() (12 files) - BUG-159: MaxMessageBytes constant in config/constants.go; replaces 1<<20 literals in serve.go and updater.go - HIGH-1: cert store rollback reads old value before write; restores previous cert on save failure All validation passes: go build, go vet, cargo check, npm typecheck |
||
|
|
344d39892f |
docs: update contributing and quick-start for Linux/ARM64/Docker
contributing.md: - Prerequisites now show Windows/Linux/ARM64 support matrix - Client commands split into Build, Tests, Typecheck/Lint/Format sections - Add 7 missing commands: lint:ox, format, format:check, knip, test:mutate, test:mutate:dry, test:browser quick-start.md: - Prerequisites table covers Windows + Linux x64 + ARM64 - Step 1 split into 3 options: pre-built binaries, Docker, build from source - Release table lists all platform artifacts (exe, AppImage, deb) - Docker quick-start references deployment.md Docker section |
||
|
|
ec5775910f |
docs: add public documentation for contributors and users
Created 12 public docs derived from internal vault: - Setup guides: quick-start, server-configuration, livekit-setup, deployment - Networking: port-forwarding, tailscale - References: api, protocol, schema, client-architecture - Community: contributing, security Updated .gitignore to only exclude docs/brain/ (internal vault), allowing docs/ to be tracked. Updated README with expanded quick start, voice/video setup, networking ports, and doc links. |