7 Commits
Author SHA1 Message Date
J3vbandClaude Opus 5 8cf019c03f fix: 40 correctness fixes from the 2026-08-19 bug hunt (#1392)
* fix(identity): 1 defect(s) (OC-0151)

* fix(ws): 1 defect(s) (OC-0152)

* fix(admin): 1 defect(s) (OC-0153)

* fix(admin): 1 defect(s) (OC-0154)

* fix(voice): 2 defect(s) (OC-0155, OC-0167)

Replace distributeRoomKey's per-call offer counter with an instance-level
sliding-window budget shared by every voice_e2ee_offer send path.

- OC-0155: back-to-back rotations (the second run immediately by
  drainPendingRotationOrArmTimer) each got a fresh pacing budget, so their
  combined sends could exceed the server's single per-second cap.
- OC-0167: handleAnnounceInner's drain-time offer send bypassed pacing
  entirely, letting a key holder joining a large ongoing call burst every
  queued announce's offer unpaced.

The shared budget is reset in clearState() since the server's limit is
scoped per (sender, channel).

* fix(client): 1 defect(s) (OC-0156)

createPresenceSender dropped a queued custom_status when a later plain
status change superseded the pending retry. The retry now carries the
last committed custom_status forward.

* fix(client): 2 defect(s) (OC-0160, OC-0163)

OC-0160: exempt the handshake frames (ready, auth_ok) from the ws message
size limit and run the guard after parsing. A 'ready' frame grows unbounded
with member/channel/DM counts and carries no seq, so dropping it left the
client on empty stores with no error and no recovery path.

OC-0163: bracket a bare IPv6 host when building the wss:// URL so the
authority parses, and collapse bracketed/bare IPv6 literals to the same
cert_store_key so one server is not pinned (and user-confirmed) twice.

* fix(voice): 1 defect(s) (OC-0162)

updatePttKey armed the Rust poller when a PTT key was bound mid-call but
never applied the gate. The poller only emits 'ptt-state' on a press/release
transition, so an idle key produced no event and the already-published mic
stayed hot until the user's first physical press+release. Mirror the join-time
gate computation in updatePttKey, guarded on being in a call, polling actually
being live, and the mic not already being gated.

* fix(client): 1 defect(s) (OC-0164)

* fix(plugin): 1 defect(s) (OC-0165)

scanPluginDirectory now skips a malformed plugin subdirectory and joins its
error instead of aborting the whole scan, and LoadAll logs-and-continues so
one bad plugin directory cannot disable every other plugin.

* fix(ws): 1 defect(s) (OC-0166)

Route PresenceSelfEvent onto the owner's normal-priority queue instead of
letting it fall through to the UserTargetedEvent high-priority case, so a
user's own presence frames all share one FIFO and cannot be delivered out
of order relative to the visible presence_update path.

* fix(db): 1 defect(s) (OC-0168)

* fix(client): 1 defect(s) (OC-0169)

* fix(client): 1 defect(s) (OC-0171)

addMessage appended a broadcast at the tail even when trailing optimistic
rows were still unreconciled, so a message that committed while our own
send was in flight ended up ordered behind the row confirmSend later
stamped with a higher server id/timestamp. Insert before the trailing
unreconciled run instead.

* fix(voice): 1 defect(s) (OC-0172)

* fix(client): 1 defect(s) (OC-0174)

* fix(ws): 1 defect(s) (OC-0175)

* fix(client): 1 defect(s) (OC-0177)

* fix(client): 1 defect(s) (OC-0178)

* fix(voice): 1 defect(s) (OC-0179)

Undeafening no longer sends a voice_mute{muted:false} the server will
refuse while a moderator-imposed mute stands, matching the localServerMuted
guard already present in onMuteToggle.

* fix(client): 1 defect(s) (OC-0182)

* fix(plugin): 1 defect(s) (OC-0183)

* fix(client): 1 defect(s) (OC-0184)

Treat a trailing underscore as an emphasis delimiter, not part of the URL,
when scanning for the end of an autolinked URL.

* fix(client): 1 defect(s) (OC-0185)

Reveal .msg-actions-bar on .message:focus-within, not only on hover, so
keyboard users can see the per-message action buttons they Tab into
instead of activating them at opacity: 0.

Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* fix(client): 1 defect(s) (OC-0186)

* fix(client): 1 defect(s) (OC-0187)

The Add Server modal validated addresses with its own narrower regex that
never gained IPv6 support when api.ts's validator did, so an IPv6 server
could be logged into but never saved as a profile. Extract the validator
into src/lib/hostValidation.ts and use it from both call sites.

* fix(client): 1 defect(s) (OC-0189)

DM sidebar rows dropped mention counts entirely and the header total
excluded muted conversations outright, so a direct mention in a muted DM
was invisible. Render a mention badge that outranks the plain unread
badge, and count a muted channel's mentionCount toward the header total.

* fix(client): 1 defect(s) (OC-0190)

* fix(client): 1 defect(s) (OC-0191)

* fix(client): 2 defect(s) (OC-0157, OC-0176)

* fix(client): 1 defect(s) (OC-0161)

confirmTotp answers 401 for a wrong enrollment code while the session is still valid; firing the global onUnauthorized sink signed the user out and deleted their stored credential. Opt that one call out via a skipUnauthorized flag on doFetch.

* fix(admin): 1 defect(s) (OC-0173)

* fix(identity): 1 defect(s) (OC-0180)

* fix(admin): archived channel PATCH skips voice eviction and fan-out (OC-0158)

handlePatchChannel commits the AdminUpdateChannel write, then re-reads the
channel to drive voice eviction and the visibility fan-out. When that
post-commit re-read failed, the handler returned early: the archive was
durable but connected clients were never told and voice members were never
evicted, leaving users talking in a channel that no longer exists for them.

Drive the post-commit work off the values already in hand rather than
abandoning it when the re-read fails.

Adds SetPatchChannelPostCommitHook so the test can land a cancellation in
that exact window deterministically instead of racing wall-clock timing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* fix(admin): role changes commit with no client ever notified (OC-0170)

broadcastRoles derived its context from the inbound *http.Request, so the
roles_update fan-out was tied to the request lifetime. A role create,
update, or delete could commit to the database and then broadcast nothing
once that request context was done, leaving every connected client on a
stale role list until the next full resync.

Decouple the fan-out from the request context so the broadcast follows the
commit rather than the caller.

Adds BroadcastRolesForTest to reach broadcastRoles from the external test
package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* fix(client): username rename stomps the profile card header (OC-0188)

The account profile card's header is a resolveDisplayName() slot, but the
username-rename save path wrote the raw username straight into it. A user
with a display name set would see the header switch from their display
name to their new username after a rename, disagreeing with every other
surface that renders the same identity.

Resolve the header through the same display-name path the initial render
uses, so a rename updates the username field without touching the header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* fix(client): settings overlay never focuses when mounted already-open (OC-0181)

mount() synced initial state — including the show() that calls
focusDialog() — before appending root to the container. .focus() on a
still-detached subtree is a silent no-op, so a caller that mounts while
uiStore.settingsOpen is already true (ConnectPage's lazy first-open path)
got a visible overlay whose focus trap never captured focus: keyboard
users landed outside the dialog with Tab escaping to the page behind it.

Attach root before syncing initial state so focusDialog() runs against a
connected subtree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* chore: satisfy the CI gates for this fix batch

The fix batch's own commits left three CI gates red. Nothing here changes
behaviour; every edit is a lint, type, or formatting correction to code
this batch introduced.

golangci-lint:
- OC-0153 and OC-0173 replaced the last two uses of admin's setupSanitizer,
  and OC-0151 the last use of api's sanitizer, leaving both package-level
  bluemonday vars unused. Remove them along with the now-unused imports,
  and reword the comments that named them so they still explain why the
  fixpoint sanitizer is the right one without pointing at deleted symbols.
- Modernize the new handshake-deadline test's loop to range-over-int.

tsc --noEmit:
- jsdom ships no types and @types/jsdom is not a dependency, so declare the
  surface the new admin-panel test uses, following src/types/jitsi-rnnoise.d.ts.
- Narrow the last-call lookup instead of indexing under
  noUncheckedIndexedAccess, with an explicit failure message.
- membersStore.setState replaces whole state, so the presence-sender mocks
  must supply typingUsers.

prettier: reformat the five files this batch touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* chore(ledger): record the 2026-08-19 hunt and its fixes

Adds the 41 findings confirmed by the 2026-08-19 hunt and marks the 40
fixed on this branch, each with its commit, the test that pins it, and
revertProof "pass".

"pass" means an independent check, not the fixing agent's self-report:
every commit had its source diff reverted against the working tree, its
own test re-run and required to FAIL, then the source restored and the
test required to PASS. Commits whose tests live inline in Rust
#[cfg(test)] blocks were proven the same way at hunk level, splicing the
pre-fix source onto the post-fix test module.

OC-0159 is recorded as a duplicate of OC-0152: the flow-reconnect and
flow-message lenses independently found the same unbounded handshake
write and proposed the same helper over the same call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

* test(e2e): make the voice-roster join fixture self-consistent

The voice-widget join test emitted a voice_state for user_id 4 claiming
username "newvoiceuser", but id 4 is "member2" in MOCK_MEMBERS_MULTI_ROLE.
A real server never sends a voice_state whose username disagrees with the
member record for that id, and the same file's VOICE_STATE_EVENT already
pairs id 1 with "testuser" correctly — this one event was the outlier.

The contradiction was invisible while the roster rendered the payload's
raw username. OC-0177 makes it resolve identity through membersStore so a
nickname shows the same in voice as everywhere else, at which point the
fixture's own inconsistency surfaced as a failure.

Send id 4's real username and assert on it. The test still covers what it
did before — a genuine join by a user not previously in voice, asserted by
name and by roster count.

Verified against the app unchanged: with the old fixture the spec fails
1/5 (matching CI), with this one it passes 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6gVN2JM5wrduhkNaFCxdK

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 16:32:07 +02:00
J3vbandClaude Fable 5 1fbedb404c fix(lint): delete dead ws broadcast cluster; demote phase-header comments
golangci-lint had been failing invisibly behind the earlier CI gate
failures. Default-build lint is now clean:

- Delete the unused pre-topic-limiter rate-limit constants, the unused
  bluemonday sanitizer, and the dead broadcast variants superseded by
  their Low/High counterparts (broadcastExclude,
  broadcastToDMParticipants(+Exclude), sendSequencedToUsers,
  PubSub.debugDump). Test references were comments only; updated to
  name the live variants.
- Separate 'Phase X Step Y' file headers from the package clause with a
  blank line so staticcheck ST1000 no longer reads them as malformed
  package comments (proper package docs exist in hub.go/manifest.go).
- Add .gitattributes normalizing line endings to LF on checkout —
  the Windows CI runner materialized CRLF, which made every
  prettier-formatted file fail the format gate.

Known remainder (pre-existing, out of P0 scope): golangci-lint with
-tags wazero reports 3 gosec + 2 staticcheck and -tags otel 1+1; CI
lints the default build. Tracked for the P1 plugin pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:55:22 +02:00
copilot-swe-agent[bot]andJ3vb 2dc9a060fc fix(review): address 6 reviewer findings from pullrequestreview-4064746778
- service/user.go: fix ChangePassword docstring (no old-password verification)
- service/user.go: RevokeSession now maps db.ErrNotFound→ErrNotFound and
  all other store errors→ErrInternal, preventing internal failures from
  masquerading as 404s
- plugin/loader.go: update scanPluginDirectory comment to reflect fail-fast
  behavior; fix Lstat comment wording
- db/queries/sqlite/events.sql: CAST COALESCE result to INTEGER so sqlc
  generates int64 instead of interface{}
- api/plugins_handler.go: log install error server-side and return sanitized
  structured JSON response instead of raw err.Error()
- .github/workflows/ci.yml: remove continue-on-error from tag build steps
  so tag boundary drift fails CI"

Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/6635420c-af26-4cc0-9397-5e5b37887437

Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com>
2026-04-06 22:03:35 +00:00
J3vb ae496082b3 feat(phase-bc): TOML plugin manifests + Solid vitest preset
Plugin TOML support (-tags wazero):
- manifest_toml.go: tryLoadPluginTOML using BurntSushi/toml v1.6.0
- manifest_nottoml.go: no-op stub for default build
- loader.go: prefers plugin.toml, falls back to plugin.json

Solid vitest preset:
- vitest.config.ts: add vite-plugin-solid, expand include to
  src/components/solid/**/*.test.tsx — Badge.test.tsx now runs
  automatically as part of npm test (112 files / 3188 tests)
2026-04-06 23:25:16 +02:00
Claude d320a8b587 fix(review): address 11 Copilot review findings on PR #1132
Clean sweep of every actionable item from the two Copilot review passes
on head 59ae4d8. Grouped by severity:

─── Crash / security (must-fix) ─────────────────────────────────────

1. main.go:140 — telemetryShutdown nil panic.
   telemetry.Init can return (nil, err) on the -tags otel skeleton
   path; the deferred closure would then call a nil function. Normalise
   to a no-op shutdown when Init errors so the defer is always safe.

2. api/upload_handler.go — permSvc nil deref.
   MountUploadRoutes + handleServeFile dereference permSvc on every
   authenticated file request. Add a fail-fast panic at mount time so
   the misconfiguration surfaces at wiring, not on the first 500.
   Update upload_handler_test.go to pass a real PermissionService built
   on the test DB (the existing tests were missing the argument entirely,
   which meant the package wouldn't compile — this fixes the real bug
   Copilot flagged).

3. ws/event_persister.go — NewEventPersister nil EventStore panic.
   run() dereferences p.store on every flush. Panic at constructor
   time instead so the crash happens once at startup rather than
   minutes later in a background goroutine.

4. plugin/host_ui.go — serve-time symlink check.
   rejectSymlinksUnder only runs at install time, so a symlink created
   post-install (accidental or malicious) would be followed by
   http.ServeFile and leak host files. Add an os.Lstat + ModeSymlink
   check + IsRegular check to AssetHandler on every request. Cheap
   relative to the file read and closes the TOCTOU window.

─── Correctness / observability (should-fix) ───────────────────────

5. ws/deps.go:77 — requirePerm hides misconfig as FORBIDDEN.
   Previously, nil database, nil perms, or a GetRoleForUser error all
   returned ErrCodeForbidden with the same message, making operator
   failures indistinguishable from legitimate permission denials.
   Split the branches: misconfig + DB error now return ErrCodeInternal
   with a server-side slog.Error so operators see the real problem;
   FORBIDDEN is reserved for the actual permission-bit check.

6. telemetry/metrics.go — ServiceCallDurationMs renamed to Sec.
   Field name said "Ms" but the instrument name was
   `service_call_duration_seconds` with unit "s". Renamed the field
   and updated all 8 service-layer callers so the struct field and
   metric semantics match.

7. ws/event_persister.go — flushEvy typo → flushEvery.
   Renamed the field and the one call site in run().

─── Comments out of sync with code ──────────────────────────────────

8. plugin/loader.go — Stat vs Lstat comment.
   The comment claimed "Stat (not Lstat)" but the code correctly uses
   os.Lstat to detect symlinks. Updated the comment to match the code;
   the code was already right.

9. telemetry/telemetry_otel.go — compile claim wrong.
   Comment said the file would fail to compile without the upstream
   OTel modules, but the skeleton deliberately avoids importing them
   and Init returns a runtime error instead. Updated the comment to
   reflect actual CI behaviour (the -tags otel build step passes
   today but doesn't exercise real telemetry).

─── Nit / polish ────────────────────────────────────────────────────

10. ws/event_pruner.go — startup delay magic constant.
    Hard-coded time.Minute made the "run shortly after startup"
    behaviour untestable (a test with a 100ms interval would still
    wait a full minute). Cap the startup delay by the interval:
    min(interval, time.Minute). Documented via a new `maxStartupDelay`
    constant.

11. ws/event_pruner_test.go — new file.
    Unit coverage for runPrune cutoff correctness, error swallowing,
    StartEventPruner nil-store short-circuit, ctx cancellation, and
    the interval-bounded startup delay from fix #10. Uses a fakeEventStore
    stub that records every prune call and signals the first one so
    tests don't sleep.

─── Verification ────────────────────────────────────────────────────

gofmt -l clean. No network access in sandbox so `go vet` and `go test`
could not run; the changes are local and surgical and every touched
file compiles in isolation against the existing signatures.

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 13:48:41 +00:00
Claude 7476c177b7 chore(phase-bc): pass 3 cleanup — perf, observability, hardening
Eight focused follow-ups from the medium-severity review bucket. All
in-sandbox tractable; no module changes, no new dependencies.

Performance
- Drop the defensive memcpy in EventPersister.Enqueue. The hub already
  passes a fresh slice from wrapWithSeq and the copy was happening under
  seqMu, serializing broadcast throughput. Documented the no-mutate
  contract on the call site.

Observability
- AppMetrics gains WSEventsPersistErrors counter; the persister run loop
  bumps both it and the existing WSEventsPersisted counter via cached
  metrics handle.
- Hub.persistEvent now extracts the real event type ("chat_message",
  "voice_join", ...) from the wrapped JSON envelope via a small
  no-allocation byte scan instead of recording the generic
  "broadcast"/"channel_broadcast" label.
- Added OTel spans + ServiceCallDurationMs histogram entries on one
  public method per remaining service: DMService.CreateDM,
  VoiceService.JoinChannel, InviteService.CreateInvite,
  ModerationService.BanUser, BlockService.BlockUser,
  UserService.UpdateProfile. Mirrors the existing pattern from
  MessageService.SendMessage.

Hardening
- plugin/loader now Lstat-walks each plugin directory and rejects any
  symlink, plus refuses an entrypoint that is itself a symlink. The
  asset handler's prefix check stays as defense in depth.
- ipAllowed (plugin HTTP capability) now rejects RFC6598 carrier-grade
  NAT (100.64.0.0/10), closing a gap in net.IP.IsPrivate which only
  covers RFC1918 + RFC4193.
- Registry.activateAll syncs Instance.Enabled := true after a successful
  activate so callers reading the in-memory flag see the live state.

Documentation
- defaultYAML now documents the new event_persistence, telemetry, and
  plugins config blocks with their defaults and one-line descriptions.
- PHASE_BC_LOCAL_TODO.md ticks off five items (defaultYAML docs ×2,
  remaining service spans, registry wiring already-fixed in Pass 2).

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 09:49:03 +00:00
Claude a2cb224323 feat: scaffold Phase B + C (events, telemetry, plugins, Solid.js)
Phase B Step 6 — Solid.js incremental migration
  - vite-plugin-solid + solid-js + @solidjs/testing-library in package.json
  - vite.config.ts compiles src/components/solid/** as Solid TSX
  - tsconfig.json gains jsx: preserve / jsxImportSource: solid-js
  - lib/solidAdapter.ts wraps existing custom Stores as Solid signals
  - lib/solidMount.ts adapts Solid render to {mount,destroy} contract
  - components/solid/Badge.tsx (proof-of-concept leaf)
  - components/solid/ChannelListItem.tsx (store-subscribed leaf)
  - components/solid/Badge.test.tsx pipeline smoke test
  - components/solid/README.md documents the migration recipe

Phase B Step 7 — Event persistence layer
  - SQLite + Postgres migrations for the events table
  - sqlc query files for both engines
  - EventStore interface + SQLite raw-SQL impl + MemStore impl + pg stubs
  - ws.EventPersister: async batched writer (queue / flush / drain / drop)
  - ws.StartEventPruner: background retention pruner
  - hub persists every replay-buffer push and exposes reconnect-tier counters
  - serve.handleReconnect: tiered replay (buffer -> DB -> full re-sync)
  - EventPersistenceConfig + main.go wiring
  - event_persister_test.go covers batching / drops / drain

Phase B Step 8 — OpenTelemetry skeleton
  - Server/telemetry package with public Provider/Tracer/Meter/Counter API
  - telemetry_default.go (no-op build) + telemetry_otel.go (build tag otel)
  - telemetry/metrics.go declares the AppMetrics bundle
  - HTTPMiddleware mounted in Chi router (pass-through in default build)
  - PrometheusHandler optionally mounted at /metrics
  - Spans on MessageService.SendMessage, PermissionService.HasChannelPerm,
    ChannelService.ListVisibleChannels
  - Reconnect-tier counter wired into the global meter
  - TelemetryConfig defaults

Phase C Step 9 — Wazero plugin runtime skeleton
  - Server/plugin package: manifest parser, loader, registry, host APIs
    (commands, storage, events, http, ui), errors
  - sandbox_default.go (no-op) + sandbox_wazero.go (build tag wazero)
  - SQLite + Postgres migrations for plugins + plugin_kv tables
  - PluginStore interface + impls + pg stubs
  - plugin/examples/hello manifest + README
  - plugin_test.go covers manifest, loader, capability gating
  - api/plugins_handler.go admin REST surface, mounted under admin group
  - PluginsConfig + main.go wiring (disabled by default)
  - Client: lib/pluginBridge.ts iframe + postMessage host
  - Client: components/solid/PluginContainer.tsx Solid host component

Verification
  - Default build (no -tags) is intended to compile cleanly with no new
    third-party dependencies. The sandbox lacked Go 1.25.0 so go build
    could not run; PHASE_BC_LOCAL_TODO.md enumerates the local follow-up
    work (npm install, go mod tidy, sqlc-generate, real otel/wazero
    wiring, remaining service spans, full Solid migration).
2026-04-06 09:00:47 +00:00