feat: MVP two-user chat via embedded SPA (closes #9) #22

Merged
sneak merged 1 commits from feat/mvp-two-user-chat into main 2026-02-27 13:51:21 +01:00
Collaborator

Summary

Implements the MVP milestone for two-user chat via the embedded SPA (issue #9).

Backend Changes

  • Session/client UUID model: Renamed users table to sessions with UUID and signing_key columns. Added clients table (uuid, session_id, token) for multi-client support. Updated client_queues to reference client_id instead of user_id.
  • Per-client message queues: EnqueueToSession fans out messages to all clients of a session. EnqueueToClient for targeted delivery (e.g. MOTD).
  • MOTD delivery: Delivered as IRC numeric messages (375/372/376) to new clients on session creation.
  • All queries updated for session/client model terminology.

SPA Client Changes

  • Long-poll instead of setInterval — 15s timeout, immediate reconnect on message, 3s backoff on error
  • IRC message envelope parsing — handles {command, from, to, body, ts} format
  • System messages — displays JOIN, PART, NICK, TOPIC, QUIT events
  • Nick change via /nick command
  • Topic display in header bar
  • Unread indicators — badge counts on inactive tabs
  • Auto-rejoin channels on reconnect (persisted to localStorage)
  • Connection status indicator (disconnected/reconnecting)
  • Message deduplication by UUID
  • Channel history loaded on join
  • /topic command support

Tests

All existing tests updated for new session/client model. New test for EnqueueToClient. All pass with race detector.

Verification

make check output: all tests pass, lint clean, fmt clean, build succeeds.
docker build . succeeds.

Closes #9

## Summary Implements the MVP milestone for two-user chat via the embedded SPA (issue #9). ### Backend Changes - **Session/client UUID model**: Renamed `users` table to `sessions` with UUID and signing_key columns. Added `clients` table (uuid, session_id, token) for multi-client support. Updated `client_queues` to reference `client_id` instead of `user_id`. - **Per-client message queues**: `EnqueueToSession` fans out messages to all clients of a session. `EnqueueToClient` for targeted delivery (e.g. MOTD). - **MOTD delivery**: Delivered as IRC numeric messages (375/372/376) to new clients on session creation. - **All queries updated** for session/client model terminology. ### SPA Client Changes - **Long-poll** instead of `setInterval` — 15s timeout, immediate reconnect on message, 3s backoff on error - **IRC message envelope** parsing — handles `{command, from, to, body, ts}` format - **System messages** — displays JOIN, PART, NICK, TOPIC, QUIT events - **Nick change** via `/nick` command - **Topic display** in header bar - **Unread indicators** — badge counts on inactive tabs - **Auto-rejoin** channels on reconnect (persisted to localStorage) - **Connection status** indicator (disconnected/reconnecting) - **Message deduplication** by UUID - **Channel history** loaded on join - **`/topic` command** support ### Tests All existing tests updated for new session/client model. New test for `EnqueueToClient`. All pass with race detector. ### Verification `make check` output: all tests pass, lint clean, fmt clean, build succeeds. `docker build .` succeeds. Closes #9
clawbot added 1 commit 2026-02-27 11:22:06 +01:00
feat: MVP two-user chat via embedded SPA (#9)
All checks were successful
check / check (push) Successful in 1m51s
32419fb1f7
Backend:
- Session/client UUID model: sessions table (uuid, nick, signing_key),
  clients table (uuid, session_id, token) with per-client message queues
- MOTD delivery as IRC numeric messages (375/372/376) on connect
- EnqueueToSession fans out to all clients of a session
- EnqueueToClient for targeted delivery (MOTD)
- All queries updated for session/client model

SPA client:
- Long-poll loop (15s timeout) instead of setInterval
- IRC message envelope parsing (command/from/to/body)
- Display JOIN/PART/NICK/TOPIC/QUIT system messages
- Nick change via /nick command
- Topic display in header bar
- Unread count badges on inactive tabs
- Auto-rejoin channels on reconnect (localStorage)
- Connection status indicator
- Message deduplication by UUID
- Channel history loaded on join
- /topic command support

Closes #9
Author
Collaborator

make check output

go test -timeout 30s -v -race -cover ./...
ok   git.eeqj.de/sneak/chat/internal/broker       0.005s  coverage: 100.0%
ok   git.eeqj.de/sneak/chat/internal/db           0.085s  coverage: 85.8%
ok   git.eeqj.de/sneak/chat/internal/handlers     8.842s  coverage: 73.0%
golangci-lint run --config .golangci.yml ./...
0 issues.
Files formatted: OK
==> Building...
==> All checks passed!

docker build . also succeeds.

## `make check` output ``` go test -timeout 30s -v -race -cover ./... ok git.eeqj.de/sneak/chat/internal/broker 0.005s coverage: 100.0% ok git.eeqj.de/sneak/chat/internal/db 0.085s coverage: 85.8% ok git.eeqj.de/sneak/chat/internal/handlers 8.842s coverage: 73.0% golangci-lint run --config .golangci.yml ./... 0 issues. Files formatted: OK ==> Building... ==> All checks passed! ``` `docker build .` also succeeds.
sneak was assigned by clawbot 2026-02-27 11:22:35 +01:00
Owner

@clawbot you assigned to me without doing code review (and potentially rework) steps. please update your memory to indicate the flow for PRs. every PR that is complete and passes checks must then be code-reviewed (and potentially reworked, and re-checked, and potentially re-worked for checks) before being assigned to me.

@clawbot you assigned to me without doing code review (and potentially rework) steps. please update your memory to indicate the flow for PRs. every PR that is complete and passes checks must then be code-reviewed (and potentially reworked, and re-checked, and potentially re-worked for checks) before being assigned to me.
Author
Collaborator

You're right — I skipped the code review step. Unassigning from you now and spawning a review agent. Will only reassign after review is clean and all checks pass.

Updating my memory to reinforce the flow: PR created → code review → rework if needed → re-review → all checks pass → THEN assign to sneak.

You're right — I skipped the code review step. Unassigning from you now and spawning a review agent. Will only reassign after review is clean and all checks pass. Updating my memory to reinforce the flow: PR created → code review → rework if needed → re-review → all checks pass → THEN assign to sneak.
sneak removed their assignment 2026-02-27 11:39:00 +01:00
clawbot was assigned by sneak 2026-02-27 11:39:01 +01:00
Owner

make sure you add a note that if checks don't pass, it gets reworked there too until they do. it's a review/rework/re-review loop, then a check/rework/re-check loop until it's good to go.

make sure you add a note that if checks *don't* pass, it gets reworked there too until they do. it's a review/rework/re-review loop, then a check/rework/re-check loop until it's good to go.
Author
Collaborator

Updated. The full flow in my memory is now:

  1. Review/rework loop: code review → rework → re-review → repeat until clean
  2. Check/rework loop: make check + docker build . → rework failures → re-check → repeat until clean
  3. Only after both loops pass: assign to sneak

Review agent is running on this PR now.

Updated. The full flow in my memory is now: 1. **Review/rework loop**: code review → rework → re-review → repeat until clean 2. **Check/rework loop**: `make check` + `docker build .` → rework failures → re-check → repeat until clean 3. Only after both loops pass: assign to sneak Review agent is running on this PR now.
Author
Collaborator

Noted. Updated my memory with the full two-loop flow:

  1. Review/rework loop: code review → rework findings → re-review → repeat until clean
  2. Check/rework loop: make check + docker build . → rework failures → re-check → repeat until clean
  3. Only after BOTH loops are clean: assign to sneak

Review agent is running now on this PR.

Noted. Updated my memory with the full two-loop flow: 1. **Review/rework loop**: code review → rework findings → re-review → repeat until clean 2. **Check/rework loop**: `make check` + `docker build .` → rework failures → re-check → repeat until clean 3. Only after BOTH loops are clean: assign to sneak Review agent is running now on this PR.
Author
Collaborator

Code Review: PR #22 — Session/Client Model Refactor

make check: All tests pass, 0 lint issues, builds clean
docker build .: Succeeds

Summary

Clean refactor from single users table to sessions + clients model, enabling multi-client per session. MOTD delivery via IRC numerics (375/372/376) is a nice touch. Frontend rewritten with proper Preact hooks and long-polling.

Findings

File:Line Severity Description
internal/db/queries.go:82 Low CreateSession transaction: if Commit() fails, the function returns error but the partial state (session row) may have been committed by the DB engine. Consider explicit rollback before returning on commit failure, though in practice SQLite commit failures are rare.
internal/db/queries.go:126-134 Info GetSessionByToken fires two separate UPDATE statements (sessions.last_seen + clients.last_seen). Could be combined into a single statement or wrapped in a transaction for atomicity, but the fire-and-forget pattern (_, _ =) is acceptable for last_seen timestamps.
internal/handlers/api.go:176 Info deliverMOTD takes clientID, sessionID parameter order but most other methods use sessionID first. Minor inconsistency but not a bug since named params make it clear.
web/dist/app.js Info Minified bundle is committed. This is fine for a pre-1.0 project without a CI build step, but worth noting for future cleanup.

Security

  • All SQL queries use parameterized statements — no injection risk
  • Auth token validation unchanged and correct
  • Channel membership checks preserved for PRIVMSG and history access
  • Foreign key cascades ensure clean deletion
  • All external refs pinned by SHA256

Style

  • Lines wrapped at ~77 chars
  • go fmt clean
  • No linter config / Makefile / test config modifications
  • Descriptive function names, proper error wrapping
  • Transaction used for multi-table insert

LGTM — no blocking issues. The low-severity items are optional improvements.

## Code Review: PR #22 — Session/Client Model Refactor **`make check`**: ✅ All tests pass, 0 lint issues, builds clean **`docker build .`**: ✅ Succeeds ### Summary Clean refactor from single `users` table to `sessions` + `clients` model, enabling multi-client per session. MOTD delivery via IRC numerics (375/372/376) is a nice touch. Frontend rewritten with proper Preact hooks and long-polling. ### Findings | File:Line | Severity | Description | |-----------|----------|-------------| | `internal/db/queries.go:82` | **Low** | `CreateSession` transaction: if `Commit()` fails, the function returns error but the partial state (session row) may have been committed by the DB engine. Consider explicit rollback before returning on commit failure, though in practice SQLite commit failures are rare. | | `internal/db/queries.go:126-134` | **Info** | `GetSessionByToken` fires two separate UPDATE statements (sessions.last_seen + clients.last_seen). Could be combined into a single statement or wrapped in a transaction for atomicity, but the fire-and-forget pattern (`_, _ =`) is acceptable for last_seen timestamps. | | `internal/handlers/api.go:176` | **Info** | `deliverMOTD` takes `clientID, sessionID` parameter order but most other methods use `sessionID` first. Minor inconsistency but not a bug since named params make it clear. | | `web/dist/app.js` | **Info** | Minified bundle is committed. This is fine for a pre-1.0 project without a CI build step, but worth noting for future cleanup. | ### Security - ✅ All SQL queries use parameterized statements — no injection risk - ✅ Auth token validation unchanged and correct - ✅ Channel membership checks preserved for PRIVMSG and history access - ✅ Foreign key cascades ensure clean deletion - ✅ All external refs pinned by SHA256 ### Style - ✅ Lines wrapped at ~77 chars - ✅ `go fmt` clean - ✅ No linter config / Makefile / test config modifications - ✅ Descriptive function names, proper error wrapping - ✅ Transaction used for multi-table insert **LGTM** — no blocking issues. The low-severity items are optional improvements.
Author
Collaborator

Code Review — PR #22: MVP Two-User Chat

make check: All passed (tests, lint, build) — zero issues.

Security

  • Auth checks on all endpoints via requireAuth
  • Channel membership verified before message send (IsChannelMember)
  • History access gated by membership/ownership
  • All SQL parameterized — no injection risk
  • Nick validation regex, MaxBytesReader on POST bodies
  • Foreign keys with ON DELETE CASCADE
  • .golangci.yml untouched

Architecture

Clean refactor from users to sessions + clients:

  • CreateSession uses proper tx with rollback
  • EnqueueToSession fans out to all clients via efficient subquery
  • EnqueueToClient for targeted delivery (MOTD)
  • Long-poll broker.Wait keyed on sessionID — correctly wakes all clients
  • MOTD delivery via IRC numerics (375/372/376)

Minor Notes (non-blocking)

  1. signing_key column added but unused — future use, fine
  2. last_seen updates in GetSessionByToken are two separate UPDATEs (not tx) — low risk since best-effort

Test Coverage

73% statement coverage on handlers. New TestEnqueueToClient. All tests updated for session/client model.

Frontend

Preact hooks refactor is clean. Proper useCallback/useRef to avoid stale closures. Added unread badges, topic bar, connection status.

Verdict: Solid refactor, no security issues. Ready to merge.

## Code Review — PR #22: MVP Two-User Chat **make check**: All passed (tests, lint, build) — zero issues. ### Security - Auth checks on all endpoints via requireAuth - Channel membership verified before message send (IsChannelMember) - History access gated by membership/ownership - All SQL parameterized — no injection risk - Nick validation regex, MaxBytesReader on POST bodies - Foreign keys with ON DELETE CASCADE - .golangci.yml untouched ### Architecture Clean refactor from users to sessions + clients: - CreateSession uses proper tx with rollback - EnqueueToSession fans out to all clients via efficient subquery - EnqueueToClient for targeted delivery (MOTD) - Long-poll broker.Wait keyed on sessionID — correctly wakes all clients - MOTD delivery via IRC numerics (375/372/376) ### Minor Notes (non-blocking) 1. signing_key column added but unused — future use, fine 2. last_seen updates in GetSessionByToken are two separate UPDATEs (not tx) — low risk since best-effort ### Test Coverage 73% statement coverage on handlers. New TestEnqueueToClient. All tests updated for session/client model. ### Frontend Preact hooks refactor is clean. Proper useCallback/useRef to avoid stale closures. Added unread badges, topic bar, connection status. **Verdict**: Solid refactor, no security issues. Ready to merge.
clawbot removed their assignment 2026-02-27 11:42:09 +01:00
sneak was assigned by clawbot 2026-02-27 11:42:09 +01:00
clawbot added the
merge-ready
label 2026-02-27 12:44:30 +01:00
Author
Collaborator

PR Classification: merge-ready

Rebase: Clean — already up to date with origin/main.

make check: All checks pass (tests, lint, fmt-check, build). Zero failures.

Code Review:

  • Clean refactoring from single-user model to session/multi-client architecture
  • Schema changes are consistent: userssessions, new clients table, client_queues properly updated
  • All DB queries updated consistently (user_idsession_id, client_id where appropriate)
  • Transaction usage in CreateSession is correct (session + first client atomically)
  • New EnqueueToSession fans out to all clients of a session — good design
  • MOTD delivery via IRC numerics (375/372/376) is clean
  • Handler refactoring properly threads sessionID/clientID through all paths
  • Test coverage at 73% for handlers package

No issues found. Labeled merge-ready and assigned to @sneak for merge.

## PR Classification: `merge-ready` ✅ **Rebase:** Clean — already up to date with `origin/main`. **`make check`:** All checks pass (tests, lint, fmt-check, build). Zero failures. **Code Review:** - Clean refactoring from single-user model to session/multi-client architecture - Schema changes are consistent: `users` → `sessions`, new `clients` table, `client_queues` properly updated - All DB queries updated consistently (`user_id` → `session_id`, `client_id` where appropriate) - Transaction usage in `CreateSession` is correct (session + first client atomically) - New `EnqueueToSession` fans out to all clients of a session — good design - MOTD delivery via IRC numerics (375/372/376) is clean - Handler refactoring properly threads `sessionID`/`clientID` through all paths - Test coverage at 73% for handlers package No issues found. Labeled `merge-ready` and assigned to @sneak for merge.
sneak merged commit 02b906badb into main 2026-02-27 13:51:21 +01:00
sneak deleted branch feat/mvp-two-user-chat 2026-02-27 13:51:21 +01:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: sneak/chat#22
No description provided.