M13b_PLAN.md 13 KB

M13b — CRUD (Companies + Sources + Telegram)

Second sub-milestone of M13. Delivers the operator's main value: real CRUD on the entities. After this lands, a super-admin can onboard a customer end-to-end (company → source → telegram bot) without touching psql. Live tail, DLQ, audit, K8s, E2E come in M13c.

Status: planning (post-M13a) Target: M13_FRONTEND_SPEC.md §4.2 Estimate: 7-8 days with one engineer


0. Recap — what M13b ships

  • Companies CRUD (super-admin full, tenant-admin read-only).
  • Sources CRUD with one-time secrets modal.
  • Telegram bots per-company: config, invite codes, bindings.
  • All forms validate (Zod), audit log writes, error toasts.
  • SPEC.md M13b row flipped to ✅.

What M13b is NOT:

  • Not live tail, DLQ, audit viewer, K8s, Playwright E2E — M13c.
  • Not groups, individuals, subscriptions, routing rules — out of scope for v1 (see M13_FRONTEND_SPEC.md §3.8).

1. Workstreams

┌──────────────────────────┐    ┌──────────────────────────┐
│  W1: Companies CRUD      │    │  W2: Sources CRUD        │
│  (super: full,           │───▶│  (HMAC, rate limits,     │
│   tenant: read-only)     │    │   allowed IPs, secret    │
│                          │    │   rotation modal)        │
└──────────────┬───────────┘    └──────────────┬───────────┘
               │                               │
               ▼                               ▼
┌──────────────────────────────────────────────────────────────┐
│  W3: Telegram bots — config, invite codes, bindings list     │
└──────────────────────────────────────────────────────────────┘
                                               │
                                               ▼
┌──────────────────────────────────────────────────────────────┐
│  W4: smoke + verification + screenshots                      │
└──────────────────────────────────────────────────────────────┘

Four workstreams. W1 → W2 → W3 → W4. W2 and W3 can overlap if a second engineer is available; otherwise sequential.


2. Workstream details

W1: Companies CRUD

Goal: Super-admin can create, list, edit, suspend, activate companies. Tenant-admin sees only their own company (read-only).

Scope:

  • Routes: /companies, /companies/:id, /companies/new.
  • Feature folder: web/src/features/companies/.
  • Components: CompanyList, CompanyDetail, CompanyForm, SuspendDialog (typed confirmation: type slug to suspend).
  • React Hook Form + Zod validation:
    • name: required, 1-100 chars.
    • slug: required, lowercase, ^[a-z0-9-]{3,40}$.
    • rate_limit_per_sec: positive int.
    • fcm_shared: boolean.
    • telegram_bot_token: optional, set on first save, encrypt at rest.
  • TanStack Query hooks: useCompanies, useCompany(id), useCreateCompany, useUpdateCompany, useSuspendCompany, useActivateCompany.
  • web/src/lib/api-client.ts regenerated from admind OpenAPI.
  • Audit log entries appear for company.create, company.update, company.suspend, company.activate.
  • Tenant-admin view: only their own company, no edit/suspend buttons.
  • web/src/lib/scope.ts — utility to read current user from auth context, gate UI by role.

Exit criteria:

  • Super-admin can log in, see companies list, create one, edit one.
  • Tenant-admin (created via psql or magic-link from M13a) logs in, sees only their own company (read-only).
  • Cross-tenant 403: tenant-admin tries GET /v1/companies/other-company-id → 403.
  • Suspend: company status=suspended, ingestd rejects (verify via curl).
  • Reactivate: company status=active, ingestd accepts again.
  • All forms validate with Zod, errors show inline + summary banner.
  • Optimistic updates for suspend/activate with rollback on error.
  • Audit log: each mutation produces a row visible in audit_log table.
  • Bundle: companies feature code-split (< 30 KB gzipped).

Estimated: 2-3 days.


W2: Sources CRUD

Goal: Sources list + create + edit + rotate secret + suspend. Includes the "save these now" modal for HMAC secrets and API keys on create/rotate.

Scope:

  • Routes: /sources, /sources/:id, /sources/new.
  • Feature folder: web/src/features/sources/.
  • Components: SourceList, SourceDetail, SourceForm, OneTimeSecretsModal, QuarantineBadge.
  • React Hook Form + Zod validation:
    • name: required, 1-100 chars.
    • type: enum (http, websocket, mqtt, snmp, grpc, custom).
    • hmac_secret: required, ≥ 32 chars (auto-generated on create if not provided).
    • api_key: optional, ≥ 32 chars.
    • rate_limit_per_sec: positive int.
    • allowed_ips: array of CIDR, validated.
    • topic_prefix: optional.
    • status: active | suspended.
  • One-time secrets modal: when create/rotate succeeds, show the raw HMAC + API key in copy-to-clipboard fields. Modal blocks dismissal until the user clicks "I've saved them" (the spec for the modal lives in M13_API_CONTRACT.md §2.2).
  • Quarantine badge: if quarantine_until is in the future, show a red badge with the remaining time.
  • Filter by company (super), type, status.
  • Cert lifecycle sub-feature (M14-ui, formerly M14 W4):
    • Tabs in Source detail: "Auth" (HMAC + API key) and "Cert" (mTLS).
    • CertStatusBadge — green/amber/red based on expiration.
    • CertUploadCSRDialog — paste a CSR, get a signed cert back.
    • CertAutoGenerateDialog — server generates key + CSR, signs it, returns a downloadable .zip with cert + chain + key.
    • CertRevokeDialog — typed confirmation.
    • CertExpirationBanner — global banner in top bar when any source cert is < 30 days from expiry.
    • New endpoints (already in M14-backend):
    • POST /v1/sources/:id/cert/csr
    • GET /v1/sources/:id/cert
    • DELETE /v1/sources/:id/cert
    • GET /v1/certs/expiring?days=30
    • Only renders if source.mtls_required=true OR the source already has a cert. Else, the Cert tab shows "mTLS not enabled for this source — enable in the Auth tab".
    • Tenant-admin sees cert info for their sources (read-only, no revoke).

Exit criteria:

  • Create source: form validates, one-time secrets shown once via modal.
  • List: sortable, paginated, filterable.
  • Edit: changes rate limit and allowed IPs persist.
  • Rotate secret: new one-time secrets shown, old HMAC rejected within 60s (verify via curl with old key).
  • Suspend: source goes to status=suspended, ingestd rejects (verified by curl).
  • Quarantine badge visible when quarantine_until is set.
  • Tenant-admin sees only their company's sources.
  • Cert tab: super-admin can upload a CSR, get a signed cert.
  • Cert tab: "Auto-generate" returns a .zip with cert + chain + private key (one-time download).
  • Cert tab: revoke removes the cert; subsequent requests with that cert fail within 60s.
  • CertExpirationBanner appears 30 days before any cert expires.
  • Tenant-admin sees cert info for their sources (read-only, no revoke).
  • M14-ui smoke: make m13b-smoke includes the cert path (mTLS-required source + cert issued + revoke + 60s window).
  • Bundle: sources feature code-split (< 50 KB gzipped, includes cert sub-feature).

Estimated: 4-6 days (was 2-3 before M14-ui was added).


W3: Telegram bots

Goal: Per-company telegram bot config, invite code generation, binding list.

Scope:

  • Routes: /companies/:id/telegram, /companies/:id/telegram/invites, /companies/:id/telegram/bindings.
  • Feature folder: web/src/features/telegram/.
  • Components: TelegramConfig, TelegramInviteForm, TelegramInvitesList, TelegramBindingsList, BotTokenRotateDialog.
  • React Hook Form + Zod:
    • bot_token: required, format validated (<bot_id>:<secret>, regex ^\d+:[A-Za-z0-9_-]{35}$).
    • On rotate: typed confirmation ("type the company slug to confirm rotation" — same pattern as suspend).
  • Bot token is NEVER displayed after save. Set and rotate are the only actions.
  • Invite code list with status badges (active/used/expired) and revoke button.
  • Bindings table: individual_id, telegram_user_id, last_seen_at.
  • Magic link generator: button → POST → returns code + t.me/<bot>?start=<code> URL, copy-to-clipboard.

Exit criteria:

  • Set bot token: encrypted at rest in admind, decryptable for emit (verify via direct API call from deliverd).
  • Generate invite: returns code + magic link, copies to clipboard.
  • Bindings list: paginated, shows last_seen_at.
  • Revoke invite: 204, invite goes to status=revoked.
  • Rotate token: old token rejected by telegram bot (verify by sending a message and seeing it fail).
  • Tenant-admin sees only their own company's telegram config.
  • Bundle: telegram feature code-split (< 30 KB gzipped).

Estimated: 2 days.


W4: smoke + verification + screenshots

Goal: End-to-end smoke that exercises all 3 modules. Screenshots for the verification doc.

Scope:

  • scripts/m13b_smoke.sh — bash + curl:
    1. Bring up stack.
    2. Bootstrap super-admin + tenant-admin.
    3. Login as super-admin.
    4. Create a company.
    5. Create a source (capture one-time secrets from response).
    6. Send 1 alert via curl to ingestd using the source's HMAC.
    7. List sources → expect 1 with alerts_24h=1.
    8. Suspend the source → expect 200.
    9. Send 1 alert with suspended source → expect 401.
    10. Login as tenant-admin → expect to see only the new company.
    11. Cross-tenant 403 attempt → expect 403.
    12. Set telegram bot token → expect 204.
    13. Generate invite → expect 201.
    14. List invites → expect 1.
  • scripts/m13b_smoke.sh exits 0/1, green required.
  • M13b_VERIFICATION.md with:
    • Smoke log (curl + responses).
    • Screenshots of: company list, company create form, source list, source create form, one-time secrets modal, telegram config, invites list, bindings list.
    • Bundle size breakdown.
    • Tenant-isolation 403 test.

Exit criteria:

  • make m13b-smoke exits 0 from a clean state.
  • 3 consecutive green runs.
  • M13a functionality not regressed.
  • M13b_VERIFICATION.md published.

Estimated: 0.5-1 day.


3. Sequencing

W1 (2-3d) ──▶ W2 (2-3d) ──▶ W3 (2d) ──▶ W4 (0.5-1d)
  • W1 alone: 2-3 days.
  • W2 after W1: 2-3 days. Can start in parallel with W1's last day if a 2nd engineer.
  • W3 after W2: 2 days. Can start in parallel with W2's last day if a 2nd engineer.
  • W4: 0.5-1 day.
  • Total: ~6.5-9 days, target 7-8 with overlap.

4. Risks specific to M13b

Risk Likelihood Impact Mitigation
embed.FS binary size creeps up Medium Medium (slow first paint) Per-feature code splitting; CI size gate per feature (< 30 KB gz)
Tenant-admin bypasses via direct API Low High W1 has the cross-tenant 403 test in the smoke; W2/W3 inherit the same enforcement
One-time secrets leaked in browser cache Low High Modal forces explicit "I've saved them" click; secrets never written to localStorage/sessionStorage; clear React Query cache on success
bot_token accidentally logged Low High Custom logger that redacts known sensitive fields (bot_token, hmac_secret, api_key); CI lint for new logging calls
Quarantine state stale in UI Medium Low Source list refetches on focus + every 30s while page is open
telegram token format changes (Telegram rotates) Low Low Regex lenient enough; show validation error with the format hint

5. Definition of done — M13b

  • All 3 modules (Companies, Sources, Telegram) have their per-workstream exit criteria checked.
  • make m13b-smoke green for 3 consecutive runs.
  • M13a functionality not regressed.
  • M13b_VERIFICATION.md published.
  • SPEC.md M13b row flipped to ✅ shipped YYYY-MM-DD.

Next step: start W1 (Companies). After W1 lands with the tenant-isolation test, W2 can run in parallel.