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.