AGENTS.md@frontend ยท diff
git:20260529.893ca3a to git:20260601.a7669b7
10 added, 10 removed. Audit A to A.
# Frontend AGENTS.md
High-signal constraints for the relay-server frontend. Only items expensive to rediscover.
## Frontend-Backend Contracts
- 1. **Public list data comes from `/state`.**
+ 1. **Public list data comes from `/ui/state`.**
Go relay returns leases only; `api/server.ts` adds frontend-owned presentation fields mirrored in `src/types/api.ts`.
- Why: the Go relay is API-only. Do not reintroduce Go HTML data injection for public lease state.
- 2. **API path constants require dual maintenance.**
- Go relay definitions live in `../types/paths.go`; frontend facade paths live in `api/server.ts`, the edge nginx template, and `src/lib/apiPaths.ts`.
- - Why: no codegen. A path mismatch produces 404s.
+ 2. **Relay and presentation API paths have separate owners.**
+ Relay paths are owned by `../types/paths.go` and mirrored in `src/lib/apiPaths.ts` for browser calls; presentation paths stay under `/ui/` in `src/lib/apiPaths.ts`, `api/server.ts`, and the edge nginx template.
+ - Why: nginx should distinguish the BFF by one prefix instead of enumerating presentation endpoints.
3. **API envelope shape must match across Go and TS.**
All JSON control-plane responses use `{ ok, data?, error?: { code, message } }`.
Go shape is `types.APIEnvelope` in `../types/api.go`; Go writers live in `../utils/api.go`; TS parser lives in `src/lib/apiClient.ts`.
- Why: backend responses that skip the envelope surface as `invalid_envelope` in the frontend.
- 4. **Admin auth uses bearer tokens returned by `/admin/auth/login`.**
- `src/hooks/useAuth.ts` stores the token through `src/lib/adminAuthToken.ts`; `src/lib/apiClient.ts` adds it to `/admin/*` and `/policy/*` requests as `Authorization: Bearer ...`.
+ 4. **Admin auth uses bearer tokens returned by `/api/admin/auth/login`.**
+ `src/hooks/useAuth.ts` stores the token through `src/lib/adminAuthToken.ts`; `src/lib/apiClient.ts` adds it to `/api/admin/*`, `/api/policy/*`, and `/ui/policy/*` requests as `Authorization: Bearer ...`.
- Why: the relay admin API must be usable by any separately hosted frontend without credentialed cookie CORS state.
5. **`VITE_PORTAL_API_BASE_URL` is the only built-in API origin knob.**
Leave it empty for same-origin development/proxying, or set it at build/dev time for a separately hosted relay API.
- Why: runtime-generated config files couple the static frontend bundle back to deployment state.
- 6. **Policy state reads are aggregated through `/policy/state`.**
- `src/hooks/useAdmin.ts` expects `{ policy, leases }`; policy settings writes go through `/policy`, while lease/IP actions use `/policy/leases` and `/policy/ips`.
+ 6. **Presentation policy state reads are aggregated through `/ui/policy/state`.**
+ `src/hooks/useAdmin.ts` expects `{ policy, leases }`; presentation policy settings writes go through `/ui/policy`, while lease/IP actions use `/ui/policy/leases` and `/ui/policy/ips`.
- Why: splitting those reads across multiple endpoints reintroduces extra request coordination and drift in the admin bootstrap path.
7. **Lease/policy lease JSON casing is snake_case.**
Go `Lease`/`PolicyLease` JSON tags live in `../types/identity.go`; TS mirrors the wire shape in `src/types/api.ts`.
- Why: the frontend should not depend on Go's implicit PascalCase encoder output.
8. **Admin policy writes identify targets in the JSON body.**
- Lease policy writes use `/policy/leases` with `identity_key`; IP policy writes use `/policy/ips` with `ip`.
+ Presentation lease policy writes use `/ui/policy/leases` with `identity_key`; IP policy writes use `/ui/policy/ips` with `ip`, then `api/server.ts` forwards to relay `/api/policy/*`.
- Why: path encoding rules add a second contract surface and are easy to drift across Go and TS.
9. **Lease metadata has a wire type and a UI parser.**
Go `LeaseMetadata` (`../types/identity.go`) mirrors TS `LeaseMetadata` (`src/types/api.ts`). UI display defaults are owned by `src/lib/metadata.ts`.
- Why: API contract fields and UI fallback behavior should not be mixed.
10. **Presentation support is frontend-owned.**
- `api/server.ts` serves `/state`, `/policy/*`, `/service/status`, and `/thumbnail/{hostname}` by composing relay data with frontend-owned state.
+ `api/server.ts` serves `/ui/state`, `/ui/policy/*`, `/ui/service/status`, and `/ui/thumbnail/{hostname}` by composing relay data with frontend-owned state.
- Why: landing-page flags, quick-start status, and generated screenshots are presentation support and should not add state or routes to the Go relay API.
11. **ApprovalMode is a closed two-value enum: `"auto"` | `"manual"`.**
TS `normalizeApprovalMode()` (`src/hooks/useAdmin.ts`) collapses any non-`"manual"` value to `"auto"`.
- Why: adding a third mode in Go without updating the TS normalizer silently collapses it to "auto".
## Frontend Conventions
1. **Do not use `useCallback` in new code.**
React Compiler (`babel-plugin-react-compiler`, enabled in `vite.config.ts`) handles memoization automatically.
- Why: manual `useCallback` is redundant with the compiler and adds noise.
2. **Feature state lives in page-level hooks and is prop-drilled. No global state library.**
`useServerList`, `useAdmin`, and `useAuth` own feature state at the page level. Theme is the exception; it uses `ThemeProvider`. `localStorage` persistence should silently fall back on errors.
- Why: the prop-drilling pattern for feature state is intentional. Adding shared state providers for feature data changes the data flow architecture.
3. **Only `handleBPSChange` uses optimistic update with rollback.**
All other admin actions use `runAdminAction()` which awaits the API call then refreshes via `fetchData()`.
- Why: treating other admin handlers as optimistic will skip the server-refresh step and show stale data.