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.classList.toggle("ok", status === "generating" || status === "neutral"); } function renderHero(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("hero-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)); document.getElementById("budget-fill").style.width = `${pct}%`; document.getElementById("budget-caption").textContent = remaining >= 0 ? "Within allocated budget" : "Over allocated budget"; 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( ([k, v]) => `
${k}
${v}
` ) .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 bar = value < 0 ? `
` : `
`; return `
${row.period}
${bar}
${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 items = [ ...domains.map( (d) => `
${d.name}${d.monthly_eur} EUR/mo · ${d.tld}
` ), ...servers.map( (s) => `
${s.name}${s.monthly_eur} EUR/mo · since ${s.started}
` ), `
Stripe · ${stripe.member_username ?? "member"}${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${data.infrastructure.stripe?.payout_account ?? "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.onchange = async () => { const next = await loadDashboard(select.value); render(next); }; } function render(data) { document.getElementById("design-link").href = data.design_reference; setBadge(document.getElementById("liquidity-badge"), data.snapshot.liquidity_status); renderHero(data); renderBudget(data); renderChart(data.history); renderInfra(data); renderTables(data); populatePeriods(data.history, data.period); } loadDashboard() .then(render) .catch((error) => { document.body.innerHTML = `
${error}
`; });