| 1 | # Frontend AGENTS.md |
| 2 | |
| 3 | High-signal constraints for the relay-server frontend. Only items expensive to rediscover. |
| 4 | |
| 5 | ## Frontend-Backend Contracts |
| 6 | |
| 7 | 1. **Public list data comes from `/ui/state`.** |
| 8 | Go relay returns leases only; `api/server.ts` adds frontend-owned presentation fields mirrored in `src/types/api.ts`. |
| 9 | - Why: the Go relay is API-only. Do not reintroduce Go HTML data injection for public lease state. |
| 10 | |
| 11 | 2. **Relay and presentation API paths have separate owners.** |
| 12 | 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. |
| 13 | - Why: nginx should distinguish the BFF by one prefix instead of enumerating presentation endpoints. |
| 14 | |
| 15 | 3. **API envelope shape must match across Go and TS.** |
| 16 | All JSON control-plane responses use `{ ok, data?, error?: { code, message } }`. |
| 17 | Go shape is `types.APIEnvelope` in `../types/api.go`; Go writers live in `../utils/api.go`; TS parser lives in `src/lib/apiClient.ts`. |
| 18 | - Why: backend responses that skip the envelope surface as `invalid_envelope` in the frontend. |
| 19 | |
| 20 | 4. **Admin auth uses bearer tokens returned by `/api/admin/auth/login`.** |
| 21 | `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 ...`. |
| 22 | - Why: the relay admin API must be usable by any separately hosted frontend without credentialed cookie CORS state. |
| 23 | |
| 24 | 5. **`VITE_PORTAL_API_BASE_URL` is the only built-in API origin knob.** |
| 25 | Leave it empty for same-origin development/proxying, or set it at build/dev time to the public edge origin/base path. Do not point it at `/api`; `/api`, `/ui`, `/sdk`, and `/discovery` are sibling paths. |
| 26 | - Why: runtime-generated config files couple the static frontend bundle back to deployment state. |
| 27 | |
| 28 | 6. **Presentation policy state reads are aggregated through `/ui/policy/state`.** |
| 29 | `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`. |
| 30 | - Why: splitting those reads across multiple endpoints reintroduces extra request coordination and drift in the admin bootstrap path. |
| 31 | |
| 32 | 7. **Lease/policy lease JSON casing is snake_case.** |
| 33 | Go `Lease`/`PolicyLease` JSON tags live in `../types/identity.go`; TS mirrors the wire shape in `src/types/api.ts`. |
| 34 | - Why: the frontend should not depend on Go's implicit PascalCase encoder output. |
| 35 | |
| 36 | 8. **Admin policy writes identify targets in the JSON body.** |
| 37 | 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/*`. |
| 38 | - Why: path encoding rules add a second contract surface and are easy to drift across Go and TS. |
| 39 | |
| 40 | 9. **Lease metadata has a wire type and a UI parser.** |
| 41 | Go `LeaseMetadata` (`../types/identity.go`) mirrors TS `LeaseMetadata` (`src/types/api.ts`). UI display defaults are owned by `src/lib/metadata.ts`. |
| 42 | - Why: API contract fields and UI fallback behavior should not be mixed. |
| 43 | |
| 44 | 10. **Presentation support is frontend-owned.** |
| 45 | `api/server.ts` serves `/ui/state`, `/ui/policy/*`, `/ui/service/status`, and `/ui/thumbnail/{hostname}` by composing relay data with frontend-owned state. |
| 46 | - 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. |
| 47 | |
| 48 | 11. **ApprovalMode is a closed two-value enum: `"auto"` | `"manual"`.** |
| 49 | TS `normalizeApprovalMode()` (`src/hooks/useAdmin.ts`) collapses any non-`"manual"` value to `"auto"`. |
| 50 | - Why: adding a third mode in Go without updating the TS normalizer silently collapses it to "auto". |
| 51 | |
| 52 | ## Frontend Conventions |
| 53 | |
| 54 | 1. **Do not use `useCallback` in new code.** |
| 55 | React Compiler (`babel-plugin-react-compiler`, enabled in `vite.config.ts`) handles memoization automatically. |
| 56 | - Why: manual `useCallback` is redundant with the compiler and adds noise. |
| 57 | |
| 58 | 2. **Feature state lives in page-level hooks and is prop-drilled. No global state library.** |
| 59 | `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. |
| 60 | - Why: the prop-drilling pattern for feature state is intentional. Adding shared state providers for feature data changes the data flow architecture. |
| 61 | |
| 62 | 3. **Only `handleBPSChange` uses optimistic update with rollback.** |
| 63 | All other admin actions use `runAdminAction()` which awaits the API call then refreshes via `fetchData()`. |
| 64 | - Why: treating other admin handlers as optimistic will skip the server-refresh step and show stale data. |