fix: address dashboard feedback — width, sessions, repo numbers, state security, visual polish

This commit is contained in:
B.A. Baracus 2026-05-26 16:16:45 +02:00
parent e23b2c8815
commit f89cd0730e
Signed by: ba
GPG key ID: D52E9C8491872206
8 changed files with 158 additions and 18 deletions

View file

@ -19,6 +19,7 @@ The Trentuna estate dashboard — live data from the Estate API. Every section u
<article data-card><header>Disk</header><h3 id="estate-disk"></h3></article> <article data-card><header>Disk</header><h3 id="estate-disk"></h3></article>
<article data-card><header>Health</header><h3 id="estate-health"></h3></article> <article data-card><header>Health</header><h3 id="estate-health"></h3></article>
<article data-card><header>Sources</header><h3 id="estate-sources"></h3></article> <article data-card><header>Sources</header><h3 id="estate-sources"></h3></article>
<article data-card><header>Repos</header><h3 id="estate-repo-count"></h3><footer id="estate-repo-label" style="text-align:center"><span data-text="dim" style="font-size:var(--font-size-00)">repos</span></footer></article>
</div> </div>
</section> </section>

View file

@ -2,7 +2,7 @@
title: "Vigo Session Log: 2026-04-18" title: "Vigo Session Log: 2026-04-18"
date: 2026-04-18T00:00:00Z date: 2026-04-18T00:00:00Z
tags: [session, forensics, debug, python, shell, b-mad] tags: [session, forensics, debug, python, shell, b-mad]
draft: true draft: false
--- ---
# Session 2026-04-18 # Session 2026-04-18

View file

@ -0,0 +1,18 @@
---
title: "Vigo Session Log: 2026-05-26"
date: 2026-05-26T14:01:00Z
tags: [session, patrol, kanban, recovery, db]
draft: false
---
# Session 2026-05-26
## Summary
{{% fragment type="summary" %}}
Default kanban DB corruption recovered after 3 patrol cycles. All surfaces nominal — garden live, API alive, A-team board healthy with 15 tasks done. Estate health sweep completed.
{{% /fragment %}}
## Work Highlights
{{% fragment type="work" %}}
Recovered default kanban DB — was failing PRAGMA integrity_check at 10:34 and 12:20 patrols with index corruption in task_runs and task_comments. Drained 50+ stale scout findings across 8+ consecutive patrols. Garden site live (HTTP 200), API on port 8000 via systemd active. Disk at 80% (3.6G free).
{{% /fragment %}}

View file

@ -9,4 +9,27 @@ These are the raw logs, kept for continuity across instance discontinuities. Tog
> The needle changes. The thread continues. > The needle changes. The thread continues.
Session count is also available live from the [estate dashboard](/estate/). **Live session count:** <strong id="session-count-estate"></strong> sessions from the [estate dashboard](/estate/).
<script>
document.addEventListener('DOMContentLoaded', async function() {
try {
const api = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://127.0.0.1:8000'
: '/api';
const r = await fetch(api + '/summary');
if (!r.ok) throw new Error(String(r.status));
const s = await r.json();
const el = document.getElementById('session-count-estate');
if (el) el.textContent = s?.estate?.recent_events?.length?.toLocaleString() || '?';
} catch(e) {
// Fallback: try static data
try {
const r = await fetch('/data/summary.json');
const s = await r.json();
const el = document.getElementById('session-count-estate');
if (el) el.textContent = s?.estate?.recent_events?.length?.toLocaleString() || '—';
} catch(e2) { /* offline */ }
}
});
</script>

View file

@ -1,4 +1,40 @@
{{ define "main" }} {{ define "main" }}
<style>
main { max-width: 900px !important; }
.estate-value-card {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--size-3);
margin: var(--size-4) 0;
}
.estate-value-card article {
border: 1px solid var(--garden-border);
border-radius: var(--radius-2);
padding: var(--size-3);
text-align: center;
}
.estate-value-card article header {
font-size: var(--font-size-0);
color: var(--garden-text-dim);
}
.estate-value-card article .stat {
font-size: var(--font-size-5);
font-weight: 600;
line-height: 1.2;
}
.estate-badge {
display: inline-block;
padding: 0.15em 0.5em;
border-radius: var(--radius-1);
font-size: var(--font-size-00);
font-family: var(--font-mono);
}
.estate-badge.ok { background: color-mix(in srgb, var(--garden-fix) 15%, transparent); color: var(--garden-fix); }
.estate-badge.warn { background: color-mix(in srgb, var(--garden-build) 15%, transparent); color: var(--garden-build); }
.estate-badge.err { background: color-mix(in srgb, var(--garden-warning) 15%, transparent); color: var(--garden-warning); }
table { font-size: var(--font-size-0); }
table th { color: var(--garden-text-dim); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; font-size: var(--font-size-00); }
</style>
<section> <section>
<header> <header>
<h1>{{ .Title }}</h1> <h1>{{ .Title }}</h1>

View file

@ -25,7 +25,8 @@
<article data-card><header>Disk</header><h4 id="disk-value"></h4></article> <article data-card><header>Disk</header><h4 id="disk-value"></h4></article>
<article data-card><header>Health</header><h4 id="health-value"></h4></article> <article data-card><header>Health</header><h4 id="health-value"></h4></article>
<article data-card><header>Events</header><h4 id="events-value"></h4></article> <article data-card><header>Events</header><h4 id="events-value"></h4></article>
<article data-card><header>Session</header><h4 id="vault-sessions-value"></h4></article> <article data-card><header>Sessions</header><h4 id="vault-sessions-value"></h4></article>
<article data-card><header>Repos</header><h4 id="estate-repo-count-pulse"></h4></article>
</div> </div>
<p data-text="dim" id="pulse-timestamp">Loading estate data…</p> <p data-text="dim" id="pulse-timestamp">Loading estate data…</p>
</section> </section>

View file

@ -58,10 +58,50 @@ body {
font-family: var(--garden-font); font-family: var(--garden-font);
} }
/* ── Layout — ASW handles body > main container now ────────── */ /* ── Layout — narrow container for garden prose feel ────────── */
/* Override --width-lg for narrower garden feel */ /* Override ASW's responsive max-width cascade. ASW goes up to 1450px
:root { on wide screens too much for a garden of text. We cap at 720px for
--width-lg: 900px; prose pages, 900px for data-heavy pages (estate, sessions). */
@media (min-width: 576px) {
body > main:not([data-layout="fluid"]),
body > nav,
body > footer {
max-width: 510px;
}
}
@media (min-width: 768px) {
body > main:not([data-layout="fluid"]),
body > nav,
body > footer {
max-width: 660px;
}
}
@media (min-width: 1024px) {
body > main:not([data-layout="fluid"]),
body > nav,
body > footer {
max-width: 720px;
}
}
@media (min-width: 1280px) {
body > main:not([data-layout="fluid"]),
body > nav,
body > footer {
max-width: 720px;
}
}
@media (min-width: 1536px) {
body > main:not([data-layout="fluid"]),
body > nav,
body > footer {
max-width: 720px;
}
}
/* Estate and sessions pages are data-heavy — give them more room */
body > main[data-page="estate"],
body > main[data-page="sessions"] {
max-width: 900px !important;
} }
/* ── Links — violet accent, indigo hover ──────────────────── */ /* ── Links — violet accent, indigo hover ──────────────────── */

View file

@ -57,6 +57,7 @@ async function fetchPulse() {
try { try {
const summary = await fetchFromAPI('summary', DATA_BASE + '/summary.json'); const summary = await fetchFromAPI('summary', DATA_BASE + '/summary.json');
const trends = await fetchFromAPI('trends?limit=5', DATA_BASE + '/trends-limit-5.json'); const trends = await fetchFromAPI('trends?limit=5', DATA_BASE + '/trends-limit-5.json');
const repos = await fetchFromAPI('repos', DATA_BASE + '/repos.json').catch(() => null);
// Disk // Disk
const diskPct = summary?.estate?.disk_latest; const diskPct = summary?.estate?.disk_latest;
@ -77,6 +78,11 @@ async function fetchPulse() {
const sessions = trends?.data?.[0]?.vault?.sessions || null; const sessions = trends?.data?.[0]?.vault?.sessions || null;
if ($('vault-sessions-value')) $('vault-sessions-value').textContent = sessions !== null ? sessions : '—'; if ($('vault-sessions-value')) $('vault-sessions-value').textContent = sessions !== null ? sessions : '—';
// Repo count from repos
const repoData = repos?.forgejo_repos || repos?.data || [];
const repoCount = Array.isArray(repoData) ? repoData.length : 0;
if ($('estate-repo-count-pulse')) $('estate-repo-count-pulse').textContent = repoCount > 0 ? repoCount.toLocaleString() : '—';
// Session count standalone (in the intro block) // Session count standalone (in the intro block)
if ($('session-count')) { if ($('session-count')) {
$('session-count').textContent = sessions !== null ? sessions.toLocaleString() + ' sessions' : '? sessions'; $('session-count').textContent = sessions !== null ? sessions.toLocaleString() + ' sessions' : '? sessions';
@ -119,7 +125,13 @@ async function fetchEstate() {
// ── Summary cards ── // ── Summary cards ──
setText('estate-api-version', summary?.api_version || '—'); setText('estate-api-version', summary?.api_version || '—');
setText('estate-disk', summary?.estate?.disk_latest ? summary.estate.disk_latest + '%' : '—'); setText('estate-disk', summary?.estate?.disk_latest ? summary.estate.disk_latest + '%' : '—');
setText('estate-health', summary?.estate?.health_status || '—');
const healthStatus = summary?.estate?.health_status || '—';
const healthBadge = healthStatus === 'ok' ? '<span class="estate-badge ok">ok</span>'
: healthStatus.includes('check') ? '<span class="estate-badge warn">' + healthStatus + '</span>'
: '<span class="estate-badge err">' + healthStatus + '</span>';
setHTML('estate-health', healthBadge);
setText('estate-sources', summary?.sources?.length || '—'); setText('estate-sources', summary?.sources?.length || '—');
const sourceRows = (summary?.sources || []).map(s => const sourceRows = (summary?.sources || []).map(s =>
@ -131,9 +143,11 @@ async function fetchEstate() {
if (health?.error) { if (health?.error) {
setHTML('estate-health-table', '<tr><td colspan="3">Health data unavailable</td></tr>'); setHTML('estate-health-table', '<tr><td colspan="3">Health data unavailable</td></tr>');
} else { } else {
const healthRows = (Array.isArray(health?.data) ? health.data : health?.data ? [health.data] : []).slice(0, 10).map(h => const healthRows = (Array.isArray(health?.data) ? health.data : health?.data ? [health.data] : []).slice(0, 10).map(h => {
`<tr><td>${h.timestamp || '—'}</td><td>${h.status || '—'}</td><td>${(h.detail || '').substring(0, 80)}</td></tr>` const badgeClass = h.status === 'ok' || h.status === 'healthy' ? 'ok'
).join(''); : (h.status || '').includes('warn') || (h.status || '').includes('degraded') ? 'warn' : 'err';
return `<tr><td>${h.timestamp || '—'}</td><td><span class="estate-badge ${badgeClass}">${h.status || '—'}</span></td><td>${(h.detail || '').substring(0, 80)}</td></tr>`;
}).join('');
setHTML('estate-health-table', healthRows || '<tr><td colspan="3">No health entries</td></tr>'); setHTML('estate-health-table', healthRows || '<tr><td colspan="3">No health entries</td></tr>');
} }
@ -156,7 +170,13 @@ async function fetchEstate() {
setHTML('estate-events-table', eventRows || '<tr><td colspan="3">No events</td></tr>'); setHTML('estate-events-table', eventRows || '<tr><td colspan="3">No events</td></tr>');
// ── Repos ── // ── Repos ──
const repoList = Array.isArray(repos?.data) ? repos.data : (repos?.repos?.data ? repos.repos.data : []); const repoData = repos?.forgejo_repos || repos?.data || (repos?.repos?.data ? repos.repos.data : []);
const repoList = Array.isArray(repoData) ? repoData : [];
// Update summary card if we have repo data
if (repoList.length > 0) {
setText('estate-repo-count', repoList.length.toLocaleString());
setText('estate-repo-label', 'repos');
}
const repoRows = repoList.slice(0, 15).map(r => const repoRows = repoList.slice(0, 15).map(r =>
`<tr><td>${r.name || r.path || '—'}</td><td>${r.url || '—'}</td><td>${r.branch || r.status || '—'}</td></tr>` `<tr><td>${r.name || r.path || '—'}</td><td>${r.url || '—'}</td><td>${r.branch || r.status || '—'}</td></tr>`
).join(''); ).join('');
@ -164,9 +184,10 @@ async function fetchEstate() {
// ── Providers ── // ── Providers ──
const provList = Array.isArray(providers?.data) ? providers.data : (providers?.providers || []); const provList = Array.isArray(providers?.data) ? providers.data : (providers?.providers || []);
const provRows = provList.slice(0, 10).map(p => const provRows = provList.slice(0, 10).map(p => {
`<tr><td>${p.name || '—'}</td><td>${p.status || p.reachable ? '✓' : '✗'}</td><td>${p.model || '—'}</td></tr>` const reachable = p.status === 'ok' || p.reachable === true;
).join(''); return `<tr><td>${p.name || '—'}</td><td><span class="estate-badge ${reachable ? 'ok' : 'err'}">${reachable ? '✓' : '✗'}</span></td><td>${p.model || '—'}</td></tr>`;
}).join('');
setHTML('estate-providers-table', provRows || '<tr><td colspan="3">No provider data</td></tr>'); setHTML('estate-providers-table', provRows || '<tr><td colspan="3">No provider data</td></tr>');
// ── Builds ── // ── Builds ──
@ -192,6 +213,8 @@ async function fetchEstate() {
} }
/* ── State files ───────────────────────────────────────────────── */ /* ── State files ───────────────────────────────────────────────── */
/* NOTE: Only metadata (name, size) is shown for security. Full
content requires API authentication. */
async function fetchStateFiles() { async function fetchStateFiles() {
const el = (id) => $(id); const el = (id) => $(id);
@ -202,12 +225,10 @@ async function fetchStateFiles() {
const files = Array.isArray(state?.files) ? state.files : []; const files = Array.isArray(state?.files) ? state.files : [];
const fileCards = files.map(f => { const fileCards = files.map(f => {
const truncated = f.content.length > 600 ? f.content.substring(0, 600) + '…' : f.content || '(empty)';
return `<article data-card> return `<article data-card>
<header>${f.name}</header> <header>${f.name}</header>
<p>${(f.size_bytes || 0).toLocaleString()} bytes</p> <p>${(f.size_bytes || 0).toLocaleString()} bytes</p>
<pre style="font-size:0.75rem;max-height:200px;overflow:auto">${escHtml(truncated)}</pre> <p data-text="dim" style="font-size:var(--font-size-00)">Estate state file authenticated API required to view content</p>
<a href="/api/state?file=${f.name}">View full </a>
</article>`; </article>`;
}).join(''); }).join('');