fire-planner: UX review pass 2 — health URL, Progress in shell, Gantt
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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.
This commit is contained in:
parent
cd1fc37f25
commit
9fd8389c26
8 changed files with 53 additions and 34 deletions
|
|
@ -2,6 +2,8 @@ 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';
|
||||
|
|
@ -53,9 +55,9 @@ export function App() {
|
|||
<Route path="/scenarios" element={<Scenarios />} />
|
||||
<Route path="/scenarios/new" element={<ScenarioNew />} />
|
||||
<Route path="/scenarios/:id/edit" element={<ScenarioEdit />} />
|
||||
<Route path="/scenarios/:id/progress" element={<ProgressPage />} />
|
||||
<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"
|
||||
|
|
@ -74,7 +76,7 @@ export function App() {
|
|||
element={<PlaceholderTab feature="Estate planning" wave={2} />}
|
||||
/>
|
||||
<Route path="settings" element={<SettingsTab />}>
|
||||
<Route index element={<MilestonesSettings />} />
|
||||
<Route index element={<Navigate to="milestones" replace />} />
|
||||
<Route path="milestones" element={<MilestonesSettings />} />
|
||||
<Route path="rates" element={<RatesSettings />} />
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||
}
|
||||
|
||||
export const api = {
|
||||
health: () => request<{ status: string; queue_depth: number }>('/healthz'),
|
||||
// /healthz is mounted at root by the FastAPI app (so k8s probes hit
|
||||
// it without the /api prefix). Bypass API_BASE to match.
|
||||
health: () =>
|
||||
fetch('/healthz')
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
|
||||
.then((d) => d as { status: string; queue_depth: number }),
|
||||
recompute: (body?: Record<string, unknown>) =>
|
||||
request<{ status: string; depth: number }>('/recompute', {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -288,6 +288,12 @@ function Inner({
|
|||
const fill = CATEGORY_FILL[ev.category] ?? CATEGORY_FILL.essential;
|
||||
const border =
|
||||
CATEGORY_BORDER[ev.category] ?? CATEGORY_BORDER.essential;
|
||||
// Edge handles are only useful on multi-year ranges. For
|
||||
// single-year (point) events the bar is too narrow to
|
||||
// distinguish "edge" from "middle" so the whole bar
|
||||
// moves and the user resizes via the popover.
|
||||
const showHandles = w >= 24 && yearEnd > ev.year_start;
|
||||
const handleW = Math.min(8, w / 4);
|
||||
return (
|
||||
<g
|
||||
key={ev.id}
|
||||
|
|
@ -306,28 +312,30 @@ function Inner({
|
|||
rx={3}
|
||||
onMouseDown={(e) => startDrag(e, ev, 'move')}
|
||||
/>
|
||||
{/* Left handle */}
|
||||
<rect
|
||||
x={x - 1}
|
||||
y={y}
|
||||
width={6}
|
||||
height={h}
|
||||
fill="rgba(0,0,0,0.25)"
|
||||
rx={3}
|
||||
onMouseDown={(e) => startDrag(e, ev, 'left')}
|
||||
style={{ cursor: 'ew-resize' }}
|
||||
/>
|
||||
{/* Right handle */}
|
||||
<rect
|
||||
x={x + w - 5}
|
||||
y={y}
|
||||
width={6}
|
||||
height={h}
|
||||
fill="rgba(0,0,0,0.25)"
|
||||
rx={3}
|
||||
onMouseDown={(e) => startDrag(e, ev, 'right')}
|
||||
style={{ cursor: 'ew-resize' }}
|
||||
/>
|
||||
{showHandles && (
|
||||
<>
|
||||
<rect
|
||||
x={x - 1}
|
||||
y={y}
|
||||
width={handleW}
|
||||
height={h}
|
||||
fill="rgba(0,0,0,0.25)"
|
||||
rx={3}
|
||||
onMouseDown={(e) => startDrag(e, ev, 'left')}
|
||||
style={{ cursor: 'ew-resize' }}
|
||||
/>
|
||||
<rect
|
||||
x={x + w - handleW + 1}
|
||||
y={y}
|
||||
width={handleW}
|
||||
height={h}
|
||||
fill="rgba(0,0,0,0.25)"
|
||||
rx={3}
|
||||
onMouseDown={(e) => startDrag(e, ev, 'right')}
|
||||
style={{ cursor: 'ew-resize' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<text
|
||||
x={x + 6}
|
||||
y={y + h / 2}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function Sidebar({ activeScenarioId }: Props) {
|
|||
<SidebarLink to="/" end>
|
||||
Dashboard
|
||||
</SidebarLink>
|
||||
{activeScenarioId != null && (
|
||||
{activeScenarioId != null && Number.isFinite(activeScenarioId) && (
|
||||
<SidebarLink to={`/scenarios/${activeScenarioId}/progress`}>Progress</SidebarLink>
|
||||
)}
|
||||
</SidebarSection>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ export function TabBar({ tabs }: { tabs: TabSpec[] }) {
|
|||
[
|
||||
'py-3 px-1 -mb-px border-b-2 whitespace-nowrap',
|
||||
isActive
|
||||
? 'border-slate-900 text-slate-900 font-medium'
|
||||
? t.stub
|
||||
? 'border-slate-300 text-slate-500 font-medium'
|
||||
: 'border-slate-900 text-slate-900 font-medium'
|
||||
: t.stub
|
||||
? 'border-transparent text-slate-300 hover:text-slate-500'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-800',
|
||||
|
|
|
|||
|
|
@ -66,6 +66,13 @@ export function Dashboard() {
|
|||
<p className="text-sm text-slate-500">Loading…</p>
|
||||
) : history.isError || !history.data ? (
|
||||
<p className="text-sm text-slate-500">History unavailable.</p>
|
||||
) : history.data.points.length < 2 ? (
|
||||
<p className="text-sm text-slate-500">
|
||||
Only {history.data.points.length} snapshot in the window — the
|
||||
chart needs at least two daily points to draw a line. The
|
||||
Wealthfolio CronJob writes one snapshot per day, so this fills
|
||||
in over time.
|
||||
</p>
|
||||
) : (
|
||||
<NetWorthChart history={history.data} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* fan, with a variance side panel.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '@/api/client';
|
||||
import { ProgressOverlay } from '@/components/ProgressOverlay';
|
||||
|
|
@ -24,11 +24,6 @@ export function ProgressPage() {
|
|||
|
||||
return (
|
||||
<section className="space-y-5">
|
||||
<div className="text-sm">
|
||||
<Link to={`/scenarios/${id}`} className="text-slate-500 hover:text-slate-900">
|
||||
← Plan
|
||||
</Link>
|
||||
</div>
|
||||
<header>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Progress</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function SettingsTab() {
|
|||
const params = useParams<{ id: string }>();
|
||||
const id = Number(params.id);
|
||||
const items = [
|
||||
{ to: `/scenarios/${id}/settings`, label: 'Milestones', end: true },
|
||||
{ to: `/scenarios/${id}/settings/milestones`, label: 'Milestones' },
|
||||
{ to: `/scenarios/${id}/settings/rates`, label: 'Rates' },
|
||||
{ to: `/scenarios/${id}/settings/notes`, label: 'Notes' },
|
||||
{ to: `/scenarios/${id}/settings/dividends`, label: 'Dividends', stub: true },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue