docs: add execution checklist to OPERATOR-PLAN

Ten phases (O.0–O.9), each ~one commit, in dependency order. Lets us tick
boxes as work lands and surfaces what's blocking what.
This commit is contained in:
Ronni Baslund
2026-05-24 00:28:54 +02:00
parent 92c5056a1d
commit fb3d7aa716
+142
View File
@@ -239,3 +239,145 @@ In rough priority order:
real investor — design has a placeholder "Read-only" role for Jonas Berg) real investor — design has a placeholder "Read-only" role for Jonas Berg)
- White-label of the operator portal (partners get their own portal eventually; - White-label of the operator portal (partners get their own portal eventually;
Dezky operator never gets white-labeled — it's our internal tool) Dezky operator never gets white-labeled — it's our internal tool)
---
## Execution checklist
Tick boxes as work lands. Each phase is roughly one commit. Phases must be
done in order — earlier ones unblock later ones.
### O.0 · Prep — service rename
- [ ] Rename `services/provisioning/``services/platform-api/`
- [ ] Update `package.json` name → `@dezky/platform-api`
- [ ] Update `docker-compose.yml`: container name, service key, network
alias, volume names, env var `PROVISIONING_INTERNAL_URL`
`PLATFORM_API_INTERNAL_URL`
- [ ] Update portal proxy routes to point at `http://platform-api:3001`
- [ ] Verify customer portal `/api/me` still works end-to-end after rename
### O.1 · Authentik — operator OAuth client
- [ ] Create `dezky-operator` OAuth provider via Authentik API
- [ ] Set redirect URIs to `https://operator.dezky.local/auth/oidc/{callback,logout}`
- [ ] Confidential client; persist client_secret to `.env` as
`OPERATOR_OIDC_CLIENT_SECRET`
- [ ] Create application binding linking the provider to a
`dezky-platform-admins`-only authorization flow (only group members can
reach the consent screen)
- [ ] Configure MFA-required policy on this provider
- [ ] Verify via `curl` that the discovery doc resolves at
`/application/o/dezky-operator/.well-known/openid-configuration`
### O.2 · platform-api — multi-audience + Partner CRUD
- [ ] `JwtAuthGuard`: accept audience list `['dezky-portal', 'dezky-operator']`
- [ ] New decorator/guard `@RequiresOperatorAudience()` enforcing
`aud === 'dezky-operator' && actor.platformAdmin`
- [ ] `schemas/partner.schema.ts` — Partner model (slug, name, domain,
status, marginPct, contactInfo, billingInfo)
- [ ] `partners/` module: controller + service + DTOs (create / read /
update / soft-delete)
- [ ] Add `partnerId?: Types.ObjectId` (ref Partner, index) to Tenant schema
- [ ] Aggregations: `Partner.customers` (count) and `Partner.mrr` (sum)
computed at query time
- [ ] Tenant lifecycle endpoints: `POST /tenants/:slug/suspend`,
`POST /tenants/:slug/resume`, plan/seat-cap change via existing PATCH
- [ ] All operator-only mutations gated by `@RequiresOperatorAudience()`
- [ ] Smoke test: `curl` create-partner with a `dezky-operator` token works,
same call with a `dezky-portal` token gets 403
### O.3 · Scaffold `apps/operator/`
- [ ] `apps/operator/package.json` (Nuxt 3, `nuxt-oidc-auth` beta.11, same
deps as portal)
- [ ] `nuxt.config.ts` with `oidc` block pointing at `dezky-operator`
- [ ] Docker compose service `operator`, with Traefik labels for
`operator.dezky.local`, `node_modules` volume, same `NODE_EXTRA_CA_CERTS`
mount for mkcert
- [ ] Network alias on Traefik: `operator.dezky.local`
- [ ] User task: add `operator.dezky.local` to `/etc/hosts`
- [ ] Session secrets in `.env`: `NUXT_OIDC_TOKEN_KEY` (base64-32),
`NUXT_OIDC_SESSION_SECRET`, `NUXT_OIDC_AUTH_SESSION_SECRET`
**distinct from** the customer portal's secrets
- [ ] Verify login: visit `https://operator.dezky.local`, bounce to Authentik,
sign in as akadmin, land on a placeholder index page
### O.4 · Design system + app shell
- [ ] `assets/styles/tokens.css` — copy with `data-theme="dark"` as default
- [ ] `assets/styles/base.css`
- [ ] Components: `NodeMark.vue`, `UiIcon.vue` (copy from portal)
- [ ] Shared primitives ported from the design: `Card`, `Button`, `Table`,
`Badge`, `Mono`, `Eyebrow`, `StatusDot`, `Avatar`, `PageHeader`
- [ ] `OpSidebar.vue` — collapsible, badges per nav item
- [ ] `OpTopbar.vue` — env badge, ⌘K trigger, on-call pill, bell, avatar
- [ ] `app.vue` shell wires sidebar + topbar + `<NuxtPage />`
- [ ] Keyboard shortcut: ⌘[ collapses sidebar, ⌘K opens palette
### O.5 · Tenant management (real backend)
- [ ] `pages/tenants/index.vue` — list with status/plan/seats/MRR columns,
filter by partner and status, search by slug/name
- [ ] `pages/tenants/[slug].vue` — detail view with tabs
- [ ] Tab: **Overview** — header card, key stats, partner link
- [ ] Tab: **Users** — list users via `GET /users?tenantSlug=…`
- [ ] Tab: **Resources** — provisioning status per integration
(Authentik / Stalwart / OCIS), error messages, "Reconcile" button
- [ ] Tab: **Billing** (mock fixtures)
- [ ] Tab: **Audit** (mock fixtures)
- [ ] Tab: **Support** (mock fixtures)
- [ ] Tab: **Danger** — suspend, resume, change plan, soft-delete; real
backend calls, confirmation modals
### O.6 · Partner management (real backend)
- [ ] `pages/partners/index.vue` — list with name/domain/status/customers/MRR
- [ ] `pages/partners/[slug].vue` — detail panel with customers list,
MRR breakdown, margin, contact info
- [ ] "Create partner" modal — POST /partners
- [ ] Attach / detach tenant to partner (PATCH on tenant.partnerId)
### O.7 · Visual-only screens (mock fixtures)
- [ ] `data/*.ts` — typed mock fixtures (tenants-extra, partners-extra,
services, incident, flags, audit, team)
- [ ] `pages/index.vue` — Overview dashboard
- [ ] `pages/operator-team.vue` — real backend (Users where
`platformAdmin === true`)
- [ ] `pages/users.vue` — global users, real read
- [ ] `pages/infrastructure.vue` — service health (mock for now;
docker health check integration is a follow-up)
- [ ] `pages/flags.vue` — feature flags (mock)
- [ ] `pages/audit.vue` — global audit (mock)
- [ ] `pages/support.vue` — placeholder
- [ ] `pages/billing.vue` — placeholder
- [ ] `pages/reports.vue` — placeholder
- [ ] `pages/settings.vue` — placeholder
### O.8 · Interactions
- [ ] `CommandPalette.vue` — ⌘K opens, fuzzy search over tenants + partners
+ flags + nav items + actions
- [ ] `ImpersonationModal.vue` — visual stub with reason field, Demo-only
badge, no-op confirm + toast
- [ ] `ImpersonationBanner.vue` — top banner shown when impersonating
- [ ] `IncidentModal.vue` — mock incident render
- [ ] `TweaksPanel.vue` — theme (light/dark), density (comfy/compact),
env (prod/staging/dev cosmetic switch)
### O.9 · Verification
- [ ] Sign in to `operator.dezky.local` as akadmin via the new OAuth client
- [ ] Confirm JWT audience is `dezky-operator` (decode in DevTools, post
response back)
- [ ] Create a real Partner via the UI, see it in Mongo
- [ ] Attach the `acme` tenant to that partner; verify count goes 0 → 1
- [ ] Suspend a tenant from the Danger tab; confirm `status: 'suspended'`
in Mongo
- [ ] Sign in to `app.dezky.local` simultaneously in another browser
profile, confirm the customer portal still works and that customer
token's `aud` is `dezky-portal`
- [ ] Tick all the relevant follow-up tasks in NEXT-STEPS.md as remaining
work, file separate issues if anything was deferred