| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- ================================================================================
- 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.
- - M13b W2 (Sources CRUD: backend + UI) is SHIPPED at f618098.
- - M13b W3 (Telegram bot CRUD: backend + UI) is SHIPPED.
- All tests green (go 22/22, web 31/31, smoke 32/32 OK).
- W4 (smoke + verification + screenshots) is 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 <RequireAuth> and <AppShell>
- - Non-shipped routes render <ComingSoon> 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 — <one-line summary>
- Commit: <hash>
- Files: <top-level paths added/changed>
- Tests: <go test / pnpm test / smoke> → <result>
- Verify: <one or more commands the next session can run to confirm>
- Notes: <known issues, follow-ups, or 'none'>
- ================================================================================
- ENTRY LOG (most recent first; append new entries at the TOP of this block)
- ================================================================================
- 2026-06-18 14:56 EDT — W3 Telegram bot CRUD shipped
- Commit: 98158da
- Files: internal/authd/telegrambots.{go,_test.go},
- cmd/authd/telegrambots.go, cmd/authd/main.go (6 routes wired),
- migrations/012_telegram_bot_fields.{up,down}.sql,
- web/src/features/telegram/{types,api,format,list,create-dialog,detail-page}.{ts,tsx},
- web/src/routes/telegram.tsx (now wired; was ComingSoon),
- web/src/components/layout/sidebar.tsx (W3 nav entry),
- web/tests/telegram/format.test.ts,
- scripts/m13b_w3_smoke.sh,
- M13b.dlog
- What: 6 routes under /v1/tenants/{id}/telegram/bots (list/create/get/
- patch/status/rotate-token). All RequireRole("super_admin").
- Migration 012 adds bot_token_hash (bcrypt), welcome_message,
- default_source_id, description, last_rotated_at to
- public.telegram_bots (already-existing table from M4);
- idx_telegram_bots_company (active-only) and
- idx_telegram_bots_default_source added; trg_telegram_bots_
- touch_updated_at trigger installed. Bot token is write-only:
- server returns `bot_token_set: bool` instead of the plaintext
- on every read. Rotate returns the same shape (no plaintext
- echo). Plaintext is stored alongside the bcrypt hash so
- telegramd can read it for outbound calls.
- Same bridge as W2: CreateTelegramBot calls
- ensurePublicCompanyRow(tenant_id, tenant.display_name) before
- INSERT, since public.telegram_bots.company_id FKs
- public.companies(id) and the auth.tenants row doesn't
- auto-create that legacy row.
- Smoke fixed: bot_token_set assertions compared to "True"
- (Python repr) but json_field() json-dumps bools as lowercase
- "true". Now compared to "true" — server response was always
- correct.
- Cleanup note: smoke's archive step archives the auth.tenant
- but leaves public.telegram_bots rows behind. Not a bug
- (telegram_bots.company_id has ON DELETE CASCADE to
- public.companies, and the smoke never deletes the company
- row), but repeated smoke runs accumulate rows. Future smoke
- passes should DELETE FROM public.telegram_bots first or
- add a cleanup step that removes the public.companies row.
- Tests: go test -count=1 ./... 22 packages, 0 failures
- cd web && pnpm run test 31 tests, 4 files, 0 failures
- cd web && pnpm run build clean
- psql -f migrations/012_telegram_bot_fields.up.sql apply (already done)
- psql -f migrations/012_telegram_bot_fields.down.sql rollback verified
- bash scripts/m13b_w3_smoke.sh 32/32 OK (authd running)
- Verify: cd /root/.openclaw/workspace/broad-announce
- go test -count=1 ./... && cd web && pnpm run test
- psql -f migrations/012_telegram_bot_fields.up.sql
- bash scripts/m13b_w3_smoke.sh
- Notes: Bundle: telegram feature code-split (exit criterion "< 30 KB
- gzipped") NOT done. Same as W2: all features ship in the
- main chunk (index-WAbnQRoa.js = 37 KB gz, +forms-BRRx31Es.js
- = 22 KB gz). Per-feature dynamic import is a v1.1 follow-up;
- not blocking W3.
- Like W2, list/create/detail UI tests are format-only; the
- v1.1 follow-up adds a component test or two.
- Open policy dial (same as W2's tenant_admin suspend):
- canManageTelegram in web/src/lib/scope.ts is super_admin
- only, matching W3 plan. If tenant_admin should manage their
- own bot, the dial is in scope.ts + the handler's role check.
- W4 is next: scripts/m13b_smoke.sh (full W1+W2+W3 walkthrough)
- + verification doc with screenshots.
- 2026-06-18 02:00 EDT — W2 Sources CRUD shipped (one-time secrets + rotate)
- Commit: <this commit>
- Files: internal/authd/sources.{go,_test.go},
- cmd/authd/sources.go, cmd/authd/main.go (6 routes wired),
- migrations/011_sources_secrets.{up,down}.sql,
- web/src/features/sources/{types,api,format,list,create-dialog,detail-page}.{ts,tsx},
- web/src/components/ui/checkbox.tsx (new),
- web/src/routes/sources.tsx (now wired; was ComingSoon),
- web/tests/sources/format.test.ts,
- scripts/m13b_w2_smoke.sh,
- M13b.dlog
- What: Pick (a) per 01:10 — reuse public.sources directly via authd.
- CRUD + status + rotate-secrets. Migration 011 adds hmac_secret_hash,
- api_key_hash, mtls_required, description (all nullable so pre-existing
- rows still load). One-time secrets: CreateSource / RotateSecrets
- return plaintext EXACTLY ONCE in `secrets`; only bcrypt hashes
- persist. auto_secrets: true by default in the UI. tenant_admin
- can manage their own sources (PATCH allowed; status changes are
- an open feature, see Notes).
- Cert UI: placeholder card on detail page (M14 — coming).
- Bridge: public.sources.company_id (TEXT) FKs public.companies.id (TEXT);
- auth.tenants.id is UUID. W2's CreateSource does an idempotent
- INSERT INTO public.companies ... ON CONFLICT DO NOTHING keyed
- by auth.tenants.id::text so the FK is satisfied on first source
- create. No change to W1's CreateTenant.
- Tests: go test ./internal/authd/ -count=1 ok (all tests, incl. new
- TestValidSourceID / TestValidSecretFormat / TestValidAPIKeyFormat
- / TestCreateSourceInput_Validate / TestUpdateSourceInput_Validate
- / TestGenerateSecret)
- web tsc -b clean
- web vitest run 17/17 ok (9 new + 6 companies + 2 login)
- web vite build clean
- bash -n scripts/m13b_w2_smoke.sh clean
- Verify: cd /root/.openclaw/workspace/broad-announce
- go test -count=1 ./... && cd web && pnpm run test
- psql -f migrations/011_sources_secrets.up.sql (apply 011)
- bash scripts/m13b_w2_smoke.sh (run against stack)
- Notes: Smoke script not yet run E2E (same as W1: needs running authd +
- Postgres). 17 web tests pass but the W2 component tests are
- format-only — list/create/detail-page UI tests are a v1.1 add.
- Decision logged: public.sources reused as-is (no auth.sources
- view layer in W2; can refactor later if the auth/admin split
- needs to harden).
- Open follow-up for v1.1: tenant_admin status-changes (suspend
- / activate their own source). The route is RequireAuth today
- and the store allows any caller; the policy dial is whether
- tenant_admin should be allowed to suspend their own. Not
- blocking W2; documenting in the .dlog.
- W3 (Telegram bot CRUD) is next; same pattern as W2.
- 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).
|