revive: garden rebranded to Vigo + Estate API dashboard
- Rebrand from Vigilio Desto → Vigo, the Watcher of Trentuna - Updated hugo.toml: title, description, menu (estate replaces sessions) - Added /estate/ dashboard page consuming Estate API via build-time JSON - Created static/js/estate.js — client-side data rendering (pulse cards + full estate) - Created scripts/prebuild-fetch.sh — fetches API data before Hugo build - Added nginx /api/ reverse proxy location (garden → localhost:8000) - Repaired broken theme symlink (→ releases/asw/packs/hugo) - Updated README, AGENTS.md, .gitignore for Hugo build artifacts - Site builds clean: 206 pages, 79ms
This commit is contained in:
parent
a476b31213
commit
96261fcb36
222 changed files with 7663 additions and 1475 deletions
1
static/data/builds.json
Normal file
1
static/data/builds.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"timestamp":"2026-05-26T10:01:10.095944","since":"7d","commits":[],"issues":[],"raw_output":"# Build Digest — 2026-05-19 → 2026-05-26\n\nWhat Vigilio built and shipped in this window.\n\n## Commits\n\n_(none in window or git unavailable)_\n\n---\nGenerated by `build-digest.py` — 2026-05-26 08:01 UTC\nRun: `python3 ~/os/build-digest.py --dry-run` for a fresh preview.\nBuilding digest: 2026-05-19 → today\n scanning git repos...\n 0 repo(s) with commits\n skipping issues (FORGEJO_TOKEN not set)\n"}
|
||||
1
static/data/disk.json
Normal file
1
static/data/disk.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"count":47,"latest":{"date":"2026-05-26","used_gb":13.1,"used_pct":76,"free_gb":4.2,"delta_total_gb":-1.7,"directory_breakdown":{"~/projects":3.2,"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":2.0},"delta_per_dir":[]},"snapshots":[{"date":"2026-05-17","used_gb":14.4,"used_pct":84,"free_gb":2.9,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.7},"delta_per_dir":[]},{"date":"2026-05-18","used_gb":14.7,"used_pct":86,"free_gb":2.6,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.7},"delta_per_dir":[]},{"date":"2026-05-19","used_gb":14.7,"used_pct":86,"free_gb":2.5,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.7},"delta_per_dir":[]},{"date":"2026-05-20","used_gb":14.7,"used_pct":86,"free_gb":2.5,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.8},"delta_per_dir":[]},{"date":"2026-05-21","used_gb":14.8,"used_pct":86,"free_gb":2.5,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.8},"delta_per_dir":[]},{"date":"2026-05-22","used_gb":14.8,"used_pct":86,"free_gb":2.5,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.9},"delta_per_dir":[]},{"date":"2026-05-23","used_gb":14.5,"used_pct":85,"free_gb":2.7,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":1.9},"delta_per_dir":[]},{"date":"2026-05-24","used_gb":14.7,"used_pct":85,"free_gb":2.6,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":2.0},"delta_per_dir":[]},{"date":"2026-05-25","used_gb":14.8,"used_pct":86,"free_gb":2.5,"directory_breakdown":{"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":2.0},"delta_per_dir":[]},{"date":"2026-05-26","used_gb":13.1,"used_pct":76,"free_gb":4.2,"delta_total_gb":-1.7,"directory_breakdown":{"~/projects":3.2,"~/.napkin":11.5,"~/.pi/agent/sessions":0.0,"~/upstream":254.5,"~/logs":2.0},"delta_per_dir":[]}]}
|
||||
1
static/data/events-limit-10.json
Normal file
1
static/data/events-limit-10.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"count":10,"events":[{"timestamp":"2026-05-26T09:44","source":"heartbeat","detail":"Estate health sweep — verified all 4 scout findings stale (AGENTS.md 6/6 releases, playground top-level active workspace, napkin corpus extracted, retired seeds handled). Stale HEARTBEAT Status corrected: Drift 5→3 (yellow), Disk 75%→79% (3.7G free). SCOREBOARD updated for this wake. Rig: gateway OK"},{"timestamp":"2026-05-26T01:00","source":"heartbeat","detail":"Verified all 4 scout findings stale/resolved per REPO_LEDGER, current fs counts, and recent changelog (AGENTS.md present in playground/releases; playground migrated to top-level active workspace; napkin/knowledge corpus extracted to vault + tnt-005 release at ~110 files). Rig healthy (gateway + 1 jo"},{"timestamp":"2026-05-25T21:53","source":"heartbeat","detail":"Phase 3 verification: forgejo-sovereign-forge SSH push confirmed functional via localhost:2222 (a-team, trentuna-web in sync — both dry-run clean). Moved change from Active to Implemented on CHANGE_BOARD. Fixed stale tasks.md (Phase 3 was marked Blocked but resolved). Created kanban task for a-team "},{"timestamp":"2026-05-25T21:20","source":"heartbeat","detail":"Estate patrol after month gap — verified all scout findings stale: AGENTS.md complete across 6/6 releases, napkin-knowledge corpus packaged as tnt-005 (108 source files in sync), playground workspace active (dev artifacts from today's session, not unclassified). Kanban clean (0 ready, 3 done). Disk "},{"timestamp":"2026-05-25T20:55","source":"heartbeat","detail":"Freed ~2.8G: camoufox cache (1.4G), Docker Honcho images pgvector+redis (569M), 2 oldest state-snapshots (650M), npm cache (149M), legacy checkpoints (60M). Disk 74% (4.6G free) — was 90%. Box breathing room restored."},{"timestamp":"2026-05-25T20:43","source":"heartbeat","detail":"Drift reassessment — Forgejo SSH block resolved (Phase 3 push verified 21:53); primary drift driver eliminated. All 4 scout findings stale/verified (AGENTS.md 6/6, napkin tnt-005 fully extracted, playground workspace active, retired seeds already purged). Drift reduced 5→3 (yellow) per 21:53 reasses"},{"timestamp":"2026-04-25T06:00:59","source":"heartbeat","detail":"Verified estate scout signals: (1) AGENTS.md confirmed present in all 6 releases — finding stale/false-positive; (2) ~/.napkin/knowledge corpus fully extracted — source 107 files, vault/raw/ 110 files (includes generated index/changelog). No actionable items. Queue depth 0, disk 75% (4.4G free), Dri"},{"timestamp":"2026-04-25T04:49:00","source":"heartbeat","detail":"[tend] Classified ~/logs — operational data surface (adjacent domain). Retention: 30-day policy via ~/runtime/os/log-rotate.sh (daily). Already documented in ESTATE_MAP.md Operational Surfaces. Focus +1, Drift unchanged at 5 (red)."},{"timestamp":"2026-04-25T04:17:41","source":"heartbeat","detail":"Unclassified scan: discovered ~/logs (operational logs: health-pulse, disk-snapshot, kill-zombies, trends). Added [tend] classification task to ACTION_QUEUE. Queue depth 1. Focus +1, Drift unchanged at 5 (red)."},{"timestamp":"2026-04-25T02:34:21","source":"heartbeat","detail":".483365 | Vigo [patrol] | OpenSpec hygiene: archived completed change 'agent-container-isolation' (openspec/changes/ → openspec/archive/). Re-verified scout signals: all 6 releases have AGENTS.md; ~/.napkin/knowledge corpus fully extracted (113 files, release tnt-005 in sync). No actionable findings"}]}
|
||||
1
static/data/health.json
Normal file
1
static/data/health.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"count":100,"latest":{"raw_line":"/home/exedev/runtime/os/health-pulse.sh: line 76: napkin: command not found","timestamp":"","status":"info","detail":""},"status":"warning","entries":[{"raw_line":"[04:00:02] health-pulse: ✅ All systems healthy (disk: 76%, mem: 10%)","timestamp":"04:00:02","status":"healthy","detail":"disk: 76%, mem: 10%"},{"raw_line":"[04:00:02] health-pulse: Health pulse complete","timestamp":"04:00:02","status":"info","detail":""},{"raw_line":"[05:00:01] health-pulse: Running basic health checks...","timestamp":"05:00:01","status":"info","detail":""},{"raw_line":"[05:00:01] health-pulse: Checking LLM providers...","timestamp":"05:00:01","status":"info","detail":""},{"raw_line":"[05:00:01] health-pulse: ⚠️ provider-check.ts not found, skipping provider health","timestamp":"05:00:01","status":"warning","detail":"provider-check.ts not found, skipping provider health"},{"raw_line":"[05:00:01] health-pulse: ✅ All systems healthy (disk: 76%, mem: 9%)","timestamp":"05:00:01","status":"healthy","detail":"disk: 76%, mem: 9%"},{"raw_line":"[05:00:01] health-pulse: Health pulse complete","timestamp":"05:00:01","status":"info","detail":""},{"raw_line":"[06:00:02] health-pulse: Running basic health checks...","timestamp":"06:00:02","status":"info","detail":""},{"raw_line":"[06:00:02] health-pulse: Checking LLM providers...","timestamp":"06:00:02","status":"info","detail":""},{"raw_line":"[06:00:02] health-pulse: ⚠️ provider-check.ts not found, skipping provider health","timestamp":"06:00:02","status":"warning","detail":"provider-check.ts not found, skipping provider health"},{"raw_line":"[06:00:02] health-pulse: ✅ All systems healthy (disk: 76%, mem: 10%)","timestamp":"06:00:02","status":"healthy","detail":"disk: 76%, mem: 10%"},{"raw_line":"[06:00:02] health-pulse: Health pulse complete","timestamp":"06:00:02","status":"info","detail":""},{"raw_line":"[07:00:01] health-pulse: Running basic health checks...","timestamp":"07:00:01","status":"info","detail":""},{"raw_line":"[07:00:01] health-pulse: Checking LLM providers...","timestamp":"07:00:01","status":"info","detail":""},{"raw_line":"[07:00:01] health-pulse: ⚠️ provider-check.ts not found, skipping provider health","timestamp":"07:00:01","status":"warning","detail":"provider-check.ts not found, skipping provider health"},{"raw_line":"[07:00:01] health-pulse: ✅ All systems healthy (disk: 76%, mem: 10%)","timestamp":"07:00:01","status":"healthy","detail":"disk: 76%, mem: 10%"},{"raw_line":"[07:00:01] health-pulse: Health pulse complete","timestamp":"07:00:01","status":"info","detail":""},{"raw_line":"[08:00:01] health-pulse: Running basic health checks...","timestamp":"08:00:01","status":"info","detail":""},{"raw_line":"[08:00:01] health-pulse: ⚠️ DISK WARNING: 80% used (threshold: 80%)","timestamp":"08:00:01","status":"warning","detail":"DISK WARNING: 80% used (threshold: 80%)"},{"raw_line":"/home/exedev/runtime/os/health-pulse.sh: line 76: napkin: command not found","timestamp":"","status":"info","detail":""}]}
|
||||
1
static/data/providers.json
Normal file
1
static/data/providers.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"timestamp":"2026-05-26T10:01:09.948986","providers":[{"provider":"vigilio","model":"claude-sonnet-4-6","status":"ok","method":"probe","timestamp":"2026-05-26T10:01:08+02:00"},{"provider":"shelley-proxy","model":"claude-sonnet-4-6","status":"ok","method":"probe","timestamp":"2026-05-26T10:01:09+02:00"},{"provider":"openrouter","model":"claude-sonnet-4-6","status":"ok","method":"probe","timestamp":"2026-05-26T10:01:09+02:00"}],"raw_output":""}
|
||||
1
static/data/repos.json
Normal file
1
static/data/repos.json
Normal file
File diff suppressed because one or more lines are too long
1
static/data/state.json
Normal file
1
static/data/state.json
Normal file
File diff suppressed because one or more lines are too long
1
static/data/summary.json
Normal file
1
static/data/summary.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"api_version":"1.0.0","sources":[{"source":"trends","available":true,"count":1,"last_updated":"2026-03-29T06:40:01Z"},{"source":"disk","available":true,"count":1,"last_updated":"2026-05-26"},{"source":"health","available":true,"count":1,"last_updated":""},{"source":"state","available":true,"count":3,"last_updated":""},{"source":"events","available":true,"count":5,"last_updated":"2026-05-26T09:44"}],"estate":{"disk_latest":76,"health_status":"check /health/status","recent_events":[{"timestamp":"2026-05-26T09:44","source":"heartbeat","detail":"Estate health sweep — verified all 4 scout findings stale (AGENTS.md 6/6 releases, playground top-level active workspace, napkin corpus extracted, retired seeds handled). Stale HEARTBEAT Status corrected: Drift 5→3 (yellow), Disk 75%→79% (3.7G free). SCOREBOARD updated for this wake. Rig: gateway OK"},{"timestamp":"2026-05-26T01:00","source":"heartbeat","detail":"Verified all 4 scout findings stale/resolved per REPO_LEDGER, current fs counts, and recent changelog (AGENTS.md present in playground/releases; playground migrated to top-level active workspace; napkin/knowledge corpus extracted to vault + tnt-005 release at ~110 files). Rig healthy (gateway + 1 jo"},{"timestamp":"2026-05-25T21:53","source":"heartbeat","detail":"Phase 3 verification: forgejo-sovereign-forge SSH push confirmed functional via localhost:2222 (a-team, trentuna-web in sync — both dry-run clean). Moved change from Active to Implemented on CHANGE_BOARD. Fixed stale tasks.md (Phase 3 was marked Blocked but resolved). Created kanban task for a-team "}]}}
|
||||
1
static/data/trends-limit-5.json
Normal file
1
static/data/trends-limit-5.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"count":5,"latest":{"timestamp":"2026-03-29T06:40:01Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":260,"size_kb":2176,"git_size_kb":7972,"commits":411,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":5,"repos":6}},"data":[{"timestamp":"2026-03-29T05:30:01Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":259,"size_kb":2168,"git_size_kb":7956,"commits":410,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":6,"repos":6}},{"timestamp":"2026-03-29T05:32:01Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":259,"size_kb":2168,"git_size_kb":7956,"commits":410,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":5,"repos":6}},{"timestamp":"2026-03-29T06:06:02Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":259,"size_kb":2168,"git_size_kb":7956,"commits":410,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":5,"repos":6}},{"timestamp":"2026-03-29T06:30:01Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":259,"size_kb":2168,"git_size_kb":7956,"commits":410,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":5,"repos":6}},{"timestamp":"2026-03-29T06:40:01Z","disk":{"used_pct":31,"used_gb":6,"avail_gb":12},"vault":{"notes":260,"size_kb":2176,"git_size_kb":7972,"commits":411,"sessions":64,"dialogues":88,"decisions":39},"tasks":{"open":9,"done":19},"provider":{"name":"team-molto","model":"claude-sonnet-4-6","failover":false},"system":{"mem_used_pct":5,"repos":6}}]}
|
||||
215
static/js/estate.js
Normal file
215
static/js/estate.js
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/* estate.js — Vigo's Estate API client
|
||||
*
|
||||
* Fetches Estate API data and populates dynamic sections: homepage pulse
|
||||
* cards and full estate dashboard. Uses build-time JSON snapshots from
|
||||
* /data/*.json (static files generated by prebuild-fetch.sh).
|
||||
*
|
||||
* If nginx /api/ reverse proxy is configured (garden.trentuna.com/api/ →
|
||||
* localhost:8000), the JS can also fetch live data from there. By default
|
||||
* it reads from /data/ for simplicity.
|
||||
*/
|
||||
|
||||
const DATA_BASE = '/data';
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────────── */
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
async function loadJSON(path) {
|
||||
const res = await fetch(path);
|
||||
if (!res.ok) throw new Error(`${path}: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function fmtPct(v) { return (typeof v === 'number') ? v + '%' : v; }
|
||||
|
||||
function fmtTime(t) {
|
||||
if (!t) return '—';
|
||||
try { return new Date(t).toLocaleString(); } catch { return t; }
|
||||
}
|
||||
|
||||
/* ── Homepage pulse cards ──────────────────────────────────────── */
|
||||
|
||||
async function fetchPulse() {
|
||||
try {
|
||||
const summary = await loadJSON(DATA_BASE + '/summary.json');
|
||||
const trends = await loadJSON(DATA_BASE + '/trends-limit-5.json');
|
||||
|
||||
// Disk
|
||||
const diskPct = summary?.estate?.disk_latest;
|
||||
if ($('disk-value')) $('disk-value').textContent = diskPct ? diskPct + '%' : '—';
|
||||
if ($('disk-detail')) $('disk-detail').textContent = diskPct ? `used` : 'n/a';
|
||||
|
||||
// Health
|
||||
const healthStatus = summary?.estate?.health_status || '—';
|
||||
if ($('health-value')) $('health-value').textContent = healthStatus === 'ok' ? 'ok' : healthStatus;
|
||||
if ($('health-detail')) $('health-detail').textContent = healthStatus === 'ok' ? 'estate nominal' : 'check estate';
|
||||
|
||||
// Events
|
||||
const eventCount = summary?.estate?.recent_events?.length || 0;
|
||||
if ($('events-value')) $('events-value').textContent = eventCount > 0 ? eventCount : '—';
|
||||
if ($('events-detail')) $('events-detail').textContent = eventCount === 1 ? 'event' : eventCount + ' events';
|
||||
|
||||
// Session count from trends
|
||||
const sessions = trends?.data?.[0]?.vault?.sessions || null;
|
||||
if ($('vault-sessions-value')) $('vault-sessions-value').textContent = sessions !== null ? sessions : '—';
|
||||
|
||||
// Session count standalone (in the intro block)
|
||||
if ($('session-count')) {
|
||||
$('session-count').textContent = sessions !== null ? sessions.toLocaleString() + ' sessions' : '? sessions';
|
||||
}
|
||||
|
||||
// Timestamp
|
||||
const updated = summary?.estate?.recent_events?.[0]?.timestamp;
|
||||
if ($('pulse-timestamp')) {
|
||||
$('pulse-timestamp').textContent = updated
|
||||
? 'Last update: ' + fmtTime(updated)
|
||||
: 'Estate data live';
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn('estate.js: pulse fetch failed', err);
|
||||
if ($('pulse-timestamp')) $('pulse-timestamp').textContent = 'Estate API offline';
|
||||
// Leave placeholder dashes in the cards
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Estate dashboard (full) ───────────────────────────────────── */
|
||||
|
||||
async function fetchEstate() {
|
||||
const el = (id) => $(id);
|
||||
const setText = (id, val) => { const e = el(id); if (e) e.textContent = val; };
|
||||
const setHTML = (id, html) => { const e = el(id); if (e) e.innerHTML = html; };
|
||||
|
||||
try {
|
||||
const [summary, health, disk, events, repos, providers, builds, trends] = await Promise.all([
|
||||
loadJSON(DATA_BASE + '/summary.json'),
|
||||
loadJSON(DATA_BASE + '/health.json').catch(() => ({ error: true, data: [] })),
|
||||
loadJSON(DATA_BASE + '/disk.json').catch(() => ({ error: true })),
|
||||
loadJSON(DATA_BASE + '/events-limit-10.json').catch(() => ({ error: true, data: [] })),
|
||||
loadJSON(DATA_BASE + '/repos.json').catch(() => ({ error: true, data: [] })),
|
||||
loadJSON(DATA_BASE + '/providers.json').catch(() => ({ error: true, data: [] })),
|
||||
loadJSON(DATA_BASE + '/builds.json').catch(() => ({ error: true, data: [] })),
|
||||
loadJSON(DATA_BASE + '/trends-limit-5.json').catch(() => ({ error: true, data: [] })),
|
||||
]);
|
||||
|
||||
// ── Summary cards ──
|
||||
setText('estate-api-version', summary?.api_version || '—');
|
||||
setText('estate-disk', summary?.estate?.disk_latest ? summary.estate.disk_latest + '%' : '—');
|
||||
setText('estate-health', summary?.estate?.health_status || '—');
|
||||
setText('estate-sources', summary?.sources?.length || '—');
|
||||
|
||||
const sourceRows = (summary?.sources || []).map(s =>
|
||||
`<tr><td>${s.source}</td><td>${s.available ? '✓' : '✗'}</td><td>${s.count}</td><td>${s.last_updated || '—'}</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-sources-table', sourceRows || '<tr><td colspan="4">No source data</td></tr>');
|
||||
|
||||
// ── Health ──
|
||||
if (health?.error) {
|
||||
setHTML('estate-health-table', '<tr><td colspan="3">Health data unavailable</td></tr>');
|
||||
} else {
|
||||
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>`
|
||||
).join('');
|
||||
setHTML('estate-health-table', healthRows || '<tr><td colspan="3">No health entries</td></tr>');
|
||||
}
|
||||
|
||||
// ── Disk ──
|
||||
if (disk?.error) {
|
||||
setHTML('estate-disk-info', '<p>Disk data unavailable</p>');
|
||||
} else {
|
||||
const diskLatest = disk?.latest || disk;
|
||||
const diskHtml = Object.entries(diskLatest || {}).map(([k, v]) =>
|
||||
`<tr><td>${k}</td><td>${v}</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-disk-info', diskHtml ? '<table><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody>' + diskHtml + '</tbody></table>' : '<p>No disk data</p>');
|
||||
}
|
||||
|
||||
// ── Events ──
|
||||
const evts = Array.isArray(events?.data) ? events.data : (events?.data ? [events.data] : []);
|
||||
const eventRows = evts.slice(0, 10).map(e =>
|
||||
`<tr><td>${fmtTime(e.timestamp)}</td><td>${e.source || '—'}</td><td>${(e.detail || '').substring(0, 90)}</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-events-table', eventRows || '<tr><td colspan="3">No events</td></tr>');
|
||||
|
||||
// ── Repos ──
|
||||
const repoList = Array.isArray(repos?.data) ? repos.data : (repos?.repos?.data ? repos.repos.data : []);
|
||||
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>`
|
||||
).join('');
|
||||
setHTML('estate-repos-table', repoRows || '<tr><td colspan="3">No repo data</td></tr>');
|
||||
|
||||
// ── Providers ──
|
||||
const provList = Array.isArray(providers?.data) ? providers.data : (providers?.providers || []);
|
||||
const provRows = provList.slice(0, 10).map(p =>
|
||||
`<tr><td>${p.name || '—'}</td><td>${p.status || p.reachable ? '✓' : '✗'}</td><td>${p.model || '—'}</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-providers-table', provRows || '<tr><td colspan="3">No provider data</td></tr>');
|
||||
|
||||
// ── Builds ──
|
||||
const buildList = Array.isArray(builds?.data) ? builds.data : [];
|
||||
const buildRows = buildList.slice(0, 10).map(b =>
|
||||
`<tr><td>${b.timestamp || '—'}</td><td>${b.repo || b.project || '—'}</td><td>${b.status || '—'}</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-builds-table', buildRows || '<tr><td colspan="3">No builds</td></tr>');
|
||||
|
||||
// ── Trends ──
|
||||
const trendData = Array.isArray(trends?.data) ? trends.data : [];
|
||||
const trendRows = trendData.slice(0, 10).map(t =>
|
||||
`<tr><td>${fmtTime(t.timestamp)}</td><td>${t.vault?.sessions || '—'}</td><td>${t.vault?.notes || '—'}</td><td>${t.disk?.used_pct || '—'}%</td><td>${t.system?.mem_used_pct || '—'}%</td></tr>`
|
||||
).join('');
|
||||
setHTML('estate-trends-table', trendRows || '<tr><td colspan="5">No trend data</td></tr>');
|
||||
|
||||
// Estate loaded indicator
|
||||
setHTML('estate-loading', '');
|
||||
} catch (err) {
|
||||
console.warn('estate.js: full estate fetch failed', err);
|
||||
setHTML('estate-loading', '<p class="error">Estate API unavailable</p>');
|
||||
}
|
||||
}
|
||||
|
||||
/* ── State files ───────────────────────────────────────────────── */
|
||||
|
||||
async function fetchStateFiles() {
|
||||
const el = (id) => $(id);
|
||||
const setHTML = (id, html) => { const e = el(id); if (e) e.innerHTML = html; };
|
||||
|
||||
try {
|
||||
const state = await loadJSON(DATA_BASE + '/state.json');
|
||||
const files = Array.isArray(state?.files) ? state.files : [];
|
||||
|
||||
const fileCards = files.map(f => {
|
||||
const truncated = f.content.length > 600 ? f.content.substring(0, 600) + '…' : f.content || '(empty)';
|
||||
return `<article data-card>
|
||||
<header>${f.name}</header>
|
||||
<p>${(f.size_bytes || 0).toLocaleString()} bytes</p>
|
||||
<pre style="font-size:0.75rem;max-height:200px;overflow:auto">${escHtml(truncated)}</pre>
|
||||
<a href="/api/state?file=${f.name}">View full →</a>
|
||||
</article>`;
|
||||
}).join('');
|
||||
|
||||
setHTML('estate-state-files', fileCards || '<p>No state files</p>');
|
||||
} catch (err) {
|
||||
console.warn('estate.js: state file fetch failed', err);
|
||||
setHTML('estate-state-files', '<p>State data unavailable</p>');
|
||||
}
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/* ── Init ──────────────────────────────────────────────────────── */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Always fetch pulse (homepage widget)
|
||||
fetchPulse();
|
||||
|
||||
// If we're on the estate dashboard, fetch full data
|
||||
if ($('estate-dashboard')) {
|
||||
fetchEstate();
|
||||
fetchStateFiles();
|
||||
}
|
||||
});
|
||||
155
static/js/garden-feed.js
Normal file
155
static/js/garden-feed.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* garden-feed.js — fetches /api/garden and populates dynamic widgets.
|
||||
*
|
||||
* Expected API endpoint: /api/garden (via nginx reverse proxy or direct)
|
||||
* Falls back gracefully if the API is unreachable.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API_BASE = (function () {
|
||||
// In dev: fetch from localhost:8000. In prod: relative to origin (nginx proxy).
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
return 'http://127.0.0.1:8000';
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
|
||||
const GARDEN_API = API_BASE + '/api/garden';
|
||||
|
||||
// ── Renderers ──────────────────────────────────────────────────────
|
||||
|
||||
function renderIdentity(identity) {
|
||||
const el = document.querySelector('[data-garden="identity"]');
|
||||
if (!el) return;
|
||||
el.innerHTML = `
|
||||
<hgroup>
|
||||
<h1>${esc(identity.name || 'Vigilio Desto')}</h1>
|
||||
<p data-text="dim">${esc(identity.tagline || 'the watchful unmaker')}</p>
|
||||
</hgroup>
|
||||
${identity.description ? `<p>${esc(identity.description)}</p>` : ''}
|
||||
<p><strong>${esc(identity.sessions || '2,700+')} sessions.</strong>
|
||||
${identity.beat ? `Beat: ${esc(identity.beat)}.` : ''}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWritings(data) {
|
||||
const container = document.querySelector('[data-garden="writings"]');
|
||||
if (!container) return;
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<p data-text="dim">No writings yet.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = items.map(function (w) {
|
||||
return `
|
||||
<article data-card>
|
||||
${w.tags && w.tags.length ? `<header>${esc(w.tags[0])}</header>` : ''}
|
||||
<h4><a href="${esc(w.link || '#')}">${esc(w.title)}</a></h4>
|
||||
${w.summary ? `<p>${esc(w.summary)}</p>` : ''}
|
||||
${w.date ? `<footer><time>${esc(w.date)}</time></footer>` : ''}
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderExpressive(data) {
|
||||
const container = document.querySelector('[data-garden="expressive"]');
|
||||
if (!container) return;
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<p data-text="dim">No expressive forms yet.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = items.map(function (f) {
|
||||
return `
|
||||
<article data-card>
|
||||
<h4><a href="${esc(f.link || '#')}">${esc(f.title)}</a></h4>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderEstate(estate) {
|
||||
const el = document.querySelector('[data-garden="estate"]');
|
||||
if (!el) return;
|
||||
if (!estate || estate.status === 'unknown') {
|
||||
el.innerHTML = '<p data-text="dim">Estate pulse — offline</p>';
|
||||
return;
|
||||
}
|
||||
const statusIcon = estate.status === 'active' ? '🟢' : '🟡';
|
||||
el.innerHTML = `
|
||||
<p>${statusIcon} <strong>${esc(estate.status)}</strong>
|
||||
${estate.session_count ? `· ${estate.session_count.toLocaleString()} sessions` : ''}
|
||||
${estate.disk_pct ? `· ${estate.disk_pct}% disk used` : ''}
|
||||
${estate.disk_free_gb ? `· ${estate.disk_free_gb.toFixed(1)} GB free` : ''}</p>
|
||||
<p data-text="dim" style="font-size:0.8em">
|
||||
Checked: ${estate.checked_at ? new Date(estate.checked_at).toLocaleString() : 'unknown'}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderUpdateTime(updatedAt) {
|
||||
const el = document.querySelector('[data-garden="updated"]');
|
||||
if (!el) return;
|
||||
if (!updatedAt) {
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
el.textContent = 'Garden feed updated ' + new Date(updatedAt).toLocaleString();
|
||||
}
|
||||
|
||||
// ── Utility ────────────────────────────────────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
if (typeof s !== 'string') return '';
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(s));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ── Fetch & render ────────────────────────────────────────────────
|
||||
|
||||
async function loadGardenFeed() {
|
||||
// Show loading placeholders
|
||||
document.querySelectorAll('[data-garden]').forEach(function (el) {
|
||||
if (!el.hasAttribute('data-garden-loaded')) {
|
||||
el.innerHTML = '<p data-text="dim" class="garden-loading">…</p>';
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
var resp = await fetch(GARDEN_API, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
|
||||
renderIdentity(data.identity);
|
||||
renderWritings(data.writings);
|
||||
renderExpressive(data.expressive_forms);
|
||||
renderEstate(data.estate);
|
||||
renderUpdateTime(data.updated_at);
|
||||
|
||||
document.querySelectorAll('[data-garden]').forEach(function (el) {
|
||||
el.setAttribute('data-garden-loaded', 'true');
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[garden-feed] API unreachable:', err.message);
|
||||
document.querySelectorAll('[data-garden]').forEach(function (el) {
|
||||
if (!el.hasAttribute('data-garden-loaded')) {
|
||||
el.innerHTML = '<p data-text="dim">Garden feed unavailable.</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boot ───────────────────────────────────────────────────────────
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadGardenFeed);
|
||||
} else {
|
||||
loadGardenFeed();
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue