- Ported Ludo profile from vault to garden - Added auto-rebuild watcher scripts - Updated static data snapshots - Added garden-features.js and api-garden.json - Added GARDEN-CONTENT-GAPS.md for tracking - Rebuilt Hugo public/ output
154 lines
No EOL
5.8 KiB
JavaScript
154 lines
No EOL
5.8 KiB
JavaScript
/**
|
|
* 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 || 'Vigo')}</h1>
|
|
<p data-text="dim">${esc(identity.tagline || 'the Watcher of Trentuna')}</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() {
|
|
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);
|
|
// Graceful degradation: if the API is down, keep existing static content
|
|
// rather than replacing it with an error message. Only show unavailable
|
|
// status on elements that still show the loading placeholder.
|
|
document.querySelectorAll('[data-garden]:not([data-garden-loaded])').forEach(function (el) {
|
|
// If the element's children are just a loading placeholder, show error.
|
|
// Otherwise leave the static Hugo-rendered content in place.
|
|
var text = (el.textContent || '').trim();
|
|
if (text === '…' || text === '' || el.children.length === 0) {
|
|
el.innerHTML = '<p data-text="dim">Garden feed unavailable.</p>';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Boot ───────────────────────────────────────────────────────────
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', loadGardenFeed);
|
|
} else {
|
|
loadGardenFeed();
|
|
}
|
|
})(); |