frontend: scaffold Vite + React 19 + TS + Tailwind v4 + TanStack Query
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled

Bare-minimum SPA that wires up to the FastAPI backend:

- Vite 6 + React 19 + TS strict, alias @/* to src/*
- Tailwind v4 via @tailwindcss/vite (no postcss)
- TanStack Query v5 with sane defaults (30s staleTime, no auto-refetch)
- React Router 7 for routing
- ECharts + Recharts available (charts land in Phase 1a)
- Vitest + @testing-library/react for tests
- Dev proxy /api → http://localhost:8080 (FastAPI)

Pages:
- Dashboard — pulls /networth, shows total + per-account cards.
  No chart yet (Phase 1a). Empty/error states for "no data" cases
  point users to the ingest CLI.

Header shows live API health (queue depth from /healthz). 274 KB JS
gzipped to 87 KB. typecheck + build pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-09 21:53:59 +00:00
parent ee6ed1d3c4
commit f4539f9e6d
16 changed files with 6145 additions and 0 deletions

View file

@ -0,0 +1,75 @@
/**
* Dashboard landing page. Wire up gets fleshed out in Phase 1a.
* For now: confirm we can reach /networth and render today's NW.
*/
import { useQuery } from '@tanstack/react-query';
import { api } from '@/api/client';
import { gbp } from '@/lib/format';
export function Dashboard() {
const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current });
if (nw.isLoading) {
return <p className="text-slate-500">Loading net worth</p>;
}
if (nw.isError) {
return (
<div className="rounded-md border border-amber-200 bg-amber-50 p-4 text-amber-800">
<p className="font-medium">Couldn&apos;t reach the API.</p>
<p className="text-sm mt-1">
Make sure FastAPI is running on :8080 and Wealthfolio ingest has populated
<code className="px-1">account_snapshot</code>.
</p>
</div>
);
}
const data = nw.data!;
if (data.accounts.length === 0) {
return (
<div className="rounded-md border border-slate-200 bg-white p-6">
<p className="font-medium">No snapshots yet.</p>
<p className="text-sm text-slate-500 mt-1">
Run <code className="px-1">python -m fire_planner ingest</code> to pull from the
wealthfolio_sync mirror.
</p>
</div>
);
}
return (
<section className="space-y-6">
<header>
<h1 className="text-3xl font-semibold tracking-tight">Net worth</h1>
<p className="text-sm text-slate-500">As of {data.snapshot_date}</p>
</header>
<div className="rounded-lg border border-slate-200 bg-white p-6">
<div className="text-4xl font-semibold tabular-nums">{gbp(data.total_gbp)}</div>
<p className="text-sm text-slate-500 mt-1">{data.accounts.length} accounts</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.accounts.map((a) => (
<div
key={a.account_id}
className="rounded-lg border border-slate-200 bg-white p-4 flex flex-col gap-1"
>
<div className="flex items-center justify-between">
<span className="font-medium truncate">{a.account_name}</span>
<span className="text-xs uppercase tracking-wide text-slate-500">
{a.account_type}
</span>
</div>
<div className="text-2xl font-semibold tabular-nums">{gbp(a.market_value_gbp)}</div>
{a.currency !== 'GBP' && (
<div className="text-xs text-slate-500">
{gbp(a.market_value, true)} {a.currency}
</div>
)}
</div>
))}
</div>
</section>
);
}