Admin UX overhaul: P0 fixes, error-UX foundations, nav cleanup, tenant detail page

From the 2026-07-14 UI/UX audit (17/40). Four phases:

P1 — Settings route crashed on Agents→Edit (Input/Textarea used but never
imported). Added imports + a shared route-level error boundary
(components/route-error.tsx) re-exported from all shell routes, so one
crashing panel degrades to an explained card with the nav intact instead of
replacing the whole app with a stack trace. Corrected the stale CLAUDE.md
claim that `npm run typecheck` crashes — it works, and would have caught the
missing import.

P2 — Error/empty/feedback foundations. Fixed useSession identity churn that
fired ~3x duplicate fetches per screen and self-inflicted 429s (referentially
stable snapshot). New lib/errors.ts (describeError → plain-language + the fix)
and components/data-state.tsx (DataState renders exactly one of
error/loading/empty/content, so a failed load never shows as "empty";
DialogError for in-dialog failures; 429 auto-retry). Rolled across all 15
list routes; every mutation now toasts. Surfaced+fixed two silent-failure
bugs (sso + buckets-CORS swallowed load errors; the latter could wipe rules
on save).

P3 — Nav IA + trust cleanup. Default-expanded rail; regrouped into 7 coherent
sections; collapsed the Apps/Plan/Entitlements stub triplication into one
honest /billing page; deleted fabricated seeded notifications and the dead
Help menu item; fixed the 403 copy (referenced a tenant switcher that doesn't
exist); removed orphan /assistant + /library routes; gated dev-seed login
hints behind DEV; renamed /activity → /audit-log with a redirect.

P4 — Tenant detail page (routes/tenants.$id.tsx + components/tenant-detail/*),
closing the provision→configure gap. 8 tabs (Overview, Plan & quotas,
Branding, Localization, Email & SMS, Feature flags, IP rules, Inbound
webhooks), each verified saving to the real backend. Row name links to detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jules
2026-07-14 13:43:57 +10:00
parent 938143f3f5
commit 7415b40240
51 changed files with 5923 additions and 4575 deletions

View File

@@ -0,0 +1,97 @@
import type { ReactNode } from "react"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card"
import { Button } from "~/components/ui/button"
import { DialogError } from "~/components/data-state"
/**
* The shared frame every tenant-detail settings tab renders through: a titled
* card whose footer holds a Save button and, on failure, an inline error in the
* same place (never a page-level banner). Keeps the eight tabs visually and
* behaviourally identical so the operator learns the surface once.
*/
export function TenantSection({
title,
description,
children,
onSubmit,
saving,
error,
errorContext,
saveLabel = "Save changes",
dirty = true,
dataAction,
footerExtra,
}: {
title: string
description?: ReactNode
children: ReactNode
onSubmit: () => void
saving: boolean
error: unknown
errorContext: string
saveLabel?: string
/** Disable Save until something changed. Defaults to always-enabled. */
dirty?: boolean
dataAction: string
/** Rendered to the left of Save (e.g. a "Send test" or "Remove" button). */
footerExtra?: ReactNode
}) {
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
{description ? <CardDescription>{description}</CardDescription> : null}
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault()
onSubmit()
}}
className="flex flex-col gap-5"
>
{children}
{error ? <DialogError error={error} context={errorContext} /> : null}
<div className="flex items-center justify-between gap-2 pt-1">
<div>{footerExtra}</div>
<Button type="submit" disabled={saving || !dirty} data-action={dataAction}>
{saving ? "Saving…" : saveLabel}
</Button>
</div>
</form>
</CardContent>
</Card>
)
}
/** A labelled form row — label, control, and an optional hint underneath. */
export function Field({
label,
htmlFor,
hint,
children,
}: {
label: string
htmlFor?: string
hint?: ReactNode
children: ReactNode
}) {
return (
<div className="flex flex-col gap-1.5">
<label htmlFor={htmlFor} className="text-sm font-medium">
{label}
</label>
{children}
{hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
</div>
)
}