fix: replay channel state on SPA reconnect (#61)
All checks were successful
check / check (push) Successful in 4s

## Summary

When closing and reopening the SPA, channel tabs were not restored because the client relied on localStorage to remember joined channels and re-sent JOIN commands on reconnect. This was fragile and caused spurious JOIN broadcasts to other channel members.

## Changes

### Server (`internal/handlers/api.go`, `internal/handlers/auth.go`)

- **`replayChannelState()`** — new method that enqueues synthetic JOIN messages plus join-numerics (332 TOPIC, 353 NAMES, 366 ENDOFNAMES) for every channel the session belongs to, targeted only at the specified client (no broadcast to other users).
- **`HandleState`** — accepts `?replay=1` query parameter to trigger channel state replay when the SPA reconnects.
- **`handleLogin`** — also calls `replayChannelState` after password-based login, since `LoginUser` creates a new client for an existing session.

### SPA (`web/src/app.jsx`, `web/dist/app.js`)

- On resume, calls `/state?replay=1` instead of `/state` so the server enqueues channel state into the message queue.
- `processMessage` now creates channel tabs when receiving a JOIN where `msg.from` matches the current nick (handles both live joins and replayed joins on reconnect).
- `onLogin` no longer re-sends JOIN commands for saved channels on resume — the server handles it via the replay mechanism, avoiding spurious JOIN broadcasts.

## How It Works

1. SPA loads, finds saved token in localStorage
2. Calls `GET /api/v1/state?replay=1` — server validates token and enqueues synthetic JOIN + TOPIC + NAMES for all session channels into the client's queue
3. `onLogin(nick, true)` sets `loggedIn = true` and requests MOTD (no re-JOIN needed)
4. Poll loop starts, picks up replayed channel messages
5. `processMessage` handles the JOIN messages, creating tabs and refreshing members/topics naturally

closes #60

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #61
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #61.
This commit is contained in:
2026-03-10 11:08:13 +01:00
committed by Jeffrey Paul
parent 687c958bd1
commit f287fdf6d1
10 changed files with 152 additions and 513 deletions

View File

@@ -1,6 +1,6 @@
---
title: Repository Policies
last_modified: 2026-02-22
last_modified: 2026-03-09
---
This document covers repository structure, tooling, and workflow standards. Code
@@ -98,6 +98,13 @@ style conventions are in separate documents:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
a new repo.
- **No build artifacts in version control.** Code-derived data (compiled
bundles, minified output, generated assets) must never be committed to the
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
should generate these at build time. Notable exception: Go protobuf generated
files (`.pb.go`) ARE committed because repos need to work with `go get`, which
downloads code but does not execute code generation.
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
- Never force-push to `main`.
@@ -144,8 +151,14 @@ style conventions are in separate documents:
- Use SemVer.
- Database migrations live in `internal/db/migrations/` and must be embedded in
the binary. Pre-1.0.0: modify existing migrations (no installed base assumed).
Post-1.0.0: add new migration files.
the binary.
- `000_migration.sql` — contains ONLY the creation of the migrations
tracking table itself. Nothing else.
- `001_schema.sql` — the full application schema.
- **Pre-1.0.0:** never add additional migration files (002, 003, etc.).
There is no installed base to migrate. Edit `001_schema.sql` directly.
- **Post-1.0.0:** add new numbered migration files for each schema change.
Never edit existing migrations after release.
- All repos should have an `.editorconfig` enforcing the project's indentation
settings.