868a305539
Real backend for the flags page (was pure mock). Built so it's ready for
the first risky rollout (likely the Stalwart JMAP client or the Stripe
billing engine).
services/platform-api:
- Flag schema (key, description, state, pct, scope.{plans, tenantSlugs,
partnerSlugs, environments}, embedded history capped at 20)
- FlagsService with CRUD + evaluateAll(tenantSlug) → { key: bool }
Eval algorithm:
off → false; on → true
targeted → require non-empty scope (empty allowlist means "nobody"),
then match every non-empty axis
rollout → match scope, then sha256(`${tenantId}:${key}`) % 100 < pct
Hash-based rollout is deterministic: bumping pct only flips the new
slice. Pure helpers (matchesScope, hasAnyScope, inRolloutBucket) are
exported for future unit tests.
- FlagsController exposes GET /flags, GET /flags/:key, POST /flags/evaluate
(JwtAuthGuard); POST/PATCH/DELETE require OperatorGuard. History entries
capture the actor's email.
- SeedService idempotently creates 10 flag keys mapping to real Dezky
concerns (jmap_native_v2, gdpr_export_v2, new_billing_engine, etc.).
$setOnInsert so operator edits survive restarts.
apps/operator:
- 6 proxies: /api/flags index get/post, [key] get/patch/delete, evaluate post
- types/flag.ts with the shape that mirrors the backend
- pages/flags.vue: useFetch real list, row click opens FlagDetail,
"New flag" opens NewFlagModal, scope summary column shows targeting
at a glance
- FlagDetail.vue: side panel with segmented state, rollout slider with
live "~N of M tenants" preview from /api/tenants, plan/tenant/env chip
pickers, dirty-tracked Save, instant Kill-switch (PATCH state=off+pct=0),
embedded change history
- NewFlagModal.vue: minimal create form (key + description). Everything
else is configured in the detail panel afterward.
- CommandPalette: feature-flag rows now come from /api/flags instead of
the dropped fixture, so newly-created flags are searchable immediately
- data/fixtures.ts: drop FLAGS / FeatureFlag exports (replaced by the
real backend)
Smoke-tested end-to-end: list renders 10 seed flags, opening gdpr_export_v2
and flipping to rollout 25% then saving persists + adds a history entry,
kill-switch sets state=off in one click, /api/flags/evaluate returns the
correct booleans for the seeded tenant, same tenant gets the same answer
on consecutive evals (determinism), and creating + deleting a flag through
the UI roundtrips correctly.
135 lines
8.8 KiB
TypeScript
135 lines
8.8 KiB
TypeScript
// Visual-only fixtures for screens we haven't wired to a real backend yet
|
|
// (infrastructure, feature flags, audit log, active incident, operator team
|
|
// extras). Real data sources are GET /tenants, /partners, /users — anything
|
|
// derivable from those should NOT live here. See OPERATOR-PLAN.md follow-ups
|
|
// for the path from each fixture to a real implementation.
|
|
|
|
import type { IconName } from '~/components/UiIcon.vue'
|
|
|
|
export type ServiceStatus = 'ok' | 'warn' | 'bad'
|
|
export interface PlatformService {
|
|
id: string
|
|
name: string
|
|
role: string
|
|
status: ServiceStatus
|
|
uptime: number // percent, 30d
|
|
p95: number // ms
|
|
err: number // percent
|
|
last: string // human duration since last incident
|
|
}
|
|
|
|
export const SERVICES: PlatformService[] = [
|
|
{ id: 'mail', name: 'Stalwart', role: 'Mail · IMAP/JMAP/SMTP', status: 'ok', uptime: 99.99, p95: 42, err: 0.002, last: '—' },
|
|
{ id: 'files', name: 'OCIS', role: 'Files · OwnCloud Infinite', status: 'ok', uptime: 99.97, p95: 88, err: 0.004, last: '11 d ago' },
|
|
{ id: 'video', name: 'Jitsi', role: 'Video meetings', status: 'ok', uptime: 99.91, p95: 124, err: 0.018, last: '4 d ago' },
|
|
{ id: 'chat', name: 'Zulip', role: 'Team chat', status: 'ok', uptime: 99.99, p95: 35, err: 0.001, last: '—' },
|
|
{ id: 'auth', name: 'Authentik', role: 'Identity · SSO · MFA', status: 'warn', uptime: 99.94, p95: 412, err: 0.052, last: 'active' },
|
|
{ id: 'db', name: 'PostgreSQL', role: 'Primary database', status: 'ok', uptime: 99.99, p95: 8, err: 0, last: '—' },
|
|
{ id: 'obj', name: 'Object storage',role: 'S3-compatible · Hetzner', status: 'ok', uptime: 99.99, p95: 22, err: 0.001, last: '—' },
|
|
{ id: 'cdn', name: 'Cloudflare', role: 'CDN · WAF', status: 'ok', uptime: 100, p95: 18, err: 0, last: '—' },
|
|
{ id: 'smtp', name: 'Outbound SMTP', role: 'Email delivery (Postmark)', status: 'ok', uptime: 99.95, p95: 280, err: 0, last: '3 d ago' },
|
|
]
|
|
|
|
export interface ActiveIncident {
|
|
id: string
|
|
title: string
|
|
severity: 'P1' | 'P2' | 'P3'
|
|
started: string
|
|
duration: string
|
|
affected: string
|
|
state: 'investigating' | 'identified' | 'monitoring'
|
|
ic: string
|
|
updates: { t: string; who: string; msg: string }[]
|
|
}
|
|
|
|
export const INCIDENT: ActiveIncident = {
|
|
id: 'INC-2026-018',
|
|
title: 'Authentik · elevated SSO login latency',
|
|
severity: 'P2',
|
|
started: '14:18',
|
|
duration: '42 min',
|
|
affected: 'Login latency p95 above 400ms · 12 tenants impacted',
|
|
state: 'investigating',
|
|
ic: 'Mikkel Nørgaard',
|
|
updates: [
|
|
{ t: '15:00', who: 'Mikkel N.', msg: 'Pod restart deployed, monitoring' },
|
|
{ t: '14:36', who: 'auto', msg: 'Page sent to on-call (Mikkel)' },
|
|
{ t: '14:22', who: 'Anne B.', msg: 'Confirmed: Postgres conn pool exhaustion on auth-db-2' },
|
|
{ t: '14:18', who: 'auto', msg: 'Alert: authentik p95 > 400ms for 5m · 12 tenants impacted' },
|
|
],
|
|
}
|
|
|
|
// Feature flags moved to a real backend at /api/flags + see types/flag.ts.
|
|
// The seed in services/platform-api/src/seed/seed.service.ts creates the
|
|
// same 10 flags this fixture used to contain.
|
|
|
|
export type AuditTone = 'info' | 'warn' | 'bad'
|
|
export interface AuditEntry {
|
|
id: string
|
|
when: string
|
|
actor: string
|
|
role: string
|
|
action: string
|
|
target: string
|
|
tenant: string
|
|
ip: string
|
|
tone: AuditTone
|
|
}
|
|
|
|
export const OP_AUDIT: AuditEntry[] = [
|
|
{ id: 'op_8821', when: '15:02:11', actor: 'Anne Baslund', role: 'platform admin', action: 'feature_flag.rollout', target: 'jmap_native_v2 · 50%', tenant: '—', ip: '10.0.4.18', tone: 'info' },
|
|
{ id: 'op_8820', when: '14:58:42', actor: 'Mikkel Nørgaard', role: 'engineer', action: 'service.pod_restart', target: 'authentik-worker-3', tenant: '—', ip: '10.0.4.21', tone: 'warn' },
|
|
{ id: 'op_8819', when: '14:48:02', actor: 'Sofie Lindberg', role: 'ops', action: 'tenant.impersonate', target: 'oliver@bygherre.dk', tenant: 'Bygherre Cloud', ip: '10.0.4.04', tone: 'info' },
|
|
{ id: 'op_8818', when: '14:36:00', actor: 'system', role: 'auto', action: 'oncall.paged', target: 'Mikkel Nørgaard', tenant: '—', ip: '—', tone: 'warn' },
|
|
{ id: 'op_8817', when: '14:18:00', actor: 'system', role: 'auto', action: 'alert.triggered', target: 'authentik p95 > 400ms', tenant: '—', ip: '—', tone: 'bad' },
|
|
{ id: 'op_8816', when: '13:21:55', actor: 'Anne Baslund', role: 'platform admin', action: 'tenant.refund_issued', target: 'INV-0480 · 980 DKK', tenant: 'Vester Foods', ip: '10.0.4.18', tone: 'info' },
|
|
{ id: 'op_8815', when: '12:09:30', actor: 'Sofie Lindberg', role: 'ops', action: 'tenant.suspended', target: 'København Kalkulator', tenant: 'København Kalkulator', ip: '10.0.4.04', tone: 'warn' },
|
|
{ id: 'op_8814', when: '11:44:00', actor: 'Anne Baslund', role: 'platform admin', action: 'partner.created', target: 'Klaussen Digital · invited', tenant: '—', ip: '10.0.4.18', tone: 'info' },
|
|
{ id: 'op_8813', when: '10:55:41', actor: 'system', role: 'auto', action: 'invoice.past_due', target: 'INV-0522 · 2.940 DKK · 21 d', tenant: 'Bygherre Cloud', ip: '—', tone: 'bad' },
|
|
{ id: 'op_8812', when: '10:12:08', actor: 'Mikkel Nørgaard', role: 'engineer', action: 'feature_flag.created', target: 'beta_ai_summaries', tenant: '—', ip: '10.0.4.21', tone: 'info' },
|
|
{ id: 'op_8811', when: '09:30:00', actor: 'Anne Baslund', role: 'platform admin', action: 'tos.published', target: 'v2026.05 · all tenants', tenant: '—', ip: '10.0.4.18', tone: 'info' },
|
|
]
|
|
|
|
// Services in the design that haven't been deployed yet. Surfaced as a
|
|
// separate "Planned" section on the Infrastructure page so the operator sees
|
|
// honest deployment state instead of a fake all-green grid.
|
|
export interface PlannedService {
|
|
id: string
|
|
name: string
|
|
role: string
|
|
note: string
|
|
}
|
|
|
|
export const PLANNED_SERVICES: PlannedService[] = [
|
|
{ id: 'jitsi', name: 'Jitsi', role: 'Video meetings', note: 'Lands with docker-compose.optional.yml (Phase 7)' },
|
|
{ id: 'zulip', name: 'Zulip', role: 'Team chat', note: 'Lands with docker-compose.optional.yml (Phase 7)' },
|
|
{ id: 'dns', name: 'simpledns.plus', role: 'DNS · authoritative', note: 'External SaaS · prod only' },
|
|
{ id: 'objstore', name: 'Hetzner Object Storage', role: 'Files · S3 backend for OCIS', note: 'External · prod only' },
|
|
{ id: 'smtp-out', name: 'Postmark', role: 'Outbound SMTP · transactional email', note: 'External SaaS · prod only' },
|
|
]
|
|
|
|
export type NotificationKind = 'security' | 'user' | 'billing' | 'integration' | 'support' | 'signin'
|
|
export type NotificationTone = 'warn' | 'info' | 'neutral' | 'ok' | 'bad'
|
|
export interface NotificationItem {
|
|
id: string
|
|
kind: NotificationKind
|
|
title: string
|
|
body: string
|
|
when: string
|
|
icon: IconName
|
|
tone: NotificationTone
|
|
unread: boolean
|
|
}
|
|
|
|
// Seed list mirrors the design screenshot. The real source will be an event
|
|
// stream / Mongo collection later; for now this is the shell other features
|
|
// can plug into. See the "notifications backend" follow-up in NEXT-STEPS.md.
|
|
export const NOTIFICATIONS: NotificationItem[] = [
|
|
{ id: 'n_2821', kind: 'security', icon: 'shield', tone: 'warn', when: '2 min ago', unread: true, title: 'DMARC policy weak on baslund.dk', body: 'Set the policy to at least quarantine to reduce spoofing.' },
|
|
{ id: 'n_2820', kind: 'user', icon: 'users', tone: 'info', when: '14 min ago', unread: true, title: 'Mikkel accepted your invitation', body: 'mikkel@dezky.com joined as Admin.' },
|
|
{ id: 'n_2819', kind: 'billing', icon: 'card', tone: 'neutral', when: '1 h ago', unread: false, title: 'Invoice INV-2026-005 paid', body: '1.940,00 DKK · Visa •••• 4242' },
|
|
{ id: 'n_2818', kind: 'integration', icon: 'plug', tone: 'neutral', when: '3 h ago', unread: false, title: 'Notion SAML connection live', body: 'Connected via Authentik. 11 users provisioned.' },
|
|
{ id: 'n_2817', kind: 'support', icon: 'help', tone: 'neutral', when: 'Yesterday', unread: false, title: 'Sofie replied to your ticket TKT-2832', body: 'Update on missing mobile recordings.' },
|
|
{ id: 'n_2816', kind: 'signin', icon: 'bell', tone: 'bad', when: '2 d ago', unread: false, title: 'Failed sign-in attempts on oliver@', body: '3 attempts from 203.0.113.4 — IP added to watchlist.' },
|
|
]
|