const euro = (value) => `€${Number(value).toFixed(2)}`; const SECTION_META = { "cost-floor": { eyebrow: "Economic Observatory · Cost Floor", title: "Operator liquidity", lede: (data) => `Period ${data.period}. Minimum viable economics: platform cost, margin, and liquidity burn from ledgers.`, }, "value-range": { eyebrow: "Economic Observatory · Value Range", title: "Customer value bands", lede: (data) => `Period ${data.period}. Segment-level willingness-to-pay hypotheses above the cost floor.`, }, "market-price": { eyebrow: "Economic Observatory · Market Price", title: "Competitive context", lede: (data) => `Reviewed ${data.market_price.last_reviewed ?? "—"}. Evidence about alternatives, capabilities, and public pricing.`, }, }; let currentData = null; let activeSection = "cost-floor"; function normalizeData(data) { const snapshot = data.snapshot ?? {}; const active = (data.pricing_models ?? []).find((model) => model.status === "active"); if (!data.cost_floor) { data.cost_floor = { cost_per_member: snapshot.cost_per_member ?? "0", monthly_total_platform_cost: snapshot.monthly_total_platform_cost ?? "0", active_price: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0", active_model_name: active?.name ?? null, }; } if (!data.value_range) { data.value_range = { current_price_eur: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0", aggregate_low_eur: active?.access_fee_amount ?? "0", aggregate_high_eur: active?.access_fee_amount ?? "0", cost_per_member_eur: snapshot.cost_per_member ?? "0", segments: [], value_drivers: [], notes: "Value range data unavailable — restart observatory.server to load the latest API.", }; } if (!data.market_price) { data.market_price = { alternative_count: 0, priced_alternative_count: 0, market_low_eur: null, market_high_eur: null, last_reviewed: null, alternatives: [], notes: "Market signals unavailable — restart observatory.server to load the latest API.", }; } return data; } 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 setSection(sectionId) { activeSection = sectionId; document.querySelectorAll(".obs-panel").forEach((panel) => { const active = panel.dataset.section === sectionId; panel.hidden = !active; panel.classList.toggle("obs-panel--active", active); }); document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => { item.active = item.dataset.section === sectionId; }); const meta = SECTION_META[sectionId]; const header = document.getElementById("page-header"); header.eyebrow = meta.eyebrow; header.title = meta.title; if (currentData) { header.lede = meta.lede(currentData); } const badge = document.getElementById("liquidity-badge"); badge.style.display = sectionId === "cost-floor" ? "" : "none"; } function bindNavigation() { document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => { item.addEventListener("click", () => setSection(item.dataset.section)); }); } function renderCostFloor(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: "Cost per member", value: euro(data.cost_floor.cost_per_member), sub: `Platform ${euro(data.cost_floor.monthly_total_platform_cost)} / mo`, }, { label: "Gross margin", value: euro(snapshot.gross_margin), sub: `${snapshot.gross_margin_pct}% of gross revenue`, }, { label: "Infrastructure / month", value: euro(snapshot.monthly_infrastructure_cost), sub: `Processing ${euro(snapshot.monthly_payment_processing_cost)}`, }, { label: "Active price", value: euro(data.cost_floor.active_price), sub: data.cost_floor.active_model_name ?? "—", }, ]; document.getElementById("metric-grid").innerHTML = cards .map( (card) => ` ${card.label}
${card.value}
${card.sub}
` ) .join(""); 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"); document.getElementById("budget-fill").style.width = `${pct}%`; document.getElementById("budget-caption").textContent = remaining >= 0 ? "Within allocated budget" : "Over allocated budget"; 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(""); const max = Math.max(...data.history.map((row) => Math.abs(Number(row.net_liquidity))), 1); document.getElementById("liquidity-chart").innerHTML = data.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(""); 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 infraItems = [ ...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 = `
${infraItems.join("")}
`; 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(""); setBadge(document.getElementById("liquidity-badge"), snapshot.liquidity_status); } function renderValueRangeBand(low, high, current) { const min = Number(low); const max = Number(high); const cur = Number(current); const span = Math.max(max - min, 0.01); const curPct = Math.max(0, Math.min(100, ((cur - min) / span) * 100)); return `
${euro(min)} ${euro(cur)} now ${euro(max)}
`; } function renderValueRange(data) { const vr = data.value_range; document.getElementById("value-summary").innerHTML = [ { label: "Current price", value: euro(vr.current_price_eur), sub: data.cost_floor.active_model_name ?? "—", }, { label: "Aggregate band", value: `${euro(vr.aggregate_low_eur)} – ${euro(vr.aggregate_high_eur)}`, sub: `${vr.segments.length} segments`, }, { label: "Cost per member", value: euro(vr.cost_per_member_eur), sub: "Cost floor reference", }, ] .map( (card) => ` ${card.label}
${card.value}
${card.sub}
` ) .join(""); document.getElementById("value-range-notes").textContent = vr.notes || ""; document.getElementById("value-segments").innerHTML = vr.segments .map( (segment) => ` ${segment.name} ${segment.confidence} ${renderValueRangeBand(segment.low_eur, segment.high_eur, vr.current_price_eur)}

${segment.evidence}

Headroom ${euro(segment.headroom_to_high_eur)} ${segment.drivers.join(" · ")}
` ) .join(""); document.getElementById("value-drivers").innerHTML = vr.value_drivers .map( (driver) => ` ${driver.note} ${driver.strength} ` ) .join(""); } function renderMarketPrice(data) { const market = data.market_price; const span = market.market_low_eur != null && market.market_high_eur != null ? `${euro(market.market_low_eur)} – ${euro(market.market_high_eur)}` : "—"; document.getElementById("market-summary").innerHTML = [ { label: "Alternatives tracked", value: market.alternative_count, sub: `${market.priced_alternative_count} with public monthly price`, }, { label: "Priced span", value: span, sub: `Reviewed ${market.last_reviewed ?? "—"}`, }, { label: "Coulomb list price", value: euro(data.value_range.current_price_eur), sub: data.cost_floor.active_model_name ?? "—", }, ] .map( (card) => ` ${card.label}
${card.value}
${card.sub}
` ) .join(""); document.getElementById("market-notes").textContent = market.notes || ""; document.getElementById("market-body").innerHTML = market.alternatives .map((alt) => { const price = alt.price_monthly_eur && Number(alt.price_monthly_eur) > 0 ? euro(alt.price_monthly_eur) : "—"; const features = (alt.features ?? []).join(", "); return ` ${alt.name}
${alt.id}
${alt.category} ${price} ${alt.signal_level} ${features} ${alt.source} · ${alt.observed}
${alt.note} `; }) .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) { currentData = normalizeData(data); document.getElementById("design-link").href = data.design_reference; loadDesignRefLabel(); setSection(activeSection); renderCostFloor(data); renderValueRange(data); renderMarketPrice(data); populatePeriods(data.history, data.period); } bindNavigation(); loadDashboard() .then(render) .catch((error) => { document.body.innerHTML = `
${error}
`; });