diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cfbf26..419f8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog – helldivers-trainer +## [2.1.1] – 2026-03-31 + +### Fixed +- **Speedrun scores never saved**: Individual scores were blocked by `if (mode !== 'speedrun')` guard; now saved per-stratagem like other modes. Final bulk-save with invalid `__speedrun__` stratagem name removed (was causing 400 errors). +- **Dashboard recent-sessions icons missing**: `r.icon` was always `undefined` since the server doesn't store icon paths in the DB. Now looks up icon from `state.stratagems` by name. +- **Variable shadowing in `renderDashboard`**: Outer `const r = eloRankFor(...)` was shadowed by `recent.map(r => ...)`. Renamed outer to `myRank`. +- **N+1 DB queries in `broadcastLobbyUpdate`**: Was running one `SELECT elo` per online user. Now batches into a single `WHERE username IN (...)` query. +- **N+1 DB queries in dashboard endpoint**: Same pattern fixed for the `/api/dashboard` online-users list. +- **Unnecessary `typeof` guards**: `typeof u === 'object'` checks in lobby/dashboard removed — `lobby-update` always sends objects, never plain strings. +- **Duplicate `else if (mode === 'endless')` branch in `nextStratagem`**: Was identical to the `else` branch below it; merged. + +--- + ## [2.1.0] – 2026-03-31 ### Added diff --git a/public/app.js b/public/app.js index f14958d..f73d037 100644 --- a/public/app.js +++ b/public/app.js @@ -364,11 +364,11 @@ async function loadDashboard() { } function renderDashboard({ stats, rank, elo, eloRank: rankLabel, online, recent, daily }) { - const r = eloRankFor(elo || 1000); + const myRank = eloRankFor(elo || 1000); setText('dash-hero-name', state.user.user); - setText('dash-rank-label', rankLabel || r.label); + setText('dash-rank-label', rankLabel || myRank.label); setText('dash-elo', elo || 1000); - setText('dash-rank-icon', r.icon); + setText('dash-rank-icon', myRank.icon); setText('dash-total-score', stats.totalScore || 0); setText('dash-rank', rank ? '#' + rank.position : 'Unranked'); @@ -389,14 +389,15 @@ function renderDashboard({ stats, rank, elo, eloRank: rankLabel, online, recent, if (!recent?.length) { tbody.innerHTML = 'No sessions yet'; } else { - tbody.innerHTML = recent.map(r => - ` - ${esc(r.stratagem)} + tbody.innerHTML = recent.map(r => { + const icon = state.stratagems.find(s => s.name === r.stratagem)?.icon || ''; + return ` + ${esc(r.stratagem)} ${esc(r.mode || 'timed')} ${r.score} ${(r.time_ms / 1000).toFixed(2)}s - ` - ).join(''); + `; + }).join(''); } updateDashboardOnline(online); @@ -412,23 +413,18 @@ function renderDailySequencePreview(sequence) { function updateDashboardOnline(online) { const el = document.getElementById('dash-online'); if (!el) return; - const players = (online || []).filter(u => { - const name = typeof u === 'object' ? u.name : u; - return name !== state.user?.user; - }); + const players = (online || []).filter(u => u.name !== state.user?.user); if (!players.length) { el.innerHTML = 'No other Helldivers online'; } else { - el.innerHTML = players.map(u => { - const name = typeof u === 'object' ? u.name : u; - const elo = typeof u === 'object' ? u.elo : ''; - return `
+ el.innerHTML = players.map(u => + `
- ${esc(name)} - ${elo ? `${elo}` : ''} - -
`; - }).join(''); + ${esc(u.name)} + ${u.elo ? `${u.elo}` : ''} + +
` + ).join(''); } } @@ -602,19 +598,13 @@ function nextStratagem() { if (!p.speedrunPool.length) { const totalMs = Date.now() - p.speedrunStart; clearInterval(p.timerHandle); - api('POST', '/scores/practice', { - stratagem: '__speedrun__', - category: 'All', - time_ms: totalMs, - score: p.score, - mode: 'speedrun', - }).catch(() => {}); showToast(`Speedrun complete! ${(totalMs / 1000).toFixed(2)}s`); openSessionSummary(); return; } strat = p.speedrunPool[0]; } else { + // timed & endless: pick random from pool const pool = getPool(); if (!pool.length) { showPracticeIdle(); return; } strat = pool[Math.floor(Math.random() * pool.length)]; @@ -836,15 +826,13 @@ function handlePracticeInput(dir) { // Score popup showScorePopup('+' + pts); - if (mode !== 'speedrun') { - api('POST', '/scores/practice', { - stratagem: p.current.name, - category: p.current.category, - time_ms: elapsed, - score: pts, - mode: mode, - }).catch(() => {}); - } + api('POST', '/scores/practice', { + stratagem: p.current.name, + category: p.current.category, + time_ms: elapsed, + score: pts, + mode: mode, + }).catch(() => {}); if (mode === 'drill') { p.drillPool.shift(); @@ -995,10 +983,7 @@ document.getElementById('btn-summary-restart')?.addEventListener('click', () => // ── Lobby ───────────────────────────────────────────────────────────────────── function updateLobbyView() { - const others = state.lobby.online.filter(u => { - const name = typeof u === 'object' ? u.name : u; - return name !== state.user?.user; - }); + const others = state.lobby.online.filter(u => u.name !== state.user?.user); const el = document.getElementById('lobby-players'); if (!el) return; @@ -1008,17 +993,14 @@ function updateLobbyView() {

No other Helldivers online.
Waiting for reinforcements...

`; } else { - el.innerHTML = others.map(u => { - const name = typeof u === 'object' ? u.name : u; - const elo = typeof u === 'object' ? u.elo : ''; - const rank = typeof u === 'object' ? u.rank : ''; - return `
+ el.innerHTML = others.map(u => + `
- ${esc(name)} - ${elo ? `${esc(rank)} · ${elo}` : ''} - -
`; - }).join(''); + ${esc(u.name)} + ${u.elo ? `${esc(u.rank)} · ${u.elo}` : ''} + +
` + ).join(''); } const challEl = document.getElementById('lobby-challenges'); diff --git a/server.js b/server.js index a1255cf..6ee3897 100644 --- a/server.js +++ b/server.js @@ -418,9 +418,15 @@ app.get('/api/dashboard', requireAuth, (req, res) => { const userRow = db.prepare('SELECT elo FROM users WHERE username = ?').get(u); const elo = userRow?.elo ?? 1000; - const onlineWithElo = [...userSockets.keys()].map(name => { - const row = db.prepare('SELECT elo FROM users WHERE username = ?').get(name); - return { name, elo: row?.elo ?? 1000, rank: eloRank(row?.elo ?? 1000) }; + const onlineNames = [...userSockets.keys()]; + const eloRows = onlineNames.length > 0 + ? db.prepare(`SELECT username, elo FROM users WHERE username IN (${onlineNames.map(() => '?').join(',')})`) + .all(...onlineNames) + : []; + const eloByUser = Object.fromEntries(eloRows.map(r => [r.username, r.elo])); + const onlineWithElo = onlineNames.map(name => { + const elo = eloByUser[name] ?? 1000; + return { name, elo, rank: eloRank(elo) }; }); res.json({ @@ -562,9 +568,17 @@ async function main() { } function broadcastLobbyUpdate() { - const online = [...userSockets.keys()].map(name => { - const row = db.prepare('SELECT elo FROM users WHERE username = ?').get(name); - return { name, elo: row?.elo ?? 1000, rank: eloRank(row?.elo ?? 1000) }; + const onlineNames = [...userSockets.keys()]; + const eloMap = onlineNames.length > 0 + ? Object.fromEntries( + db.prepare(`SELECT username, elo FROM users WHERE username IN (${onlineNames.map(() => '?').join(',')})`) + .all(...onlineNames) + .map(r => [r.username, r.elo]) + ) + : {}; + const online = onlineNames.map(name => { + const elo = eloMap[name] ?? 1000; + return { name, elo, rank: eloRank(elo) }; }); wss.clients.forEach(ws => { if (ws.readyState !== WebSocket.OPEN || !ws.userId) return;