From fb3d7aa716bd9fcdc813fe04a66c74d801e773d7 Mon Sep 17 00:00:00 2001 From: Ronni Baslund Date: Sun, 24 May 2026 00:28:54 +0200 Subject: [PATCH] docs: add execution checklist to OPERATOR-PLAN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/OPERATOR-PLAN.md | 142 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/docs/OPERATOR-PLAN.md b/docs/OPERATOR-PLAN.md index 197eea5..d34a7c7 100644 --- a/docs/OPERATOR-PLAN.md +++ b/docs/OPERATOR-PLAN.md @@ -239,3 +239,145 @@ In rough priority order: real investor — design has a placeholder "Read-only" role for Jonas Berg) - White-label of the operator portal (partners get their own portal eventually; 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 + `` +- [ ] 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