feat(dashboard): add WYSIWYG-style command palette, whiteboard, markdown widget, collapsible panels, and live widgets
This commit is contained in:
parent
cc677eecb1
commit
11156be20e
|
|
@ -1,84 +1,309 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Stackchain Dashboard</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
||||
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display:flex; gap: 16px; align-items: center; justify-content: space-between; position: sticky; top: 0; background: inherit; z-index: 5; }
|
||||
main { display: grid; grid-template-columns: 280px 1fr 320px; gap: 12px; padding: 12px; }
|
||||
@media (max-width: 980px) { main { grid-template-columns: 1fr; } }
|
||||
aside, section { border: 1px solid #ddd; border-radius: 10px; padding: 10px; background: #fff; }
|
||||
aside { background: #fafafa; }
|
||||
h2 { font-size: 13px; margin: 8px 0; text-transform: uppercase; letter-spacing: .08em; color: #374151; }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
.muted { color: #6b7280; }
|
||||
.pill { display:inline-block; padding: 2px 8px; border-radius: 999px; background:#e5e7eb; color:#111; font-size: 12px; }
|
||||
.btn { padding: 8px 10px; border-radius: 8px; border: 1px solid #ccc; background:#fff; cursor: pointer; }
|
||||
.suggestion { padding: 8px; border-radius: 8px; border: 1px solid #e5e7eb; background: #fff; margin-bottom: 8px; }
|
||||
.suggestion.high { border-color: #fca5a5; background: #fef2f2; }
|
||||
.suggestion.medium { border-color: #fcd34d; background: #fffbeb; }
|
||||
.suggestion.low { border-color: #d1fae5; background: #ecfdf5; }
|
||||
#nav .kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; }
|
||||
</style>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Stackchain Dashboard</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
||||
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display:flex; gap: 16px; align-items: center; justify-content: space-between; position: sticky; top: 0; background: inherit; z-index: 20; }
|
||||
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||||
.panel { border: 1px solid #ddd; border-radius: 10px; padding: 10px; background: #fff; }
|
||||
aside, section { background: #fff; }
|
||||
.sidebar { background: #fafafa; }
|
||||
h2 { font-size: 13px; margin: 8px 0; text-transform: uppercase; letter-spacing: .08em; color: #374151; }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
.muted { color: #6b7280; }
|
||||
.pill { display:inline-block; padding: 2px 8px; border-radius: 999px; background:#e5e7eb; color:#111; font-size: 12px; }
|
||||
.kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; }
|
||||
.kv .label { color: #6b7280; }
|
||||
.kv .value { color: #111; }
|
||||
main { display: grid; grid-template-columns: 260px 1fr 300px; gap: 12px; padding: 12px; }
|
||||
@media (max-width: 980px) { main { grid-template-columns: 1fr; } }
|
||||
.collapsible summary { cursor: pointer; font-weight: 600; }
|
||||
.suggestion { padding: 8px; border-radius: 8px; border: 1px solid #e5e7eb; background: #fff; margin-bottom: 8px; }
|
||||
.suggestion.high { border-color: #fca5a5; background: #fef2f2; }
|
||||
.suggestion.medium { border-color: #fcd34d; background: #fffbeb; }
|
||||
.suggestion.low { border-color: #d1fae5; background: #ecfdf5; }
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.stack { display: flex; flex-direction: column; gap: 10px; }
|
||||
.widget { border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px; background: #fff; }
|
||||
.widget h3 { margin: 4px 0 8px; font-size: 13px; color: #374151; }
|
||||
canvas { display: block; width: 100%; height: 260px; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; cursor: crosshair; }
|
||||
pre { margin: 0; padding: 8px; background: #0f172a; color: #e2e8f0; border-radius: 8px; overflow: auto; max-height: 220px; font-size: 12px; }
|
||||
input[type="text"], textarea { width: 100%; padding: 8px; border-radius: 8px; border: 1px solid #ccc; }
|
||||
textarea { resize: vertical; min-height: 120px; }
|
||||
#cmd-palette { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: min(720px, 94vw); background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; box-shadow: 0 20px 50px rgba(0,0,0,.25); padding: 10px; z-index: 30; display: none; }
|
||||
#cmd-palette.open { display: block; }
|
||||
.cmd-item { padding: 8px; cursor: pointer; border-radius: 8px; }
|
||||
.cmd-item:hover { background: #f3f4f6; }
|
||||
#whiteboard-modal, #markdown-modal { position: fixed; inset: 0; background: rgba(15,23,42,.35); display: none; align-items: center; justify-content: center; z-index: 40; }
|
||||
#whiteboard-modal.open, #markdown-modal.open { display: flex; }
|
||||
.modal { background: #fff; border-radius: 12px; padding: 14px; width: min(980px, 94vw); max-height: 92vh; overflow: auto; }
|
||||
.modal-actions { display: flex; gap: 10px; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.badge { font-size: 11px; padding: 4px 8px; border-radius: 999px; background: #e5e7eb; color: #111; }
|
||||
.small { font-size: 12px; color: #6b7280; }
|
||||
.status { display:inline-flex; gap:6px; align-items:center; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div><strong>Stackchain</strong> <span class="muted">Dashboard</span></div>
|
||||
<header>
|
||||
<div><strong>Stackchain</strong> <span class="muted">Dashboard</span></div>
|
||||
<div class="toolbar">
|
||||
<div class="status"><span class="dot"></span><span class="small" id="status">Live</span></div>
|
||||
<button class="btn" id="refresh">Refresh</button>
|
||||
</header>
|
||||
<main>
|
||||
<aside id="nav"><h2>Context</h2><div class="muted">Loading…</div></aside>
|
||||
<section id="work">
|
||||
<h2>Work</h2>
|
||||
<div id="issues"><div class="muted">Loading…</div></div>
|
||||
<hr />
|
||||
<div id="prs"><div class="muted">Loading…</div></div>
|
||||
</section>
|
||||
<aside id="ai"><h2>Live AI</h2><div class="muted">Loading…</div></aside>
|
||||
</main>
|
||||
<script>
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/context', { headers: { 'Accept': 'application/json' } });
|
||||
const data = await res.json();
|
||||
const nav = document.getElementById('nav');
|
||||
nav.innerHTML = '<h2>Context</h2>' +
|
||||
'<div class="kv"><div>User</div><div>' + (data.user.full_name || data.user.login) + '</div>' +
|
||||
'<div>Repos</div><div>' + data.repos.length + '</div>' +
|
||||
'<div>Issues</div><div>' + data.issues.length + '</div>' +
|
||||
'<div>PRs</div><div>' + data.pull_requests.length + '</div></div>';
|
||||
<button class="btn" id="open-palette">Command</button>
|
||||
<span class="small muted" id="clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
const issuesBox = document.getElementById('issues');
|
||||
const openIssues = data.issues.filter(i => i.state === 'open').slice(0, 10);
|
||||
issuesBox.innerHTML = '<h2>Open Issues</h2>' + (openIssues.length ? openIssues.map(i =>
|
||||
'<div><a href="' + i.url + '">#' + i.number + ' ' + i.title + '</a>' +
|
||||
'<div class="muted">' + (i.labels || []).map(l => '<span class="pill">' + l + '</span>').join(' ') + '</div></div>'
|
||||
).join('') : '<div class="muted">No open issues.</div>');
|
||||
<main>
|
||||
<aside class="sidebar panel stack" id="nav">
|
||||
<details open>
|
||||
<summary>Context</summary>
|
||||
<div class="muted" id="context">Loading…</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>View</summary>
|
||||
<div class="muted" id="view-hint">—</div>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
const prsBox = document.getElementById('prs');
|
||||
const openPrs = data.pull_requests.slice(0, 10);
|
||||
prsBox.innerHTML = '<h2>Pull Requests</h2>' + (openPrs.length ? openPrs.map(p =>
|
||||
'<div><a href="' + p.url + '">#' + p.number + ' ' + p.title + '</a>' +
|
||||
'<div class="muted">' + p.state + ' by ' + p.user + '</div></div>'
|
||||
).join('') : '<div class="muted">No PRs.</div>');
|
||||
<section class="stack" id="work">
|
||||
<div class="panel" id="issues">
|
||||
<h2>Issues</h2>
|
||||
<div class="muted">Loading…</div>
|
||||
</div>
|
||||
<div class="panel" id="prs">
|
||||
<h2>Pull Requests</h2>
|
||||
<div class="muted">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
const ai = document.getElementById('ai');
|
||||
const deltas = data.deltas || [];
|
||||
ai.innerHTML = '<h2>Live AI</h2>' + (deltas.length ? deltas.map(d =>
|
||||
'<div class="suggestion ' + d.priority + '"><span class="pill">' + d.priority + '</span> <strong>' + d.action + '</strong> ' + d.target + '<div class="muted">' + d.panel + '</div></div>'
|
||||
).join('') : '<div class="muted">No suggestions yet.</div>');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
<aside class="sidebar panel stack" id="right">
|
||||
<details open>
|
||||
<summary>AI</summary>
|
||||
<div id="ai" class="muted">Loading…</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Widgets</summary>
|
||||
<div id="widgets" class="stack"></div>
|
||||
</details>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
||||
<input id="cmd-input" type="text" placeholder="Type a command, e.g. whiteboard, markdown..." />
|
||||
<div id="cmd-results" class="stack" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="whiteboard-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-actions">
|
||||
<strong>Whiteboard</strong>
|
||||
<div>
|
||||
<span class="badge">Click & drag to draw</span>
|
||||
<button class="btn" id="close-whiteboard">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="wb"></canvas>
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<button class="btn" id="wb-clear">Clear</button>
|
||||
<button class="btn" id="wb-save">Save PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="markdown-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-actions">
|
||||
<strong>Markdown Widget</strong>
|
||||
<button class="btn" id="close-markdown">Close</button>
|
||||
</div>
|
||||
<textarea id="md-input" placeholder="# Write markdown here... - bullet - **bold** - `code`"></textarea>
|
||||
<div id="md-preview" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const API_BASE = '';
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const fmt = (d) => new Date(d).toLocaleString();
|
||||
|
||||
function setStatus(msg) {
|
||||
qs('#status').textContent = msg || 'Live';
|
||||
}
|
||||
function setClock() {
|
||||
qs('#clock').textContent = fmt(new Date());
|
||||
}
|
||||
setClock();
|
||||
setInterval(setClock, 1000);
|
||||
|
||||
async function load() {
|
||||
setStatus('Loading…');
|
||||
try {
|
||||
const res = await fetch('/api/v1/context', { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
qs('#context').innerHTML = '<div class="kv"><div class="label">User</div><div class="value">' + escapeHtml(data.user.full_name || data.user.login) + '</div>' +
|
||||
'<div class="label">Repos</div><div class="value">' + data.repos.length + '</div>' +
|
||||
'<div class="label">Issues</div><div class="value">' + data.issues.length + '</div>' +
|
||||
'<div class="label">PRs</div><div class="value">' + data.pull_requests.length + '</div></div>';
|
||||
const view = data.view || 'dashboard';
|
||||
qs('#view-hint').textContent = 'Active view: ' + view;
|
||||
|
||||
const issuesBox = qs('#issues');
|
||||
const openIssues = (data.issues || []).filter(i => i.state === 'open').slice(0, 12);
|
||||
issuesBox.innerHTML = '<h2>Issues</h2>' + (openIssues.length ? openIssues.map(i => '<div style="margin:6px 0;"><a href="' + escAttr(i.url) + '">#' + i.number + ' ' + escapeHtml(i.title) + '</a>' +
|
||||
'<div class="muted">' + (i.labels || []).map(l => '<span class="pill">' + escapeHtml(String(l)) + '</span>').join(' ') + '</div></div>').join('') : '<div class="muted">No open issues.</div>');
|
||||
issuesBox.querySelectorAll('a').forEach(a => a.target = '_blank');
|
||||
|
||||
const prsBox = qs('#prs');
|
||||
const openPrs = (data.pull_requests || []).slice(0, 12);
|
||||
prsBox.innerHTML = '<h2>Pull Requests</h2>' + (openPrs.length ? openPrs.map(p => '<div style="margin:6px 0;"><a href="' + escAttr(p.url) + '">#' + p.number + ' ' + escapeHtml(p.title) + '</a>' +
|
||||
'<div class="muted">' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user || '')) + '</div></div>').join('') : '<div class="muted">No PRs.</div>');
|
||||
prsBox.querySelectorAll('a').forEach(a => a.target = '_blank');
|
||||
|
||||
const ai = qs('#ai');
|
||||
const deltas = data.deltas || [];
|
||||
ai.innerHTML = deltas.length ? deltas.map(d => '<div class="suggestion ' + d.priority + '"><span class="pill">' + escapeHtml(d.priority) + '</span> <strong>' + escapeHtml(d.action) + '</strong> ' + escapeHtml(d.target || '') + '<div class="muted">' + escapeHtml(d.panel) + '</div></div>').join('') : '<div class="muted">No suggestions yet.</div>';
|
||||
|
||||
setStatus('Live · updated just now');
|
||||
setClock();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus('Unavailable');
|
||||
qs('#context').innerHTML = '<div class="muted">Could not reach backend.</div>';
|
||||
}
|
||||
load();
|
||||
document.getElementById('refresh').addEventListener('click', load);
|
||||
setInterval(load, 8000);
|
||||
</script>
|
||||
}
|
||||
|
||||
function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c])); }
|
||||
function escAttr(s) { return escapeHtml(s); }
|
||||
|
||||
function openModal(id) { qs('#' + id).classList.add('open'); }
|
||||
function closeModal(id) { qs('#' + id).classList.remove('open'); }
|
||||
|
||||
/* Whiteboard */
|
||||
function initWhiteboard() {
|
||||
const canvas = qs('#wb');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let drawing = false;
|
||||
function resize() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
canvas.width = r.width * dpr;
|
||||
canvas.height = r.height * dpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.strokeStyle = '#111';
|
||||
ctx.lineWidth = 2;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
const p = (e) => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||
};
|
||||
canvas.addEventListener('pointerdown', (e) => { drawing = true; const pos = p(e); ctx.beginPath(); ctx.moveTo(pos.x, pos.y); });
|
||||
canvas.addEventListener('pointermove', (e) => { if (!drawing) return; const pos = p(e); ctx.lineTo(pos.x, pos.y); ctx.stroke(); });
|
||||
canvas.addEventListener('pointerup', () => { drawing = false; });
|
||||
canvas.addEventListener('pointerleave', () => { drawing = false; });
|
||||
qs('#wb-clear').addEventListener('click', () => { const r = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, r.width, r.height); });
|
||||
qs('#wb-save').addEventListener('click', () => { const a = document.createElement('a'); a.href = canvas.toDataURL(); a.download = 'whiteboard.png'; a.click(); });
|
||||
}
|
||||
initWhiteboard();
|
||||
|
||||
/* Markdown widget */
|
||||
qs('#close-markdown').addEventListener('click', () => closeModal('markdown-modal'));
|
||||
qs('#md-input').addEventListener('input', () => renderMarkdown());
|
||||
function renderMarkdown() {
|
||||
const raw = qs('#md-input').value || '';
|
||||
const html = raw
|
||||
.replace(/^#{1,6}\s.*$/gm, (m) => { const lvl = m.match(/^(#{1,6})/)[1].length; return '<h' + lvl + '>' + m.replace(/^#{1,6}\s/, '') + '</h' + lvl + '>'; })
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/^-\s?(.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
.replace(/\n/g, '<br>');
|
||||
qs('#md-preview').innerHTML = '<pre>' + escapeHtml(raw) + '</pre><div style="margin-top:8px;">' + html + '</div>';
|
||||
}
|
||||
|
||||
/* Command palette / widgets */
|
||||
const commands = [
|
||||
{ name: 'Open whiteboard', run: () => { openModal('whiteboard-modal'); initWhiteboard(); resizeWhiteboard(); } },
|
||||
{ name: 'Open markdown widget', run: () => { openModal('markdown-modal'); renderMarkdown(); } },
|
||||
{ name: 'Focus issues', run: () => qs('#issues').scrollIntoView({ behavior: 'smooth', block: 'start' }) },
|
||||
{ name: 'Focus PRs', run: () => qs('#prs').scrollIntoView({ behavior: 'smooth', block: 'start' }) },
|
||||
{ name: 'Focus AI', run: () => qs('#right').scrollIntoView({ behavior: 'smooth', block: 'start' }) },
|
||||
{ name: 'Refresh now', run: load },
|
||||
];
|
||||
function resizeWhiteboard(){ const canvas = qs('#wb'); if(!canvas) return; const r = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = r.width * dpr; canvas.height = r.height * dpr; canvas.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0); canvas.getContext('2d').lineCap = 'round'; canvas.getContext('2d').lineJoin = 'round'; }
|
||||
function renderCommands(filter) {
|
||||
const el = qs('#cmd-results');
|
||||
const term = String(filter || '').toLowerCase();
|
||||
const items = commands.filter(c => c.name.toLowerCase().includes(term));
|
||||
el.innerHTML = items.map((c, idx) => '<div class="cmd-item" data-idx="' + idx + '">' + escapeHtml(c.name) + '</div>').join('');
|
||||
el.querySelectorAll('.cmd-item').forEach((item) => {
|
||||
item.addEventListener('click', () => { commands[Number(item.dataset.idx)].run(); qs('#cmd-palette').classList.remove('open'); qs('#cmd-input').value = ''; });
|
||||
});
|
||||
}
|
||||
qs('#open-palette').addEventListener('click', () => { qs('#cmd-palette').classList.add('open'); qs('#cmd-input').focus(); renderCommands(''); });
|
||||
qs('#cmd-input').addEventListener('input', (e) => renderCommands(e.target.value));
|
||||
document.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); qs('#cmd-palette').classList.toggle('open'); if (qs('#cmd-palette').classList.contains('open')) { qs('#cmd-input').focus(); renderCommands(''); } } });
|
||||
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
|
||||
|
||||
qs('#refresh').addEventListener('click', load);
|
||||
load();
|
||||
setInterval(load, 8000);
|
||||
|
||||
/* Live widgets */
|
||||
const widgetRegistry = {
|
||||
markdown: () => ({ title: 'Markdown', html: '<div class="small">Launches a tiny markdown scratchpad.</div><button class="btn" id="widget-markdown-launch">Open</button>' }),
|
||||
countdown: () => ({ title: 'Live clock', html: '<div class="small" id="widget-clock">—</div>' }),
|
||||
repoAtAGlance: () => ({ title: 'Repo mix', html: '<div class="small" id="repo-mix">Loading…</div>' }),
|
||||
};
|
||||
const widgetOrder = ['countdown', 'repoAtAGlance'];
|
||||
const widgetsEl = qs('#widgets');
|
||||
function paintWidgets() {
|
||||
widgetsEl.innerHTML = widgetOrder.map(k => {
|
||||
const w = widgetRegistry[k]();
|
||||
return '<div class="widget"><h3>' + escapeHtml(w.title) + '</h3>' + w.html + '</div>';
|
||||
}).join('');
|
||||
qs('#widget-markdown-launch')?.addEventListener('click', () => { openModal('markdown-modal'); renderMarkdown(); });
|
||||
}
|
||||
function tickWidgets() {
|
||||
const clock = qs('#widget-clock');
|
||||
if (clock) clock.textContent = fmt(new Date());
|
||||
}
|
||||
paintWidgets();
|
||||
setInterval(tickWidgets, 1000);
|
||||
|
||||
window.repoAtAGlance = async function() {
|
||||
const el = qs('#repo-mix');
|
||||
if (!el) return;
|
||||
try {
|
||||
const res = await fetch('/api/v1/context', { headers: { Accept: 'application/json' } });
|
||||
const data = await res.json();
|
||||
const repos = data.repos || [];
|
||||
const issues = data.issues || [];
|
||||
const prs = data.pull_requests || [];
|
||||
el.innerHTML = '<div class="kv"><div class="label">Repos</div><div class="value">' + repos.length + '</div>' +
|
||||
'<div class="label">Open issues</div><div class="value">' + issues.filter(i => i.state === 'open').length + '</div>' +
|
||||
'<div class="label">Open PRs</div><div class="value">' + prs.length + '</div></div>';
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="muted">Widget unavailable.</div>';
|
||||
}
|
||||
};
|
||||
setInterval(window.repoAtAGlance, 8000);
|
||||
window.repoAtAGlance();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user