feat: shared visual layer + redesigned topbar (#4)
Introduce new design token set (paper/ink/line/accent + radius/shadow) with backward-compat aliases for legacy --surface/--navy/--text names. Swap DM Sans for Plus Jakarta Sans, add JetBrains Mono with tabular numerics. Replace .app-header with sticky .topbar partial (brand + segmented nav + Next update / Last refresh meta + Refresh all button). Add POST /api/refresh-all that runs refreshAllPlayersInDB() with an in-memory mutex and returns the rendered topbar so HTMX can swap it in. "Next update" is computed as first Tuesday of next month (approximation of PDGA's monthly cycle). "Last refresh" derives from MAX(players.last_updated).
This commit is contained in:
+13
-1
@@ -185,6 +185,17 @@ function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLastRefresh(callback) {
|
||||
db.get(
|
||||
'SELECT MAX(last_updated) AS lastRefresh FROM players',
|
||||
[],
|
||||
(err, row) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, row ? row.lastRefresh : null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPlayerFromDB,
|
||||
savePlayerToDB,
|
||||
@@ -194,5 +205,6 @@ module.exports = {
|
||||
getLastRoundUpdateDate,
|
||||
updateLastRoundUpdateDate,
|
||||
saveRoundHistoryToDB,
|
||||
savePredictedRatingToDB
|
||||
savePredictedRatingToDB,
|
||||
getLastRefresh
|
||||
};
|
||||
|
||||
+7
-4
@@ -1,12 +1,15 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getTopbarLocals } = require('../services/topbar-service');
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
router.get('/', async (req, res) => {
|
||||
const topbar = await getTopbarLocals();
|
||||
res.render('index', { activePage: 'players', ...topbar });
|
||||
});
|
||||
|
||||
router.get('/courses', (req, res) => {
|
||||
res.render('courses');
|
||||
router.get('/courses', async (req, res) => {
|
||||
const topbar = await getTopbarLocals();
|
||||
res.render('courses', { activePage: 'courses', ...topbar });
|
||||
});
|
||||
|
||||
// Keep old URL working
|
||||
|
||||
@@ -6,9 +6,29 @@ const { fetchPlayerDataHTTP, parsePlayerData, fetchRatingHistory, parseRatingHis
|
||||
const { getOfficialRatingHistory, getOptimizedPlayerRounds } = require('../scrapers/player-puppeteer');
|
||||
const { launchBrowser } = require('../scrapers/browser');
|
||||
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
|
||||
const { getTopbarLocals } = require('../services/topbar-service');
|
||||
const { calculatePredictedRating } = require('../services/rating-calculator');
|
||||
const logger = require('../logger');
|
||||
|
||||
let refreshInProgress = false;
|
||||
|
||||
router.post('/api/refresh-all', async (req, res) => {
|
||||
if (refreshInProgress) {
|
||||
logger.info('refresh-all already in progress, rejecting');
|
||||
return res.status(409).json({ error: 'Refresh already in progress' });
|
||||
}
|
||||
refreshInProgress = true;
|
||||
try {
|
||||
await refreshAllPlayersInDB();
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'refresh-all failed');
|
||||
} finally {
|
||||
refreshInProgress = false;
|
||||
}
|
||||
const locals = await getTopbarLocals();
|
||||
res.render('../partials/topbar', { activePage: req.query.page || 'players', ...locals });
|
||||
});
|
||||
|
||||
router.get('/partials/ratings-table', async (req, res) => {
|
||||
try {
|
||||
const ratings = await getAllRatingsFromDB();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const { getLastRefresh } = require('../models/player');
|
||||
const logger = require('../logger');
|
||||
|
||||
function formatRelative(isoString) {
|
||||
if (!isoString) return 'Never';
|
||||
const then = new Date(isoString.replace(' ', 'T') + (isoString.endsWith('Z') ? '' : 'Z'));
|
||||
const diffMs = Date.now() - then.getTime();
|
||||
if (Number.isNaN(diffMs) || diffMs < 0) return 'Just now';
|
||||
const sec = Math.floor(diffMs / 1000);
|
||||
if (sec < 60) return 'Just now';
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min} min ago`;
|
||||
const hr = Math.floor(min / 60);
|
||||
if (hr < 24) return `${hr} h ago`;
|
||||
const day = Math.floor(hr / 24);
|
||||
if (day === 1) return 'Yesterday';
|
||||
if (day < 7) return `${day} days ago`;
|
||||
return then.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// First Tuesday of next month — approximation of PDGA's monthly cycle
|
||||
function computeNextUpdate(now = new Date()) {
|
||||
const year = now.getUTCFullYear();
|
||||
const month = now.getUTCMonth() + 1; // next month, may roll over
|
||||
const candidate = new Date(Date.UTC(month === 12 ? year + 1 : year, month === 12 ? 0 : month, 1));
|
||||
// 0=Sun, 1=Mon, 2=Tue
|
||||
const offset = (2 - candidate.getUTCDay() + 7) % 7;
|
||||
candidate.setUTCDate(1 + offset);
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return `${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][candidate.getUTCDay()]} ${candidate.getUTCDate()} ${months[candidate.getUTCMonth()]}`;
|
||||
}
|
||||
|
||||
async function getTopbarLocals() {
|
||||
try {
|
||||
const lastIso = await new Promise((resolve, reject) => {
|
||||
getLastRefresh((err, val) => (err ? reject(err) : resolve(val)));
|
||||
});
|
||||
return { lastRefresh: formatRelative(lastIso), nextUpdate: computeNextUpdate() };
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'topbar locals fallback');
|
||||
return { lastRefresh: 'Unknown', nextUpdate: computeNextUpdate() };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getTopbarLocals, formatRelative, computeNextUpdate };
|
||||
Reference in New Issue
Block a user