From ff4d33cee8d9ef0b480fdfef81cb9e4f47716722 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 10:39:13 +0000 Subject: [PATCH 1/2] Initial plan From 7abfa6e0ae38db321f76a96351b35e8238eacd21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 10:42:19 +0000 Subject: [PATCH 2/2] feat: add zero-dependency ChessLens dashboard server Agent-Logs-Url: https://github.com/Andy1Blue/ChessLens/sessions/25a2c0b2-0b3c-42d3-a4e0-455f2da14164 Co-authored-by: Andy1Blue <34073209+Andy1Blue@users.noreply.github.com> --- README.md | 21 +++- index.js | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 index.js diff --git a/README.md b/README.md index 1221035..4fcbe44 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # ChessLens -♟ChessLens pulls live data from the chess.com Public API and renders a beautiful, mobile-ready stats dashboard — no npm install, no bundler, no framework. Just Node.js and a browser. + +♟ ChessLens is a zero-dependency Node.js web app that fetches live data from the chess.com Public API and renders a dark, mobile-responsive stats dashboard. + +## Run + +```bash +node index.js +``` + +Then open: + +- `http://localhost:3000/` +- `http://localhost:3000/?u=` (example: `?u=hikaru`) + +## Dashboard data + +- Bullet / Blitz / Rapid / Daily ratings +- W/D/L record with per-mode win-rate progress bars +- Tactics best score +- Puzzle Rush best score diff --git a/index.js b/index.js new file mode 100644 index 0000000..80c8aa1 --- /dev/null +++ b/index.js @@ -0,0 +1,304 @@ +const http = require('http'); +const https = require('https'); +const { URL } = require('url'); + +const PORT = process.env.PORT || 3000; + +function safeText(value = '') { + return String(value).replace(/[&<>'"]/g, (char) => { + switch (char) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case "'": + return '''; + case '"': + return '"'; + default: + return char; + } + }); +} + +function requestJson(url) { + return new Promise((resolve, reject) => { + https + .get( + url, + { + headers: { + 'User-Agent': 'ChessLens/1.0 (+https://github.com/Andy1Blue/ChessLens)', + Accept: 'application/json' + } + }, + (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error(`Chess.com API returned ${res.statusCode}`)); + return; + } + + try { + resolve(JSON.parse(data)); + } catch (error) { + reject(new Error('Invalid JSON response from Chess.com API')); + } + }); + } + ) + .on('error', (error) => { + reject(new Error(`Unable to reach Chess.com API: ${error.message}`)); + }); + }); +} + +function extractMode(stats, key) { + const section = stats[key] || {}; + const record = section.record || {}; + const win = Number(record.win || 0); + const loss = Number(record.loss || 0); + const draw = Number(record.draw || 0); + const total = win + loss + draw; + const winRate = total > 0 ? Math.round((win / total) * 100) : 0; + + return { + rating: section.last && section.last.rating ? section.last.rating : '—', + win, + draw, + loss, + winRate + }; +} + +function renderDashboard(username, stats, errorMessage) { + const cleanUser = safeText(username || ''); + const modes = { + Bullet: extractMode(stats || {}, 'chess_bullet'), + Blitz: extractMode(stats || {}, 'chess_blitz'), + Rapid: extractMode(stats || {}, 'chess_rapid'), + Daily: extractMode(stats || {}, 'chess_daily') + }; + + const tactics = stats && stats.tactics && stats.tactics.highest ? stats.tactics.highest.rating : '—'; + const puzzleRush = stats && stats.puzzle_rush && stats.puzzle_rush.best ? stats.puzzle_rush.best.score : '—'; + + const cards = Object.entries(modes) + .map( + ([label, mode]) => ` +
+

${label}

+
${safeText(mode.rating)}
+

W/D/L: ${mode.win}/${mode.draw}/${mode.loss}

+ +

Win Rate: ${mode.winRate}%

+
+ ` + ) + .join(''); + + return ` + + + + + ChessLens + + + +
+

ChessLens

+

Live chess.com stats dashboard

+ +
+ + +
+ + ${errorMessage ? `

${safeText(errorMessage)}

` : ''} + + ${username ? ` +
+
+

Player

+
${cleanUser}
+

Data source: chess.com public API

+
+
+

Tactics Best

+
${safeText(tactics)}
+
+
+

Puzzle Rush Best

+
${safeText(puzzleRush)}
+
+
+
${cards}
+ ` : '

Pass a username via ?u=<nick> or use the input above.

'} +
+ +`; +} + +const server = http.createServer(async (req, res) => { + const requestUrl = new URL(req.url, `http://${req.headers.host}`); + const username = (requestUrl.searchParams.get('u') || '').trim(); + + let stats = null; + let errorMessage = ''; + + if (username) { + try { + stats = await requestJson(`https://api.chess.com/pub/player/${encodeURIComponent(username)}/stats`); + } catch (error) { + errorMessage = error.message; + } + } + + const html = renderDashboard(username, stats, errorMessage); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(html); +}); + +server.listen(PORT, () => { + console.log(`ChessLens running on http://localhost:${PORT}`); +});