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
- | Environment |
- Purpose |
- Promotion Gate |
+ Field |
+ Current Deployment |
Status |
- | DEV | Local implementation and diagnostics | Developer validation | PASS |
- | IST | Integrated system testing | Cross-feature validation | PASS |
- | UAT | User acceptance testing | Owner review | PASS |
- | PRD | Production runtime | EOD approval | PASS |
+ | Environment name | Loading | PENDING |
+ | Hosting model | Loading | PENDING |
+ | Site URL | Loading | PENDING |
+ | API URL | Loading | PENDING |
+ | Database model | Loading | PENDING |
+ | Storage folder | Loading | PENDING |
+ | Last health check | Loading | PENDING |
+
+
+
+
+
+ Environment Map
+
+
+ | Environment |
+ Hosting model |
+ Database model |
+ Storage folder |
+
+
+
+ | Local | VS Code + Local API | Local Docker PostgreSQL | /local |
+ | DEV | Local Docker | Local Docker PostgreSQL | /dev |
+ | IST | Local Docker | Local Docker PostgreSQL | /ist |
+ | UAT | Cloudflare | Supabase PostgreSQL | /uat |
+ | PRD | Cloudflare | Supabase 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");