Add Economic Observatory web UI with ledger-backed API

Introduce ui/ dashboard (dark observatory layout), JSON API, and local
dev server. All metrics load from expense and payment record ledgers.
Links Claude design reference for visual alignment.
This commit is contained in:
2026-06-22 02:48:52 +02:00
parent 7b84d34ea6
commit 9c1c2142fc
8 changed files with 795 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from typing import Any
from .economics import build_liquidity_summary, build_snapshot
from .load import (
default_data_dir,
latest_period,
load_budget,
load_expense_records,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def _load_json_catalog(data_dir: Path, name: str) -> dict:
path = data_dir / "infrastructure" / name
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def build_dashboard_payload(data_dir: Path | None = None, period: str | None = None) -> dict:
root = data_dir or default_data_dir()
product = load_product(root)
budget = load_budget(root)
models = load_pricing_models(root)
members = load_membership(root)
payments = load_payment_records(root)
expenses = load_expense_records(root)
ledger = load_monthly_ledger(root)
target_period = period or latest_period(ledger)
snapshot = build_snapshot(target_period, product, models, members, payments, ledger)
liquidity = build_liquidity_summary(budget, payments, ledger, target_period)
payment_by_period = {record.period: record for record in payments}
history = []
for month in sorted(ledger, key=lambda row: row.period):
if month.period > target_period:
continue
payment = payment_by_period.get(month.period)
net_payment = payment.net_amount if payment else Decimal("0")
history.append(
{
"period": month.period,
"active_members": month.active_members,
"gross_revenue": month.gross_revenue,
"infrastructure_cost": month.infrastructure_cost,
"payment_processing_cost": month.payment_processing_cost,
"total_platform_cost": month.total_platform_cost,
"net_payment": net_payment,
"net_liquidity": net_payment - month.infrastructure_cost,
}
)
return _serialize(
{
"design_reference": "https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511",
"period": target_period,
"product": product,
"budget": budget,
"snapshot": snapshot,
"liquidity": liquidity,
"history": history,
"pricing_models": models,
"members": members,
"payments": payments,
"expense_record_count": len(expenses),
"infrastructure": {
"domains": _load_json_catalog(root, "domains.json"),
"virtual_servers": _load_json_catalog(root, "virtual_servers.json"),
"stripe": _load_json_catalog(root, "stripe.json"),
},
}
)
def payload_json(data_dir: Path | None = None, period: str | None = None) -> str:
return json.dumps(build_dashboard_payload(data_dir, period), indent=2)