diff --git a/backend/database.js b/backend/database.js index db711f2..0789b16 100644 --- a/backend/database.js +++ b/backend/database.js @@ -84,6 +84,46 @@ function initializeDatabase() { // Column already exists, ignore error } + // Add upvotes and downvotes columns to games if they don't exist + try { + db.exec(`ALTER TABLE games ADD COLUMN upvotes INTEGER DEFAULT 0`); + } catch (err) { + // Column already exists, ignore error + } + + try { + db.exec(`ALTER TABLE games ADD COLUMN downvotes INTEGER DEFAULT 0`); + } catch (err) { + // Column already exists, ignore error + } + + // Migrate existing popularity_score to upvotes/downvotes if needed + try { + const gamesWithScore = db.prepare(` + SELECT id, popularity_score FROM games + WHERE popularity_score != 0 AND (upvotes = 0 AND downvotes = 0) + `).all(); + + if (gamesWithScore.length > 0) { + const updateGame = db.prepare(` + UPDATE games + SET upvotes = ?, downvotes = ? + WHERE id = ? + `); + + for (const game of gamesWithScore) { + if (game.popularity_score > 0) { + updateGame.run(game.popularity_score, 0, game.id); + } else { + updateGame.run(0, Math.abs(game.popularity_score), game.id); + } + } + console.log(`Migrated popularity scores for ${gamesWithScore.length} games`); + } + } catch (err) { + console.error('Error migrating popularity scores:', err); + } + // Packs table for pack-level favoriting db.exec(` CREATE TABLE IF NOT EXISTS packs ( @@ -113,6 +153,20 @@ function initializeDatabase() { ) `); + // Add message_hash column if it doesn't exist + try { + db.exec(`ALTER TABLE chat_logs ADD COLUMN message_hash TEXT`); + } catch (err) { + // Column already exists, ignore error + } + + // Create index on message_hash for fast duplicate checking + try { + db.exec(`CREATE INDEX IF NOT EXISTS idx_chat_logs_hash ON chat_logs(message_hash)`); + } catch (err) { + // Index already exists, ignore error + } + console.log('Database initialized successfully'); } diff --git a/backend/routes/sessions.js b/backend/routes/sessions.js index dc6e079..b4784e2 100644 --- a/backend/routes/sessions.js +++ b/backend/routes/sessions.js @@ -1,9 +1,18 @@ const express = require('express'); +const crypto = require('crypto'); const { authenticateToken } = require('../middleware/auth'); const db = require('../database'); const router = express.Router(); +// Helper function to create a hash of a message +function createMessageHash(username, message, timestamp) { + return crypto + .createHash('sha256') + .update(`${username}:${message}:${timestamp}`) + .digest('hex'); +} + // Get all sessions router.get('/', (req, res) => { try { @@ -175,7 +184,9 @@ router.get('/:id/games', (req, res) => { g.game_type, g.min_players, g.max_players, - g.popularity_score + g.popularity_score, + g.upvotes, + g.downvotes FROM session_games sg JOIN games g ON sg.game_id = g.id WHERE sg.session_id = ? @@ -270,7 +281,7 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { // Get all games played in this session with timestamps const sessionGames = db.prepare(` - SELECT sg.game_id, sg.played_at, g.title + SELECT sg.game_id, sg.played_at, g.title, g.upvotes, g.downvotes FROM session_games sg JOIN games g ON sg.game_id = g.id WHERE sg.session_id = ? @@ -282,15 +293,26 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { } let votesProcessed = 0; + let duplicatesSkipped = 0; const votesByGame = {}; + const sessionId = parseInt(req.params.id); + const voteMatches = []; // Debug: track which votes matched to which games const insertChatLog = db.prepare(` - INSERT INTO chat_logs (session_id, chatter_name, message, timestamp, parsed_vote) - VALUES (?, ?, ?, ?, ?) + INSERT INTO chat_logs (session_id, chatter_name, message, timestamp, parsed_vote, message_hash) + VALUES (?, ?, ?, ?, ?, ?) `); - const updatePopularity = db.prepare(` - UPDATE games SET popularity_score = popularity_score + ? WHERE id = ? + const checkDuplicate = db.prepare(` + SELECT id FROM chat_logs WHERE message_hash = ? LIMIT 1 + `); + + const updateUpvote = db.prepare(` + UPDATE games SET upvotes = upvotes + 1, popularity_score = popularity_score + 1 WHERE id = ? + `); + + const updateDownvote = db.prepare(` + UPDATE games SET downvotes = downvotes + 1, popularity_score = popularity_score - 1 WHERE id = ? `); const processVotes = db.transaction((messages) => { @@ -301,6 +323,16 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { continue; } + // Create hash for this message + const messageHash = createMessageHash(username, message, timestamp); + + // Check if we've already imported this exact message + const existing = checkDuplicate.get(messageHash); + if (existing) { + duplicatesSkipped++; + continue; // Skip this duplicate message + } + // Check for vote patterns let vote = null; if (message.includes('thisgame++')) { @@ -309,13 +341,14 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { vote = 'thisgame--'; } - // Insert into chat logs + // Insert into chat logs with hash insertChatLog.run( - req.params.id, + sessionId, username, message, timestamp, - vote + vote, + messageHash ); if (vote) { @@ -345,8 +378,24 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { } if (matchedGame) { - const points = vote === 'thisgame++' ? 1 : -1; - updatePopularity.run(points, matchedGame.game_id); + const isUpvote = vote === 'thisgame++'; + + // Debug: track this vote match + voteMatches.push({ + username: username, + vote: vote, + timestamp: timestamp, + timestamp_ms: messageTime, + matched_game: matchedGame.title, + game_played_at: matchedGame.played_at, + game_played_at_ms: new Date(matchedGame.played_at).getTime() + }); + + if (isUpvote) { + updateUpvote.run(matchedGame.game_id); + } else { + updateDownvote.run(matchedGame.game_id); + } if (!votesByGame[matchedGame.game_id]) { votesByGame[matchedGame.game_id] = { @@ -356,7 +405,7 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { }; } - if (points > 0) { + if (isUpvote) { votesByGame[matchedGame.game_id].upvotes++; } else { votesByGame[matchedGame.game_id].downvotes++; @@ -373,8 +422,17 @@ router.post('/:id/chat-import', authenticateToken, (req, res) => { res.json({ message: 'Chat log imported and processed successfully', messagesImported: chatData.length, + duplicatesSkipped, votesProcessed, - votesByGame + votesByGame, + debug: { + sessionGamesTimeline: sessionGames.map(sg => ({ + title: sg.title, + played_at: sg.played_at, + played_at_ms: new Date(sg.played_at).getTime() + })), + voteMatches: voteMatches + } }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/backend/routes/stats.js b/backend/routes/stats.js index a83fe03..a0b1e52 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -14,14 +14,14 @@ router.get('/', (req, res) => { activeSessions: db.prepare('SELECT COUNT(*) as count FROM sessions WHERE is_active = 1').get(), totalGamesPlayed: db.prepare('SELECT COUNT(*) as count FROM session_games').get(), mostPlayedGames: db.prepare(` - SELECT g.id, g.title, g.pack_name, g.play_count, g.popularity_score + SELECT g.id, g.title, g.pack_name, g.play_count, g.popularity_score, g.upvotes, g.downvotes FROM games g WHERE g.play_count > 0 ORDER BY g.play_count DESC LIMIT 10 `).all(), topRatedGames: db.prepare(` - SELECT g.id, g.title, g.pack_name, g.play_count, g.popularity_score + SELECT g.id, g.title, g.pack_name, g.play_count, g.popularity_score, g.upvotes, g.downvotes FROM games g WHERE g.popularity_score > 0 ORDER BY g.popularity_score DESC diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 04dbd1c..d90b042 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,7 +30,7 @@ function App() {
{branding.app.name}
-
JGP
+
Jackbox Game Picker
{/* Desktop Navigation Links */} @@ -167,11 +167,11 @@ function App() { {/* Footer */}