const euro = (value) => `€${Number(value).toFixed(2)}`; async function loadDashboard(period) { const query = period ? `?period=${encodeURIComponent(period)}` : ""; const response = await fetch(`/api/dashboard${query}`); if (!response.ok) throw new Error("Failed to load dashboard"); return response.json(); } function setBadge(el, status) { el.textContent = status; el.active = status === "generating" || status === "neutral"; el.draft = status === "burning"; } function renderMetrics(data) { const { snapshot, liquidity } = data; const cards = [ { label: "Remaining budget", value: euro(liquidity.remaining_budget), sub: `${liquidity.months_tracked} months tracked`, }, { label: "Period net liquidity", value: euro(snapshot.period_net_liquidity), sub: snapshot.liquidity_status, }, { label: "Active members", value: snapshot.active_members, sub: data.members[0]?.username ? `@${data.members[0].username}` : "—", }, { label: "Member payment (gross)", value: euro(snapshot.monthly_revenue), sub: `Net ${euro(data.payments.at(-1)?.net_amount ?? 0)} after Stripe`, }, { label: "Infrastructure / month", value: euro(snapshot.monthly_infrastructure_cost), sub: `Processing ${euro(snapshot.monthly_payment_processing_cost)}`, }, { label: "Cumulative net liquidity", value: euro(liquidity.cumulative_net_liquidity), sub: liquidity.liquidity_status, }, ]; document.getElementById("metric-grid").innerHTML = cards .map( (card) => ` ${card.label}
${card.value}
${card.sub}
` ) .join(""); } function renderBudget(data) { const { liquidity } = data; const initial = Number(liquidity.initial_budget); const remaining = Number(liquidity.remaining_budget); const pct = Math.max(0, Math.min(100, (remaining / initial) * 100)); const banner = document.getElementById("budget-banner"); const caption = remaining >= 0 ? "Within allocated budget" : "Over allocated budget"; document.getElementById("budget-fill").style.width = `${pct}%`; document.getElementById("budget-caption").textContent = caption; banner.variant = remaining < initial * 0.25 ? "warn" : "info"; document.getElementById("budget-stats").innerHTML = [ ["Initial budget", euro(initial)], ["Cumulative member payments (net)", euro(liquidity.cumulative_member_payments)], ["Cumulative infrastructure", euro(liquidity.cumulative_infrastructure_cost)], ["Cumulative processing", euro(liquidity.cumulative_payment_processing_cost)], ] .map( ([label, value]) => ` ${value} ` ) .join(""); } function renderChart(history) { const max = Math.max(...history.map((row) => Math.abs(Number(row.net_liquidity))), 1); document.getElementById("liquidity-chart").innerHTML = history .map((row) => { const value = Number(row.net_liquidity); const width = (Math.abs(value) / max) * 50; const barClass = value < 0 ? "obs-liquidity-row__bar--neg" : "obs-liquidity-row__bar--pos"; const valueClass = value < 0 ? "obs-liquidity-row__value--neg" : ""; return `
${row.period}
${euro(value)}
`; }) .join(""); } function renderInfra(data) { const domains = data.infrastructure.domains?.domains ?? []; const servers = data.infrastructure.virtual_servers?.servers ?? []; const stripe = data.infrastructure.stripe?.membership ?? {}; const payout = data.infrastructure.stripe?.payout_account ?? "payout"; const items = [ ...domains.map( (d) => `${d.monthly_eur} EUR/mo · ${d.tld}` ), ...servers.map( (s) => `${s.monthly_eur} EUR/mo · since ${s.started}` ), `${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${payout}`, ]; document.getElementById("infra-stack").innerHTML = `
${items.join("")}
`; } function renderTables(data) { document.getElementById("history-body").innerHTML = data.history .map( (row) => ` ${row.period} ${row.active_members} ${euro(row.gross_revenue)} ${euro(row.infrastructure_cost)} ${euro(row.payment_processing_cost)} ${euro(row.net_liquidity)} ` ) .join(""); document.getElementById("pricing-body").innerHTML = data.pricing_models .map( (model) => ` ${model.id} ${model.name} ${model.model_type} ${model.status} ` ) .join(""); } function populatePeriods(history, current) { const select = document.getElementById("period-select"); select.innerHTML = [...history].reverse().map( (row) => `` ); select.value = current; if (!select._obsBound) { select.addEventListener("wn-change", async (event) => { const next = await loadDashboard(event.detail.value); render(next); }); select._obsBound = true; } } async function loadDesignRefLabel() { try { const response = await fetch("/ui/vendor/whynot-design/.whynot-design-ref"); if (!response.ok) return; const ref = (await response.text()).trim().slice(0, 7); const label = document.getElementById("design-ref-label"); if (label && ref) label.textContent = `whynot-design @ ${ref}`; } catch { // optional footer detail } } function render(data) { document.getElementById("design-link").href = data.design_reference; loadDesignRefLabel(); const header = document.getElementById("page-header"); header.lede = `Period ${data.period}. Ledger-backed view of infrastructure spend, member payments, and remaining budget. Customer cost-pass-through billing is not active.`; setBadge(document.getElementById("liquidity-badge"), data.snapshot.liquidity_status); renderMetrics(data); renderBudget(data); renderChart(data.history); renderInfra(data); renderTables(data); populatePeriods(data.history, data.period); } loadDashboard() .then(render) .catch((error) => { document.body.innerHTML = `
${error}
`; });