Skip to content

release: v0.1-MVP dependency updates - #38

Closed
fiwon123 wants to merge 28 commits into
mainfrom
dev
Closed

fiwon123 wants to merge 28 commits into
mainfrom
dev

Conversation

@fiwon123

Copy link
Copy Markdown
Owner

Description

Release PR integrating the dependency and CI updates merged to dev since the
v0.1-MVP release (main @ db04adb).

No application behavior changes — only dependency manifest/lockfile bumps plus
one CI action bump. main already contains all v0.1-MVP feature work; the only
content difference between main and dev is these bumps.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD changes
  • Other (please describe): dependency updates

Related Issues

Closes nothing. This release PR closes no issues — all v0.1-MVP issues were
closed when their feature PRs merged to dev, and Closes #N is a no-op when
the base is not the default branch.

Changes Made

Testing

  • Unit tests pass (make check) — verified locally on dev: backend 43 tests, frontend 17 tests
  • Linting passes (cargo clippy --all-targets --locked -- -D warnings)
  • Build succeeds (cargo build --locked)
  • Manual testing performed — N/A (dependency-only change)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

fiwon123 and others added 17 commits October 11, 2026 01:46
* chore: scaffold axum backend crate with live check gate

Adds an Axum + Tokio crate (edition 2021, Rust 1.83) with config-from-env,
tracing, TraceLayer, and a /healthz probe. Cargo.lock is committed so every
check step runs with --locked; hyper-util pinned to 0.1.10 so the dependency
set stays on the 1.83 toolchain (newer releases need cargo's edition2024).

make check now exercises fmt, clippy, test, and build in backend/ instead of
printing 'skipped'.

* chore: avoid logging database credentials at startup
* feat: postgres schema, models, and storage layer

Adds sqlx migrations (users, rooms, room_memberships, messages), the
FromRow model types, and a storage module with compile-time-checked
queries for users/rooms/memberships/messages. The server now connects to
Postgres on startup and applies pending migrations.

Fidelity details:
- messages(room_id, created_at, id) index backs history pagination; the
  cursor join is scoped to the room; limit clamped to 1..=100.
- created_by and messages.user_id use ON DELETE RESTRICT so deleting a
  user cannot silently destroy rooms or punch holes in shared history.
- messages carry uuid ids + timestamptz created_at for stable ordering
  across reconnects (save-before-broadcast comes in a later issue).

Also bumps the toolchain to Rust 1.99 (rust-toolchain.toml, mise.toml,
Dockerfile, edition 2024): current sqlx/hyper-util manifests require
cargo's edition2024 feature (stable since 1.85), so the 1.83 pin could
not build a modern dependency tree. The committed .sqlx/ offline cache
lets CI build without a database.

* ci: resolve sqlx queries from the offline cache in the gate and CI

The check target and CI job set SQLX_OFFLINE=true so compile-time sqlx
macros use the committed .sqlx/ cache instead of a live database. This
makes the gate deterministic and matches CI (which has no DB service),
and fails loudly when a query changed without regenerating the cache.
DB-backed integration tests still connect at runtime via DATABASE_URL.
* feat: register/login with argon2 and JWT HS256

Adds the auth surface: POST /api/auth/register, POST /api/auth/login, and
GET /api/me, plus an AuthUser extractor other handlers can reuse.

- Passwords hashed with argon2id (random per-hash salt); the hash is never
  serialized or logged.
- Stateless HS256 JWTs (sub = user id, 24h expiry) signed with JWT_SECRET.
- Login returns the same 401 for unknown user and wrong password, and burns
  comparable hashing time on the unknown-user path to blunt enumeration.
- Validation: username 3-32 chars [a-z0-9_], password 8-128 chars.
- Unified AppError -> JSON {error} responses (400/401/403/404/409/500).
- Unit tests for hashing, JWT, and validation; DB-backed integration tests
  over the real router that skip gracefully when no database is reachable.
- Wire JWT_SECRET through docker-compose and document it in the env template.

* refactor: harden auth input handling, token parsing, and secret default

Review follow-ups:
- Bound login input (username/password length) before the DB query and
  argon2 verify, returning the generic 401 so an over-long body cannot burn
  hashing time or reach the database.
- Accept any case for the Bearer scheme (RFC 7235) instead of exact
  'Bearer '; add parse_bearer unit tests.
- Warn at startup when JWT_SECRET is unset (committed insecure default),
  and expose Config::uses_default_jwt_secret with tests.
- Add a JWT test pinning HS256: a token whose header claims HS384 is
  rejected (algorithm-confusion defense).
- Extract username length bounds into validation consts; drop the unused
  UNKNOWN_USER_SUBJECT const.

Username enumeration on register (409) is a conscious, documented trade-off:
registration reveals availability by design.

* ci: run DB-backed integration tests against a Postgres service

The auth integration tests skip when no database is reachable, and CI had
no database, so they never actually ran there -- 'skipped is not passed'.
Add a throwaway postgres:16 service to the rust job and point DATABASE_URL
at it, so register/login/me are exercised on every release PR. Compile-time
queries still resolve from the committed .sqlx cache (SQLX_OFFLINE).
* feat: rooms, membership, and message-history API

Adds the authenticated room surface and enforces membership on history:

- POST /api/rooms (creator auto-joined), GET /api/rooms (with 'member'
  flag), GET /api/rooms/{id}, POST /api/rooms/{id}/join and /leave
  (idempotent), GET /api/rooms/{id}/messages (newest-first, limit 1-100
  clamped, 'before' message-id cursor).
- RoomService orchestrates create/list/get/join/leave/history; history
  returns 403 when the caller is not a member, 404 for unknown rooms.
- Room names validated: trimmed, non-empty, <= 64 chars.
- History responses join the author username (MessageWithAuthor) so the
  UI renders authors without a second lookup; ids + timestamps keep the
  ordering stable across reconnects.
- Storage: RoomMemberships::leave, Messages::list_with_author (two
  pagination variants); new queries added to the .sqlx offline cache.
- 9 DB-backed integration tests (auto-join, name validation, auth gate,
  membership flag, join/leave idempotency, 403/404 history access,
  pagination + cursor, unknown cursor); skip gracefully without a DB.

make check green: 16 unit + 18 integration tests, clippy -D warnings.

* refactor: rooms review follow-ups

Review findings addressed:
- Create a room and join the creator atomically: single CTE statement in
  Rooms::create, so a room row can never exist without the creator's
  membership (replaces two non-transactional statements).
- Reject control characters in room names with a clean 400 instead of a
  Postgres 500.
- Make the history test deterministic: seed messages with explicit spaced
  created_at so the random-UUID tie-break cannot reorder the page.
- RoomResponse::from_storage now consumes its argument (no name clone;
  list_rooms uses into_iter).
- Drop the unused rows-affected claim on RoomMemberships::leave.

Deliberately unchanged (documented): rooms are public by design — any
authenticated user can join, so 403-on-history is a membership gate, not
privacy; room names are not unique (rooms are id-addressed).
* feat: WebSocket chat with save-before-broadcast and room isolation

Adds GET /api/ws?token=..&room_id=..: JWT + room + membership checked
before the upgrade, then an in-memory per-room broadcast session.

- run_session: subscribe-then-join, split socket, merged broadcast +
  direct-error send lane, lag-tolerant receive, presence on join/leave
- save-before-broadcast: message is INSERTed first; the row's id and
  timestamptz are what every client (sender included) receives
- per-message validation: trimmed non-empty body, 4000-char cap,
  membership re-check on every send; failed frames return an error
  event and keep the connection alive
- RoomRegistry: lazy per-room broadcast::channel + user map, single
  instance by design (Redis fan-out is a later milestone)
- AppState gains registry; axum ws feature + futures-util deps
- integration tests over a real socket on an ephemeral port: handshake
  rejections (401/404/403/101), intra-room broadcast with presence and
  cross-room isolation, persistence via history, validation-error
  resilience (3 new tests; 40 total green)

Closes #18

* refactor: harden WebSocket session against review findings

- enforce room access on the receive path too: a session whose membership
  was revoked over HTTP is closed at the next message instead of receiving
  room content forever (per-message is_member check before delivery)
- reference-count connected users in the registry so multiple tabs emit a
  single joined/left presence transition (0->1 joined, 1->0 left)
- stop leaking the JWT query parameter into logs: custom trace span logs
  method + path only, never the query string
- cap WebSocket message size at 64 KiB before JSON parsing (axum default
  allows 64 MiB) so oversized frames cannot force a big allocation
- validation: message bodies reject control characters like room names
- error events go out over try_send so a slow/non-reading client can never
  stall the receive loop
- poison-safe registry locks; drop the redundant outer Arc around
  RoomRegistry in AppState
- tests: own-joined sequencing removes the connect/subscribe race; added
  revoked-membership teardown + duplicate-connection presence tests
* docs: add API and WebSocket protocol reference

- docs/api.md: auth, rooms, history, WS handshake + event protocol,
  validation rules, save-before-broadcast semantics, error shape
- README: replace stale 'scaffold only' status with the implemented
  feature set and link the new reference

Closes #20

* docs: correct error-shape, token-issuance, and upgrade-request claims

Fact-check follow-ups on docs/api.md:
- error shape only applies to the app's own AppError responses; extractor
  rejections (missing Content-Type -> 415, malformed JSON/UUID -> 400,
  wrong shape -> 422, non-upgrade /api/ws -> 426) are plain text
- Content-Type: application/json is mandatory on the JSON POST endpoints
- tokens are issued by register and login only (not /api/me)
- note the case-insensitive bearer scheme
- WS requires a genuine upgrade request; a plain GET is rejected before the
  401/404 checks; bad query params -> plain-text 400
* feat: scaffold React + Vite + TypeScript frontend

Add the frontend component with the project's chosen toolchain: Vite + React
+ TypeScript on npm, Oxlint for linting and Oxfmt for formatting (no
ESLint/Prettier). The component Makefile exposes a `check` gate that runs
format check -> lint -> typecheck -> production build and fails on the first
problem.

* docs: document the frontend component

Topology, directory layout, dev commands, key commands, and agent list now
include the React + Vite + TypeScript frontend. AGENTS.md and
PROJECT_CONTEXT.md stay in sync.

* chore: wire frontend into build, CI, dependabot, and agents

Add frontend to COMPONENTS and root clean; add an npm Dependabot entry for
/frontend targeting dev; add the CI frontend job (Node 24, npm ci, format
check, lint, typecheck, build) and include frontend/** in the paths filter;
add the frontend and visual subagents and register them in opencode.json.

* fix: address review findings on frontend scaffold

- Root Makefile: accumulate per-component status so a failing component is
  no longer masked by a later passing one (the loop now exits non-zero).
- frontend tsconfigs: add "strict": true so the new typecheck/build gate is
  actually strict.
- frontend/Makefile: skip only when npm is absent; missing node_modules fails
  loudly with instructions (consistent with backend).
- Makefile clean: remove frontend/dist even without node_modules.
- opencode.json: visual agent bash is deny-by-default; frontend edit keeps the
  secret-file deny rules.
- Docs: distinguish skipped (toolchain absent) from failed (deps missing).
* feat: implement chat UI (auth, rooms, real-time messaging)

Typed API client for every endpoint in docs/api.md (ApiError surfaces the
backend message; extractor plain-text rejections pass through), a WebSocket
helper that builds /api/ws?token=&room_id=, and pure message merge/dedupe
helpers. Hooks for auth (token persistence + /api/me validation) and a live
room (history + socket with capped backoff reconnect). Login, room-list, and
chat-panel components; App composes auth + rooms + chat. Dev server proxies
/api (HTTP + WS) and /healthz to the backend. Removes the Vite demo assets.

* test: cover client logic and run tests in the frontend gate

Add vitest unit tests for URL/request building, error parsing, and message
merge/dedupe/ordering (17 tests, no DOM). Wire npm run test into the frontend
Makefile check and the CI frontend job. Disable oxlint's set-state-in-effect
rule with a rationale (data-loading effects legitimately drive state).

Docs: note the test step and the dev proxy in the frontend README; add the
test step to the AGENTS.md component description.
Backend + frontend MVP is on dev, but the root README still called the
frontend 'planned'. Make the status, stack, layout, and run instructions
reflect the implemented React + Vite + TS client and note that make check
covers both components. Also refresh the now-stale frontend note in
PROJECT_CONTEXT.md.
* chore: start the frontend with make dev-up

Add a frontend service to the compose dev stack so one command brings up
Postgres + API + Vite. A small node:24-alpine dev image bakes node_modules into
a layer; compose bind-mounts the source and keeps deps in a named volume, and
sets VITE_API_PROXY_TARGET=http://dev:8080 (service name) since 'localhost'
inside the container is the frontend itself. Polling is enabled for reliable
HMR through bind mounts, ports are overridable (FRONTEND_HOST_PORT), and
--strictPort fails loudly on a collision. Documented in README, AGENTS.md
ports table, PROJECT_CONTEXT.md, and frontend/README.md.

* fix: keep frontend deps fresh and wait for a healthy API

A named volume is seeded from the image only when empty, so 'up --build'
would shadow a rebuilt image's node_modules with the stale volume. Install
deps at container start-up instead, guarded by a package-lock.json hash so it
only reinstalls when the lockfile changes, and drop the unused image-baked
'npm ci' layer. Also give the dev service a healthcheck on /healthz and gate
the frontend on service_healthy so proxied /api calls don't 502 during cargo's
first build.

* fix: harden the frontend dev entrypoint and API health gate

Fail loudly when package-lock.json is missing and compute its hash without a
pipeline so a sha256sum failure isn't masked by cut. Widen the dev healthcheck
window (start_period 300s, retries 60) so a cold cargo build can't leave the
frontend waiting on service_healthy.
Use the shorter /health path (handler renamed to match) and update every
reference: the Vite dev proxy, the new compose dev healthcheck, and the docs
(README, docs/api.md, frontend/README.md). Response body unchanged; no
external consumer of /healthz exists on dev.
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the tokio group in /backend with 2 updates: [tower-http](https://github.com/tower-rs/tower-http) and [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite).


Updates `tower-http` from 0.6.11 to 0.7.1
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](tower-rs/tower-http@tower-http-0.6.11...tower-http-0.7.1)

Updates `tokio-tungstenite` from 0.26.2 to 0.29.0
- [Changelog](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md)
- [Commits](snapview/tokio-tungstenite@v0.26.2...v0.29.0)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: tokio
- dependency-name: tokio-tungstenite
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: tokio
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the sqlx group in /backend with 1 update: [sqlx](https://github.com/launchbadge/sqlx).


Updates `sqlx` from 0.8.6 to 0.9.0
- [Changelog](https://github.com/transact-rs/sqlx/blob/main/CHANGELOG.md)
- [Commits](transact-rs/sqlx@v0.8.6...v0.9.0)

---
updated-dependencies:
- dependency-name: sqlx
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sqlx
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Felipe Inoue <felipeinoue@yahoo.com.br>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.19.2 to 26.6.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the tooling group in /frontend with 1 update: [typescript](https://github.com/microsoft/TypeScript).


Updates `typescript` from 6.0.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](microsoft/TypeScript@v6.0.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: tooling
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@fiwon123 fiwon123 added the release Release label Oct 12, 2026
@fiwon123 fiwon123 self-assigned this Oct 12, 2026
@fiwon123

Copy link
Copy Markdown
Owner Author

CI passed on this release PR — rust ✅ (3m52s) and frontend ✅ (11s). Awaiting explicit user confirmation to merge dev → main.

fiwon123 and others added 10 commits October 12, 2026 00:23
* feat: design system, brand mark, favicon and inline icon set

- Rewrite index.css with a teal/emerald dark design system: surfaces,
  type scale, radii, shadows, focus ring and prefers-reduced-motion
- Add Logo/LogoMark (speech bubble + pulse) and a self-contained inline
  SVG Icon set (no icon dependency)
- Replace the Vite default favicon with the custom mark; update the
  document title/theme-color
- Split App.css into styles/{primitives,auth,shell,chat}.css

Refs #39

* refactor: address review feedback on brand/a11y tokens

- Make LogoMark decorative when paired with the wordmark (avoid double
  accessible name); use useId for the gradient
- Restore accessible loading status on the splash screen
- Add --panel-hover/--border-hover/--accent-border tokens; lighten
  --faint to meet AA contrast
- Drop redundant scoped :disabled rules covered by the global base

Refs #39
* feat: redesigned register/login screen with brand panel

- Split layout: brand panel (logo, gradient tagline, features) + form card
- Segmented animated tabs, icons on fields, password visibility toggle,
  live validation checklist while registering, error banner, busy spinner
- Add src/auth/validation.ts (pure rules) with unit tests

Refs #40

* refactor: address auth review feedback

- Password checklist rule now reflects the full 8-128 range so it stays
  in sync with credentialsValid
- Require non-empty credentials before submitting in login mode
- Roving tabindex + arrow-key navigation for the mode tabs
- Gradient tagline color fallback for non-clip browsers

Refs #40
* feat: app shell, room list and chat visual overhaul

- Topbar: user avatar, icon logout, mobile menu toggle; rooms become an
  off-canvas drawer under 860px
- Room list: header with count, create form with plus icon, live filter
  with clear button, hash icons, join tags, empty states and shimmer
  skeletons
- Chat: avatars with deterministic colors, author grouping, date
  dividers, hover timestamps, auto-resize composer with send icon,
  online presence avatars, jump-to-newest button, pinned-scroll logic
  that survives older-page loads, error banner
- New pure helpers (isSameDay/shouldGroup/formatMessageTime/
  formatDayLabel, avatarColor/initialsOf) with unit tests

Refs #41

* refactor: clear stale older-load scroll guard when the load settles

A failed or empty load-older request never changes messages, so the
scroll-preservation ref could stay latched and corrupt the next pinned
scroll; sync it with the loadingOlder prop instead.

Refs #41
* feat: client-side chat niceties (emoji, linkify, sound, toasts, unread)

- Emoji picker in the composer (curated built-in set, inserts at cursor)
- Auto-linkify message URLs with safe rel/target
- Web Audio chime for incoming messages; browser notifications while the
  tab is hidden (permission requested on login)
- Per-room unread badges + tab-title count, persisted to localStorage
- Toast notifications for room create/join/errors
- Ctrl/Cmd+K room search shortcut; persisted dark/light theme toggle

Refs #42

* fix: address review — link brackets, light-theme contrast, a11y, cleanup

- linkify: only strip unbalanced closing brackets (keeps /Foo_(bar) intact)
- light theme: add --good/--warn/--bad, darken accent gradient for contrast
- toasts: clear pending timers on unmount; cap at 3; keep live region mounted
- room unread badge: aria-hidden glyph + visually-hidden count text
- theme: update theme-color meta + inline pre-paint script (no FOUC)
- emoji picker: aria-haspopup/controls + focus return on Escape
- drop dead exports (EMOJI_BY_NAME, hasAnyLink); test the bracket cases

Refs #42
* feat: typing indicators over WebSocket

- Backend: add Incoming::Typing and WsOutgoing::Typing; membership-checked,
  relayed per session with a 1.5s throttle, never persisted
- docs/api.md: document the typing frame on both directions
- Frontend: extend ServerEvent/ClientSend, encodeTyping, useChatRoom exposes
  sendTyping (client-throttled) + typing list with a 3.5s TTL
- ChatPanel: 'X is typing…' indicator with animated dots; hint on composer input
- Integration test: room-scoped typing, throttle, and non-persistence
- Unit tests for typingLabel and frame encoding

Closes #43

* fix: don't echo own typing; throttle before DB; harden client

- Backend: skip relaying a user's own typing event back to them; move the
  throttle gate ahead of the membership query so spamming can't hit Postgres
- Client: defensively filter own user from the indicator; reset the typing
  send-gate per room; guard onmessage against post-cleanup frames
- Test: assert no self-echo, send the throttled frame immediately, fix comment
- docs: note no self-echo and that clients ignore their own typing

Refs #43
- Backend: migration for message_reactions (composite PK, FKs, cascade)
- Storage: reactions module (insert/remove/toggle/list/group_by_message, MAX_EMOJI_LEN 16), unit tests
- Services/routes/WS: include reactions in MessageResponse/history, handle_react/Reaction frames (membership + message-in-room validation), validate emoji, broadcast to room
- Tests: reactions integration in ws_chat; validate_emoji unit tests; grouping unit tests
- Frontend: types/ws/encodeReact, applyReaction helper + tests, useChatRoom.react, ChatPanel chips + hover quick reactions, styles, update history types; encodeReact test; reactions test
- Docs: API updates (reactions in history/message, react client frame, reaction server frame, semantics)
- .sqlx: regenerate query cache entries
* feat: unread counts and read receipts (#45)

Per-member read cursors backed by a new message_reads table, an HTTP
mark-read endpoint, ephemeral WS read receipts, and server-counted
unread totals surfaced through room responses.

Backend:
- migration 0006: message_reads(room_id, user_id, last_read_message_id);
  nullable cursor = everything unread; advance-only upsert in Reads
- Rooms: list/get return RoomSummary { room, unread_count, last_message }
  (DISTINCT ON newest message per room); mark_read validates membership,
  message ownership, and never moves the cursor backwards
- POST /api/rooms/{id}/read marks all (or up to a message) read and
  broadcasts WsOutgoing::Read so members see who caught up
- tests: read_receipts.rs (cursor math, 403/404/400) + WS broadcast test

Frontend:
- Room { unread_count, last_message }, ServerEvent read, api.read()
- useChatRoom tracks read cursors; ChatPanel shows Seen on the viewer's
  newest own message; RoomList shows last-message previews
- App reconciles local unread badges with server counts (syncFromRooms)
  and marks read on enter/tab-focus (immediate) and on visible incoming
  messages (throttled, trailing)

docs/api.md updated for the new endpoint, room fields, and read frame.

* fix: address review findings for unread/read receipts (#45)

- Reads::mark_read returns the effective cursor (Some only when it moved)
  and re-checks the advance-only condition on the conflicting row, so
  concurrent out-of-order marks cannot move the cursor backwards
- unread_counts/unread_count exclude the caller's own messages
- Room list/get gate last_message and unread_count on membership:
  non-members see no preview and no badge (matches member-gated history)
- Only broadcast a read receipt when the cursor actually advanced
- syncFromRooms is server-authoritative: assign server counts and drop
  stale local bumps (a bump implies the server already persisted, so a
  later fetch reports >= 1); test updated
- Clear the trailing read cursor when leaving a room (not just switching)
- docs/api.md: member-gated preview, reminder that a JSON body is required
  (JSON {} with null message_id), and effective-cursor semantics

Tests: read_receipts.rs now seeds from a second author, asserts own
messages never count, asserts the cursor never reports a stale re-mark,
and covers non-member gating. Full make check green.
Bumps the tokio group in /backend with 1 update: [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite).


Updates `tokio-tungstenite` from 0.29.0 to 0.30.0
- [Changelog](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md)
- [Commits](snapview/tokio-tungstenite@v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: tokio-tungstenite
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: tokio
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: Playwright screenshot harness (#46)

Seed a two-user room over the public API + WebSocket, then capture the
key UI states (auth, room list with unread badge, live chat, read
receipt) in dark and light themes via a headless browser.

- frontend/scripts/screenshots.mjs: stack wait + seeding + captures
- frontend/playwright.config.ts: canonical browser settings
- make screenshots target (root + frontend Makefiles), npm script
- frontend/screenshots/ gitignored; README run instructions
- Verified end-to-end in the sandbox up to browser launch; the seed
  path (register/create/join/WS reactions+messages) is exercised
  against the real stack. PNG production needs Chromium system deps
  (npx playwright install --with-deps chromium on Linux).

* fix: address review findings for screenshot harness (#46)

- register() now returns the password it used, so login() can fill the
  current-password field (playwright fill rejects undefined)
- wait for .status-open instead of the nonexistent .status-live
  (the pill class is status-${status} with status "open"; "live" is
  only the display label)
- close the browser/contexts on every failure path via try/finally
- guard for the global WebSocket (Node 22+) with a clear error
- abort the stack-wait fetches after 3s so the 20s deadline holds
- close the seed socket and reject cleanly on WS error / non-JSON data
* feat: optional Redis Pub/Sub cross-instance broadcasting

Room events now fan out through Redis Pub/Sub when REDIS_URL is set:
- RoomRegistry gains with_redis(); send() publishes to room:{uuid} and
  sessions consume their own subscription, so the local channel is never
  written in pub/sub mode (no double delivery)
- Presence join/leave announces through the same publish path; per-instance
  refcounts stay as designed (documented cross-instance limitation)
- Redis connect failure at startup logs a warning and falls back to in-memory
- WsOutgoing/UserRef/MessageResponse are now Deserialize: the JSON sent to
  clients doubles as the cross-instance payload
- tests/pubsub.rs: two in-process instances on one DB + one Redis prove
  cross-instance delivery and absence of double delivery; skips when
  REDIS_URL is unset

Closes #9 once merged

* docs: wire up REDIS_URL in compose, env template, and project docs

- docker-compose.yml: add an enabled redis service (host 6381, healthcheck)
  and pass REDIS_URL through the dev service
- env template: document and uncomment the REDIS_HOST_PORT / REDIS_URL pair
- AGENTS.md/PROJECT_CONTEXT.md/README.md: Redis is no longer 'later'; it is
  optional and enabled via REDIS_URL with in-memory fallback

* fix: review follow-ups for redis pub/sub

- subscribe failure no longer silently blinds a session: events() returns
  RoomEvents::Unavailable, the session closes, and the client's existing
  reconnect loop retries the subscription
- a single malformed redis payload skips that event instead of severing
  every session in the room (only a dropped connection closes)
- compose: bind the redis service to 127.0.0.1 (unauthenticated chat
  traffic must not be published to the LAN); fix comment typo
- docs: mid-flight publish failures drop the event (history recovers
  messages); presence refcounts are per-instance (AGENTS/PROJECT_CONTEXT)
…) (#58)

* fix: tolerate missing optional fields from version-skewed backends (#57)

RoomList white-screened with "Cannot read properties of undefined
(reading 'username')" against a backend older than #45: the guard at
RoomList.tsx tested `!== null`, but an absent JSON key is `undefined`,
so the null check passed and the nested read threw.

- normalize rooms and messages at the API boundary (listRooms,
  createRoom, history) via normalizeRoom/normalizeMessage, with payload
  types that model older backends' shapes
- apply the same normalization to the WS message ingest in useChatRoom
- RoomList: use `!= null` so absent keys are treated as "no preview"
- tests for the helpers and the endpoints (87 frontend tests)

* refactor: normalize server frames at the decode boundary (#58 review)

Review follow-up (PR #58): the WS ingest normalization was untestable
inside the hook and createRoom normalization had no coverage.

- move the payload normalizers to api/normalize.ts (shared by HTTP and WS)
- add decodeServerEvent to ws.ts: parse + normalize a message frame at the
  single WS trust boundary; the hook now decodes through it
- hook drops its inline normalizeMessage call (frames arrive normalized)
- move helper tests to normalize.test.ts; add decodeServerEvent tests and
  a createRoom normalization test (92 frontend tests)
@fiwon123

Copy link
Copy Markdown
Owner Author

Closing as superseded. This PR was opened before the v0.2-polish and v0.3 milestones and only integrated dependency/CI bumps (its body still lists v0.1-MVP issues). A fresh release PR now brings the full v0.2 + v0.3 work from dev to main.

@fiwon123 fiwon123 closed this Oct 12, 2026
@fiwon123 fiwon123 mentioned this pull request Oct 12, 2026
13 of 17 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant