|
@@ -0,0 +1,245 @@
|
|
|
|
|
+================================================================================
|
|
|
|
|
+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.
|
|
|
|
|
+
|
|
|
|
|
+================================================================================
|
|
|
|
|
+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 in flight" block at the bottom.
|
|
|
|
|
+
|
|
|
|
|
+================================================================================
|
|
|
|
|
+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
|
|
|
|
|
+
|