浏览代码

M13b: add M13b.dlog deployment log

Cumulative status document for the M13b admin console work. Read
this file first when resuming after an interruption; it shows
what's shipped, what was tested, and what's next.
Jarvis 1 月之前
父节点
当前提交
63202f23e2
共有 1 个文件被更改,包括 245 次插入0 次删除
  1. 245 0
      M13b.dlog

+ 245 - 0
M13b.dlog

@@ -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
+