// Mock fixtures for portal dev mode — port of the handoff `data.js`. // // Used in two places: // 1. MSW handlers (intercept `/api/tenants/...` and friends so the portal // renders without the tenant-registry service up). // 2. Server components that render the design with realistic data when no // live backend is available. // // Deterministic by design — same NOW + same seed = same data every reload. import type { OrgRole, TenantStatus } from "@/lib/session"; // ---- deterministic RNG (mulberry32) ------------------------------------- function rng(seed: number) { let a = seed >>> 0; return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const pick = (r: () => number, arr: T[]): T => arr[Math.floor(r() * arr.length)]!; // Frozen "now" so every reload is identical. export const NOW = new Date("2026-06-04T09:12:00Z"); const DAY = 86_400_000; const ago = (ms: number) => new Date(NOW.getTime() - ms); // ---- product catalog ---------------------------------------------------- export type ProductStatus = "live" | "soon"; export type ProductDef = { id: string; slug: string; name: string; mono: string; status: ProductStatus; blurb: string; frameworks: string[]; }; export const PRODUCTS: ProductDef[] = [ { id: "compliance-scanner", slug: "compliance-scanner", name: "Compliance Scanner", mono: "CS", status: "live", blurb: "Continuous control scanning across cloud, code & infrastructure.", frameworks: ["ISO 27001", "BSI C5", "NIS2"], }, { id: "certifai", slug: "certifai", name: "CERTifAI", mono: "Ai", status: "live", blurb: "AI-act conformity & model evidence — EU AI Act Annex IV dossiers.", frameworks: ["EU AI Act", "ISO 42001"], }, { id: "policyforge", slug: "policyforge", name: "PolicyForge", mono: "PF", status: "soon", blurb: "Policy authoring with mapped controls and approval trails.", frameworks: ["ISO 27001", "TISAX"], }, { id: "residency", slug: "residency-monitor", name: "Residency Monitor", mono: "RM", status: "soon", blurb: "Data-residency & transfer-impact monitoring for GDPR Ch. V.", frameworks: ["GDPR", "Schrems II"], }, ]; export const productById = (id: string): ProductDef | undefined => PRODUCTS.find((p) => p.id === id || p.slug === id); // ---- pools -------------------------------------------------------------- const CONTROLS: [string, string][] = [ ["ISO 27001 A.8.3", "Information access restriction"], ["ISO 27001 A.5.23", "Information security for cloud services"], ["ISO 27001 A.8.16", "Monitoring activities"], ["ISO 27001 A.8.24", "Use of cryptography"], ["BSI C5 KRY-03", "Encryption of data in transit"], ["BSI C5 IDM-09", "Privileged access review"], ["BSI C5 RB-21", "Logging of security events"], ["NIS2 Art.21 (2c)", "Business continuity & backup"], ["NIS2 Art.21 (2d)", "Supply-chain security"], ["EU AI Act Art.9", "Risk-management system"], ["EU AI Act Art.12", "Record-keeping / logging"], ["EU AI Act Annex IV", "Technical documentation"], ["GDPR Art.32", "Security of processing"], ["GDPR Art.30", "Records of processing activities"], ["ISO 42001 6.1.2", "AI risk assessment"], ["TISAX 1.5.1", "Identity & access management"], ]; const FINDING_TITLES = [ "S3 bucket without enforced TLS policy", "IAM role with wildcard privileges", "Production DB snapshot unencrypted at rest", "Audit log retention below 365 days", "Model card missing intended-purpose section", "Public container registry exposes image digests", "MFA not enforced for 3 admin accounts", "Backup restore not tested in 90 days", "Sub-processor list out of date in RoPA", "Training-data lineage not recorded", "Egress to non-EU region detected", "Secrets found in CI pipeline variables", "Vendor TIA missing for US-based CDN", "Container image runs as root", "Conformity dossier lacks bias-evaluation evidence", "Logging disabled on inference endpoint", ]; // ---- types -------------------------------------------------------------- export type Severity = "critical" | "high" | "medium" | "low"; export type Finding = { id: string; severity: Severity; product: string; control: string; controlName: string; title: string; status: "open" | "resolved"; opened: string; ageDays: number; owner: string; }; export type ActivityRow = { ts: Date; when: string; date: string; time: string; actor: string; verb: string; product: string; target: string; }; export type AuditRow = { ts: Date; date: string; time: string; event: string; actor: string; product: string; ip: string; result: "ok" | "denied"; }; export type Invoice = { id: string; period: string; issued: string; seats: number; net: number; vat: number; total: number; status: "paid" | "due"; }; export type TeamMember = { name: string; email: string; roles: OrgRole[]; status: "active" | "invited"; last: string; }; export type TenantMetrics = { openFindings: number; critical: number; lastScan: string; lastScanDate: string; evidence: number; controlsPassing: number; controlsTotal: number; severity: Record; resolved7: number; findingsDelta: number; }; export type TenantSeries = { findings30: number[]; evidence30: number[]; controls30: number[]; heatmap: number[]; prodSeries: Record; }; export type TenantSeats = { used: number; total: number }; export type TenantRecord = { id: string; domain: string; name: string; short: string; mono: string; status: TenantStatus; legalType: string; city: string; country: string; vat: string; plan: string; planCode: string; seats: TenantSeats; monthly: number; contact: string; contactEmail: string; renewal: string; since: string; entitled: string[]; trialing: string[]; trialDaysLeft?: number; trialEnds?: string; frozenReason?: string; archivedOn?: string; retentionClosed?: string; seed: number; findingCount: number; // generated products: ProductDef[]; findings: Finding[]; activity: ActivityRow[]; audit: AuditRow[]; invoices: Invoice[]; team: TeamMember[]; series: TenantSeries; metrics: TenantMetrics; }; // ---- date helpers ------------------------------------------------------- function fmtDate(d: Date): string { return d.toISOString().slice(0, 10); } function fmtTime(d: Date): string { return d.toISOString().slice(11, 16); } export function relTime(d: Date): string { const diff = NOW.getTime() - d.getTime(); const m = Math.floor(diff / 60000); if (m < 1) return "just now"; if (m < 60) return m + "m ago"; const h = Math.floor(m / 60); if (h < 24) return h + "h ago"; const days = Math.floor(h / 24); if (days < 30) return days + "d ago"; return Math.floor(days / 30) + "mo ago"; } export { fmtDate, fmtTime }; // ---- generators --------------------------------------------------------- function genFindings(seed: number, products: string[], count: number): Finding[] { const r = rng(seed); const out: Finding[] = []; const sevPool: Severity[] = [ "critical", "high", "high", "medium", "medium", "medium", "low", "low", ]; for (let i = 0; i < count; i++) { const ctrl = pick(r, CONTROLS); const prod = pick(r, products); const ageDays = Math.floor(r() * 41) + 1; const sev = pick(r, sevPool); const resolved = r() < 0.32; out.push({ id: "FND-" + (1000 + Math.floor(r() * 8999)), severity: sev, product: prod, control: ctrl[0], controlName: ctrl[1], title: pick(r, FINDING_TITLES), status: resolved ? "resolved" : "open", opened: fmtDate(ago(ageDays * DAY)), ageDays, owner: "—", }); } const sevRank: Record = { critical: 0, high: 1, medium: 2, low: 3 }; out.sort( (a, b) => (a.status === b.status ? 0 : a.status === "open" ? -1 : 1) || sevRank[a.severity] - sevRank[b.severity] || a.ageDays - b.ageDays ); return out; } function genActivity( seed: number, products: string[], people: string[], count: number ): ActivityRow[] { const r = rng(seed); const verbs: [string, string | null][] = [ ["ran a scan on", "compliance-scanner"], ["resolved finding", null], ["exported evidence for", null], ["invited", null], ["updated control mapping in", null], ["approved dossier in", "certifai"], ["acknowledged finding", null], ["generated Annex IV report in", "certifai"], ["rotated API key for", null], ["assigned owner to", null], ["downloaded audit bundle for", null], ]; const out: ActivityRow[] = []; let cursor = 0; for (let i = 0; i < count; i++) { cursor += Math.floor(r() * 9 * DAY) + 3_600_000; const d = ago(cursor); const v = pick(r, verbs); const prod = v[1] || pick(r, products); const target = pick(r, [ "FND-" + (1000 + Math.floor(r() * 8999)), prod, pick(r, CONTROLS)[0], pick(r, people).split(" ")[0] + "@", ]); out.push({ ts: d, when: relTime(d), date: fmtDate(d), time: fmtTime(d), actor: pick(r, people), verb: v[0], product: prod, target, }); } out.sort((a, b) => b.ts.getTime() - a.ts.getTime()); return out; } function genAudit( seed: number, products: string[], people: string[], count: number ): AuditRow[] { const r = rng(seed); const events = [ "auth.login", "auth.login", "scan.completed", "finding.resolved", "evidence.exported", "user.invited", "billing.viewed", "settings.sso.viewed", "product.opened", "finding.assigned", "report.generated", "apikey.rotated", "auth.failed", "policy.published", ]; const out: AuditRow[] = []; let cursor = 0; for (let i = 0; i < count; i++) { cursor += Math.floor(r() * 2.4 * DAY) + 600_000; const d = ago(cursor); const ev = pick(r, events); out.push({ ts: d, date: fmtDate(d), time: fmtTime(d), event: ev, actor: pick(r, people), product: ev.startsWith("auth") || ev.startsWith("billing") || ev.startsWith("settings") ? "—" : pick(r, products), ip: [10, Math.floor(r() * 255), Math.floor(r() * 255), Math.floor(r() * 255)].join("."), result: ev === "auth.failed" ? "denied" : "ok", }); } out.sort((a, b) => b.ts.getTime() - a.ts.getTime()); return out; } function genInvoices(seed: number, monthly: number, seats: number, months: number): Invoice[] { const r = rng(seed); const out: Invoice[] = []; for (let i = 0; i < months; i++) { const d = new Date(NOW.getFullYear(), NOW.getMonth() - i, 1); const amt = monthly + (i === 0 ? 0 : Math.floor((r() - 0.5) * 4) * 120); out.push({ id: "INV-" + d.getFullYear() + "-" + String(months - i).padStart(4, "0"), period: d.toLocaleString("en", { month: "short" }) + " " + d.getFullYear(), issued: fmtDate(d), seats, net: amt, vat: Math.round(amt * 0.19), total: amt + Math.round(amt * 0.19), status: i === 0 ? "due" : "paid", }); } return out; } // ---- tenant build ------------------------------------------------------- type PersonSeed = [string, string, OrgRole[]] | [string, string, OrgRole[], "active" | "invited"]; type TenantSeed = Omit< TenantRecord, "products" | "findings" | "activity" | "audit" | "invoices" | "team" | "series" | "metrics" > & { people: PersonSeed[]; }; function buildTenant(cfg: TenantSeed): TenantRecord { const findingProducts = cfg.entitled.filter((p) => ["compliance-scanner", "certifai"].includes(p) ); const findings = genFindings( cfg.seed, findingProducts.length ? findingProducts : ["compliance-scanner"], cfg.findingCount ); const people = cfg.people.map((p) => p[0]); const activity = genActivity(cfg.seed + 7, cfg.entitled, people, 26); const audit = genAudit(cfg.seed + 13, cfg.entitled, people, 38); const invoices = genInvoices(cfg.seed + 21, cfg.monthly, cfg.seats.total, 9); const team: TeamMember[] = cfg.people.map((p, i) => ({ name: p[0], email: p[1] + "@" + cfg.domain, roles: p[2], status: p[3] || "active", last: p[3] === "invited" ? "—" : relTime(ago(Math.floor(i * 1.7 + 0.4) * DAY)), })); const open = findings.filter((f) => f.status === "open"); const crit = open.filter((f) => f.severity === "critical").length; const lastScan = activity.find((a) => a.verb.includes("scan")); const evidence = 180 + Math.floor(cfg.seed % 400); const controlsPassing = 88 + (cfg.seed % 9); const r = rng(cfg.seed + 99); const findings30: number[] = []; let cur = open.length + Math.floor(r() * 6) + 3; for (let i = 0; i < 30; i++) { cur += Math.round((r() - 0.45) * 4); cur = Math.max(1, cur); findings30.push(cur); } findings30[29] = open.length; const evidence30: number[] = []; let ev = evidence - Math.floor(r() * 60) - 30; for (let i = 0; i < 30; i++) { ev += Math.floor(r() * 5); evidence30.push(ev); } evidence30[29] = evidence; const controls30: number[] = []; for (let i = 0; i < 30; i++) { controls30.push( Math.min(99, controlsPassing + Math.round((r() - 0.6) * 6) - (i < 24 ? 2 : 0)) ); } controls30[29] = controlsPassing; const heatmap: number[] = []; for (let i = 0; i < 35; i++) { const x = r(); const dow = i % 7; const weekend = dow >= 5 ? 0.55 : 1; heatmap.push(x * weekend < 0.3 ? 0 : x * weekend < 0.55 ? 1 : x * weekend < 0.78 ? 2 : x * weekend < 0.92 ? 3 : 4); } const prodSeries: Record = {}; ["compliance-scanner", "certifai"].forEach((pid, k) => { const rr = rng(cfg.seed + 17 * (k + 1)); const base = open.filter((f) => f.product === pid).length; const arr: number[] = []; let c = base + Math.floor(rr() * 4) + 1; for (let i = 0; i < 20; i++) { c += Math.round((rr() - 0.45) * 3); c = Math.max(0, c); arr.push(c); } arr[19] = base; prodSeries[pid] = arr; }); return { ...cfg, products: PRODUCTS, findings, activity, audit, invoices, team, series: { findings30, evidence30, controls30, heatmap, prodSeries }, metrics: { openFindings: open.length, critical: crit, lastScan: lastScan ? lastScan.when : "—", lastScanDate: lastScan ? lastScan.date : "—", evidence, controlsPassing, controlsTotal: 240, severity: { critical: open.filter((f) => f.severity === "critical").length, high: open.filter((f) => f.severity === "high").length, medium: open.filter((f) => f.severity === "medium").length, low: open.filter((f) => f.severity === "low").length, }, resolved7: 1 + (cfg.seed % 5), findingsDelta: findings30[29] - findings30[22], }, }; } // ---- tenants ------------------------------------------------------------ export const TENANTS: Record = { acme: buildTenant({ id: "acme", domain: "acme.eu", name: "Acme Logistik GmbH", short: "Acme", mono: "AC", status: "active", legalType: "GmbH", city: "München, DE", country: "Germany", vat: "DE 811 204 557", plan: "Scale", planCode: "BP-SCALE", seats: { used: 34, total: 50 }, monthly: 4200, contact: "Lena Brandt", contactEmail: "lena.brandt@acme.eu", renewal: "2026-11-01", since: "2023-04-12", entitled: ["compliance-scanner", "certifai"], trialing: [], seed: 1337, findingCount: 13, people: [ ["Lena Brandt", "lena.brandt", ["IT_ADMIN", "CXO"]], ["Tomas Vogel", "tomas.vogel", ["USER"]], ["Aylin Demir", "aylin.demir", ["LEGAL"]], ["Jonas Weber", "jonas.weber", ["FINANCE"]], ["Sophie Maurer", "sophie.maurer", ["USER"]], ["Lukas Berger", "lukas.berger", ["USER", "LEGAL"]], ["Paul Schmid", "paul.schmid", ["CXO"]], ["Nora Fischer", "nora.fischer", ["USER"], "invited"], ], }), hello: buildTenant({ id: "hello", domain: "hello.io", name: "Hallo Software AG", short: "Hallo", mono: "HA", status: "trial", trialDaysLeft: 8, trialEnds: "2026-06-12", legalType: "AG", city: "Berlin, DE", country: "Germany", vat: "DE 290 117 884", plan: "Trial — Growth", planCode: "BP-TRIAL", seats: { used: 6, total: 10 }, monthly: 0, contact: "Marie Keller", contactEmail: "marie.keller@hello.io", renewal: "—", since: "2026-05-15", entitled: ["compliance-scanner"], trialing: ["certifai"], seed: 4242, findingCount: 9, people: [ ["Marie Keller", "marie.keller", ["IT_ADMIN"]], ["Felix Wagner", "felix.wagner", ["USER"]], ["Ada Novak", "ada.novak", ["USER", "CXO"]], ["Stefan Huber", "stefan.huber", ["FINANCE"], "invited"], ], }), globex: buildTenant({ id: "globex", domain: "globex.at", name: "Globex Energie GmbH", short: "Globex", mono: "GX", status: "frozen", frozenReason: "Payment failed — invoice INV-2026-0009 overdue 14 days.", legalType: "GmbH", city: "Wien, AT", country: "Austria", vat: "ATU 6634 2178", plan: "Scale", planCode: "BP-SCALE", seats: { used: 22, total: 25 }, monthly: 3100, contact: "Stefan Huber", contactEmail: "stefan.huber@globex.at", renewal: "overdue", since: "2022-09-01", entitled: ["compliance-scanner", "certifai"], trialing: [], seed: 909, findingCount: 11, people: [ ["Stefan Huber", "stefan.huber", ["IT_ADMIN"]], ["Nora Fischer", "nora.fischer", ["LEGAL"]], ["Lukas Berger", "lukas.berger", ["FINANCE"]], ["Sophie Maurer", "sophie.maurer", ["USER"]], ["Paul Schmid", "paul.schmid", ["USER"]], ], }), oldco: buildTenant({ id: "oldco", domain: "altmann.de", name: "Altmann & Co. KG", short: "Altmann", mono: "AL", status: "archived", archivedOn: "2026-03-30", retentionClosed: "2026-05-30", legalType: "KG", city: "Hamburg, DE", country: "Germany", vat: "DE 118 552 030", plan: "—", planCode: "—", seats: { used: 0, total: 0 }, monthly: 0, contact: "Klaus Altmann", contactEmail: "klaus.altmann@altmann.de", renewal: "—", since: "2021-02-10", entitled: [], trialing: [], seed: 70, findingCount: 4, people: [["Klaus Altmann", "klaus.altmann", ["IT_ADMIN"]]], }), sandbox: buildTenant({ id: "sandbox", domain: "sandbox.breakpilot.eu", name: "Breakpilot Sandbox", short: "Sandbox", mono: "SB", status: "demo", legalType: "—", city: "Shared tenant", country: "—", vat: "—", plan: "Demo", planCode: "BP-DEMO", seats: { used: 1, total: 99 }, monthly: 0, contact: "Breakpilot", contactEmail: "support@breakpilot.eu", renewal: "—", since: "—", entitled: ["compliance-scanner", "certifai"], trialing: [], seed: 5151, findingCount: 8, people: [ ["Sandbox Guest", "guest", ["USER", "IT_ADMIN"]], ["Demo Operator", "operator", ["CXO"]], ], }), }; // ---- sign-in fixtures (the 5 + demo) ----------------------------------- export type SignInFixture = { id: string; email: string; tenant: string; name: string; roles: OrgRole[]; showcase: string; }; export const FIXTURES: SignInFixture[] = [ { id: "admin-acme", email: "admin@acme", tenant: "acme", name: "Lena Brandt", roles: ["IT_ADMIN", "CXO"], showcase: "Full admin — every screen, all controls live.", }, { id: "user-acme", email: "user@acme", tenant: "acme", name: "Tomas Vogel", roles: ["USER"], showcase: "Restricted — only assigned products, no settings.", }, { id: "trial-hello", email: "trial@hello", tenant: "hello", name: "Marie Keller", roles: ["IT_ADMIN"], showcase: "Trial chrome — countdown banner + upgrade CTA.", }, { id: "frozen-globex", email: "frozen@globex", tenant: "globex", name: "Stefan Huber", roles: ["IT_ADMIN"], showcase: "Frozen — read-only banner, 402 on writes.", }, { id: "archived-oldco", email: "archived@oldco", tenant: "oldco", name: "Klaus Altmann", roles: ["IT_ADMIN"], showcase: "Archived — full-page lockout + export.", }, { id: "demo-sandbox", email: "guest@sandbox", tenant: "sandbox", name: "Sandbox Guest", roles: ["USER", "IT_ADMIN"], showcase: "Demo sandbox — watermark on every page.", }, ]; // ---- routes / RBAC ----------------------------------------------------- export type RouteKey = | "dashboard" | "products" | "workflows" | "org" | "team" | "billing" | "audit" | "sso"; export const ROUTES: Record = { dashboard: { label: "Overview", roles: ["IT_ADMIN", "CXO", "FINANCE", "LEGAL", "USER"] }, products: { label: "Products", roles: ["IT_ADMIN", "CXO", "USER"] }, workflows: { label: "Workflows", roles: ["IT_ADMIN"] }, org: { label: "Organization", roles: ["IT_ADMIN"] }, team: { label: "Team", roles: ["IT_ADMIN"] }, billing: { label: "Billing", roles: ["IT_ADMIN", "CXO", "FINANCE"] }, audit: { label: "Audit log", roles: ["IT_ADMIN", "LEGAL"] }, sso: { label: "SSO", roles: ["IT_ADMIN"] }, }; const DEFAULT_LANDING: Record = { IT_ADMIN: "dashboard", CXO: "dashboard", FINANCE: "billing", LEGAL: "audit", USER: "dashboard", }; export function landingFor(roles: OrgRole[]): RouteKey { for (const r of ["IT_ADMIN", "CXO", "FINANCE", "LEGAL", "USER"] as OrgRole[]) { if (roles.includes(r)) return DEFAULT_LANDING[r]; } return "dashboard"; } export function canAccess(roles: OrgRole[], route: RouteKey): boolean { const def = ROUTES[route]; if (!def) return false; return roles.some((r) => def.roles.includes(r)); } export function tenantById(id: string): TenantRecord | undefined { return TENANTS[id]; } export function tenantBySlug(slug: string): TenantRecord | undefined { return TENANTS[slug]; }