================================================================================ M13b.dlog — M13b (admin UI) deployment log ================================================================================ Project: broad-announce Milestone: M13b — admin console UI (M13a was the auth gate; M13b is the SPA) Owner: Luis Rosales Last update: 2026-06-17 18:44 EDT Purpose: Resume point after any session/model interruption. Read this file first; it tells you where the work is, what's done, what was tested, and what's next. Git history shows WHAT changed; this file shows WHY and HOW to verify it. ================================================================================ STANDING RULE — DEPLOYMENT PROTOCOL (binding for every session) ================================================================================ Rule: Update this file at the END of every deployment. This is a hard rule, not a suggestion. It applies to every session, every model, every operator. What counts as a "deployment" (UPDATE the file): • Code changes that pass tests and are committed • Schema migrations (up OR down) • New HTTP routes / new endpoints • New UI features / new pages • Smoke scripts that exercise real endpoints • Bug fixes that change behavior • Any change to deliverd-*, routerd, archiverd, ingestd, authd, admind • Config / env changes that affect runtime behavior What does NOT count (DO NOT update the file just for these): • Documentation changes (README, comments, this file itself) • Architecture decisions / plans / surveys • Pure refactors with no behavior change • Test-only changes that don't exercise new code paths • Cosmetic UI tweaks that don't add a feature What an entry must contain (use the template at the bottom): 1. Timestamp (EDT) 2. One-line summary of what shipped 3. Commit hash 4. Top-level files added/changed (paths only, not full diffs) 5. Tests run + result (go test / pnpm test / smoke) 6. Verification commands the next session can run 7. Known issues / next step Why: Luis needs to resume after any interruption — model crash, rate limit, gateway restart, end of session — by reading THIS file instead of analyzing the codebase. The first 40 lines must be enough to know exactly where we are. When to update: at the END of the session, OR when an obvious natural break appears (milestone shipped, smoke verified). Never mid-implementation; wait until the commit is in. ================================================================================ TL;DR — where we are right now ================================================================================ - M13a (auth gate, JWT, refresh, role middleware) is SHIPPED. See commit fa84398 ("M13a W5: route admin endpoints through the JWT gate"). - M13b W0 (SPA shell, embed into admind, scaffold) is SHIPPED. - M13b W1 (Companies CRUD: backend + UI) is SHIPPED at c5e15f7. All tests green. Smoke script written but not yet run E2E (needs stack). - M13b W2 (Sources CRUD) and W3 (Telegram bot CRUD) are next. If you only have 60 seconds: read the W1 block below; everything before it is in production. ================================================================================ W0 — SPA shell + embed into admind [SHIPPED] ================================================================================ Commit: b2c4365 "M13b W0: SPA shell + embed into admind" Goal: a runnable React+TS SPA that admind can serve, so the W1-W3 features have a place to live. Files added (under web/): - React 18 + Vite 5 + TypeScript 5 + Tailwind 3 + Radix + TanStack Query - Routes: /login, /forbidden, /, /companies/*, /sources/*, /telegram/*, /tail, /dlq, /audit, * - All non-auth routes wrapped in and - Non-shipped routes render with the W1/W2/W3 badge - Auth: AuthProvider with /v1/auth/refresh-then-/v1/auth/me boot, in-memory access token, httpOnly refresh cookie, refresh-on-401 with single-flight guard - API helpers: fetchWithAuth, apiGet, apiSend, ApiError - Theme: light/dark/system, persisted in localStorage - Role-based sidebar - 1 vitest: tests/login.test.tsx (form renders + accepts input) Files changed (under cmd/admind/): - main.go: //go:embed web-dist (empty allowed; 503 stub if no index.html), wireSPA() serves SPA history for all M13b routes - /assets/* immutable cache; / no-cache so deploys pick up new bundles - /dlq keeps the M8 HTML UI; /v1/* keeps the JWT gate Build tooling (Makefile): - web-install, web-build, web-dev, web-test, web-typecheck - build-with-web alias: web-build then go build Verification at W0: pnpm run build clean (vite v5.4.21, ~328 kB total, ~104 kB gz) go build ./... clean go vet ./... clean go test ./cmd/admind/ clean vitest 2 passed ================================================================================ W1 — Companies CRUD [SHIPPED] ================================================================================ Commit: c5e15f7 ("M13b W1: Companies CRUD (backend + UI)") Goal: operators can list, create, edit, suspend, activate, and archive tenants. tenant_admin gets a read-only view of their own tenant. --- Schema (migrations/010_tenants_fields.{up,down}.sql) ------------------- Adds two columns to auth.tenants: rate_limit_per_sec INTEGER NOT NULL DEFAULT 10000 (CHECK 1..1000000) fcm_shared BOOLEAN NOT NULL DEFAULT TRUE Both have safe defaults, so applying to a populated DB is a no-op for existing rows. Reversible (down migration drops both columns). Verified the migration applies and rolls back cleanly against PG 17. --- Backend (Go) ---------------------------------------------------------- New files: internal/authd/tenants.go - type Tenant (wire shape, snake_case JSON) - ErrTenantNotFound, ErrTenantSlugTaken, ErrTenantInvalid - TenantFilter (q, status, limit, offset, scope) - ListTenants(ctx, filter) (items, total, err) - GetTenant(ctx, id) - CreateTenantInput + Validate() (slug regex, email, rate limit) - UpdateTenantInput (pointer fields = PATCH semantics) - CreateTenant (writes audit_log "tenant.create") - UpdateTenant (writes audit_log "tenant.update", enforces actorScopeAll for restricted fields) - SetTenantStatus (active<->suspended, any->archived; archived is terminal; writes audit_log "tenant.status" with {from,to}) - Inline validators: validSlug, looksLikeEmail internal/authd/tenants_test.go - TestValidSlug, TestLooksLikeEmail, TestCreateTenantInput_Validate, TestUpdateTenantInput_Validate (pure Go, no DB needed) - Note: tests caught a real bug in looksLikeEmail (didn't reject leading/trailing dot in domain). Fixed. cmd/authd/tenants.go - HTTP handlers for /v1/tenants/* (see routes below) - canAccessTenant(claims, id) — super_admin any, others own only - isUUID(s) — lenient format check so 400s stay 400s Routes wired in cmd/authd/main.go (RequireAuth / RequireRole): GET /v1/tenants — any auth (scope: super_admin all, tenant_admin own only) POST /v1/tenants — super_admin only GET /v1/tenants/{id} — any auth, per-id scope check PATCH /v1/tenants/{id} — any auth; tenant_admin only display_name + contact_email on own tenant POST /v1/tenants/{id}/status — super_admin only Errors: 400 — bad input (validation, JSON parse, bad UUID) 403 — role not allowed, or tenant_admin trying another tenant 404 — tenant id not found 409 — duplicate slug on create 500 — unexpected DB error --- Smoke (scripts/m13b_w1_smoke.sh) -------------------------------------- End-to-end, bash + curl + jq-less python. Covers: 1. authd /health 2. super_admin login 3. GET /v1/tenants (initial) 4. POST /v1/tenants (create) → 201 5. GET /v1/tenants/{id} → 200 6. PATCH /v1/tenants/{id} → 200 7. POST /v1/tenants/{id}/status suspend → 200 8. POST /v1/tenants/{id}/status activate → 200 9. POST /v1/tenants (dup slug) → 409 10. POST /v1/tenants (bad slug) → 400 11. tenant_admin can login + GET own 11c. tenant_admin can PATCH own display_name → 200 11d. tenant_admin CANNOT change rate_limit → 400 12. tenant_admin GET other tenant → 403 13. tenant_admin POST /v1/tenants → 403 14. tenant_admin POST /v1/tenants/{id}/status → 403 15. POST /v1/tenants/{id}/status (bad value) → 400 16. cleanup: super_admin archives Syntax-verified (bash -n); not yet run end-to-end (needs live stack). --- Frontend (web/) ------------------------------------------------------- New feature folder web/src/features/companies/: types.ts — Tenant, TenantStatus, ListTenantsResponse, CreateTenantInput, UpdateTenantInput api.ts — useTenantsList, useTenant, useCreateTenant, useUpdateTenant, useSetTenantStatus (TanStack Query), getErrorMessage(err) format.tsx — statusLabel, statusVariant, StatusBadge, formatRateLimit, formatDate list.tsx — table + debounced search + status filter buttons, empty state, link to detail; super_admin sees "New company" button create-dialog.tsx — Radix Dialog + react-hook-form + zod (slug regex, email, rate limit 1..1e6, fcm_shared) duplicate-slug surfaces as a field error detail-page.tsx — form + Suspend/Activate/Archive actions (Archive requires typed confirmation dialog) Metadata panel; tenant_admin sees form but rate_limit and fcm_shared are disabled New UI primitives (web/src/components/ui/): badge.tsx — variants: default, secondary, outline, success, warning, danger, muted dialog.tsx — Radix Dialog wrapper (Overlay, Content, Header, Footer, Title, Description, Trigger, Close, Portal) textarea.tsx — matching Input style table.tsx — Table, TableHeader, TableBody, TableRow, TableHead, TableCell empty-state.tsx — icon + title + description + action Updated: routes/companies.tsx — replaced ComingSoon with a nested Routes (index → list, :id → detail) lib/scope.ts — added canViewCompanies, canViewSources, canManageTelegram, canViewTelegram; kept canManageCompanies as super_admin-only Test added (web/tests/companies.test.tsx): - statusLabel, statusVariant, formatRateLimit, formatDate - Note: caught a real bug in formatDate (try/catch around toLocaleDateString doesn't catch "Invalid Date" string). Replaced with Number.isNaN(d.getTime()). Bundle delta (W1 vs W0): index chunk +0.08 kB, +1 module (1733 → 1734 modules transformed). --- Verification (run from /root/.openclaw/workspace/broad-announce) ----- go build ./... clean go vet ./... clean go test -count=1 ./... 22 packages, 0 failures cd web && pnpm run test 8 tests, 2 files, 0 failures cd web && pnpm run build clean psql -f migrations/009_auth.up.sql apply psql -f migrations/010_tenants_fields.up.sql apply psql -f migrations/010_tenants_fields.down.sql rollback bash -n scripts/m13b_w1_smoke.sh syntax OK (not run E2E) ================================================================================ Next — M13b W2 (Sources CRUD) and W3 (Telegram bot CRUD) ================================================================================ Per the W2/W3 badges already rendered in the UI: W2: Sources CRUD - /v1/sources endpoints in authd (or a new sourcerd package? the existing `public.sources` table is the canonical source-of-truth per M4; M13b needs a new layer that scopes by tenant) - SPA: web/src/features/sources/ (list, create, detail) - Fields: name, type (FCM|Telegram|WebHook), destination URL/token, rate_limit, status, optional signing_secret - Reuse the same patterns from W1 (Query hooks, zod, scope helpers) W3: Telegram bot CRUD - /v1/telegram/bots endpoints in authd - SPA: web/src/features/telegram/ - Fields: bot token (write-only), display name, welcome message, default source id - The token is write-only (write hashes a secret, never returned on read). The deliverd-telegram service consumes the bot list. ================================================================================ Quick resume instructions ================================================================================ If you start a new session, run these commands to verify state: cd /root/.openclaw/workspace/broad-announce git log --oneline -5 # confirm W0 is at HEAD # (W1 commit is next) git status --short # should be empty after # W1 commit go test -count=1 ./... # all green cd web && pnpm run test # all green cat M13b.dlog # this file To pick up W2: 1. Re-read web/src/features/companies/ to copy the patterns 2. The authd.Tenant and the public.sources table are the references 3. The route stubs in web/src/routes/sources.tsx and telegram.tsx still render ComingSoon — replace them the same way as web/src/routes/companies.tsx ================================================================================ ENTRY TEMPLATE (copy this for each new deployment) ================================================================================ YYYY-MM-DD HH:MM EDT — Commit: Files: Tests: Verify: Notes: ================================================================================ ENTRY LOG (most recent first; append new entries at the TOP of this block) ================================================================================ 2026-06-17 18:44 EDT — W1 Companies CRUD shipped + this .dlog created Commit: c5e15f7 (W1), 63202f2 (.dlog creation) Files: internal/authd/tenants.{go,_test.go}, cmd/authd/tenants.go, cmd/authd/main.go (routes wired), migrations/010_tenants_fields.{up,down}.sql, web/src/features/companies/*.{ts,tsx}, web/src/components/ui/{badge,dialog,textarea,table,empty-state}.tsx, web/src/lib/scope.ts, web/src/routes/companies.tsx, web/tests/companies.test.tsx, scripts/m13b_w1_smoke.sh, M13b.dlog Tests: go build/vet ./... clean; go test -count=1 ./... 22/22 ok; pnpm run test 8/8 ok; pnpm run build clean; migrations apply + roll back cleanly on PG 17 Verify: cd /root/.openclaw/workspace/broad-announce go test -count=1 ./... && cd web && pnpm run test bash -n scripts/m13b_w1_smoke.sh (syntax only; needs live stack) psql -f migrations/009_auth.up.sql psql -f migrations/010_tenants_fields.up.sql Notes: Smoke script not yet run E2E (requires running authd + Postgres). W2 (Sources) and W3 (Telegram bot) are next. Test for sort + order; the W1 test caught a real bug in looksLikeEmail and formatDate (both fixed).