feat(cleanup): auto-expire inactive rooms daily

Amolith created

Adds deleteInactiveRooms() function to db.ts that finds and removes
rooms
where the most recent item or vote activity is older than 30 days.

The cleanup runs automatically on server startup and then every 24 hours
via setInterval. Foreign key CASCADE constraints handle automatic
deletion
of associated items and votes.

Implements: b3d438a
Assisted-by: Claude Sonnet 4.5 via Crush

Change summary

db.ts     | 19 +++++++++++++++++++
server.ts | 12 ++++++++++++
2 files changed, 31 insertions(+)

Detailed changes

db.ts 🔗

@@ -301,3 +301,22 @@ export function getItemsWithVotes(roomCode: string) {
     GROUP BY i.id
   `).all(roomCode) as Array<{id: string, text: string, votes: string | null}>;
 }
+
+export function deleteInactiveRooms(daysInactive: number = 30): number {
+  const cutoffTimestamp = Math.floor(Date.now() / 1000) - (daysInactive * 24 * 60 * 60);
+  
+  // Find rooms where the most recent activity (item or vote) is older than the cutoff
+  const result = db.prepare(`
+    DELETE FROM rooms
+    WHERE code IN (
+      SELECT r.code
+      FROM rooms r
+      LEFT JOIN items i ON r.code = i.room_code
+      LEFT JOIN votes v ON i.id = v.item_id
+      GROUP BY r.code
+      HAVING COALESCE(MAX(i.created_at), MAX(v.created_at), r.created_at) < ?
+    )
+  `).run(cutoffTimestamp);
+  
+  return result.changes || 0;
+}

server.ts 🔗

@@ -367,6 +367,18 @@ function handleWebSocket(ws: WebSocket, roomCode: string, userId: string) {
   };
 }
 
+// Room cleanup scheduler
+function cleanupInactiveRooms() {
+  const deleted = db.deleteInactiveRooms(30);
+  if (deleted > 0) {
+    console.log(`🧹 Cleaned up ${deleted} inactive room(s)`);
+  }
+}
+
+// Run cleanup on startup and schedule daily
+cleanupInactiveRooms();
+setInterval(cleanupInactiveRooms, 24 * 60 * 60 * 1000); // Every 24 hours
+
 serve(async (req) => {
   const url = new URL(req.url);