${(f.size_bytes || 0).toLocaleString()} bytes
${escHtml(truncated)}
View full →
/* estate.js — Vigo's Estate API client * * Fetches Estate API data and populates dynamic sections: homepage pulse * cards and full estate dashboard. * * Primary source: live Estate API via /api/ (nginx reverse proxy to * localhost:8000). Fallback: build-time JSON snapshots from /data/*.json * (generated by prebuild-fetch.sh). */ const DATA_BASE = '/data'; const API_BASE = (function () { if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { return 'http://127.0.0.1:8000'; } return '/api'; })(); /* ── 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(); } /** * fetchFromAPI — try live API endpoint first, fall back to static data file. * @param {string} endpoint - API path (e.g. 'summary', 'health') * @param {string} dataFile - static data file path (e.g. '/data/summary.json') * @returns {object} parsed JSON */ async function fetchFromAPI(endpoint, dataFile) { const apiUrl = API_BASE + '/' + endpoint; try { const res = await fetch(apiUrl); if (res.ok) return await res.json(); throw new Error('HTTP ' + res.status); } catch (err) { console.log('[estate] live API unreachable (' + apiUrl + '), falling back to ' + dataFile); return loadJSON(dataFile); } } 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 fetchFromAPI('summary', DATA_BASE + '/summary.json'); const trends = await fetchFromAPI('trends?limit=5', 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([ fetchFromAPI('summary', DATA_BASE + '/summary.json'), fetchFromAPI('health', DATA_BASE + '/health.json').catch(() => ({ error: true, data: [] })), fetchFromAPI('disk', DATA_BASE + '/disk.json').catch(() => ({ error: true })), fetchFromAPI('events?limit=10', DATA_BASE + '/events-limit-10.json').catch(() => ({ error: true, data: [] })), fetchFromAPI('repos', DATA_BASE + '/repos.json').catch(() => ({ error: true, data: [] })), fetchFromAPI('providers', DATA_BASE + '/providers.json').catch(() => ({ error: true, data: [] })), fetchFromAPI('builds', DATA_BASE + '/builds.json').catch(() => ({ error: true, data: [] })), fetchFromAPI('trends?limit=10', 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 => `
Disk data unavailable
'); } else { const diskLatest = disk?.latest || disk; const diskHtml = Object.entries(diskLatest || {}).map(([k, v]) => `| Metric | Value |
|---|
No disk data
'); } // ── Events ── const evts = Array.isArray(events?.data) ? events.data : (events?.data ? [events.data] : []); const eventRows = evts.slice(0, 10).map(e => `Estate API unavailable
'); } } /* ── 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 fetchFromAPI('state', 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 `${(f.size_bytes || 0).toLocaleString()} bytes
${escHtml(truncated)}
View full →
No state files
'); } catch (err) { console.warn('estate.js: state file fetch failed', err); setHTML('estate-state-files', 'State data unavailable
'); } } 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(); } });