M13b.dlog 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. ================================================================================
  2. M13b.dlog — M13b (admin UI) deployment log
  3. ================================================================================
  4. Project: broad-announce
  5. Milestone: M13b — admin console UI (M13a was the auth gate; M13b is the SPA)
  6. Owner: Luis Rosales
  7. Last update: 2026-06-17 18:44 EDT
  8. Purpose: Resume point after any session/model interruption. Read this
  9. file first; it tells you where the work is, what's done, what
  10. was tested, and what's next. Git history shows WHAT changed;
  11. this file shows WHY and HOW to verify it.
  12. ================================================================================
  13. TL;DR — where we are right now
  14. ================================================================================
  15. - M13a (auth gate, JWT, refresh, role middleware) is SHIPPED. See commit
  16. fa84398 ("M13a W5: route admin endpoints through the JWT gate").
  17. - M13b W0 (SPA shell, embed into admind, scaffold) is SHIPPED.
  18. - M13b W1 (Companies CRUD: backend + UI) is SHIPPED at c5e15f7.
  19. All tests green. Smoke script written but not yet run E2E (needs stack).
  20. - M13b W2 (Sources CRUD) and W3 (Telegram bot CRUD) are next.
  21. If you only have 60 seconds: read the "W1 in flight" block at the bottom.
  22. ================================================================================
  23. W0 — SPA shell + embed into admind [SHIPPED]
  24. ================================================================================
  25. Commit: b2c4365 "M13b W0: SPA shell + embed into admind"
  26. Goal: a runnable React+TS SPA that admind can serve, so the W1-W3
  27. features have a place to live.
  28. Files added (under web/):
  29. - React 18 + Vite 5 + TypeScript 5 + Tailwind 3 + Radix + TanStack Query
  30. - Routes: /login, /forbidden, /, /companies/*, /sources/*, /telegram/*,
  31. /tail, /dlq, /audit, *
  32. - All non-auth routes wrapped in <RequireAuth> and <AppShell>
  33. - Non-shipped routes render <ComingSoon> with the W1/W2/W3 badge
  34. - Auth: AuthProvider with /v1/auth/refresh-then-/v1/auth/me boot,
  35. in-memory access token, httpOnly refresh cookie, refresh-on-401
  36. with single-flight guard
  37. - API helpers: fetchWithAuth, apiGet, apiSend, ApiError
  38. - Theme: light/dark/system, persisted in localStorage
  39. - Role-based sidebar
  40. - 1 vitest: tests/login.test.tsx (form renders + accepts input)
  41. Files changed (under cmd/admind/):
  42. - main.go: //go:embed web-dist (empty allowed; 503 stub if no
  43. index.html), wireSPA() serves SPA history for all M13b routes
  44. - /assets/* immutable cache; / no-cache so deploys pick up new bundles
  45. - /dlq keeps the M8 HTML UI; /v1/* keeps the JWT gate
  46. Build tooling (Makefile):
  47. - web-install, web-build, web-dev, web-test, web-typecheck
  48. - build-with-web alias: web-build then go build
  49. Verification at W0:
  50. pnpm run build clean (vite v5.4.21, ~328 kB total, ~104 kB gz)
  51. go build ./... clean
  52. go vet ./... clean
  53. go test ./cmd/admind/ clean
  54. vitest 2 passed
  55. ================================================================================
  56. W1 — Companies CRUD [SHIPPED]
  57. ================================================================================
  58. Commit: c5e15f7 ("M13b W1: Companies CRUD (backend + UI)")
  59. Goal: operators can list, create, edit, suspend, activate, and archive
  60. tenants. tenant_admin gets a read-only view of their own tenant.
  61. --- Schema (migrations/010_tenants_fields.{up,down}.sql) -------------------
  62. Adds two columns to auth.tenants:
  63. rate_limit_per_sec INTEGER NOT NULL DEFAULT 10000 (CHECK 1..1000000)
  64. fcm_shared BOOLEAN NOT NULL DEFAULT TRUE
  65. Both have safe defaults, so applying to a populated DB is a no-op for
  66. existing rows. Reversible (down migration drops both columns).
  67. Verified the migration applies and rolls back cleanly against PG 17.
  68. --- Backend (Go) ----------------------------------------------------------
  69. New files:
  70. internal/authd/tenants.go
  71. - type Tenant (wire shape, snake_case JSON)
  72. - ErrTenantNotFound, ErrTenantSlugTaken, ErrTenantInvalid
  73. - TenantFilter (q, status, limit, offset, scope)
  74. - ListTenants(ctx, filter) (items, total, err)
  75. - GetTenant(ctx, id)
  76. - CreateTenantInput + Validate() (slug regex, email, rate limit)
  77. - UpdateTenantInput (pointer fields = PATCH semantics)
  78. - CreateTenant (writes audit_log "tenant.create")
  79. - UpdateTenant (writes audit_log "tenant.update", enforces
  80. actorScopeAll for restricted fields)
  81. - SetTenantStatus (active<->suspended, any->archived; archived is
  82. terminal; writes audit_log "tenant.status" with {from,to})
  83. - Inline validators: validSlug, looksLikeEmail
  84. internal/authd/tenants_test.go
  85. - TestValidSlug, TestLooksLikeEmail, TestCreateTenantInput_Validate,
  86. TestUpdateTenantInput_Validate (pure Go, no DB needed)
  87. - Note: tests caught a real bug in looksLikeEmail (didn't reject
  88. leading/trailing dot in domain). Fixed.
  89. cmd/authd/tenants.go
  90. - HTTP handlers for /v1/tenants/* (see routes below)
  91. - canAccessTenant(claims, id) — super_admin any, others own only
  92. - isUUID(s) — lenient format check so 400s stay 400s
  93. Routes wired in cmd/authd/main.go (RequireAuth / RequireRole):
  94. GET /v1/tenants — any auth (scope: super_admin all,
  95. tenant_admin own only)
  96. POST /v1/tenants — super_admin only
  97. GET /v1/tenants/{id} — any auth, per-id scope check
  98. PATCH /v1/tenants/{id} — any auth; tenant_admin only
  99. display_name + contact_email on
  100. own tenant
  101. POST /v1/tenants/{id}/status — super_admin only
  102. Errors:
  103. 400 — bad input (validation, JSON parse, bad UUID)
  104. 403 — role not allowed, or tenant_admin trying another tenant
  105. 404 — tenant id not found
  106. 409 — duplicate slug on create
  107. 500 — unexpected DB error
  108. --- Smoke (scripts/m13b_w1_smoke.sh) --------------------------------------
  109. End-to-end, bash + curl + jq-less python. Covers:
  110. 1. authd /health
  111. 2. super_admin login
  112. 3. GET /v1/tenants (initial)
  113. 4. POST /v1/tenants (create) → 201
  114. 5. GET /v1/tenants/{id} → 200
  115. 6. PATCH /v1/tenants/{id} → 200
  116. 7. POST /v1/tenants/{id}/status suspend → 200
  117. 8. POST /v1/tenants/{id}/status activate → 200
  118. 9. POST /v1/tenants (dup slug) → 409
  119. 10. POST /v1/tenants (bad slug) → 400
  120. 11. tenant_admin can login + GET own
  121. 11c. tenant_admin can PATCH own display_name → 200
  122. 11d. tenant_admin CANNOT change rate_limit → 400
  123. 12. tenant_admin GET other tenant → 403
  124. 13. tenant_admin POST /v1/tenants → 403
  125. 14. tenant_admin POST /v1/tenants/{id}/status → 403
  126. 15. POST /v1/tenants/{id}/status (bad value) → 400
  127. 16. cleanup: super_admin archives
  128. Syntax-verified (bash -n); not yet run end-to-end (needs live stack).
  129. --- Frontend (web/) -------------------------------------------------------
  130. New feature folder web/src/features/companies/:
  131. types.ts — Tenant, TenantStatus, ListTenantsResponse,
  132. CreateTenantInput, UpdateTenantInput
  133. api.ts — useTenantsList, useTenant, useCreateTenant,
  134. useUpdateTenant, useSetTenantStatus (TanStack Query),
  135. getErrorMessage(err)
  136. format.tsx — statusLabel, statusVariant, StatusBadge,
  137. formatRateLimit, formatDate
  138. list.tsx — table + debounced search + status filter buttons,
  139. empty state, link to detail; super_admin sees
  140. "New company" button
  141. create-dialog.tsx — Radix Dialog + react-hook-form + zod
  142. (slug regex, email, rate limit 1..1e6, fcm_shared)
  143. duplicate-slug surfaces as a field error
  144. detail-page.tsx — form + Suspend/Activate/Archive actions
  145. (Archive requires typed confirmation dialog)
  146. Metadata panel; tenant_admin sees form but
  147. rate_limit and fcm_shared are disabled
  148. New UI primitives (web/src/components/ui/):
  149. badge.tsx — variants: default, secondary, outline, success,
  150. warning, danger, muted
  151. dialog.tsx — Radix Dialog wrapper (Overlay, Content, Header,
  152. Footer, Title, Description, Trigger, Close, Portal)
  153. textarea.tsx — matching Input style
  154. table.tsx — Table, TableHeader, TableBody, TableRow,
  155. TableHead, TableCell
  156. empty-state.tsx — icon + title + description + action
  157. Updated:
  158. routes/companies.tsx — replaced ComingSoon with a nested Routes
  159. (index → list, :id → detail)
  160. lib/scope.ts — added canViewCompanies, canViewSources,
  161. canManageTelegram, canViewTelegram; kept
  162. canManageCompanies as super_admin-only
  163. Test added (web/tests/companies.test.tsx):
  164. - statusLabel, statusVariant, formatRateLimit, formatDate
  165. - Note: caught a real bug in formatDate (try/catch around
  166. toLocaleDateString doesn't catch "Invalid Date" string).
  167. Replaced with Number.isNaN(d.getTime()).
  168. Bundle delta (W1 vs W0): index chunk +0.08 kB, +1 module
  169. (1733 → 1734 modules transformed).
  170. --- Verification (run from /root/.openclaw/workspace/broad-announce) -----
  171. go build ./... clean
  172. go vet ./... clean
  173. go test -count=1 ./... 22 packages, 0 failures
  174. cd web && pnpm run test 8 tests, 2 files, 0 failures
  175. cd web && pnpm run build clean
  176. psql -f migrations/009_auth.up.sql apply
  177. psql -f migrations/010_tenants_fields.up.sql apply
  178. psql -f migrations/010_tenants_fields.down.sql rollback
  179. bash -n scripts/m13b_w1_smoke.sh syntax OK (not run E2E)
  180. ================================================================================
  181. Next — M13b W2 (Sources CRUD) and W3 (Telegram bot CRUD)
  182. ================================================================================
  183. Per the W2/W3 badges already rendered in the UI:
  184. W2: Sources CRUD
  185. - /v1/sources endpoints in authd (or a new sourcerd package? the
  186. existing `public.sources` table is the canonical source-of-truth
  187. per M4; M13b needs a new layer that scopes by tenant)
  188. - SPA: web/src/features/sources/ (list, create, detail)
  189. - Fields: name, type (FCM|Telegram|WebHook), destination URL/token,
  190. rate_limit, status, optional signing_secret
  191. - Reuse the same patterns from W1 (Query hooks, zod, scope helpers)
  192. W3: Telegram bot CRUD
  193. - /v1/telegram/bots endpoints in authd
  194. - SPA: web/src/features/telegram/
  195. - Fields: bot token (write-only), display name, welcome message,
  196. default source id
  197. - The token is write-only (write hashes a secret, never returned
  198. on read). The deliverd-telegram service consumes the bot list.
  199. ================================================================================
  200. Quick resume instructions
  201. ================================================================================
  202. If you start a new session, run these commands to verify state:
  203. cd /root/.openclaw/workspace/broad-announce
  204. git log --oneline -5 # confirm W0 is at HEAD
  205. # (W1 commit is next)
  206. git status --short # should be empty after
  207. # W1 commit
  208. go test -count=1 ./... # all green
  209. cd web && pnpm run test # all green
  210. cat M13b.dlog # this file
  211. To pick up W2:
  212. 1. Re-read web/src/features/companies/ to copy the patterns
  213. 2. The authd.Tenant and the public.sources table are the references
  214. 3. The route stubs in web/src/routes/sources.tsx and telegram.tsx
  215. still render ComingSoon — replace them the same way as
  216. web/src/routes/companies.tsx