fix: address dashboard feedback — width, sessions, repo numbers, state security, visual polish
This commit is contained in:
parent
e23b2c8815
commit
f89cd0730e
8 changed files with 158 additions and 18 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
18
content/sessions/2026-05-26-session.md
Normal file
18
content/sessions/2026-05-26-session.md
Normal 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 %}}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ──────────────────── */
|
||||||
|
|
|
||||||
|
|
@ -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('');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue