# 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. **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. - [ ] Bundle: sources feature code-split (< 40 KB gzipped). **Estimated:** 2-3 days. --- ### 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 (`:`, 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/?start=` 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.