Merge pull request 'fix: Render repo mix before populating counts' (#49) from timmy/48-fix-repo-mix-widget-initialization-race into main
All checks were successful
CI / lint (push) Successful in 9s
Release / release-candidate (push) Successful in 4s
CI / build-frontend (push) Successful in 5s

Merge pull request 'fix: Render repo mix before populating counts' (#49) from timmy/48-fix-repo-mix-widget-initialization-race into main
This commit is contained in:
rockachopa 2026-08-05 22:49:54 +00:00
commit 352d70e859
2 changed files with 34 additions and 27 deletions

View File

@ -106,7 +106,16 @@ textarea { resize: vertical; min-height: 120px; }
</details>
<details class="panel" data-panel-key="widgets" open>
<summary><h2>Live Widgets</h2></summary>
<div id="widgets" class="stack"></div>
<div id="widgets" class="stack">
<div class="widget">
<h3>Live clock</h3>
<div class="small" id="widget-clock"></div>
</div>
<div class="widget">
<h3>Repo mix</h3>
<div class="small" id="repo-mix">Loading…</div>
</div>
</div>
</details>
</section>
@ -358,33 +367,20 @@ textarea { resize: vertical; min-height: 120px; }
tickWidgets();
/* Widgets */
const widgetOrder = ['countdown','repoAtAGlance'];
const widgetRegistry = {
countdown: () => ({ title: 'Live clock', html: '<div class="small" id="widget-clock"></div>' }),
repoAtAGlance: async () => {
const el = qs('#repo-mix');
if (!el) return { title:'Repo mix', html:'<div class="small">Unavailable</div>' };
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||[]).filter(i=>i.state==='open');
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.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>'; }
return { title:'Repo mix', html:'<div class="small" id="repo-mix">Loading…</div>' };
},
};
const widgetsEl = qs('#widgets');
function paintWidgets() {
widgetsEl.innerHTML = '';
widgetOrder.forEach(async k => {
const w = await widgetRegistry[k]();
const node = document.createElement('div'); node.className='widget'; node.innerHTML = '<h3>' + escapeHtml(w.title) + '</h3>' + w.html;
widgetsEl.appendChild(node);
});
async function updateRepoMix() {
const el = qs('#repo-mix');
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||[]).filter(i=>i.state==='open');
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.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>';
}
}
paintWidgets();
updateRepoMix();
function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); }
setInterval(widgetTick, 1000);

11
tests/test_widgets.py Normal file
View File

@ -0,0 +1,11 @@
import pytest
from src.views import dashboard
@pytest.mark.anyio
async def test_dashboard_renders_repo_mix_placeholder_before_javascript_runs():
html = await dashboard()
server_rendered_html = html.split("<script>", 1)[0]
assert '<div class="small" id="repo-mix">Loading…</div>' in server_rendered_html