docs(agents): update AGENTS.md

Amolith created

- Reflect ES module split (app.js β†’ render.js/ui.js)
- Document tie-breaker protocol (break_tie/tie_broken)
- Note daily inactive-room cleanup
- Update file structure, noscript fallback, and protocol/testing details

Assisted-by: GPT-5 via Crush

Change summary

AGENTS.md | 78 ++++++++++++++++++++++++++++++++++----------------------
1 file changed, 47 insertions(+), 31 deletions(-)

Detailed changes

AGENTS.md πŸ”—

@@ -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