Commit Graph

4 Commits

Author SHA1 Message Date
Ronni Baslund c71e782dc0 feat(operator): command palette, impersonation, incident, tweaks (O.8)
- CommandPalette + useCommandPalette: ⌘K opens a search-and-jump panel over
  real tenants/partners + fixture flags + nav + actions. Arrow keys + Enter
  navigate, Escape/backdrop close. Recents are intentionally omitted for now;
  add when there's something to recent over.
- Impersonation stub: useImpersonation + ImpersonationModal + ImpersonationBanner.
  Modal opens from tenant detail and from the palette. Banner stays at the top
  of the shell until exited. No real OBO token is minted — wiring OAuth Token
  Exchange is tracked as a follow-up.
- IncidentModal + useIncidentModal: opened from the Overview and Infrastructure
  incident banners, renders the mock INCIDENT data with metrics, timeline and
  draft composer.
- TweaksPanel + useTweaks: floating bottom-right panel for theme (dark/light),
  density (comfy/compact), env badge (prod/staging/dev). Saved to localStorage.
- Theme/density apply via [data-theme] + [data-density] overrides in
  tokens.css. Topbar env badge now reads from useTweaks instead of a prop.
- Layout wires ⌘K + ⌘[ at the document level and mounts the palette + modals
  + banner + tweaks panel once for all pages.
2026-05-24 08:34:34 +02:00
Ronni Baslund e0ac643e80 feat(operator): visual-only screens with real-data overview (O.7)
- Overview (pages/index.vue): KPIs from real /tenants /partners /users,
  status meter, recent + needs-follow-up tables. Mock activity stream and
  incident banner overlay come from data/fixtures.ts.
- Operator team: real GET /users filtered to platformAdmin === true,
  with last-seen + tenant counts.
- Users (global): real read with All/Admins/Inactive views and search.
- Infrastructure / Feature flags / Audit: mock fixtures only — wiring to
  real backends (Prometheus, OpenFeature, append-only audit) is tracked
  as follow-ups in OPERATOR-PLAN.md.
- Placeholder pages (support/billing/reports/settings) via OpPlaceholder.
- Shared: Stat, MetricCell, OpPlaceholder components, /api/users proxy,
  PlatformUser type.
- .gitignore: scope the docker volumes data/ rule so apps/*/data/ is
  tracked again (operator carries mock fixtures there).
2026-05-24 08:17:26 +02:00
Ronni Baslund 8e81730372 feat(operator): tenant list + 7-tab detail with real lifecycle (O.5)
Operator can now manage tenants end-to-end from the UI:

  - pages/tenants/index.vue — list with status/plan/domains/created/
    provisioning-state columns, search by slug or name, status chips
    with live counts (all/active/pending/suspended), click-through
    to detail
  - pages/tenants/[slug].vue — 7-tab detail (Overview, Users, Resources,
    Billing, Audit, Support, Danger zone)
  - 3 tabs hit real backends: Overview (identity + billing fields),
    Users (lazy-loaded via new GET /tenants/:slug/users endpoint),
    Resources (live provisioning state per integration + Reconcile button)
  - 3 tabs render mock fixtures with warn-tone "mock" badges: Billing
    (Stripe placeholder), Audit (sample log lines), Support (placeholder
    pending the ticket queue work)
  - Danger zone: 3 real-backend cards (Suspend / Resume / Soft-delete),
    each gated by a ConfirmDialog modal. Verified live — clicked
    Suspend on acme, status flipped to 'suspended' in Mongo, then
    Resumed back to 'active'

platform-api additions:
  - GET /tenants/:slug/users returns users with this tenant in their
    tenantIds, sorted by last login. Same authorization rule as the
    existing /tenants/:slug — platform admins always pass,
    non-admins must be a member of the tenant
  - tenants.module imports User schema for the new lookup

New components (apps/operator/components/):
  - Tabs.vue — horizontal strip with optional per-tab counts, v-model
  - ConfirmDialog.vue — Teleport-to-body modal, Escape/backdrop close,
    danger/primary tone for the confirm button

Server proxy infrastructure (apps/operator/server/):
  - utils/platform-api.ts — single helper encapsulating
    access-token-from-session + bearer-forward + error normalization.
    Every operator proxy route is now a one-liner against this helper
  - api/tenants/index.get.ts, [slug]/{index.get,index.patch,index.delete,
    users.get,suspend.post,resume.post,reconcile.post}.ts

Two real bugs found and fixed during the smoke test:

  - Mongoose subdocument `_id` leaks into JSON when iterating
    tenant.provisioningStatus. Switched to an explicit
    `['authentik', 'stalwart', 'ocis']` whitelist in both v-fors
  - Documents created before provisioningErrors was added (like the
    acme tenant) don't have the field at all in JSON. Use optional
    chaining (`tenant.provisioningErrors?.[k]`) instead of bracket
    access. Without it: 'Cannot read properties of undefined (reading
    "authentik")' during the Resources tab render
2026-05-24 07:44:23 +02:00
Ronni Baslund 8e6f73a921 feat(operator): design system port + persistent shell (O.4)
Operator portal now wears its real chrome instead of placeholder spans.
Sidebar + topbar + page header all rendered against the carbon palette
from tokens.css.

Components ported from the source design (operator-app.jsx,
platform-ui.jsx, operator-screens.jsx) as Vue 3 SFCs in
apps/operator/components/:

  Foundation: NodeMark (copied from portal), UiIcon (expanded to 31 icons
  covering sidebar/topbar/sort/arrows)

  Primitives: Card (3 surface variants), UiButton (primary / secondary /
  ghost / dark / danger × sm / md / lg), DataTable (header + rows),
  Badge (7 tones), Avatar (deterministic palette by name hash), Mono,
  Eyebrow, StatusDot, PageHeader (with actions slot)

  Shell: OpSidebar (collapsible 232<->56px, 12 nav items in 4 sections,
  active-row highlight from route, badge slot, brand + user footer);
  OpTopbar (env badge with prod/staging/dev variants, palette trigger
  stub for the ⌘K work in O.8, on-call pill, bell, avatar)

Layouts: layouts/default.vue wires sidebar + topbar + slot; layouts/blank.vue
is used by the login page (definePageMeta layout:'blank'). app.vue now
wraps NuxtPage in NuxtLayout (the missing piece — without it Nuxt warns
"Your project has layouts but the <NuxtLayout /> component has not been
used" and renders nothing chrome-wise).

Composable composables/useSidebar.ts holds the collapsed state shared
between OpSidebar's toggle button and layouts/default.vue's ⌘[ keyboard
shortcut.

Verified in the browser:
  - Sidebar renders all 12 nav links with section dividers, env badge shows
    PROD, PageHeader resolves to the user's display name from
    useOidcAuth().user
  - Collapse toggle flips sidebar width 232↔56; nav rows become icon-only
  - Smoke test on the placeholder home still returns 409 for the seeded
    test-partner (token forwarding survives the layout refactor)

Gotcha documented in the plan: Vite 7.3 added a strict
server.allowedHosts check that returns plaintext 403 for any host header
that isn't the dev origin. The customer portal pre-dates this Vite
version; operator needs allowedHosts: ['operator.dezky.local'] in
nuxt.config.ts under vite.server.

Pages/index.vue replaces the bare HTML placeholder from O.3 with the
new PageHeader + Card primitives — same smoke-test functionality, much
better visual fidelity.

Real screen content (Tenants, Partners, Infrastructure, etc.) lands in
O.5+. This commit is the chrome, the smoke test, and the verification
that the design system primitives compose correctly.
2026-05-24 07:32:08 +02:00