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]) => `
`
)
.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}`;
});