fire-planner/frontend/src/App.tsx
Viktor Barzin 9fd8389c26
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fire-planner: UX review pass 2 — health URL, Progress in shell, Gantt
single-year drag, Settings highlight, Dashboard empty state

Round 2 of agent-driven UX review.

- api status badge was always "unreachable" because client called
  /api/healthz but FastAPI mounts /healthz at root (so k8s probes
  hit it without the SPA prefix). Client now calls /healthz directly.
- Progress page rendered without the shell sidebar/tabs because its
  route was sibling to ScenarioShell; moved it inside as a nested
  route (also drops the redundant "← Plan" breadcrumb since the tab
  bar handles that now).
- EventGantt: single-year (point) events no longer render edge handles
  since the bar is too narrow to distinguish "edge grab" from "middle
  grab" — for points the whole bar moves; resize via the popover.
  Bars wider than 24px keep their edge handles.
- Settings sub-nav: Milestones now points at /settings/milestones
  (consistent active highlight); /settings index redirects there.
- Dashboard "Last 12 months" chart shows an explainer when the
  history has fewer than 2 snapshots instead of an empty axis.
- Stub tabs that are currently active get a slate-300 underline +
  slate-500 text rather than the bold slate-900 — visually honest
  about what's a placeholder.

Frontend typecheck/test/build green.
2026-05-10 17:49:05 +00:00

125 lines
4.8 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { NavLink, Route, Routes, Link } from 'react-router-dom';
import { api } from '@/api/client';
import { Navigate } from 'react-router-dom';
import { Compare } from '@/pages/Compare';
import { Dashboard } from '@/pages/Dashboard';
import { CashflowTab } from '@/pages/CashflowTab';
import { MilestonesSettings } from '@/pages/MilestonesSettings';
import { NotesSettings } from '@/pages/NotesSettings';
import { PlaceholderTab } from '@/pages/PlaceholderTab';
import { ProgressPage } from '@/pages/ProgressPage';
import { RatesSettings } from '@/pages/RatesSettings';
import { ScenarioDetail } from '@/pages/ScenarioDetail';
import { ScenarioEdit } from '@/pages/ScenarioEdit';
import { ScenarioNew } from '@/pages/ScenarioNew';
import { ScenarioShell } from '@/pages/ScenarioShell';
import { Scenarios } from '@/pages/Scenarios';
import { SettingsTab } from '@/pages/SettingsTab';
import { WhatIf } from '@/pages/WhatIf';
export function App() {
const health = useQuery({ queryKey: ['health'], queryFn: api.health });
return (
<div className="min-h-screen flex flex-col">
<header className="border-b border-slate-200 bg-white">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-6">
<Link to="/" className="text-xl font-semibold tracking-tight">
fire-planner
</Link>
<nav className="flex gap-4 text-sm">
<NavTab to="/">Dashboard</NavTab>
<NavTab to="/scenarios">Scenarios</NavTab>
<NavTab to="/what-if">What if</NavTab>
</nav>
</div>
<div className="text-xs text-slate-500">
api:{' '}
<span className={health.data ? 'text-emerald-600' : 'text-amber-600'}>
{health.isLoading
? '…'
: health.data
? `ok (queue=${health.data.queue_depth})`
: 'unreachable'}
</span>
</div>
</div>
</header>
<main className="flex-1 max-w-7xl w-full mx-auto px-6 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/scenarios" element={<Scenarios />} />
<Route path="/scenarios/new" element={<ScenarioNew />} />
<Route path="/scenarios/:id/edit" element={<ScenarioEdit />} />
<Route path="/scenarios/:id" element={<ScenarioShell />}>
<Route index element={<ScenarioDetail />} />
<Route path="progress" element={<ProgressPage />} />
<Route path="cash-flow" element={<CashflowTab />} />
<Route
path="tax-analytics"
element={<PlaceholderTab feature="Tax Analytics" wave={2} />}
/>
<Route
path="compare"
element={<PlaceholderTab feature="Side-by-side Compare" wave={2} />}
/>
<Route
path="reports"
element={<PlaceholderTab feature="Reports / PDF export" wave={2} />}
/>
<Route
path="estate"
element={<PlaceholderTab feature="Estate planning" wave={2} />}
/>
<Route path="settings" element={<SettingsTab />}>
<Route index element={<Navigate to="milestones" replace />} />
<Route path="milestones" element={<MilestonesSettings />} />
<Route path="rates" element={<RatesSettings />} />
<Route
path="dividends"
element={<PlaceholderTab feature="Dividends override matrix" wave={2} />}
/>
<Route
path="bonds"
element={<PlaceholderTab feature="Bond allocation editor" wave={2} />}
/>
<Route
path="tax"
element={<PlaceholderTab feature="Tax mode toggle + per-jurisdiction overrides" wave={2} />}
/>
<Route
path="metrics"
element={<PlaceholderTab feature="Right-sidebar metric picker" wave={2} />}
/>
<Route
path="other"
element={<PlaceholderTab feature="Currency / horizon / seed defaults" wave={2} />}
/>
<Route path="notes" element={<NotesSettings />} />
</Route>
</Route>
<Route path="/compare" element={<Compare />} />
<Route path="/what-if" element={<WhatIf />} />
</Routes>
</main>
</div>
);
}
function NavTab({ to, children }: { to: string; children: React.ReactNode }) {
return (
<NavLink
to={to}
end
className={({ isActive }) =>
`${isActive ? 'text-slate-900 font-medium' : 'text-slate-500 hover:text-slate-800'}`
}
>
{children}
</NavLink>
);
}