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,309 @@
:root {
color-scheme: dark;
--bg: #0b1020;
--panel: #121a2f;
--panel-border: #24304d;
--text: #e8edf8;
--muted: #93a0bf;
--accent: #5eead4;
--accent-soft: rgba(94, 234, 212, 0.12);
--warn: #fb7185;
--warn-soft: rgba(251, 113, 133, 0.14);
--ok: #86efac;
--ok-soft: rgba(134, 239, 172, 0.14);
--shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
font-family: "Segoe UI", system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background:
radial-gradient(circle at top left, rgba(94, 234, 212, 0.08), transparent 28%),
radial-gradient(circle at top right, rgba(96, 165, 250, 0.08), transparent 24%),
var(--bg);
color: var(--text);
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 28px 20px 48px;
}
.topbar,
.panel,
.hero-card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 18px;
box-shadow: var(--shadow);
}
.topbar {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: center;
padding: 22px 24px;
margin-bottom: 20px;
}
.eyebrow {
margin: 0 0 6px;
color: var(--muted);
font-size: 0.82rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2 {
margin: 0;
font-weight: 650;
}
.topbar-actions {
display: flex;
gap: 14px;
align-items: end;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 0.82rem;
}
select {
background: #0d1426;
color: var(--text);
border: 1px solid var(--panel-border);
border-radius: 10px;
padding: 10px 12px;
min-width: 140px;
}
.badge {
padding: 10px 14px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
background: var(--warn-soft);
color: var(--warn);
}
.badge.ok {
background: var(--ok-soft);
color: var(--ok);
}
.hero-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-bottom: 20px;
}
.hero-card {
padding: 18px 18px 16px;
}
.hero-card .label {
color: var(--muted);
font-size: 0.82rem;
margin-bottom: 8px;
}
.hero-card .value {
font-size: 1.7rem;
font-weight: 700;
letter-spacing: -0.03em;
}
.hero-card .sub {
margin-top: 8px;
color: var(--muted);
font-size: 0.85rem;
}
.panel {
padding: 20px 22px;
margin-bottom: 20px;
}
.panel-head {
margin-bottom: 16px;
}
.panel-head p {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.split-grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 20px;
}
.budget-bar {
height: 14px;
border-radius: 999px;
background: #0d1426;
overflow: hidden;
margin-bottom: 14px;
}
.budget-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--accent), #60a5fa);
transition: width 0.35s ease;
}
.budget-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.stat {
padding: 12px 14px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
}
.stat .k {
color: var(--muted);
font-size: 0.8rem;
}
.stat .v {
margin-top: 6px;
font-size: 1.1rem;
font-weight: 650;
}
.chart {
display: grid;
gap: 10px;
}
.chart-row {
display: grid;
grid-template-columns: 72px 1fr 72px;
gap: 10px;
align-items: center;
}
.chart-label,
.chart-value {
font-size: 0.82rem;
color: var(--muted);
}
.chart-value {
text-align: right;
}
.chart-bar-wrap {
height: 28px;
background: #0d1426;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.chart-bar {
position: absolute;
top: 0;
bottom: 0;
border-radius: 8px;
}
.chart-bar.neg {
left: 50%;
background: linear-gradient(90deg, transparent, var(--warn));
}
.chart-bar.pos {
right: 50%;
background: linear-gradient(270deg, transparent, var(--ok));
}
.infra-list {
display: grid;
gap: 10px;
}
.infra-item {
padding: 12px 14px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
}
.infra-item strong {
display: block;
margin-bottom: 4px;
}
.infra-item span {
color: var(--muted);
font-size: 0.88rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 11px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
text-align: left;
font-size: 0.92rem;
}
th {
color: var(--muted);
font-weight: 600;
}
td.num,
th.num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.footer {
color: var(--muted);
font-size: 0.86rem;
}
.footer a {
color: var(--accent);
}
@media (max-width: 900px) {
.split-grid {
grid-template-columns: 1fr;
}
.topbar {
flex-direction: column;
align-items: flex-start;
}
}