Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion src/frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2638,6 +2638,54 @@ function renderSettings(container) {
loadLLMSettings();
}

async function syncLeaderboard() {
var btn = document.getElementById('syncBtn');
if (btn) btn.textContent = 'Syncing...';
try {
var resp = await fetch('/api/leaderboard/sync', { method: 'POST' });
var data = await resp.json();
if (data.ok) {
showToast('Stats synced to global leaderboard!');
loadGlobalLeaderboard();
} else {
showToast('Sync failed: ' + (data.error || 'unknown'));
}
} catch (e) { showToast('Sync error: ' + e.message); }
if (btn) btn.textContent = 'Sync to Global Leaderboard';
}

async function loadGlobalLeaderboard() {
var board = document.getElementById('globalBoard');
if (!board) return;
try {
var resp = await fetch('/api/leaderboard/remote');
var data = await resp.json();
if (!data.users || data.users.length === 0) {
board.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-muted)">No one here yet. Sync your stats to be first!</div>';
return;
}
var html = '';
data.users.forEach(function(u, i) {
var t = u.stats.today || {};
var tot = u.stats.totals || {};
html += '<div class="lb-global-row">';
html += '<span class="lb-rank' + (i < 3 ? ' lb-rank-' + (i+1) : '') + '">#' + (i+1) + '</span>';
html += '<img class="lb-global-avatar" src="' + escHtml(u.avatar || '') + '" alt="">';
html += '<div class="lb-global-info">';
html += '<div class="lb-global-name">' + escHtml(u.name || u.username) + '</div>';
html += '<div class="lb-global-handle">@' + escHtml(u.username) + '</div>';
html += '</div>';
html += '<div class="lb-global-stats">';
html += '<span><strong>' + (t.messages || 0) + '</strong> today</span>';
html += '<span><strong>' + (tot.messages || 0).toLocaleString() + '</strong> total</span>';
html += '<span><strong>' + (tot.hours || 0) + 'h</strong></span>';
if (u.stats.streak > 1) html += '<span class="lb-streak-badge">' + u.stats.streak + 'd streak</span>';
html += '</div></div>';
});
board.innerHTML = html;
} catch { board.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-muted)">Could not load global leaderboard</div>'; }
}

async function githubConnect() {
try {
showToast('Starting GitHub auth...');
Expand Down Expand Up @@ -2779,10 +2827,24 @@ async function renderLeaderboard(container) {
});
html += '</div>';

html += '<div class="lb-footer">Active days: ' + data.activeDays + ' | ID: ' + escHtml(data.anon.name) + '</div>';
// Sync button + Global leaderboard
if (gh.authenticated) {
html += '<div style="text-align:center;margin:20px 0">';
html += '<button class="lb-github-btn" onclick="syncLeaderboard()" id="syncBtn">Sync to Global Leaderboard</button>';
html += '</div>';
}

// Global leaderboard
html += '<div class="lb-section-title">Global Leaderboard</div>';
html += '<div id="globalBoard"><div class="loading">Loading...</div></div>';

html += '<div class="lb-footer">Active days: ' + data.activeDays + ' | <a href="https://codedash-leaderboard.valeriy.workers.dev" target="_blank" style="color:var(--accent-blue)">View public leaderboard</a></div>';
html += '</div>';

container.innerHTML = html;

// Load global leaderboard async
loadGlobalLeaderboard();
} catch (e) {
container.innerHTML = '<div class="empty-state">Failed to load stats: ' + escHtml(e.message) + '</div>';
}
Expand Down
26 changes: 26 additions & 0 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,32 @@ body {
user-select: all;
}

.lb-global-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 6px;
transition: border-color 0.2s;
}
.lb-global-row:hover { border-color: var(--accent-blue); }

.lb-rank { font-size: 16px; font-weight: 700; width: 32px; text-align: center; color: var(--text-muted); }
.lb-rank-1 { color: #ffd700; }
.lb-rank-2 { color: #c0c0c0; }
.lb-rank-3 { color: #cd7f32; }

.lb-global-avatar { width: 40px; height: 40px; border-radius: 50%; border: 2px solid var(--border); }
.lb-global-info { flex: 1; min-width: 0; }
.lb-global-name { font-weight: 600; font-size: 14px; }
.lb-global-handle { font-size: 12px; color: var(--text-muted); }
.lb-global-stats { display: flex; gap: 12px; font-size: 12px; color: var(--text-muted); flex-shrink: 0; }
.lb-global-stats strong { color: var(--text-primary); }
.lb-streak-badge { background: rgba(251,146,60,0.15); color: var(--accent-orange); padding: 2px 8px; border-radius: 10px; font-weight: 600; }

/* ── Changelog ──────────────────────────────────────────────── */

.changelog-container { padding: 20px; max-width: 700px; }
Expand Down
63 changes: 63 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ function startServer(host, port, openBrowser = true) {
json(res, stats);
}

else if (req.method === 'POST' && pathname === '/api/leaderboard/sync') {
syncLeaderboard().then(data => json(res, data)).catch(e => json(res, { error: e.message }, 500));
}

else if (req.method === 'GET' && pathname === '/api/leaderboard/remote') {
fetchRemoteLeaderboard().then(data => json(res, data)).catch(e => json(res, { error: e.message }, 500));
}

// ── GitHub Auth (Device Flow) ────────────
else if (req.method === 'POST' && pathname === '/api/github/device-code') {
githubDeviceCode().then(data => json(res, data)).catch(e => json(res, { error: e.message }, 400));
Expand Down Expand Up @@ -557,6 +565,61 @@ function saveGitHubProfile(profile) {
}
}

// ── Leaderboard Sync ──────────────────────
const LEADERBOARD_API = 'https://codedash-leaderboard.valeriy.workers.dev';

async function syncLeaderboard() {
const profile = loadGitHubProfile();
if (!profile || !profile.authenticated) throw new Error('Connect GitHub first');

const stats = getLeaderboardStats();
const payload = {
username: profile.username,
avatar: profile.avatar,
name: profile.name,
stats: {
today: stats.today,
totals: stats.totals,
agents: stats.agents,
streak: stats.streak,
activeDays: stats.activeDays,
},
};

return new Promise((resolve, reject) => {
const body = JSON.stringify(payload);
const parsed = new URL(LEADERBOARD_API + '/api/stats');
const req = https.request({
hostname: parsed.hostname, path: parsed.pathname, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
timeout: 10000,
}, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try {
const r = JSON.parse(data);
log('SYNC', `Pushed stats to leaderboard as @${profile.username}`);
resolve(r);
} catch { reject(new Error('Bad response')); }
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}

async function fetchRemoteLeaderboard() {
return new Promise((resolve, reject) => {
https.get(LEADERBOARD_API + '/api/leaderboard', { timeout: 10000 }, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error('Parse error')); } });
}).on('error', reject);
});
}

// ── LLM Config ─────────────────────────────

const LLM_CONFIG_FILE = path.join(os.homedir(), '.claude', 'codedash-llm.json');
Expand Down
Loading