@@ -4,7 +4,7 @@ Guide for AI agents working in this codebase.
## Project Overview
-Sift is a collaborative list application with real-time voting and ranking. Users create or join rooms via an ID, then collaboratively add items and vote (upvote, downvote, or veto). Items are automatically sorted by score, with vetoed items pushed to the bottom and dimmed.
+Sift is a collaborative list application with real-time voting and ranking. Users create or join rooms via an ID, then collaboratively add items and vote (upvote, downvote, or veto). Items are automatically sorted by score, with vetoed items pushed to the bottom and dimmed. Rooms can optionally have custom titles.
Tech stack:
- Runtime: Deno (TypeScript)
@@ -42,10 +42,11 @@ sqlite3 lists.db "SELECT * FROM votes;"
### Data Flow
```
-User creates/joins room β WebSocket connection β Server sends current state
+User creates/joins room β WebSocket connection β Server sends current state (including room title)
User adds items β Client sends 'add_items' β Server inserts to DB β Broadcasts to all clients in room
User votes β Client sends 'vote' β Server upserts vote β Broadcasts vote change
User deletes item β Client sends 'delete_item' β Server cascades deletion (and explicitly deletes votes) β Broadcasts deletion
+User sets room title β Client sends 'set_title' β Server updates room β Broadcasts title change
```
### Component Structure
@@ -53,31 +54,40 @@ User deletes item β Client sends 'delete_item' β Server cascades deletion (a
- server.ts
- HTTP request handler serving static files and API endpoints
- WebSocket connection manager with room-based broadcasting
- - SQLite database with schema initialization, migration, and queries
+ - SQLite database with schema initialization, migration (v0βv1βv2βv3), and queries
- Room ID generation via UUID
+ - SVG icon serving from static/icons/
- static/app.js
- WebSocket client with message queuing before connection ready
- - State management for items and votes
+ - State management for items, votes, and room title
- Server-authoritative state (no optimistic updates)
- - Keyboard shortcuts (j/k nav, 1/2/3 votes, Enter upvote, e edit, Del delete, Ctrl/Cmd+Z undo)
+ - Connection status tracking with last sync time display
+ - UI disabled state while connecting (isReady flag)
+ - Keyboard shortcuts (j/k nav, 1/2/3 votes, Enter upvote, e edit, Del delete, Ctrl/Cmd+Z undo, ? help)
- Inline edit flow using 'edit_item' and FLIP-style reordering animation
- Reset votes action (broadcasts 'votes_reset')
+ - Set room title action (broadcasts 'set_title')
+ - Deep linking: auto-join room from URL ?room=CODE parameter
- static/index.html
- Two-screen UI: start screen (create/join) and list screen
- - Connection status indicators and header actions (copy link, copy items, reset votes, leave)
+ - Connection status indicators (dot + text + last sync time)
+ - Header actions (set title, copy link, copy items, reset votes, leave)
- Help modal markup and keyboard shortcut reference
- Input methods: single-item input and bulk textarea
+ - Inline SVG icons embedded in buttons
- static/style.css
- CSS custom properties for theming
- Light/dark mode via prefers-color-scheme
- Responsive design
- Styles for connection status, help modal, selected item, veto state, and animations
+ - Reduced motion support via @media query
-### Database Schema (schema_version = 2)
+### Database Schema (schema_version = 3)
rooms
- code (TEXT PRIMARY KEY NOT NULL)
- created_at (INTEGER, Unix timestamp)
+- title (TEXT, nullable, added in v3)
items
- id (TEXT PRIMARY KEY NOT NULL)
@@ -102,22 +112,25 @@ Constraints
- Vote type enforced: 'up' | 'down' | 'veto'
Migration
-- On startup, migrates legacy data to schema v2 when needed
+- On startup, migrates legacy data to latest schema when needed
- From v0: creates new tables, sanitizes/truncates item text to 200 chars, filters vote types; sets user_version = 2
- From v1: rebuilds rooms table (removes last_activity and length constraint); sets user_version = 2
+- From v2: adds title column to rooms; sets user_version = 3
+- For fresh databases: creates tables with schema v3 directly
- Transactional (BEGIN/COMMIT with rollback on failure)
### WebSocket Protocol
Server β Client messages
```js
-{ type: 'state', items: [...], userId: '...' }
+{ type: 'state', items: [...], roomTitle: string|null, userId: '...' }
{ type: 'items_added', items: [{id, text, votes}, ...] }
{ type: 'vote_changed', itemId, userId, voteType }
{ type: 'vote_removed', itemId, userId }
{ type: 'item_edited', itemId, text }
{ type: 'item_deleted', itemId }
{ type: 'votes_reset' }
+{ type: 'title_changed', title: string|null }
```
Client β Server messages
@@ -128,6 +141,7 @@ Client β Server messages
{ type: 'edit_item', itemId, text }
{ type: 'delete_item', itemId }
{ type: 'reset_votes' }
+{ type: 'set_title', title: string|null } // null to clear title
```
### State Synchronization
@@ -135,14 +149,17 @@ Client β Server messages
Connection lifecycle
1. Client connects with ?room=CODE&user=UUID query params
2. Server verifies room exists, upgrades to WebSocket
-3. Server sends full state via 'state' message
+3. Server sends full state via 'state' message (items, roomTitle, userId)
4. Client stores userId from server (may differ from localStorage if not provided)
-5. Subsequent changes broadcast to all clients in room (sender included)
+5. Client sets isReady=true and enables UI
+6. Subsequent changes broadcast to all clients in room (sender included)
Client-side optimism: None β server is authoritative. Client waits for broadcast confirmation.
Room cleanup: When a socket closes, it's removed from the in-memory clients Map. No periodic expiration/cleanup is implemented.
+Connection status: Client tracks connection state (connecting/connected/disconnected) with visual indicators and last sync timestamp, updated every second.
+
### Rendering Logic
Client-side sort
@@ -153,9 +170,17 @@ Client-side sort
Visual states
- Vetoed items: 40% opacity, max-height constraint, hidden overflow
- Active vote buttons: darker background
+- Selected item: outline with accent color
+- Items with active votes: left border colored
- Items display current score with +/- prefix
- Reordering uses FLIP animation
+Room title
+- Displayed in header when set
+- Hidden when null/empty
+- Editable via set-title-btn (pencil icon)
+- Sanitized using same rules as item text (1-200 chars)
+
### Server-Side Validation
Limits (server.ts)
@@ -171,6 +196,7 @@ Message validation
- vote: voteType must be one of 'up'|'down'|'veto'
- edit_item: text sanitized and length-checked (1β200)
- reset_votes: clears all votes for items in the room
+- set_title: title can be null/undefined (clears title) or string; if string, sanitized and length-checked (1β200)
Validation helpers
- safeParseMessage(): size check + JSON parse with error handling
@@ -186,7 +212,7 @@ Invalid messages
## Code Patterns
Naming conventions
-- Variables: camelCase (roomCode, userId, messageQueue)
+- Variables: camelCase (roomCode, userId, messageQueue, roomTitle)
- Constant limits: UPPER_SNAKE_CASE (MAX_ITEM_TEXT_LEN, MAX_BULK_ITEMS, MAX_WS_MESSAGE_BYTES)
- Other constants/instances: camelCase (db, clients)
- Database columns: snake_case (room_code, created_at, vote_type)
@@ -209,7 +235,7 @@ Error handling
Security notes
- No authentication: rooms protected only by knowing the room ID
- User IDs: client-generated UUIDs in localStorage (spoofable)
-- XSS prevention: escapeHtml() used when rendering item text
+- XSS prevention: escapeHtml() used when rendering item text and room title
- Input validation: server-side sanitization and length limits
- Cross-room protection: server validates item-room membership
- Message size limit: WebSocket messages limited to 32KB
@@ -223,9 +249,13 @@ Security notes
4) Broadcast 'except' param unused by callers: all clients, including sender, receive broadcasts.
5) Item-room validation enforced: prevents cross-room vote/edit/delete.
6) Port is hardcoded: server always runs on port 8294.
-7) Static files served via dedicated routes; no generic file server.
+7) Static files served via dedicated routes including SVG icons from /icons/ path.
8) No build step: Deno runs TypeScript directly.
9) Database file location: lists.db is created in the project root.
+10) Deep linking works: Opening a URL with ?room=CODE auto-joins that room.
+11) UI disabled while connecting: Inputs/buttons disabled until 'state' message received (isReady flag).
+12) Room title is optional: null/empty titles are hidden; title uses same text sanitization as items.
+13) Position restoration after voting: Client remembers position in sorted list and re-selects item at same position after vote-triggered re-sort.
## Testing Approach
@@ -237,9 +267,13 @@ Core functionality
3. Bulk add: paste multi-line text; verify all items added
4. Edit item: edit an item; verify all clients update (item_edited)
5. Reset votes: use the reset button; verify votes clear (votes_reset)
-6. Veto sorting: veto an item; verify it moves to bottom and dims
-7. Disconnection: stop server; clients show alert and reset
-8. Room joining: joining a non-existent room returns 404
+6. Set room title: use set title button; verify all clients see updated title
+7. Veto sorting: veto an item; verify it moves to bottom and dims
+8. Disconnection: stop server; clients show alert and reset
+9. Room joining: joining a non-existent room returns 404
+10. Deep linking: open URL with ?room=CODE; verify auto-join
+11. Connection status: watch status dot change and last sync time update
+12. Keyboard shortcuts: test j/k navigation, 1/2/3 voting, e edit, Del delete, ? help
Validation testing (check server logs)
1. Empty item: whitespace-only text β ignored, logged
@@ -249,7 +283,8 @@ Validation testing (check server logs)
5. Cross-room manipulation: vote/edit/delete for an item in another room β rejected, logged
6. Large message: >32KB WebSocket message β socket closed with code 1009
7. DB constraints: sqlite3 lists.db ".schema" β verify CHECKs, NOT NULL, indexes
-8. Migration: start with old-format data; verify migration to v2 runs and data preserved
+8. Migration: start with old-format data; verify migration to v3 runs and data preserved
+9. Title validation: set title >200 chars β rejected, logged; set null title β clears title
## Development Workflow
@@ -282,10 +317,11 @@ Adding new message types
βββ static/
βββ index.html # HTML structure
βββ app.js # Client-side JavaScript
- βββ style.css # Styles with light/dark theme
+ βββ style.css # Styles with light/dark theme
+ βββ icons/ # SVG icons (served via /icons/ route)
```
-No nested directories, no build artifacts, no dependencies beyond Deno stdlib and sqlite3.
+No nested directories beyond icons/, no build artifacts, no dependencies beyond Deno stdlib and sqlite3.
## Important Constants
@@ -296,7 +332,7 @@ No nested directories, no build artifacts, no dependencies beyond Deno stdlib an
- Default userId: generated via crypto.randomUUID() and stored in localStorage
Validation limits
-- MAX_ITEM_TEXT_LEN: 200 characters
+- MAX_ITEM_TEXT_LEN: 200 characters (applies to both items and room titles)
- MAX_BULK_ITEMS: 100
- MAX_WS_MESSAGE_BYTES: 32768 (32KB)