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
M13b row flipped to ✅.What M13b is NOT:
M13_FRONTEND_SPEC.md §3.8).┌──────────────────────────┐ ┌──────────────────────────┐
│ 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.
Goal: Super-admin can create, list, edit, suspend, activate companies. Tenant-admin sees only their own company (read-only).
Scope:
/companies, /companies/:id, /companies/new.web/src/features/companies/.CompanyList, CompanyDetail, CompanyForm,
SuspendDialog (typed confirmation: type slug to suspend).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.useCompanies, useCompany(id),
useCreateCompany, useUpdateCompany, useSuspendCompany,
useActivateCompany.web/src/lib/api-client.ts regenerated from admind OpenAPI.web/src/lib/scope.ts — utility to read current user from auth
context, gate UI by role.Exit criteria:
GET /v1/companies/other-company-id → 403.status=suspended, ingestd rejects (verify
via curl).status=active, ingestd accepts again.audit_log table.Estimated: 2-3 days.
Goal: Sources list + create + edit + rotate secret + suspend. Includes the "save these now" modal for HMAC secrets and API keys on create/rotate.
Scope:
/sources, /sources/:id, /sources/new.web/src/features/sources/.SourceList, SourceDetail, SourceForm,
OneTimeSecretsModal, QuarantineBadge.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.M13_API_CONTRACT.md §2.2).quarantine_until is in the future, show
a red badge with the remaining time.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.POST /v1/sources/:id/cert/csrGET /v1/sources/:id/certDELETE /v1/sources/:id/certGET /v1/certs/expiring?days=30source.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".Exit criteria:
status=suspended, ingestd rejects
(verified by curl).quarantine_until is set..zip with cert +
chain + private key (one-time download).make m13b-smoke includes the cert path
(mTLS-required source + cert issued + revoke + 60s window).Estimated: 4-6 days (was 2-3 before M14-ui was added).
Goal: Per-company telegram bot config, invite code generation, binding list.
Scope:
/companies/:id/telegram,
/companies/:id/telegram/invites,
/companies/:id/telegram/bindings.web/src/features/telegram/.TelegramConfig, TelegramInviteForm,
TelegramInvitesList, TelegramBindingsList,
BotTokenRotateDialog.bot_token: required, format validated
(<bot_id>:<secret>, regex ^\d+:[A-Za-z0-9_-]{35}$).t.me/<bot>?start=<code> URL, copy-to-clipboard.Exit criteria:
admind, decryptable
for emit (verify via direct API call from deliverd).status=revoked.Estimated: 2 days.
Goal: End-to-end smoke that exercises all 3 modules. Screenshots for the verification doc.
Scope:
scripts/m13b_smoke.sh — bash + curl:
scripts/m13b_smoke.sh exits 0/1, green required.M13b_VERIFICATION.md with:
Exit criteria:
make m13b-smoke exits 0 from a clean state.M13b_VERIFICATION.md published.Estimated: 0.5-1 day.
W1 (2-3d) ──▶ W2 (2-3d) ──▶ W3 (2d) ──▶ W4 (0.5-1d)
| 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 |
make m13b-smoke green for 3 consecutive runs.M13b_VERIFICATION.md published.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.