import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; const PAGE_READY_TIMEOUT = 60_000; function json(body, status = 200) { return { status, contentType: "application/json", body: JSON.stringify(body), }; } async function primeSuperuserSession(page) { const token = "superuser-system-status-token"; await seedAuthenticatedState(page, token); } async function bootSuperuser(page, { permissions = ["superuser", "user"], edgeGateways = false } = {}) { await mockApi(page, { authenticated: true, permissions, edgeGateways, }); await primeSuperuserSession(page); } async function installSystemStatusMock(page, snapshot) { await page.route(/\/superuser\/system\/status(?:\?.*)?$/, async (route) => { await route.fulfill( json({ success: true, data: snapshot, meta: {}, includes: {}, }) ); }); } function createGateway(id, overrides = {}) { return { id, label: `Gateway ${id}`, hostname: `gateway-${id}.truckwash.test`, department_id: 11, status: "ONLINE", discovery_status: "READY", last_heartbeat_at: "2026-04-08T09:00:00.000Z", active_operation: null, diagnostics: [], error_state: null, ...overrides, }; } function createGatewayFleetUsage(rows) { return { gateways: { total: rows.length, online: rows.filter((gateway) => gateway.status === "ONLINE").length, degraded: rows.filter((gateway) => gateway.status === "DEGRADED").length, offline: rows.filter((gateway) => gateway.status === "OFFLINE").length, }, }; } async function installGatewayFleetMock(page, { rows, departments }) { await page.route(/\/edge-gateways(?:\?.*)?$/, async (route) => { await route.fulfill( json({ success: true, data: rows, meta: { fleet_usage: createGatewayFleetUsage(rows), }, includes: {}, }) ); }); await page.route(/\/departments(?:\?.*)?$/, async (route) => { await route.fulfill( json({ success: true, data: departments, meta: {}, includes: {}, }) ); }); } const degradedSnapshot = { overall_status: "degraded", generated_at: "2026-04-08T08:45:00.000Z", refresh_after_seconds: 30, runtime: { cpu: { status: "ok", usage_percent: 34.5, source: "proc_stat", checked_at: "2026-04-08T08:45:00.000Z" }, memory: { status: "degraded", usage_percent: 91.3, used_bytes: 6442450944, total_bytes: 8589934592, source: "cgroup_v2", checked_at: "2026-04-08T08:45:00.000Z", }, disk: { status: "ok", usage_percent: 56.2, used_bytes: 53687091200, total_bytes: 107374182400, path: "/var/www/html", checked_at: "2026-04-08T08:45:00.000Z", }, }, dependencies: { database: { status: "ok", latency_ms: 4.2, database: "truckwash", server_version: "8.0.36", checked_at: "2026-04-08T08:45:00.000Z", replication: { status: "ok", min_percent: 100, average_percent: 100, replicas: [{ id: 2, label: "mysql-replica-1", replication_percent: 100 }], blockers: [], }, }, redis: { status: "down", latency_ms: 12.8, database: 0, checked_at: "2026-04-08T08:45:00.000Z", error: "Connection refused", replication: { status: "degraded", min_percent: 81.2, average_percent: 81.2, replicas: [{ id: 4, label: "redis-replica-1", replication_percent: 81.2 }], blockers: ["Redis replica link to primary is not up."], }, }, minio: { status: "degraded", latency_ms: 7.1, endpoint: "http://minio:9000", checked_at: "2026-04-08T08:45:00.000Z", buckets: [ { name: "attachments", status: "ok" }, { name: "backups", status: "down", error: "Missing bucket" }, ], replication: { status: "degraded", min_percent: 67.4, average_percent: 67.4, replicas: [{ id: 6, label: "minio-replica-1", replication_percent: 67.4 }], blockers: ["MinIO replica has not caught up."], }, }, }, modules: [ { key: "openai", enabled: true, configured: true, probe_supported: true, status: "ok", status_reason: "OpenAI API connectivity confirmed.", status_reason_key: "http_ok", status_reason_params: { label: "OpenAI API" }, checked_at: "2026-04-08T08:45:00.000Z", }, { key: "bird", enabled: true, configured: true, probe_supported: true, status: "degraded", status_reason: "Bird API returned HTTP 503.", status_reason_key: "http_status", status_reason_params: { label: "Bird API", httpStatus: 503 }, checked_at: "2026-04-08T08:45:00.000Z", }, ], sessions: { active_window_minutes: 15, active_users: 3, active_sessions: 4, recent_sessions: [ { session_kind: "user", principal_id: 101, display_name: "Acme Logistics", context_label: "Customer 1001", context_label_key: "customer_number", context_label_params: { number: 1001 }, customer_number_context: 1001, device_type: "desktop", user_agent: "Mozilla/5.0", last_route: "/superuser/vehicles", first_seen_at: "2026-04-08T08:20:00.000Z", last_seen_at: "2026-04-08T08:44:00.000Z", active: true, }, ], }, warnings: ["Redis is unavailable; module probe caching is bypassed."], warning_entries: [ { key: "redis_cache_bypass", params: {}, message: "Redis is unavailable; module probe caching is bypassed.", }, ], }; const databaseCompatibilitySnapshot = { ...degradedSnapshot, overall_status: "ok", runtime: { ...degradedSnapshot.runtime, memory: { status: "ok", usage_percent: 48.2, used_bytes: 4294967296, total_bytes: 8589934592, source: "cgroup_v2", checked_at: "2026-04-08T08:45:00.000Z", }, }, dependencies: { ...degradedSnapshot.dependencies, redis: { status: "ok", latency_ms: 1.9, database: 0, checked_at: "2026-04-08T08:45:00.000Z", }, minio: { status: "ok", latency_ms: 5.4, endpoint: "http://minio:9000", checked_at: "2026-04-08T08:45:00.000Z", buckets: [ { name: "attachments", status: "ok" }, { name: "backups", status: "ok" }, ], }, }, warnings: [], }; const longReasonSnapshot = { ...degradedSnapshot, modules: [ { key: "shelly", enabled: true, configured: true, probe_supported: true, status: "configured", status_reason: "Shelly cloud credentials are configured, but no known device id is available for a safe read-only probe.", status_reason_key: "shelly_no_device_id", status_reason_params: {}, checked_at: "2026-04-08T08:45:00.000Z", }, ], }; const longTitleSnapshot = { ...degradedSnapshot, modules: [ { key: "licenseplaterecognizer", enabled: true, configured: true, probe_supported: true, status: "degraded", status_reason: "License Plate Recognizer quota usage is near the limit.", status_reason_key: "licenseplaterecognizer_quota_near_limit", status_reason_params: { used: "2250", quota: "2500", remaining: "250", percent: "90.0", }, checked_at: "2026-04-08T08:45:00.000Z", usage: { provider: "licenseplaterecognizer", calls_used: 2250, quota_calls: 2500, calls_remaining: 250, usage_percent: 90, version: "1.54.0", }, }, { key: "virkdata", enabled: true, configured: true, probe_supported: false, status: "configured", status_reason: "Configuration is present, but no safe read-only probe is available.", status_reason_key: "safe_probe_unavailable", status_reason_params: {}, checked_at: "2026-04-08T08:45:00.000Z", }, ], }; test.describe("Superuser system status smoke", () => { test.describe.configure({ timeout: 120_000 }); test("@smoke @pr superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({ page, }) => { await bootSuperuser(page); await installSystemStatusMock(page, degradedSnapshot); await page.goto("/superuser"); await expect(page).toHaveURL(/\/superuser$/); await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("system-status-dashboard-tabs")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("status-card-database")).toContainText("truckwash", { timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering"); await expect(page.getByTestId("status-card-minio-replication")).toContainText("67.4% replikering"); await page.getByTestId("system-status-tab-sessions").click(); await expect(page.locator("body")).toContainText("Acme Logistics"); await expect(page.locator("body")).toContainText("Kunde"); await expect(page.locator("body")).toContainText("Bruger"); await expect(page.locator("body")).toContainText("Computer"); await expect(page.locator("body")).toContainText("/superuser/vehicles"); await page.getByTestId("system-status-tab-modules").click(); await expect(page.locator("body")).toContainText("Forbindelse til OpenAI API bekræftet."); await expect(page.locator("body")).toContainText("Redis er utilgængelig; cache til modulprober omgås."); await expect(page.locator("body")).not.toContainText("OpenAI API connectivity confirmed."); await expect(page.locator("body")).not.toContainText("Redis is unavailable; module probe caching is bypassed."); await expect(page.locator("body")).not.toContainText("Customer 1001"); await expect(page.locator('a[href="/superuser/configuration/openai"]').last()).toBeVisible(); }); test("@smoke superuser database compatibility page renders shared database status snapshot", async ({ page }) => { await bootSuperuser(page); await installSystemStatusMock(page, databaseCompatibilitySnapshot); await page.goto("/superuser/system/database"); await expect(page).toHaveURL(/\/superuser\/system\/database$/); await expect(page.getByTestId("database-status-card")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("database-status-card")).toContainText("truckwash"); await expect(page.getByTestId("database-status-card")).toContainText("8.0.36"); }); test("@smoke @pr superuser replication route redirects to the system dashboard", async ({ page }) => { await bootSuperuser(page); await installSystemStatusMock(page, degradedSnapshot); await page.goto("/superuser/system/replication"); await expect(page).toHaveURL(/\/superuser$/); await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering"); await expect(page.getByTestId("status-card-minio-replication")).toContainText("67.4% replikering"); await expect(page.locator('a[href="/superuser/system/replication"]')).toBeVisible(); await expect(page.getByTestId("replication-management-page")).toHaveCount(0); }); test("@smoke superuser replication dashboard link preserves infrastructure snapshot", async ({ page }) => { await bootSuperuser(page); await installSystemStatusMock(page, degradedSnapshot); await page.goto("/superuser"); await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await page.locator('a[href="/superuser/system/replication"]').click(); await expect(page).toHaveURL(/\/superuser$/); await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("system-status-panel-infrastructure")).toBeVisible(); await expect(page.getByTestId("status-card-database")).toContainText("truckwash"); await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering"); }); test("@smoke superuser module cards keep long Danish status reasons below the header row", async ({ page }) => { await bootSuperuser(page); await installSystemStatusMock(page, longReasonSnapshot); await page.goto("/superuser"); await page.getByTestId("system-status-tab-modules").click(); const card = page.getByTestId("module-card-shelly"); const title = card.locator(".module-card__title"); const pill = card.locator(".status-pill"); const reason = page.getByTestId("module-reason-shelly"); await expect(card).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(reason).toContainText("der findes ikke et kendt device-id"); const [titleBox, pillBox, reasonBox] = await Promise.all([ title.boundingBox(), pill.boundingBox(), reason.boundingBox(), ]); expect(titleBox).not.toBeNull(); expect(pillBox).not.toBeNull(); expect(reasonBox).not.toBeNull(); const headerBottom = Math.max(titleBox.y + titleBox.height, pillBox.y + pillBox.height); expect(reasonBox.y).toBeGreaterThanOrEqual(headerBottom - 1); }); test("@smoke superuser module headers keep long titles and status pills inside their own cards", async ({ page }) => { await bootSuperuser(page); await installSystemStatusMock(page, longTitleSnapshot); await page.goto("/superuser"); await page.getByTestId("system-status-tab-modules").click(); const card = page.getByTestId("module-card-licenseplaterecognizer"); const title = card.locator(".module-card__title"); const pill = card.locator(".status-pill"); await expect(card).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(title).toContainText("License Plate Recognizer"); await expect(page.getByTestId("module-reason-licenseplaterecognizer")).toContainText( "License Plate Recognizer-kvoten er næsten opbrugt" ); const usage = page.getByTestId("module-usage-licenseplaterecognizer"); await expect(usage).toContainText("Kvoteudnyttelse"); await expect(usage).toContainText("90.0%"); await expect(usage).toContainText("Brugte kald"); await expect(usage).toContainText("2.250"); await expect(usage).toContainText("Kald tilbage"); await expect(usage).toContainText("250"); await expect(usage).toContainText("Version: 1.54.0"); const [cardBox, titleBox, pillBox] = await Promise.all([ card.boundingBox(), title.boundingBox(), pill.boundingBox(), ]); expect(cardBox).not.toBeNull(); expect(titleBox).not.toBeNull(); expect(pillBox).not.toBeNull(); expect(titleBox.x).toBeGreaterThanOrEqual(cardBox.x - 1); expect(titleBox.x + titleBox.width).toBeLessThanOrEqual(cardBox.x + cardBox.width + 1); expect(pillBox.x).toBeGreaterThanOrEqual(cardBox.x - 1); expect(pillBox.x + pillBox.width).toBeLessThanOrEqual(cardBox.x + cardBox.width + 1); const overlapWidth = Math.max( 0, Math.min(titleBox.x + titleBox.width, pillBox.x + pillBox.width) - Math.max(titleBox.x, pillBox.x) ); const overlapHeight = Math.max( 0, Math.min(titleBox.y + titleBox.height, pillBox.y + pillBox.height) - Math.max(titleBox.y, pillBox.y) ); expect(overlapWidth * overlapHeight).toBeLessThanOrEqual(1); }); test("@smoke superuser status dashboard renders gateway health cards and gateway links", async ({ page }) => { const gatewayRows = [ createGateway(301, { label: "Gateway Atlas", department_id: 11, status: "OFFLINE", discovery_status: "FAILED", last_heartbeat_at: "2026-04-08T08:00:00.000Z", error_state: { message: "MQTT disconnected" }, }), createGateway(302, { label: "Gateway Bering", department_id: 12, status: "OFFLINE", discovery_status: "FAILED", last_heartbeat_at: "2026-04-08T08:05:00.000Z", }), createGateway(303, { label: "Gateway Carls", department_id: 11, status: "OFFLINE", discovery_status: "STALE", last_heartbeat_at: "2026-04-08T08:10:00.000Z", }), createGateway(304, { label: "Gateway Delta", department_id: 12, status: "DEGRADED", discovery_status: "READY", last_heartbeat_at: "2026-04-08T08:15:00.000Z", active_operation: { summary: { label: "Udruller opdatering" }, }, }), createGateway(305, { label: "Gateway Echo", department_id: 11, status: "DEGRADED", discovery_status: "STALE", last_heartbeat_at: "2026-04-08T08:20:00.000Z", diagnostics: [{ message: "Discovery har ikke rapporteret i 30 minutter." }], }), createGateway(306, { label: "Gateway Fjord", department_id: 12, status: "DEGRADED", discovery_status: "PENDING", last_heartbeat_at: "2026-04-08T08:25:00.000Z", }), createGateway(307, { label: "", hostname: "gw-307.truckwash.test", department_id: 11, status: "ONLINE", discovery_status: "READY", last_heartbeat_at: "2026-04-08T08:30:00.000Z", }), createGateway(308, { label: "Gateway Haven", department_id: 12, status: "ONLINE", discovery_status: "READY", last_heartbeat_at: "2026-04-08T08:35:00.000Z", }), createGateway(309, { label: "Gateway Ist", department_id: 11, status: "ONLINE", discovery_status: "READY", last_heartbeat_at: "2026-04-08T08:40:00.000Z", }), createGateway(310, { label: "Gateway Jutland", department_id: 12, status: "ONLINE", discovery_status: "READY", last_heartbeat_at: "2026-04-08T08:45:00.000Z", }), ]; await bootSuperuser(page, { permissions: ["superuser", "user", "modules_shelly_config"], edgeGateways: false, }); await installSystemStatusMock(page, degradedSnapshot); await installGatewayFleetMock(page, { rows: gatewayRows, departments: [ { id: 11, name: "Odense" }, { id: 12, name: "Aarhus" }, ], }); await page.goto("/superuser"); await page.getByTestId("system-status-tab-gateways").click(); await expect(page.getByTestId("system-status-gateways")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); await expect(page.getByTestId("gateway-summary-card-total")).toContainText("10"); await expect(page.getByTestId("gateway-summary-card-online")).toContainText("4"); await expect(page.getByTestId("gateway-summary-card-degraded")).toContainText("3"); await expect(page.getByTestId("gateway-summary-card-offline")).toContainText("3"); await expect(page.locator('[data-testid="system-status-gateways"] .section-link')).toBeVisible(); const gatewayCards = page.locator('[data-testid^="gateway-card-"]'); await expect(gatewayCards).toHaveCount(8); await expect(gatewayCards.nth(0)).toContainText("Gateway Atlas"); await expect(gatewayCards.nth(0)).toContainText("Odense"); await expect(gatewayCards.nth(0)).toContainText("MQTT disconnected"); await expect(gatewayCards.nth(3)).toContainText("Udruller opdatering"); await expect(gatewayCards.nth(4)).toContainText("Discovery har ikke rapporteret i 30 minutter."); await expect(gatewayCards.nth(6)).toContainText("gw-307.truckwash.test"); await expect(page.locator('a[href="/superuser/selfserve/edge-agents/301/overview"]')).toBeVisible(); await expect(page.locator('[data-testid="gateway-card-309"]')).toHaveCount(0); }); });