From 6556e73efcd8ae13b51b56288416c88688e67634 Mon Sep 17 00:00:00 2001 From: Charlie Team <97194984+ToolboxAid@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:16:56 -0400 Subject: [PATCH] PR_26175_CHARLIE_007-system-health-environment-identity --- admin/system-health.html | 43 ++++-- assets/theme-v2/js/admin-system-health.js | 44 +++++- ..._007-system-health-environment-identity.md | 31 ++++ .../reports/coverage_changed_js_guardrail.txt | 11 +- .../reports/playwright_v8_coverage_report.txt | 38 ++--- src/dev-runtime/server/local-api-router.mjs | 145 ++++++++++++++++++ .../AdminHealthOperations.test.mjs | 16 ++ .../tools/AdminHealthOperationsPage.spec.mjs | 31 +++- 8 files changed, 315 insertions(+), 44 deletions(-) create mode 100644 docs_build/dev/reports/PR_26175_CHARLIE_007-system-health-environment-identity.md diff --git a/admin/system-health.html b/admin/system-health.html index 65ff3c12e..9aebcf0bd 100644 --- a/admin/system-health.html +++ b/admin/system-health.html @@ -38,7 +38,8 @@

Admin

Health Sections
-

Environment Summary

+

Environment Identity

+

Environment Map

Local API Startup

Database Health

Storage Health

@@ -57,21 +58,43 @@

Admin

System Health Tables

- - +
Environment Summary
+ - - - + + - - - - + + + + + + + + +
Environment Identity
EnvironmentPurposePromotion GateFieldCurrent Deployment Status
DEVLocal implementation and diagnosticsDeveloper validationPASS
ISTIntegrated system testingCross-feature validationPASS
UATUser acceptance testingOwner reviewPASS
PRDProduction runtimeEOD approvalPASS
Environment nameLoadingPENDING
Hosting modelLoadingPENDING
Site URLLoadingPENDING
API URLLoadingPENDING
Database modelLoadingPENDING
Storage folderLoadingPENDING
Last health checkLoadingPENDING
+
+
+ + + + + + + + + + + + + + + +
Environment Map
EnvironmentHosting modelDatabase modelStorage folder
LocalVS Code + Local APILocal Docker PostgreSQL/local
DEVLocal DockerLocal Docker PostgreSQL/dev
ISTLocal DockerLocal Docker PostgreSQL/ist
UATCloudflareSupabase PostgreSQL/uat
PRDCloudflareSupabase PostgreSQL/prd
diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js index 72ddd7ea8..6ba5c0f2a 100644 --- a/assets/theme-v2/js/admin-system-health.js +++ b/assets/theme-v2/js/admin-system-health.js @@ -22,6 +22,14 @@ function asText(value, fallback = "not available") { class AdminSystemHealthController { constructor(root) { this.root = root; + this.environmentValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-environment-value]")).map((node) => [ + node.dataset.adminSystemHealthEnvironmentValue, + node, + ])); + this.environmentStatuses = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-environment-status]")).map((node) => [ + node.dataset.adminSystemHealthEnvironmentStatus, + node, + ])); this.dbValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-db-value]")).map((node) => [ node.dataset.adminSystemHealthDbValue, node, @@ -43,7 +51,7 @@ class AdminSystemHealthController { } init() { - if ((!this.dbValues.size && !this.storageValues.size) || document.querySelector("[data-session-access-blocked='admin']") || window.GameFoundrySessionGuard?.blocked === true) { + if ((!this.environmentValues.size && !this.dbValues.size && !this.storageValues.size) || document.querySelector("[data-session-access-blocked='admin']") || window.GameFoundrySessionGuard?.blocked === true) { return; } this.load(); @@ -56,6 +64,18 @@ class AdminSystemHealthController { } } + setEnvironmentValue(key, value, fallback) { + const node = this.environmentValues.get(key); + if (node) { + node.textContent = asText(value, fallback); + } + } + + setEnvironmentStatus(key, status, reason = "") { + const node = this.environmentStatuses.get(key); + this.setStatusNode(node, status, reason); + } + setStorageValue(key, value, fallback) { const node = this.storageValues.get(key); if (node) { @@ -78,6 +98,9 @@ class AdminSystemHealthController { } renderPending(reason) { + ["name", "hostingModel", "siteUrl", "apiUrl", "databaseModel", "storageFolder", "lastHealthCheck"].forEach((key) => { + this.setEnvironmentStatus(key, "PENDING", reason); + }); ["host", "database", "migration", "connection"].forEach((key) => { this.setStatus(key, "PENDING", reason); }); @@ -85,6 +108,24 @@ class AdminSystemHealthController { this.renderStoragePending(reason); } + renderEnvironmentIdentity(environmentIdentity = {}) { + const reason = environmentIdentity.message || "Current deployment environment identity returned by the safe Admin System Health API."; + this.setEnvironmentValue("name", environmentIdentity.name, "Unknown"); + this.setEnvironmentStatus("name", environmentIdentity.status, reason); + this.setEnvironmentValue("hostingModel", environmentIdentity.hostingModel, "not configured"); + this.setEnvironmentStatus("hostingModel", environmentIdentity.hostingModel ? "PASS" : "WARN", reason); + this.setEnvironmentValue("siteUrl", environmentIdentity.siteUrl, "not configured"); + this.setEnvironmentStatus("siteUrl", environmentIdentity.siteUrlStatus, reason); + this.setEnvironmentValue("apiUrl", environmentIdentity.apiUrl, "not configured"); + this.setEnvironmentStatus("apiUrl", environmentIdentity.apiUrlStatus, reason); + this.setEnvironmentValue("databaseModel", environmentIdentity.databaseModel, "not configured"); + this.setEnvironmentStatus("databaseModel", environmentIdentity.databaseModel ? "PASS" : "WARN", reason); + this.setEnvironmentValue("storageFolder", environmentIdentity.storageFolder, "not configured"); + this.setEnvironmentStatus("storageFolder", environmentIdentity.storageFolderStatus, reason); + this.setEnvironmentValue("lastHealthCheck", environmentIdentity.lastHealthCheck, "not available"); + this.setEnvironmentStatus("lastHealthCheck", environmentIdentity.lastHealthCheck ? "PASS" : "WARN", reason); + } + renderStoragePending(reason) { ["bucket", "list", "read", "write", "delete"].forEach((key) => { this.setStorageStatus(key, "PENDING", reason); @@ -250,6 +291,7 @@ class AdminSystemHealthController { this.renderPending("Safe Admin System Health API refused to render because the response exposed secret controls."); return; } + this.renderEnvironmentIdentity(data?.environmentIdentity || {}); this.renderPostgresStatus(data?.databaseStatus || {}); this.renderStartupDiagnostics(data?.localApiStartup || {}); this.renderStorageStatus(data?.storageStatus || {}); diff --git a/docs_build/dev/reports/PR_26175_CHARLIE_007-system-health-environment-identity.md b/docs_build/dev/reports/PR_26175_CHARLIE_007-system-health-environment-identity.md new file mode 100644 index 000000000..50d7fc884 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_CHARLIE_007-system-health-environment-identity.md @@ -0,0 +1,31 @@ +# PR_26175_CHARLIE_007 System Health Environment Identity + +## Scope + +Team: Charlie + +Purpose: Add current deployment environment identity to Admin System Health and keep the all-environment map as static reference only. + +## Changes + +- Added a server-owned current environment identity model for Local, DEV, IST, UAT, and PRD. +- Added current deployment fields for environment name, hosting model, site URL, API URL, database model, storage folder, and last health check. +- Replaced the active multi-environment summary table with a current deployment identity table. +- Added a static Environment Map for Local, DEV, IST, UAT, and PRD without health status fields. +- Updated focused API and Playwright tests. + +## Architecture Constraint + +PASS. System Health displays the current deployment environment only. The Environment Map is static reference content and does not actively health-check other environments. + +## Validation + +- PASS: `node --check src/dev-runtime/server/local-api-router.mjs` +- PASS: `node --check assets/theme-v2/js/admin-system-health.js` +- PASS: `git diff --check` +- PASS: `node --test tests/dev-runtime/AdminHealthOperations.test.mjs` +- PASS: `npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line` + +## Notes + +GitHub CLI is unavailable in this workspace. Branch push uses local `git`; draft PR creation uses the GitHub connector when available, otherwise the manual compare URL is reported. diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt index 83a4ad849..833366a34 100644 --- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt +++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt @@ -6,13 +6,8 @@ Missing changed runtime JS files are WARN, not FAIL. Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. Changed runtime JS files considered: -(0%) src/dev-runtime/messages/messages-postgres-service.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/text-to-speech/text2speech.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/text-to-speech/tts-profile-store.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 288/288; executed functions 33/37 Guardrail warnings: -(0%) src/dev-runtime/messages/messages-postgres-service.mjs - WARNING: changed runtime JS file missing from coverage; advisory only -(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file missing from coverage; advisory only -(0%) toolbox/text-to-speech/text2speech.js - WARNING: changed runtime JS file missing from coverage; advisory only -(0%) toolbox/text-to-speech/tts-profile-store.js - WARNING: changed runtime JS file missing from coverage; advisory only +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt index 5a0ac58e8..c5c648f78 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -12,31 +12,31 @@ Note: entry percentages use function coverage when available, otherwise line cov Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran. Exercised tool entry points detected: -(0%) Toolbox Index - not exercised by this Playwright run +(46%) Toolbox Index - exercised 1 runtime JS files (0%) Tool Template V2 - not exercised by this Playwright run -(0%) Theme V2 Shared JS - not exercised by this Playwright run +(78%) Theme V2 Shared JS - exercised 4 runtime JS files Changed runtime JS files covered: -(0%) src/dev-runtime/messages/messages-postgres-service.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/text-to-speech/text2speech.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -(0%) toolbox/text-to-speech/tts-profile-store.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 288/288; executed functions 33/37 Files with executed line/function counts where available: -(100%) none - no covered runtime files +(36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14 +(46%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 12/26 +(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14 +(65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26 +(74%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1001/1001; executed functions 69/93 +(80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 +(83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 288/288; executed functions 33/37 +(91%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 10/11 +(100%) src/api/admin-system-health-api-client.js - executed lines 19/19; executed functions 3/3 Uncovered or low-coverage changed JS files: -(0%) src/dev-runtime/messages/messages-postgres-service.mjs - WARNING: uncovered changed runtime JS file; advisory only -(0%) toolbox/messages/messages.js - WARNING: uncovered changed runtime JS file; advisory only -(0%) toolbox/text-to-speech/text2speech.js - WARNING: uncovered changed runtime JS file; advisory only -(0%) toolbox/text-to-speech/tts-profile-store.js - WARNING: uncovered changed runtime JS file; advisory only +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only Changed JS files considered: -(0%) src/dev-runtime/messages/messages-postgres-service.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/dev-runtime/DbSeedIntegrity.test.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/playwright/tools/MessagesTool.spec.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/tools/MessagesPlaybackSource.test.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/tools/Text2SpeechShell.test.mjs - changed JS file not collected as browser runtime coverage -(0%) toolbox/messages/messages.js - changed JS file not collected as browser runtime coverage -(0%) toolbox/text-to-speech/text2speech.js - changed JS file not collected as browser runtime coverage -(0%) toolbox/text-to-speech/tts-profile-store.js - changed JS file not collected as browser runtime coverage +(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage +(0%) tests/dev-runtime/AdminHealthOperations.test.mjs - changed JS file not collected as browser runtime coverage +(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage +(89%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs index 1c6e9c126..b632d31aa 100644 --- a/src/dev-runtime/server/local-api-router.mjs +++ b/src/dev-runtime/server/local-api-router.mjs @@ -347,6 +347,43 @@ const STORAGE_CONNECTIVITY_ACTIONS = Object.freeze([ ]); const STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT = "Game Foundry Studio storage connectivity test object.\n"; const STORAGE_CONNECTIVITY_TEST_OBJECT_RELATIVE_PATH = "connectivity/storage-connectivity-test.txt"; +const SYSTEM_HEALTH_ENVIRONMENT_MODELS = Object.freeze([ + Object.freeze({ + databaseModel: "Local Docker PostgreSQL", + hostingModel: "VS Code + Local API", + name: "Local", + storageFolder: "/local", + }), + Object.freeze({ + databaseModel: "Local Docker PostgreSQL", + hostingModel: "Local Docker", + name: "DEV", + storageFolder: "/dev", + }), + Object.freeze({ + databaseModel: "Local Docker PostgreSQL", + hostingModel: "Local Docker", + name: "IST", + storageFolder: "/ist", + }), + Object.freeze({ + databaseModel: "Supabase PostgreSQL", + hostingModel: "Cloudflare", + name: "UAT", + storageFolder: "/uat", + }), + Object.freeze({ + databaseModel: "Supabase PostgreSQL", + hostingModel: "Cloudflare", + name: "PRD", + storageFolder: "/prd", + }), +]); +const SYSTEM_HEALTH_ENVIRONMENT_BY_NAME = new Map(SYSTEM_HEALTH_ENVIRONMENT_MODELS.map((model) => [model.name, model])); +const SYSTEM_HEALTH_ENVIRONMENT_BY_FOLDER = new Map([ + ...SYSTEM_HEALTH_ENVIRONMENT_MODELS.map((model) => [model.storageFolder, model.name]), + ["/prod", "PRD"], +]); const ADMIN_OPERATION_GROUPS = Object.freeze([ Object.freeze({ id: "project-packaging", @@ -851,6 +888,97 @@ function systemHealthLocalApiStartupDiagnostics(env = process.env) { }; } +function systemHealthEnvironmentMap() { + return SYSTEM_HEALTH_ENVIRONMENT_MODELS.map((model) => ({ ...model })); +} + +function topLevelStorageFolder(value) { + const segment = String(value || "") + .trim() + .replace(/\\/g, "/") + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean)[0] || ""; + return segment ? `/${segment.toLowerCase()}` : ""; +} + +function normalizeEnvironmentName(value) { + const upperValue = String(value || "").trim().toUpperCase(); + if (!upperValue) { + return ""; + } + if (["LOCAL", "DEV", "IST", "UAT", "PRD"].includes(upperValue)) { + return upperValue === "LOCAL" ? "Local" : upperValue; + } + if (upperValue === "PROD" || upperValue === "PRODUCTION") { + return "PRD"; + } + if (upperValue.includes("LOCAL")) { + return "Local"; + } + return ""; +} + +function inferSystemHealthEnvironmentName(env = process.env) { + const configuredStorageFolder = topLevelStorageFolder(env.GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX || dotEnvValue(STORAGE_PROJECTS_PREFIX_ENV_KEY).value); + const folderMatch = SYSTEM_HEALTH_ENVIRONMENT_BY_FOLDER.get(configuredStorageFolder); + if (folderMatch) { + return { + configuredStorageFolder, + name: folderMatch, + source: STORAGE_PROJECTS_PREFIX_ENV_KEY, + status: "PASS", + }; + } + + const labelMatch = normalizeEnvironmentName(env.GAMEFOUNDRY_ENVIRONMENT_LABEL); + if (labelMatch) { + return { + configuredStorageFolder, + name: labelMatch, + source: "GAMEFOUNDRY_ENVIRONMENT_LABEL", + status: "WARN", + }; + } + + return { + configuredStorageFolder, + name: "Local", + source: "local default", + status: configuredStorageFolder ? "WARN" : "PASS", + }; +} + +function systemHealthEnvironmentIdentity(env = process.env, lastHealthCheck = new Date().toISOString()) { + const inferred = inferSystemHealthEnvironmentName(env); + const model = SYSTEM_HEALTH_ENVIRONMENT_BY_NAME.get(inferred.name) || SYSTEM_HEALTH_ENVIRONMENT_BY_NAME.get("Local"); + const siteUrl = localApiStartupUrlDisplay(String(env.GAMEFOUNDRY_SITE_URL || "").trim()); + const bindTarget = localApiStartupBindTarget(env); + const configuredApiUrl = String(env.GAMEFOUNDRY_API_URL || "").trim(); + const apiUrl = localApiStartupUrlDisplay(configuredApiUrl || `http://${bindTarget.value}/api`); + const storageFolderMatches = !inferred.configuredStorageFolder + || inferred.configuredStorageFolder === model.storageFolder + || (model.name === "PRD" && inferred.configuredStorageFolder === "/prod"); + + return { + apiUrl, + apiUrlStatus: apiUrl === "not configured" || apiUrl === "invalid URL" ? "WARN" : "PASS", + databaseModel: model.databaseModel, + hostingModel: model.hostingModel, + lastHealthCheck, + message: storageFolderMatches + ? `Current deployment identity resolved as ${model.name} from ${inferred.source}.` + : `Current deployment identity defaulted to ${model.name}; ${STORAGE_PROJECTS_PREFIX_ENV_KEY} did not match an approved environment folder.`, + name: model.name, + siteUrl, + siteUrlStatus: siteUrl === "not configured" || siteUrl === "invalid URL" ? "WARN" : "PASS", + source: inferred.source, + status: storageFolderMatches ? inferred.status : "WARN", + storageFolder: model.storageFolder, + storageFolderStatus: storageFolderMatches ? inferred.status : "WARN", + }; +} + function systemHealthSummary(rows) { const counts = systemHealthCounts(rows); const total = counts.PASS + counts.WARN + counts.FAIL; @@ -3625,6 +3753,9 @@ LIMIT 1; async adminSystemHealthStatus() { const session = await this.requireAdminSession(); const authStatus = this.authStatus(); + const checkedAt = new Date().toISOString(); + const environmentIdentity = systemHealthEnvironmentIdentity(process.env, checkedAt); + const environmentMap = systemHealthEnvironmentMap(); const databaseStatus = await this.ownerDatabaseStatus(); const storageStatus = this.ownerStorageStatus(); const environmentStatus = storageProjectsPrefixStatus(); @@ -3647,6 +3778,11 @@ LIMIT 1; ? "Current session is authenticated with Admin access." : "Sign in with an Admin account to view system health.", }, + { + area: "Environment identity", + status: environmentIdentity.status, + summary: `Current deployment is ${environmentIdentity.name}; System Health does not actively check peer environments.`, + }, { area: "Product Data / Local DB", status: databaseStatus.configured === true ? databaseStatus.status || "PASS" : "WARN", @@ -3710,6 +3846,13 @@ LIMIT 1; details: [ { area: "Account/session readiness", field: "Session", status: session.authenticated ? "PASS" : "FAIL", value: session.authenticated ? "authenticated" : "not authenticated" }, { area: "Account/session readiness", field: "Role", status: session.isAdmin ? "PASS" : "FAIL", value: session.isAdmin ? "Admin" : "Admin required" }, + { area: "Environment identity", field: "Environment name", status: environmentIdentity.status, value: environmentIdentity.name }, + { area: "Environment identity", field: "Hosting model", status: "PASS", value: environmentIdentity.hostingModel }, + { area: "Environment identity", field: "Site URL", status: environmentIdentity.siteUrlStatus, value: environmentIdentity.siteUrl }, + { area: "Environment identity", field: "API URL", status: environmentIdentity.apiUrlStatus, value: environmentIdentity.apiUrl }, + { area: "Environment identity", field: "Database model", status: "PASS", value: environmentIdentity.databaseModel }, + { area: "Environment identity", field: "Storage folder", status: environmentIdentity.storageFolderStatus, value: environmentIdentity.storageFolder }, + { area: "Environment identity", field: "Last health check", status: "PASS", value: environmentIdentity.lastHealthCheck }, { area: "Product Data / Local DB", field: "Database host", status: databaseStatus.hostStatus || "WARN", value: databaseStatus.host || "not configured" }, { area: "Product Data / Local DB", field: "Database name", status: databaseStatus.databaseNameStatus || "WARN", value: databaseStatus.databaseName || "not configured" }, { area: "Project Asset Storage / R2", field: "Endpoint", status: storageStatus.endpointStatus || "WARN", value: storageStatus.endpoint || "not configured" }, @@ -3745,6 +3888,8 @@ LIMIT 1; limits: limitRows, localApiStartup, message: "Admin System Health loaded safe status only.", + environmentIdentity, + environmentMap, operationsHealth, overview, pressureLabels: SYSTEM_HEALTH_LIMIT_PRESSURE_LABELS, diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs index 69f2b146d..e3669cced 100644 --- a/tests/dev-runtime/AdminHealthOperations.test.mjs +++ b/tests/dev-runtime/AdminHealthOperations.test.mjs @@ -109,7 +109,9 @@ async function apiJson(baseUrl, pathName, request = {}) { test("Admin can view operational health while Creator sessions are blocked", async () => { await withEnv({ GAMEFOUNDRY_API_URL: "http://api-user:api-secret@127.0.0.1:5501/api", + GAMEFOUNDRY_ENVIRONMENT_LABEL: "Local", GAMEFOUNDRY_SITE_URL: "http://site-user:site-secret@127.0.0.1:5500", + GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX: "/local/projects/", GAMEFOUNDRY_SUPABASE_ANON_KEY: undefined, GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: undefined, GAMEFOUNDRY_SUPABASE_URL: undefined, @@ -129,6 +131,20 @@ test("Admin can view operational health while Creator sessions are blocked", asy method: "POST", }); const health = await apiJson(server.baseUrl, "/api/admin/system-health/status"); + assert.equal(health.environmentIdentity.name, "Local"); + assert.equal(health.environmentIdentity.hostingModel, "VS Code + Local API"); + assert.equal(health.environmentIdentity.databaseModel, "Local Docker PostgreSQL"); + assert.equal(health.environmentIdentity.storageFolder, "/local"); + assert.equal(health.environmentIdentity.siteUrl.includes("site-user"), false); + assert.equal(health.environmentIdentity.apiUrl.includes("api-user"), false); + assert.deepEqual( + health.environmentMap.map((row) => row.name), + ["Local", "DEV", "IST", "UAT", "PRD"], + ); + assert.equal( + health.environmentMap.some((row) => Object.prototype.hasOwnProperty.call(row, "status")), + false, + ); assert.equal(health.localApiStartup.secretEditingAllowed, false); assert.equal(health.localApiStartup.secretsExposed, false); assert.equal(Array.isArray(health.localApiStartup.rows), true); diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs index f3c860186..10120dbba 100644 --- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs @@ -12,6 +12,11 @@ const TEST_RUNTIME_ENV_VALUES = Object.freeze({ GAMEFOUNDRY_ADMIN_HEALTH_TOKEN: "admin-health-token-secret", }); +const TEST_PUBLIC_SYSTEM_HEALTH_ENV_VALUES = Object.freeze({ + GAMEFOUNDRY_ENVIRONMENT_LABEL: "DEV", + GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX: "/dev/projects/", +}); + async function setSessionUser(server, userKey) { await fetch(`${server.baseUrl}/api/session/mode`, { body: JSON.stringify({ modeId: "local-db" }), @@ -27,7 +32,10 @@ async function setSessionUser(server, userKey) { async function openAdminSystemHealthPage(page, userKey) { const previousRuntimeEnvValues = {}; - Object.entries(TEST_RUNTIME_ENV_VALUES).forEach(([key, value]) => { + Object.entries({ + ...TEST_RUNTIME_ENV_VALUES, + ...TEST_PUBLIC_SYSTEM_HEALTH_ENV_VALUES, + }).forEach(([key, value]) => { previousRuntimeEnvValues[key] = process.env[key]; process.env[key] = value; }); @@ -125,10 +133,18 @@ test("Admin System Health renders Postgres diagnostics through the safe status A try { await expect(page).toHaveTitle(/System Health - Game Foundry Studio LLC/); await expect(page.getByRole("heading", { exact: true, name: "System Health" })).toBeVisible(); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("DEV"); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("IST"); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("UAT"); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("PRD"); + await expect(page.getByRole("table", { name: "Environment identity" })).toContainText("Environment name"); + await expect(page.locator("[data-admin-system-health-environment-value='name']")).toHaveText("DEV"); + await expect(page.locator("[data-admin-system-health-environment-value='hostingModel']")).toHaveText("Local Docker"); + await expect(page.locator("[data-admin-system-health-environment-value='databaseModel']")).toHaveText("Local Docker PostgreSQL"); + await expect(page.locator("[data-admin-system-health-environment-value='storageFolder']")).toHaveText("/dev"); + await expect(page.getByRole("table", { name: "Environment identity" })).not.toContainText("env-secret"); + await expect(page.getByRole("table", { name: "Environment map" })).toContainText("Local"); + await expect(page.getByRole("table", { name: "Environment map" })).toContainText("DEV"); + await expect(page.getByRole("table", { name: "Environment map" })).toContainText("IST"); + await expect(page.getByRole("table", { name: "Environment map" })).toContainText("UAT"); + await expect(page.getByRole("table", { name: "Environment map" })).toContainText("PRD"); + await expect(page.getByRole("table", { name: "Environment map" }).locator("[data-health-status]")).toHaveCount(0); await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Approved diagnostics format"); await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Environment Variables + All Runtime Ports"); await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Configurable multiple runtime ports"); @@ -197,7 +213,8 @@ test("Creator sessions cannot access Admin System Health operations", async ({ p try { await expect(page.getByRole("heading", { name: "Admin role required" })).toBeVisible(); await expect(page.locator("[data-session-access-blocked='admin']")).toBeVisible(); - await expect(page.getByRole("table", { name: "Environment summary" })).toHaveCount(0); + await expect(page.getByRole("table", { name: "Environment identity" })).toHaveCount(0); + await expect(page.getByRole("table", { name: "Environment map" })).toHaveCount(0); expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(false); expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toBe(false); expect(context.pageErrors).toEqual([]); @@ -217,6 +234,8 @@ test("Admin System Health operations page keeps scripts and styles external", as expect(pageSource).not.toMatch(/data-health-status="(?:WARN|FAIL)"/); expect(pageSource).not.toContain("No active failure is declared"); expect(pageSource).not.toContain("SQLite"); + expect(pageSource).toContain("Environment Identity"); + expect(pageSource).toContain("Environment Map"); expect(pageSource).toContain("Diagnostics Plan"); expect(pageSource).toContain("Local API Startup Diagnostics"); expect(pageSource).toContain("Server-owned Postgres health reader");