@@ -15,7 +15,7 @@ Sift is a collaborative list application with real-time voting and ranking. User
Tech stack:
- Runtime: Deno (TypeScript)
- Backend: WebSocket server with SQLite database
-- Frontend: Vanilla JavaScript (no frameworks)
+- Frontend: Vanilla JavaScript ES modules (no frameworks)
- Database: SQLite3 via https://deno.land/x/sqlite3@0.11.1/mod.ts
- Server: Deno std HTTP server https://deno.land/std@0.208.0/http/server.ts
@@ -53,6 +53,7 @@ User adds items β Client sends 'add_items' β Server inserts to DB β Broadc
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
+Top tie present β Client may send 'break_tie' β Server randomly selects a winner among tied top items β Broadcasts 'tie_broken'
```
### Component Structure
@@ -60,20 +61,12 @@ User sets room title β Client sends 'set_title' β Server updates room β Br
- server.ts
- HTTP request handler serving static files and API endpoints
- WebSocket connection manager with room-based broadcasting
- - 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, votes, and room title
- - Server-authoritative state (no optimistic updates)
- - 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
+ - Validation of message size, payload types, and room/item membership
+ - Schedules daily cleanup of inactive rooms (see db.deleteInactiveRooms)
+- db.ts
+ - SQLite database initialization, foreign keys, schema migration (v0βv1βv2βv3)
+ - Query helpers for state assembly, item/vote CRUD, room title updates
+ - Inactive room cleanup by last activity timestamp (created_at of rooms/items/votes)
- static/index.html
- Two-screen UI: start screen (create/join) and list screen
- Connection status indicators (dot + text + last sync time)
@@ -81,12 +74,19 @@ User sets room title β Client sends 'set_title' β Server updates room β Br
- 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
+ - Loads ES module script with `<script type="module" src="/app.js"></script>`
+- static/app.js (ES module entry)
+ - WebSocket client with message queuing before connection ready
+ - State management for items, votes, selected item, room title
+ - URL deep-linking and history updates
+ - Delegates rendering/utilities to render.js and keyboard/help UX to ui.js
+- static/render.js
+ - Sorting, escaping, connection status, last-sync display, and enabled/disabled UI toggling
+- static/ui.js
+ - Keyboard shortcuts (j/k nav, 1/2/3 votes, Enter upvote, e edit, Del delete, Ctrl/Cmd+Z undo, ? help)
+ - Help modal open/close and minimal press-lock for buttons
+- static/style.css, static/palette.css
+ - CSS custom properties, light/dark theme, responsive layout, reduced motion support
### Database Schema (schema_version = 3)
@@ -137,6 +137,7 @@ Server β Client messages
{ type: 'item_deleted', itemId }
{ type: 'votes_reset' }
{ type: 'title_changed', title: string|null }
+{ type: 'tie_broken', itemId, text }
```
Client β Server messages
@@ -148,9 +149,10 @@ Client β Server messages
{ type: 'delete_item', itemId }
{ type: 'reset_votes' }
{ type: 'set_title', title: string|null } // null to clear title
+{ type: 'break_tie' } // request server to randomly choose among tied top items
```
-### State Synchronization
+### State Synchronization & Maintenance
Connection lifecycle
1. Client connects with ?room=CODE&user=UUID query params
@@ -162,7 +164,9 @@ Connection lifecycle
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.
+Room cleanup
+- Socket close: connection is removed from the in-memory clients Map
+- Inactive rooms: daily job deletes rooms with no activity for 30 days (based on max of room/items/votes created_at)
Connection status: Client tracks connection state (connecting/connected/disconnected) with visual indicators and last sync timestamp, updated every second.
@@ -175,12 +179,16 @@ Client-side sort
Visual states
- Vetoed items: 40% opacity, max-height constraint, hidden overflow
-- Active vote buttons: darker background
+- Active vote buttons: darker background (press-lock to avoid flicker)
- Selected item: outline with accent color
-- Items with active votes: left border colored
- Items display current score with +/- prefix
+- Highest-scoring non-vetoed items get a left "top-check" indicator
- Reordering uses FLIP animation
+Tie breaker
+- When a top score tie exists, a "Break the tie" UI appears
+- Clicking it sends 'break_tie'; server broadcasts 'tie_broken' with the chosen item for display
+
Room title
- Displayed in header when set
- Hidden when null/empty
@@ -203,6 +211,7 @@ Message validation
- 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)
+- break_tie: server computes tie among top non-vetoed items and broadcasts a random winner
Validation helpers
- safeParseMessage(): size check + JSON parse with error handling
@@ -255,13 +264,15 @@ 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 including SVG icons from /icons/ path.
+7) Static files served via dedicated routes; icons are inline in HTML. An /icons/ route exists for optional SVGs if added under static/icons/.
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.
+14) ES modules: index.html loads app.js as type=module which imports render.js and ui.js.
+15) Cleanup: a daily job deletes rooms with no activity for 30 days; this is irreversible.
## Testing Approach
@@ -280,6 +291,8 @@ Core functionality
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
+13. Top tie: when multiple non-vetoed items share the highest score, "Break the tie" appears; clicking it shows a "Winner: ..." message (tie_broken)
+14. Noscript fallback: with JS disabled, verify noscript message displays
Validation testing (check server logs)
1. Empty item: whitespace-only text β ignored, logged
@@ -295,7 +308,7 @@ Validation testing (check server logs)
## Development Workflow
Making changes
-1. Edit source files (server.ts or static/*)
+1. Edit source files (server.ts, db.ts, or static/*)
2. Restart Deno server (no hot reload)
3. Hard refresh browser (Ctrl+Shift+R) to bypass cache
4. Test in multiple tabs for WebSocket sync
@@ -318,16 +331,19 @@ Adding new message types
```
/
-βββ server.ts # Deno server: HTTP, WebSocket, SQLite
+βββ server.ts # Deno HTTP/WebSocket server
+βββ db.ts # SQLite schema, migrations, queries, cleanup
βββ lists.db # SQLite database (created on first run)
βββ static/
βββ index.html # HTML structure
- βββ app.js # Client-side JavaScript
+ βββ app.js # Entry module; WS client and state
+ βββ render.js # Rendering/util helpers
+ βββ ui.js # Keyboard/help UI logic
βββ style.css # Styles with light/dark theme
- βββ icons/ # SVG icons (served via /icons/ route)
+ βββ palette.css # Color tokens and CSS variables
```
-No nested directories beyond icons/, no build artifacts, no dependencies beyond Deno stdlib and sqlite3.
+No build artifacts, no dependencies beyond Deno stdlib and sqlite3. An optional static/icons/ directory may be added if serving SVGs via /icons/.
## Important Constants