Commit Graph
657 Commits
Author SHA1 Message Date
J3vb 5de92b9510 Merge pull request #1191 from J3vb/claude/blueprints-architectural-audit-k927qb
feat(client): connection-status store + no-silent-failure batch
2026-07-19 21:43:53 +02:00
Claude 45e51cca1c fix(client): review follow-ups for the connection-status batch
Fixes from an adversarial review of the previous commit:

- MainPage banner: sync the banner with the current store status at mount.
  The selector subscription baselines on the current value and only fires
  on change, so a MainPage mounted mid-outage (status already
  "reconnecting") would never show the banner — the whole retry cycle maps
  to the same 3-state value. The status→banner dispatch is extracted to
  ServerBanner.applyConnectionStatus and unit-tested.
- History-fetch failure is no longer silent when the channel already has
  rows (live broadcasts / optimistic sends): the inline error region only
  renders in an empty channel, so loadMessages now also raises a toast in
  that case.
- Composer disable reason distinguishes "Reconnecting…" from
  "Not connected" per the spec §3 table (it previously showed
  "Reconnecting…" while disconnected, contradicting the banner).
- The single-writer wiring is extracted to
  dispatcher.wireConnectionStatus(ws) and pinned by a test (it was
  previously an untestable main.ts module-scope line — deleting it would
  have failed zero tests).
- Docs honesty: messaging.md's transport-drop diagram arm now shows both
  codes (channel full → NETWORK, closed/not-open → OFFLINE) instead of
  claiming NETWORK for both; README §3's callout now explicitly lists the
  voice column ("frozen" during reconnect) as a remaining gap instead of
  implying the section is fully closed; the composer table documents both
  offline reasons.
- New pinning tests: SidebarArea passes ws to UserBar (the production-bug
  fix was previously unasserted), ServerBanner.showDisconnected,
  applyConnectionStatus mapping, ChannelController onRetryLoad /
  onRetry-resend / onDeleteDraft, composer reason per status, and the
  history-failure toast fallback.

Verified: tsc + full client unit suite (3234 tests) + oxlint/eslint +
prettier all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:36 +00:00
Claude fc5a94cb50 feat(client): connection-status store + no-silent-failure batch
Implements the next four gaps from the client UX spec (docs/architecture/ux).

Connection status as single source of truth (spec §3):
- main.ts registers the one writer: ws.onStateChange → toConnectionStatus
  (new 5→3 state mapper exported from ws.ts) → ui.store.connectionStatus.
- Consumers now subscribe to the store instead of ad-hoc ws wirings: the
  MainPage reconnect banner (which also gains a "Disconnected" state via
  ServerBanner.showDisconnected instead of going stale), ChannelController
  composer gating + per-click send guard, and the UserBar presence picker.
- Fixes a latent production bug: SidebarArea never passed ws to UserBar, so
  the status picker was permanently disabled and its presence_update path
  dead. It now gates on the store and receives the ws send path.
- The one-shot connected-overlay wiring stays on ws.onStateChange by design
  (it needs the exact internal transition); LiveKit voice reconnection stays
  independent ("retrying underneath").

Transport backpressure surfaced (spec §5):
- ws.ts sendRaw no longer drops local send failures silently: send() passes
  the envelope id, and failures notify a new onSendFailure(id, code)
  listener — channel full → NETWORK, closed/not-open → OFFLINE (deferred a
  microtask on the not-open path so the optimistic row registers first).
- The dispatcher fails the matching pending row via markSendFailed, exactly
  like a server error reply; id-less sends (heartbeat) and fire-and-forget
  sends (typing, presence) stay silent by design. MessageList renders the
  new NETWORK reason ("Connection problem — message not sent").

uploadFile honors global 401 handling (spec §5):
- api.uploadFile now calls onUnauthorized and throws ApiClientError(401)
  like every other REST call; main.ts sets the "Your session expired — sign
  in again." transient error so the connect page shows the reason.

History fetch loading/error states (messaging.md §1):
- messages.store gains per-channel historyLoadState (loading/error, absent
  = idle) with setChannelLoading/setChannelLoadError; setMessages and
  clearChannelMessages clear it.
- MessageController.loadMessages sets loading synchronously before the
  fetch and marks error inline instead of a toast; MessageList renders an
  in-region spinner placeholder or an inline error + Retry (onRetryLoad
  re-invokes loadMessages via ChannelController).

Also fixes two pre-existing eslint errors in api.ts (redundant assertions).

Docs: the corresponding gap callouts in docs/architecture/ux are updated
(README §3/§5, messaging.md §1/§3/§6, channels-members-dms.md block-gating
note no longer claims the composer lacks a read-only mode).

Verified: tsc + full client unit suite (3225 tests) + oxlint/eslint +
prettier all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:08:19 +00:00
J3vb 7d0c7a61e0 Merge pull request #1190 from J3vb/claude/blueprints-architectural-audit-k927qb
feat(client): optimistic message send + composer permission gating
2026-07-19 20:31:46 +02:00
J3vb f8e5e538b4 Merge pull request #1188 from J3vb/claude/repo-setup-tooling-axqgp1
chore: Claude Code project config, git hooks, and session setup
2026-07-19 20:27:38 +02:00
Claude e0ab0744ee feat(client): optimistic message send + composer permission gating
Implements the two highest-impact gaps from the client UX spec.

Optimistic send:
- messages.store gains addOptimisticMessage / markSendFailed /
  removeOptimistic, and confirmSend now stamps the real id + "sent" on
  the ack. addMessage reconciles the broadcast by real id (idempotent,
  replay-safe) with a defensive author match, so an echo never
  duplicates. Message gains status/correlationId/errorCode.
- ChannelController.performSend renders a pending row immediately and
  supports retry / delete-draft (retry preserves attachments).
- MessageList renders pending (dimmed) and failed (reason + Retry /
  Delete) rows; the hover action bar is limited to confirmed rows.
- Failures are precise: the server echoes the request id on error
  replies (buildErrorMsgWithID), so the dispatcher maps SLOW_MODE /
  FORBIDDEN / RATE_LIMITED / BAD_REQUEST to the exact row instead of
  dropping the code. An offline send is shown failed, not silently lost.

Composer permission + connection gating:
- The server computes an authoritative per-channel can_send in the ready
  payload (channelCanSend mirrors MessageService.checkSendPermission:
  READ|SEND, MANAGE_MESSAGES for announcement, admin bypass, channel
  overrides). channels.store carries it as Channel.canSend.
- MessageInput gains a disabled-with-reason mode; ChannelController
  derives the reason from can_send + channel type + connection status and
  disables the composer reactively (announcement read-only, no-permission,
  reconnecting) rather than accepting a click and failing. Older servers
  that omit can_send default permissive.

Docs: the corresponding "Current gap" callouts in docs/architecture/ux
are updated to reflect the implementation.

Verified: full server suite + client tsc + 3204 unit tests + lint + gofmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 17:47:39 +00:00
J3vb 3e5ad35a99 Merge pull request #1189 from J3vb/claude/blueprints-architectural-audit-k927qb
docs: client UX specification (target-state flows + per-view state maps)
2026-07-19 19:09:34 +02:00
Claude 0c86264b8c docs: add client UX specification (target-state flows + per-view states)
Adds docs/architecture/ux/ — a prescriptive (to-be) behavior spec for the
Tauri client, complementing the as-built module map in
docs/architecture/client.md. Covers every view and how it should react to
server events, permission state, and failure:

  - README.md            — view-state vocabulary, feedback primitives,
                           connection-status contract, the global
                           event->reaction map, and the error/permission
                           reaction matrix
  - connection-and-auth  — boot, profiles/health, login, TOTP,
                           register-by-invite, connected handshake,
                           reconnect, cert-TOFU trust
  - messaging            — composer permission/connection gating, optimistic
                           send lifecycle, edit/delete, reactions,
                           attachments, pins, search, read/unread, slow-mode
  - channels-members-dms — channel list/switch/categories, member list +
                           presence + typing, DM open/close, blocking
  - voice-and-e2ee       — join/leave, mute/deafen/camera/screenshare, PTT,
                           active-speaker, and the E2EE securing/secured
                           indicators
  - settings-and-admin   — settings tabs, profile/password/2FA/delete,
                           theming, inline admin (ban/kick/roles, channel
                           CRUD, invites), updater

Each flow carries dated "Current gap" callouts where today's code diverges
from the target (grounded in file:line references), so the set doubles as a
UX improvement backlog. Notable gaps captured: non-optimistic send with a
dead pending-send path, no composer read-only/permission gating (incl.
announcement channels), silently-dropped WS error codes, no E2EE "securing"
indicator, no updater download progress, client-local logout that never
revokes the server session, and a duplicated role store.

All 12 Mermaid diagrams validated; intra-repo links checked. Indexed from
docs/architecture/README.md and the top-level Docs Index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:58:43 +00:00
Claude fc0f86118c chore: add Claude Code project config, git hooks, and session setup
- CLAUDE.md (root + Server + client) documenting commands, generated-code
  rules, and the known-red client unit suite policy
- .claude/skills: ci-check (local CI mirror), protocol-change, db-change
  workflows for the sqlc/protocol codegen invariants CI enforces
- .githooks pre-commit/pre-push mirroring CI's fast gates (gofmt, go vet,
  oxlint, prettier, tsc, tag-variant builds, sqlc/protocol staleness),
  enabled via 'npm run hooks:install'
- .claude SessionStart hook installing client npm deps and warming the Go
  module cache so remote sessions can run tests/linters immediately
- .mcp.json with Playwright and Context7 MCP servers
- .gitignore: commit shared Claude config; keep local-only overrides ignored

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDcVJqGjHcJVLoV4x1HFjW
2026-07-19 16:54:13 +00:00
J3vb 719c910330 Merge pull request #1187 from J3vb/claude/blueprints-architectural-audit-k927qb
Adopt sqlc as the query layer (D2) — revive dead dbgen across all domains
2026-07-19 18:39:18 +02:00
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).

SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:

  - service.Store   (service/datastore.go, renamed from store/store.go)
  - ws.EventStore   (ws/eventstore.go)
  - plugin.PluginStore (plugin/pluginstore.go)

The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.

Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.

Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:33:58 +00:00
Claude 071426c0d8 feat(server,client): announcement channels (D1, closes A-2026-07-01)
Make 'announcement' a real channel type, resolving the contradiction where
it was documented and offered by the admin API but hard-rejected by the
migration-013 DB triggers.

Model: announcement channels are readable like text channels (same
READ_MESSAGES visibility), but posting is restricted to users with
MANAGE_MESSAGES — no new permission bit, migration, or client permission
plumbing needed.

Server:
- migrations/016: recreate the channel-type triggers to allow
  text/voice/announcement/dm.
- service/message.go: checkSendPermission now takes the channel type and
  rejects posts to announcement channels from users lacking MANAGE_MESSAGES
  (SendMessage + CanPost paths). Added a service test.
- Unread counts: ready-payload builder (ws/serve.go) and
  GetChannelUnreadCounts (db) now include announcement channels alongside
  text, so they track unread/last-message like text channels.

Client:
- ChannelSidebar renders announcement channels with a megaphone icon
  (added to the icon set) instead of the '#' text prefix; they otherwise
  behave like text channels (already typed in ChannelType).

Specs + trackers (api.md, protocol.md, schema.md incl. migration 016,
architecture/data-model.md, audit A-2026-07-01, decisions D1) updated.

Verified: go build ./...; go test ./service ./db ./ws ./api ./admin;
sqlc-verify; client tsc + oxlint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:00:18 +00:00
Claude f66d6c5274 refactor(server/db): delegate messages + reactions to dbgen; finalize D2
messages/reactions: CreateMessage, GetMessage (messageFromGen mapper),
EditMessage (EditMessageContent), DeleteMessage (SoftDeleteMessage),
AddReaction, RemoveReaction, GetReactions (GetReactionCounts),
SetMessagePinned, UpdateReadState. Retired the obsolete scanMessage.

Kept raw by design (no clean sqlc mapping): FTS search, cursor-paginated
GetMessages/GetMessagesForAPI/GetPinnedMessages, getReactionsBatch,
GetChannelUnreadCounts, GetLatestMessageID (interface{} MAX result).

D2 status: 97 db.DB methods now delegate to dbgen across every domain;
43 raw d.sqlDB calls remain by design (db.go passthroughs, migrate.go,
variable-length IN(), FTS, multi-statement transactions, PRAGMA/VACUUM).
sqlc is no longer dead code — audit A-2026-07-05 resolved. Full rationale
+ the kept-raw list in docs/plans/sqlc-adoption.md.

Verified: go build ./...; go test ./db ./service ./ws ./auth; sqlc-verify;
gofmt + go vet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:48:39 +00:00
Claude c7e6702c8c refactor(server/db): delegate voice, dm, channels, admin to dbgen (D2)
voice: all reads (GetVoiceState, GetChannelVoiceStates, GetAllVoiceStates)
and writes (join/leave/mute/deafen/camera/screenshare/clear, capacity +
camera-limit atomic guards); CountChannelVoiceUsers stays raw (no query).
dm: OpenDM, CloseDM, IsDMParticipant, GetDMParticipantIDs; GetOrCreateDMChannel
(serializable tx), GetUserDMChannels (aggregate), GetDMRecipient stay raw.
channels: List/Get (shared channelFromFields mapper), Create/Update/Delete,
slow-mode/max-users setters, permission overrides get/list-for-role/upsert/
delete; ListChannelRoleOverrides + GetChannelTypes (variable IN) stay raw.
admin: UserCount, GetServerStats counts (PRAGMA stays raw), ListAllUsers,
UpdateUserRole, ForceLogoutUser, GetUserSessions, AdminUpdateChannel,
AdminDeleteChannel, LogAudit, GetAuditLog, GetSetting, SetSetting,
GetAllSettings, CountUsersWithoutTOTP; AdminCreateChannel + Backup stay raw.

Added b2i64 and strToNullPtr mapper helpers; retired the obsolete
scanChannel/nullableString. Behavior and public signatures unchanged.

Verified: go build ./...; go test ./db ./service ./permissions ./admin;
sqlc-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:43:46 +00:00
Claude d769b8bdb6 refactor(server/db): delegate invites + attachments to dbgen (D2)
invites: CreateInvite, GetInvite, UseInviteAtomic, RevokeInvite,
ListInvites. attachments: CreateAttachment, GetAttachmentByID,
GetAttachmentWithChannel, DeleteOrphanedAttachments. Added ptrI64toI /
ptrItoI64 helpers for the *int64<->*int narrowing (invite max_uses,
attachment width/height). LinkAttachmentsToMessage and
GetAttachmentsByMessageIDs keep raw SQL (variable-length IN() lists sqlc
can't express). Behavior and signatures unchanged.

Verified: go build ./...; go test ./db (Invite, Attachment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:36:59 +00:00
Claude e46bd0e015 refactor(server/db): delegate users, sessions, profile to dbgen (D2)
Convert the auth_queries.go user + session reads/writes and
profile_queries.go to the sqlc-generated layer, adding shared
userFromGen/sessionFromGen mappers (db/mappers.go) for the
int64/*string -> int/bool/string domain-model narrowing.

Delegated: GetUserByID, GetUserByUsername, UpdateUserStatus,
UpdateUserTOTPSecret, ResetAllUserStatuses, BanUser, UnbanUser,
ListMembers, CreateSession (EvictOldestSessions + InsertSession),
GetSessionByTokenHash, GetSessionWithBanStatus, DeleteSession,
DeleteOtherSessions, DeleteExpiredSessions, TouchSession,
UpdateUserProfile, UpdateUserPassword, ListUserSessions,
DeleteSessionByID (query changed to :execresult so the RowsAffected
ErrNotFound check is preserved). Behavior and public signatures unchanged.

Verified: go build ./...; go test ./db ./service ./auth; sqlc-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:34:27 +00:00
Claude 44323373e3 refactor(server/db): adopt sqlc as the query layer — phase 1 (D2)
Wire the sqlc-generated dbgen package into db.DB so it stops being dead
code (audit A-2026-07-05) and becomes the real, CI-verified query layer.

db.DB now holds a *dbgen.Queries (initialized in Open via dbgen.New).
Query method bodies delegate to it; sqlc owns the SQL text and parameter
binding (make sqlc-verify), while db keeps its stable public API and
domain model types so no caller in api/admin/ws/service changes. The
migration is incremental — a method either delegates to d.q.* or still
runs raw SQL — so both layers are correct during the transition.

Converted domains (now load-bearing through sqlc):
- blocks: BlockUser, UnblockUser, IsBlocked, IsEitherBlocked,
  ListBlockedUsers (added the query to blocks.sql + regenerated).
  Empty ListBlockedUsers now returns []int64{} instead of nil, matching
  the MemStore backend — a latent inconsistency fixed, not a regression.
- lockouts: UpsertLockout, LoadActiveLockouts, CleanupExpiredLockouts,
  DeleteLockout (RFC3339 time formatting/parsing kept in the wrappers).
- roles: GetRoleByID, ListRoles, GetRoleForUser via a shared roleFromGen
  mapper (int64 position/is_default -> int/bool). GetUserWithRole stays
  raw for now.

Remaining domains stay on raw SQL and are tracked in
docs/plans/sqlc-adoption.md; store/ event+plugin SQL is intentionally
excluded (that layer is removed in D3). Decisions doc + audit closure
updated (A-2026-07-05 -> in progress).

Verified: go build ./...; go test -race ./db ./service ./auth ./ws (api
green non-race, race run matches CI's -timeout 20m); make sqlc-verify and
protocol-verify pass with the regenerated output committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:01:54 +00:00
J3vb 752c8f2bcc Merge pull request #1186 from J3vb/claude/blueprints-architectural-audit-k927qb
Client HTTP TOFU proxy: cert-pin the REST path (closes A-2026-07-02)
2026-07-19 16:28:16 +02:00
Claude dab4d73e09 feat(client): TOFU HTTP proxy for REST — close audit A-2026-07-02 (D5)
The REST path previously used tauri-plugin-http with
danger.acceptInvalidCerts, so it accepted ANY certificate while the WS
and LiveKit paths were TOFU-pinned in Rust — and the bearer token rides
every REST request. This routes REST through a new Rust loopback
TCP->TLS proxy that pins the server certificate to the same
trust-on-first-use fingerprint as the WS proxy.

Rust (src-tauri):
- New http_proxy.rs: per-host loopback tunnels (HttpProxyState map);
  per-connection TOFU via CaptureVerifier + tofu_check, sharing
  ws_proxy's cert store (cert_store_key) and emitting the same
  cert-tofu events (first-use banner / mismatch modal). First request's
  Host is rewritten and Connection: close injected so one request rides
  each connection. Mismatch returns a clean 502 to the loopback fetch.
- Register HttpProxyState + start_http_proxy/stop_http_proxy in lib.rs.
- Drop the dangerous-settings feature from tauri-plugin-http.

TypeScript (src):
- New lib/httpProxy.ts: ensureHttpProxy(host) (per-host cache +
  concurrent-start dedup) / stopHttpProxy(host).
- api.ts, profiles.ts (health), attachments.ts (image + download) resolve
  server URLs to http://127.0.0.1:{port}; remove the allowSelfSigned
  config field and every acceptInvalidCerts block. External hosts (CDNs,
  OG previews, YouTube) keep normal TLS validation.
- main.ts constructs the API client without allowSelfSigned.
- capabilities/default.json: allow http://127.0.0.1:* fetch scope.

Tests:
- New tests/unit/http-proxy.test.ts (cache, dedup, stop/restart).
- api.test.ts and attachments-render.test.ts: mock httpProxy, replace the
  acceptInvalidCerts assertions with proxy-origin assertions.

Verified: tsc --noEmit clean; new + affected vitest suites green
(176 tests); the http_proxy pure logic (host validation, header rewrite)
passes as standalone Rust unit tests; oxlint/eslint counts unchanged
from HEAD; prettier clean. The full Tauri build (cargo) requires GUI
system libs not present in this environment and runs on CI/real runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 14:26:25 +00:00
J3vb 4bb388c738 Merge pull request #1185 from J3vb/claude/blueprints-architectural-audit-k927qb
Audit follow-through: decisions, quick wins, protocol codegen, spec refresh, Solid removal
2026-07-19 16:07:38 +02:00
Claude 8c768bf4c6 docs: add design note for the client HTTP TOFU proxy (D5)
Pins the implementation approach for closing audit finding A-2026-07-02:
a loopback TCP-to-TLS tunnel reusing the livekit_proxy pattern and the
shared per-host fingerprint store, with ws_proxy-equivalent TOFU
first-trust/mismatch flows (required because the first TLS contact with
a server is the login HTTP request), per-host tunnel lifecycle for
multi-profile health polling, and removal of the acceptInvalidCerts
path plus the dangerous-settings feature flag as the ratchet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:58:58 +00:00
Claude bf508c9e6b chore(client): remove abandoned SolidJS beachhead (D6)
The Solid.js migration was abandoned (per CHANGELOG); the 154-LOC
beachhead and its scaffolding remained in-tree, leaving two UI paradigms
for contributors. Removed:

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:57:28 +00:00
Claude ca3b0fee3e docs: refresh api.md, protocol.md, schema.md against current code (D7)
One-PR spec refresh per decision D7, using the 2026-07-19 audit's
conformance matrix as the checklist:

api.md
- Remove the deleted version field from /health and /api/v1/info
  (anti-fingerprinting C-2) and document the removal.
- Document the profile surface (PATCH /users/me, PUT /users/me/password,
  GET/DELETE /users/me/sessions), the user-blocks surface
  (GET/PUT/DELETE /api/v1/blocks), and the plugin admin surface
  (/api/v1/admin/plugins).
- Correct GET /api/v1/files/{id}: auth is required and caching is
  'private, no-cache' (was documented as public/immutable, unauthenticated).
- Add search (30/min) and upload (10/min) rate limits; note announcement
  channel type as planned-only.

protocol.md
- Document auth_ok.replay_source and the 3-tier reconnect replay
  (ring buffer -> events table -> full resync) with the visibility
  watermark.
- Add the Voice End-to-End Encryption section (voice_e2ee_announce/offer
  in both directions, key-holder semantics, rate limits) and
  voice_token.is_key_holder.
- Document user_update; mark voice_speakers and member_leave as defined
  but not currently emitted; extend voice_config fields.
- Update the reference tables (19 client->server / 30 server->client)
  and point them at protocol-schema.json as the generated inventory.

schema.md
- Correct the migration history to the real 001-015 numbering.
- Document the previously missing tables: login_attempts, settings,
  emoji, sounds (dead schema), rate_lockouts, user_blocks, events,
  plugins, plugin_kv; add attachments.uploader_id and new indexes.
- Fix the channel-type list to text/voice/dm (013 triggers) and correct
  the permission formula to (base & ~deny) | allow to match
  permissions.EffectivePerms.
- Note the sqlc/dbgen layer and link the architecture data-model doc.

Also correct the audit_log_v6 claim (transient rename inside migration
003, not a coexisting table) in the audit and data-model blueprint, and
update the audit/decisions trackers (A-2026-07-03 closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:51:44 +00:00
Claude 2e7a80171b feat(server,client): protocol codegen + audit quick-wins batch
Protocol codegen (decision D4, audit A-2026-07-08):
- Add docs/protocol-schema.json as the real single source of truth for
  WS message-type constants, making the long-standing 'generated from'
  comment in both constant files true.
- Add Server/scripts/genprotocol, a generator emitting both
  Server/ws/message_types.go and Client .../lib/protocolTypes.ts
  (constants byte-for-byte value-identical to before; only headers,
  ordering alignment, and provenance comments changed).
- Add make protocol-generate / protocol-verify and wire protocol-verify
  into CI next to sqlc-verify.

Quick wins (decision D8):
- admin: log LogAudit write failures in the backup handlers instead of
  discarding them (prior audit #10).
- api: fix self-contradictory upload Cache-Control to 'private,
  no-cache' per remediation plan W3-4; drop the now-unused
  fileCacheMaxAgeSeconds constant; update test.
- ws: route the hub settings cache through db.GetSetting instead of
  inline SQL.
- ws: fix a latent data race — main.go wires SetEventPersister and
  SetEventStore after NewRouter has already started the hub Run loop,
  which reads those fields on the broadcast/replay paths. They (and
  pluginSink, which one test sets post-Run) are now atomic pointers;
  the remaining pre-Run-only setters reject late calls with an error
  log instead of racing silently.

Update the audit closure table and decisions doc statuses accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:32:58 +00:00
Claude fde254cea0 docs: record maintainer decisions for the 2026-07-19 audit
Add docs/plans/audit-2026-07-19-decisions.md capturing the eight decisions
taken on the audit's open questions (announcement channels: implement;
data layer: adopt sqlc + remove store/; protocol constants: real codegen;
client HTTP TOFU pinning: next security work; Solid beachhead: delete;
specs: one refresh PR after codegen) with sequencing, and mark the greenlit
items (protocol codegen + quick-wins batch).

Update the closure table in docs/audit-2026-07-19.md from OPEN to DECIDED
for the seven findings these decisions cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 12:51:06 +00:00
J3vb 145132d203 Merge pull request #1184 from J3vb/claude/blueprints-architectural-audit-k927qb
docs: architecture blueprints + 2026-07-19 spec-conformance audit
2026-07-19 14:43:02 +02:00
Claude 522f996fd7 chore: retrigger CI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 12:19:45 +00:00
Claude cf8e5aae24 docs: add architecture blueprints and 2026-07-19 audit
Add docs/architecture/ — a curated blueprint set with 10 Mermaid diagrams
covering system context, deployment topology, server package map, REST
request lifecycle, WebSocket auth/replay/dispatch, the full data model
(migrations 001-015), voice/E2EE flow, and the client module map.

Add docs/audit-2026-07-19.md — successor to audit-2026-04-07.md:
re-verifies carried-over findings, catalogues spec-vs-code drift in
api.md/protocol.md/schema.md (incl. the announcement channel-type
contradiction and the undocumented voice-E2EE protocol surface), records
server/client/CI findings with file:line evidence, and closes with a
12-item prioritized improvement backlog.

Link both from the README docs index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 12:18:20 +00:00
J3vb 7f3e5d18d8 Merge pull request #1183 from J3vb/claude/repo-issues-features-w6r87a
Fix open issues: window restore, Wayland, password reveal, screenshare volume/FPS, dual-homed voice, private channels
2026-07-19 13:30:20 +02:00
Claude 8c590c0b6d fix(server): harden private-channel visibility propagation
Three review findings on the #93 feature:

- RefreshChannelVisibility targeted clients by their connect-time role
  snapshot; a user whose role changed mid-session was evaluated against the
  stale role. Resolve the current role from the DB per client (fail closed).
- Visibility updates are targeted, unsequenced messages, so a client that
  disconnected before an override change and later resumed via replay never
  converged (stale sidebar until a fresh connect). Track a visibility-change
  sequence watermark and force resumes from at/before it onto the
  full-ready path.
- The admin SPA interpolated channel/user names into single-quoted JS
  strings inside onclick attributes with HTML-escaping only; a name
  containing a quote broke out of the string literal (XSS in the admin
  panel, reachable by any user allowed to create channels). Add a jsq()
  helper (JS-escape then HTML-escape) and use it for every onclick name
  interpolation.

Follow-up to #93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:15 +00:00
Claude ccb8c54dd2 fix(server): regenerate empty livekit.yaml instead of treating it as user-managed
A zero-byte or whitespace-only livekit.yaml (truncated write, touch(1)
placeholder) has no auto-generated marker and was permanently treated as a
user-managed config, wedging LiveKit startup with an empty config file.

Follow-up to #111.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:14 +00:00
Claude d576f06aa1 fix(client): make source-quality screen share FPS take effect at capture
createLocalScreenTracks injects a default 1080p30 resolution when none is
set and mutates the passed options object, so (a) a 'source' share was
captured at 30 fps regardless of the FPS setting, with only a best-effort
applyConstraints afterwards, and (b) the shared 'source' preset object was
permanently mutated after the first share. Capture options are now always
copies; 'source' with an explicit 60/120 override passes a zero-size
resolution sentinel (uncapped in livekit's constraint translation) with the
frame rate in the raw video constraints, so the fps applies at
getDisplayMedia time.

Follow-up to #115.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:01 +00:00
Claude 1201992ab0 fix(client): skip window-state save while minimized
A minimized window reports placeholder coordinates (-32000 on Windows); the
move event fired by minimize was persisting them, so quitting while
minimized silently discarded the remembered position (the new off-screen
validation then falls back to centered). Skip the save while minimized so
the last real geometry survives.

Follow-up to #124.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:00 +00:00
Claude 3cb8dc34d5 chore(server): drop unnecessary int64 conversion in perms test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:03:59 +00:00
Claude 5ac407a71a test(client): account for the screen share FPS select in settings tests
Follow-up to the screen share FPS setting (#115).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:11 +00:00
Claude 9e6ff47194 feat(server,admin): private channels via per-role permission overrides
The read side of channel visibility was already complete — channel_overrides
rows deny ReadMessages and every list/focus/send/voice path enforces them —
but nothing could write those rows. Add the missing write path and UI:

- db: UpsertChannelOverride / DeleteChannelOverride / ListChannelRoleOverrides
  (roles LEFT JOIN overrides so the UI gets everything in one call)
- admin API: GET/PUT/DELETE /admin/api/channels/{id}/permissions[/{roleId}]
  with unknown permission bits masked via the new permissions.AllPerms,
  audit logging, and immediate permission-cache invalidation
- ws: Hub.RefreshChannelVisibility sends targeted channel_create /
  channel_delete to connected clients after an override change, unsubscribes
  hidden clients from the channel topic, and clears their focus. Sent outside
  the sequenced replay path on purpose: a replayed channel_delete would be
  filtered by the post-change allowed-channel set, inverting its audience.
- admin panel: per-channel Access modal (lock icon) with per-role
  "Can access" checkboxes; unchecking writes deny = ReadMessages|ConnectVoice

Known limits (follow-ups): users offline during a revoke keep a stale
sidebar entry until their next fresh connect (server still denies access),
and users already in a voice channel are not kicked when it goes private.

Closes #93

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:11 +00:00
Claude b9180bdbeb feat(server): support dual-homed voice hosts and user-managed livekit.yaml
Servers reachable via both a LAN IP and a public IP could only serve voice
on one of them: config.yaml accepts a single voice.node_ip and OwnCord
regenerates data/livekit.yaml on every start, discarding manual edits.
LiveKit has no multi-IP list, but it does support advertising internal host
candidates alongside the external mapping.

- New voice.advertise_internal_ip (OWNCORD_VOICE_ADVERTISE_INTERNAL_IP):
  emits rtc.advertise_internal_ip: true so LAN clients get a reachable
  candidate while remote clients keep using node_ip.
- livekit.yaml escape hatch: if the file exists without the auto-generated
  marker header, OwnCord leaves it untouched, giving operators access to
  every LiveKit option (ips.includes, interfaces, stun_servers, ...). The
  generated header documents how to take ownership.

Closes #111

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:11 +00:00
Claude 6f0a04113f feat(client): screen share FPS setting with 60 and 120 fps options
Screen share frame rate was hardcoded per quality (5/15/30). Add a
"Screen Share FPS" setting (30 default / 60 / 120) next to Stream Quality:

- 30 keeps the existing per-quality caps unchanged
- 60/120 override the capture constraints and publish maxFramerate for all
  qualities, with bitrate scaled 1.5x/2x to keep the image sharp
- "source" quality (no fixed resolution) applies the fps to the live
  capture track via applyConstraints, best-effort

Actual delivered fps still depends on what the capture source and display
can sustain.

Closes #115

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:11 +00:00
Claude 85f05e999c fix(client): make the screenshare volume slider actually change volume
Three defects made the screenshare tile's volume slider ineffective:

- The 0-200 slider mapped to element volume /200 clamped to [0,1], while
  the element attached at 1.0 — dragging the upper half did nothing. The
  screenshare slider is now 0-100 with 100 = 1.0 (HTMLAudioElement.volume
  cannot exceed 1.0; mic tiles keep the 0-200 boost range via LiveKit's
  GainNode-backed setVolume).
- Setting a volume before the screenshare audio track attached was silently
  dropped. The per-user volume now persists independently of the element
  map and is applied on attach.
- Changing the master output volume overwrote per-user screenshare volumes
  with just the master multiplier; they now scale together.

The slider and mute button also initialize from persisted state when a tile
is rebuilt, and unmuting via the button re-applies the restored volume.

Fixes #121

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:10 +00:00
Claude 3eb990165d fix(client): hide native WebView2 password reveal icon
WebView2/Edge renders its own password-reveal eye inside password inputs,
stacking with the app's custom toggle on the login form. Hide the native
::-ms-reveal / ::-ms-clear controls globally.

Fixes #123

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:10 +00:00
Claude 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
2026-07-19 10:50:10 +00:00
Claude 54430eed45 fix(client): validate saved window position against connected monitors
Restoring a stale window position (e.g. from a disconnected monitor) placed
the window off-screen with no way to see it. Before applying the saved
position, check that the rect is reachable on some monitor reported by
availableMonitors(): at least 100px of horizontal overlap and a grabbable
title bar row. If not, keep the default centered placement. Also reject
non-finite or non-positive saved dimensions. If monitors cannot be queried,
restore proceeds unchanged.

Fixes #124

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:10 +00:00
J3vbandClaude Fable 5 9a0404cb6f docs: refresh README badges, platform matrix, and branch policy for alpha.2
- Replace static badge block with CI, release, status, Go, Tauri,
  platforms, and license badges (release badge tracks the public
  OwnCord-releases repo)
- Update platform support table for v1.1.0-alpha.2 assets (Linux x64
  server, Linux x64/ARM64 client)
- Stamp build examples with the current version
- Point contributing docs at main now that dev is pruned

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:29:04 +02:00
J3vbandClaude Fable 5 66cdd2ad9f chore(release): stamp client version 1.1.0-alpha.2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:50:37 +02:00
J3vb 8e62f38d25 Merge pull request #1182 from J3vb/fix/security-hardening-review
P1: security-hardening remediation (Postgres deletion + W1-1..W2-7)
2026-07-19 10:49:31 +02:00
J3vbandClaude Fable 5 963bea9a64 test(service): update moderation callsites for context parameter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:18:40 +02:00
J3vbandClaude Fable 5 bc7d65ab29 fix(service): thread request context through BanUser/UnbanUser
contextcheck (CI lint) flagged the admin handler calling BanUser without
the request context — the service opened its telemetry span from
context.Background(), detaching the ban from its request trace. Both
moderation entrypoints now take ctx; the span joins the caller's trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:18:40 +02:00
J3vbandClaude Fable 5 4c2fecbf02 test(ws): update emit-test hub literal for the single event channel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:33:33 +02:00
J3vbandClaude Fable 5 0baccb58ee fix(ws): process register/unregister on one ordered channel
Register and Unregister travelled on two separate channels, and Run's
select picks randomly when both are ready: a fast connect/disconnect could
process the unregister first (a silent no-op for a not-yet-known client)
and then the register — admitting an already-dead connection as a ghost
client that held presence and swallowed broadcasts until the stale sweep
reaped it minutes later. One tagged event channel preserves each
connection's Register→Unregister submission order, making the inversion
structurally impossible. Found via TestHub_ConcurrentRegisterUnregister
failing the P1 gate under -race on windows-latest (2 ghosts after churn);
that test now settles in milliseconds instead of polling out its deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:33:33 +02:00
J3vbandClaude Fable 5 2eec831d6a refactor(updater): export FileSHA256 and reuse it for the update snapshot (W3-2)
admin's fileSHA256 duplicated VerifyChecksum's hashing body. One exported
helper now serves both the TOCTOU snapshot in handleApplyUpdate and
VerifyChecksum itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:04:28 +02:00