refact: enhance AGENTS.md with detailed explanations for admin lease contracts and conventions

cognitive committed Apr 3, 2026 at 08:19 UTC d622c1ad0f3e51c58bc66eebb931cab57a6e2fb4
1 file changed +25 -2
frontend/AGENTS.md
+25 -2
@@ -11,7 +11,6 @@ High-signal constraints for the relay-server frontend. Only items expensive to r
11 2. **API path constants require dual maintenance.**
12 Go definitions live in `types/paths.go`; TS duplicates live in `src/lib/apiPaths.ts`.
13 - Why: no codegen. A path mismatch produces same-origin 404s.
14 - - Current Go runtime serves `/admin` and `/admin/snapshot` for admin reads; `/admin/leases/*`, `/admin/settings/approval-mode`, and related auth/IP routes are action surfaces. Extra TS admin paths still need backend work or they will 404.
14
15 3. **API envelope shape must match across Go and TS.**
16 All JSON control-plane responses use `{ ok, data?, error?: { code, message } }`.
@@ -23,15 +22,39 @@ High-signal constraints for the relay-server frontend. Only items expensive to r
22 - Why: any tooling or script assuming `index.html` post-build will fail.
23
24 5. **HTML metadata placeholders must match between HTML and Go.**
26 - `index.html` (renamed to `portal.html`) contains `[%OG_TITLE%]`, `[%OG_DESCRIPTION%]`, `[%LANDING_PAGE_ENABLED%]`, `[%SERVER_OWNER_ADDRESS%]`, `[%RELEASE_VERSION%]`. Server-side substitution happens in `cmd/relay-server/frontend.go`.
25 + `index.html` (renamed to `portal.html`) contains `[%..%]` placeholders substituted server-side in `cmd/relay-server/frontend.go`.
26 - Why: renaming a placeholder in one place without the other leaves raw placeholder strings in production HTML.
27
28 6. **Admin state reads are aggregated through `/admin/snapshot`.**
29 `src/hooks/useAdmin.ts` expects one payload carrying `leases` and `approval_mode`.
30 - Why: splitting those reads across multiple endpoints reintroduces extra request coordination and drift in the admin bootstrap path.
31
32 +7. **Lease/AdminLease JSON casing is a mixed implicit/explicit contract.**
33 + `Lease` (`../types/identity.go`): `Name` has `json:"name"` tag, but `ExpiresAt`, `FirstSeenAt`, `LastSeenAt`, `Hostname`, `Ready` have NO tags — Go defaults to PascalCase. `AdminLease`: `IdentityKey` and `Address` have snake_case json tags, but `BPS`, `ClientIP`, `ReportedIP`, `IsApproved`, `IsBanned`, `IsDenied`, `IsIPBanned` have NO tags — also PascalCase. TS `PublicLeaseData` and `AdminLeaseData` (`src/hooks/useSSRData.ts`) consume a subset of these fields and match this mixed casing.
34 + - Why: adding a `json:"..."` tag to any currently untagged field silently changes the wire name and breaks the TS consumer. Go also sends `UDPEnabled`, `TCPEnabled`, `TCPAddr` on `Lease` which the frontend does not consume — these are not part of the frontend contract.
35 +
36 +8. **Admin lease paths use base64-url encoding with URI-component escaping.**
37 + TS `encodePathPart()` (`src/lib/apiPaths.ts`): `btoa(value)` → replace `+/=` with `-/_/""` → `encodeURIComponent()`. Go decodes via `utils.DecodeBase64URLString()`.
38 + - Why: two-layer codec. Changing either side silently produces 400s on admin lease actions.
39 +
40 +9. **`Metadata` is typed `unknown` in TS but has a concrete Go struct.**
41 + Go `LeaseMetadata` (`../types/identity.go`): `description`, `owner`, `thumbnail`, `tags`, `hide` — all with json tags. TS declares `Metadata: unknown` in `PublicLeaseData`, then runtime-parses in `src/lib/metadata.ts`.
42 + - Why: adding or renaming a Go metadata field silently drops data in the frontend. No compile-time contract exists.
43 +
44 +10. **ApprovalMode is a closed two-value enum: `"auto"` | `"manual"`.**
45 + TS `normalizeApprovalMode()` (`src/hooks/useAdmin.ts`) collapses any non-`"manual"` value to `"auto"`.
46 + - Why: adding a third mode in Go without updating the TS normalizer silently collapses it to "auto".
47 +
48 ## Frontend Conventions
49
50 1. **Do not use `useCallback` in new code.**
51 React Compiler (`babel-plugin-react-compiler`, enabled in `vite.config.ts`) handles memoization automatically.
52 - Why: manual `useCallback` is redundant with the compiler and adds noise.
53 +
54 +2. **Feature state lives in page-level hooks and is prop-drilled. No global state library.**
55 + `useServerList`, `useAdmin`, `useAuth` own feature state at the page level. Theme is the exception — it uses a dedicated `ThemeProvider` context (`src/components/ThemeProvider.tsx`). `localStorage` for persistence (favorites, theme, tunnel seed) with silent fallback on errors.
56 + - Why: the prop-drilling pattern for feature state is intentional. Adding shared state providers for feature data changes the data flow architecture.
57 +
58 +3. **Only `handleBPSChange` uses optimistic update with rollback.**
59 + All other admin actions use `runAdminAction()` which awaits the API call then refreshes via `fetchData()`. BPS is the exception: it mutates local state immediately and rolls back on error (`src/hooks/useAdmin.ts`).
60 + - Why: treating other admin handlers as optimistic will skip the server-refresh step and show stale data.