Add API client, /me bootstrap and useJob polling hook
Independent review Codex gpt-6-sol high: CHANGES_REQUIRED (1 medium)
CI / ci (push) Successful in 1m34s
CI / ci (pull_request) Successful in 1m32s

web/src/api provides createApiClient().apiFetch: same-origin JSON with
the cached /api/v1/me CSRF token on non-GET requests and a typed
ApiError from the server's error body. A 401 navigates to
/auth/login?return={path}; a csrf 403 refreshes /me once and retries
once; any other 403 shows the "not in this household" screen.

App loads /me before the routes, showing the Skeleton meanwhile. The
QueryClient retries only ApiErrors the server marks retryable. useJob
polls GET /api/v1/jobs/{id} backing off 500 ms to 2 s, exposing status,
partial result and error until a terminal status, and stops on unmount.

Maestro task 21051318-15ae-4d0e-a9a1-2143f87de759.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 08:33:34 -04:00
co-authored by Claude Opus 5.5
parent c899f07b1f
commit 913c1e5337
18 changed files with 794 additions and 24 deletions
+31 -14
View File
@@ -1,6 +1,8 @@
import { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClientProvider } from '@tanstack/react-query'
import { Route, Routes } from 'react-router'
import { ApiProvider, createQueryClient, type ApiClient } from './api/index.ts'
import { AuthBootstrap } from './AuthBootstrap.tsx'
import { PlaceholderPage } from './pages/PlaceholderPage.tsx'
import { AppShell } from './shell/AppShell.tsx'
@@ -23,19 +25,34 @@ export const routes = [
{ path: '/settings/calendar', title: 'Calendar' },
] as const
// App expects to be rendered inside a router (BrowserRouter in main.tsx,
// MemoryRouter in tests).
export default function App() {
const [queryClient] = useState(() => new QueryClient())
// AppRoutes is the shell and its pages, shown once /api/v1/me has loaded.
export function AppRoutes() {
return (
<QueryClientProvider client={queryClient}>
<Routes>
<Route element={<AppShell />}>
{routes.map(({ path, title }) => (
<Route key={path} path={path} element={<PlaceholderPage title={title} />} />
))}
</Route>
</Routes>
</QueryClientProvider>
<Routes>
<Route element={<AppShell />}>
{routes.map(({ path, title }) => (
<Route key={path} path={path} element={<PlaceholderPage title={title} />} />
))}
</Route>
</Routes>
)
}
export type AppProps = {
api: ApiClient
}
// App expects to be rendered inside a router (BrowserRouter in main.tsx,
// MemoryRouter in tests).
export default function App({ api }: AppProps) {
const [queryClient] = useState(createQueryClient)
return (
<ApiProvider client={api}>
<QueryClientProvider client={queryClient}>
<AuthBootstrap>
<AppRoutes />
</AuthBootstrap>
</QueryClientProvider>
</ApiProvider>
)
}