Work spanning May 7-10 across multiple sessions:
Poll winner detection + source column (May 7):
- Fix race condition in handleEndPolling where WS voting.ended cleared
leadingGame before the setTimeout could capture the winner
- Add pollActiveRef guard to prevent late poll.leading messages from
re-activating an ended poll
- Add 'source' column to session_games (dice/manual/poll) with backward-
compatible fallback from manually_added flag
- Show indigo "Poll" badge in game lists (Picker, Home, SessionDetail)
- Include source in session export (JSON and text formats)
Multi-admin poll state sync (May 9):
- Enrich poll.start broadcast with pollStartedAt timestamp so all admin
clients can start their timers from the correct time
- Enrich voting.ended broadcast with winnerGameId/Label/Votes so all
admins see the winner prompt, not just the one who clicked End Poll
- Add poll.start WS handler in SessionInfo so Admin B sees polls started
by Admin A without refreshing
- Make handleStartPolling optimistic with rollback on failure
WebSocket keepalive + auto-reconnect (May 9):
- Add 30s ping interval to SessionInfo WS connection (matching server's
60s timeout) to prevent silent disconnects
- Add auto-reconnect on close with 3s delay
- Proper cleanup of ping interval, reconnect timeout, and onclose handler
Sync selected game across admin clients (May 10):
- New POST/DELETE /sessions/:id/game-selection endpoints with DB
persistence (pending_game_id, pending_game_source columns)
- Broadcast game.picked/game.dismissed WS events to session subscribers
- handleDismissGame replaces inline setSelectedGame(null) calls
- Restore pending game selection on page load for late-joining admins
- Clear pending selection when game is formally added to session
Poll ending countdown timer (May 10):
- POST /:id/voting/end now accepts optional { delay } (0-300 seconds)
- New POST /:id/voting/cancel-end to abort a scheduled end
- New poll.ending and poll.ending.cancelled WS events
- poll_ending_at column on sessions table for crash recovery
- rescheduleEndingPolls() called on server startup to resume countdowns
- End Poll button opens popover with End Now / 5s / 10s / 30s / custom
- Red "Poll Ending" card with countdown display and Cancel button
- Document new WS events in docs/api/websocket.md
Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.2 KiB
JavaScript
72 lines
2.2 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const cors = require('cors');
|
|
const { bootstrapGames, bootstrapTickers } = require('./bootstrap');
|
|
const { WebSocketManager, setWebSocketManager } = require('./utils/websocket-manager');
|
|
const { cleanupAllShards } = require('./utils/ecast-shard-client');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 5000;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Health check
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'ok', message: 'Jackbox Game Picker API is running' });
|
|
});
|
|
|
|
// Routes
|
|
const authRoutes = require('./routes/auth');
|
|
const gamesRoutes = require('./routes/games');
|
|
const sessionsRoutes = require('./routes/sessions');
|
|
const { rescheduleEndingPolls } = require('./routes/sessions');
|
|
const statsRoutes = require('./routes/stats');
|
|
const pickerRoutes = require('./routes/picker');
|
|
const votesRoutes = require('./routes/votes');
|
|
const webhooksRoutes = require('./routes/webhooks');
|
|
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/games', gamesRoutes);
|
|
app.use('/api/sessions', sessionsRoutes);
|
|
app.use('/api/stats', statsRoutes);
|
|
app.use('/api', pickerRoutes);
|
|
app.use('/api/votes', votesRoutes);
|
|
app.use('/api/webhooks', webhooksRoutes);
|
|
|
|
// Error handling middleware
|
|
app.use((err, req, res, next) => {
|
|
console.error(err.stack);
|
|
res.status(500).json({ error: 'Something went wrong!', message: err.message });
|
|
});
|
|
|
|
// Create HTTP server and attach WebSocket
|
|
const server = http.createServer(app);
|
|
|
|
// Initialize WebSocket Manager
|
|
const wsManager = new WebSocketManager(server);
|
|
setWebSocketManager(wsManager);
|
|
|
|
if (require.main === module) {
|
|
bootstrapGames();
|
|
bootstrapTickers();
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Server is running on port ${PORT}`);
|
|
console.log(`WebSocket server available at ws://localhost:${PORT}/api/sessions/live`);
|
|
rescheduleEndingPolls();
|
|
});
|
|
|
|
const shutdown = async () => {
|
|
console.log('Shutting down gracefully...');
|
|
await cleanupAllShards();
|
|
server.close(() => process.exit(0));
|
|
};
|
|
|
|
process.on('SIGTERM', shutdown);
|
|
process.on('SIGINT', shutdown);
|
|
}
|
|
|
|
module.exports = { app, server };
|