Skip to content

Frontend architecture

The admin dashboard lives inside the main server repo at tappass/frontend/. Not a separate repo — it ships alongside the server so API contracts can’t drift.

LayerWhat we use
FrameworkReact 19 + React Router 7
LanguageTypeScript (strict)
BundlerVite 6
StylingTailwind CSS 4 (via @tailwindcss/vite, no tailwind.config.ts)
UI primitivesRadix UI (dialog, tabs, tooltip, select, …)
Iconslucide-react
ChartsRecharts
HTTPNative fetch wrapped in src/lib/api.ts
AnalyticsPostHog

No Redux, no Zustand, no React Query. State is kept small and local.

frontend/
├── src/
│ ├── pages/ # Top-level route components — one file per route
│ ├── components/
│ │ ├── ui/ # Radix + Tailwind primitives (button, card, badge, …)
│ │ ├── Layout.tsx # Sidebar + top bar wrapper
│ │ ├── ConnectAgentGuide.tsx # Framework logos + integration steps
│ │ └── ...
│ ├── lib/
│ │ ├── api.ts # fetch wrapper, handles auth + errors
│ │ └── utils.ts # cn(), date helpers, etc.
│ ├── styles/
│ │ └── globals.css # Tailwind @theme + CSS variables
│ ├── App.tsx # Router
│ └── main.tsx # Entry
├── index.html
├── vite.config.ts
└── package.json

Layout.tsx is the single shell. Every page renders inside it. The shell owns:

  • Collapsible sidebar (240px expanded / 56px collapsed)
  • Top bar with environment indicator + user menu
  • Theme state (light/dark via data-theme on documentElement)
  • Responsive mobile menu

Pages receive no props — they fetch their own data via src/lib/api.ts.

  • No global store. Each page fetches its own data on mount.
  • URL is state. Search params carry filters, pagination, selected agent, etc. useSearchParams() from React Router 7.
  • Local component state for ephemeral UI (open/closed, hover). useState is enough.
  • Context for cross-cutting concerns only. Currently just theme. We refuse to add a context for “the current user” because that’s just localStorage + useEffect.

If you catch yourself reaching for Zustand, the page is probably doing too much. Split it.

React Router 7 declarative. Routes list in App.tsx:

<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Overview />} />
<Route path="agents" element={<Agents />} />
<Route path="agents/:id" element={<AgentDetail />} />
<Route path="sessions" element={<Sessions />} />
<Route path="audit-trail" element={<AuditTrail />} />
<Route path="playground" element={<Playground />} />
<Route path="settings/*" element={<Settings />} />
<Route path="copilot" element={<Copilot />} />
</Route>
<Route path="login" element={<Login />} />
</Routes>

Shipped pages: Overview, Agents, Pipelines, Sessions, AuditTrail, Copilot, Playground, Settings.

Parked (don’t add without product sign-off): Incidents, Compliance, Integrations, Federation.

  • All colours go through CSS variables in globals.css (HSL-based) — see the file for the palette.
  • Tailwind utilities for almost everything; bespoke CSS is a smell.
  • Card corner radius: rounded-xl (12px). Button radius: rounded-lg (8px). Icon container radius: rounded-lg.
  • Hover motion: hover:-translate-y-0.5 + shadow bump. Don’t over-animate.
  • Dark mode uses system preference by default; users can override in the top bar menu.

The in-product Getting Started page is the reference for our visual language. When in doubt, match it.

Every call goes through src/lib/api.ts:

export async function api<T>(
path: string,
init?: RequestInit,
): Promise<T> {
const response = await fetch(`/api/v1${path}`, {
...init,
credentials: "include",
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
if (!response.ok) throw await apiError(response);
return response.json();
}
  • Authentication is cookie-based (SSO session), not token-based — so credentials: "include" is the default.
  • Errors are thrown as typed ApiError objects with .status, .code, .message.
  • 401 triggers a redirect to /login; the page is never rendered with stale data.

Always, for: dialogs, tabs, tooltips, select, dropdowns, toggles, popovers.

Rewrite only when: accessibility requirements Radix can’t meet (rare), or the primitive is trivially simple (a badge — we have ui/badge.tsx).

Currently sparse. If you’re adding tests, put them next to the component as Component.test.tsx and use Vitest. Long-term we want visual regression (Playwright), but that’s not shipped yet.

From tappass/frontend/:

Terminal window
npm ci
npm run dev # http://localhost:5173 with HMR
npm run build # outputs to dist/

The build output is served by the Python server at / — the FastAPI app has a static-file mount. No separate deploy pipeline.

  • Next.js. We don’t have server-side rendering needs; Vite is faster for a dashboard.
  • Tailwind plugins for animations / forms. Tailwind 4 + Radix covers it.
  • CSS-in-JS. Makes styling hard to grep.
  • Form libraries (Formik, React Hook Form). The forms we have are small; native state + validation is enough.
  • Component libraries with opinions (MUI, Chakra). Radix + Tailwind gives us full control.