diff --git a/admin/system-health.html b/admin/system-health.html index 9aebcf0bd..b4b9f477f 100644 --- a/admin/system-health.html +++ b/admin/system-health.html @@ -124,12 +124,11 @@

System Health Tables

- ProviderPostgresPASS - HostConfigured host placeholderPENDING - Port5432PASS - DatabaseConfigured database placeholderPENDING - Migration VersionPending migration readerPENDING - StatusConnection check pendingPENDING + Database typeLoadingPENDING + ConnectivityLoadingPENDING + Response timeLoadingPENDING + VersionLoadingPENDING + Last checkedLoadingPENDING diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js index 6ba5c0f2a..61b446a3e 100644 --- a/assets/theme-v2/js/admin-system-health.js +++ b/assets/theme-v2/js/admin-system-health.js @@ -101,7 +101,7 @@ class AdminSystemHealthController { ["name", "hostingModel", "siteUrl", "apiUrl", "databaseModel", "storageFolder", "lastHealthCheck"].forEach((key) => { this.setEnvironmentStatus(key, "PENDING", reason); }); - ["host", "database", "migration", "connection"].forEach((key) => { + ["type", "connectivity", "responseTime", "version", "lastChecked"].forEach((key) => { this.setStatus(key, "PENDING", reason); }); this.renderStartupPending(reason); @@ -134,25 +134,21 @@ class AdminSystemHealthController { } renderPostgresStatus(databaseStatus = {}) { - const migrationCounts = databaseStatus.migrationCounts || {}; - const lastMigration = databaseStatus.lastMigration || {}; - const migrationSummary = lastMigration.name - ? `${asText(lastMigration.type, "migration")} ${lastMigration.name}${lastMigration.appliedAt ? ` at ${lastMigration.appliedAt}` : ""}` - : `DDL=${migrationCounts.DDL || 0}; DML=${migrationCounts.DML || 0}; last migration not recorded`; - const connectionReason = databaseStatus.message || "Postgres diagnostic status returned by the safe Admin System Health API."; + const reason = databaseStatus.message || "Current environment database health returned by the safe Admin System Health API."; + const responseTime = Number.isFinite(databaseStatus.responseTimeMs) + ? `${databaseStatus.responseTimeMs} ms` + : "not available"; - this.setValue("provider", "Postgres"); - this.setStatus("provider", "PASS"); - this.setValue("host", databaseStatus.host, "not configured"); - this.setStatus("host", databaseStatus.hostStatus, connectionReason); - this.setValue("port", databaseStatus.port ? String(databaseStatus.port) : "", "not configured"); - this.setStatus("port", databaseStatus.portStatus, connectionReason); - this.setValue("database", databaseStatus.databaseName, "not configured"); - this.setStatus("database", databaseStatus.databaseNameStatus, connectionReason); - this.setValue("migration", migrationSummary); - this.setStatus("migration", databaseStatus.lastMigrationStatus || databaseStatus.migrationStatus, connectionReason); - this.setValue("connection", databaseStatus.configured === true ? connectionReason : databaseStatus.message || "Postgres configuration is not complete."); - this.setStatus("connection", databaseStatus.status, connectionReason); + this.setValue("type", databaseStatus.databaseType, "PostgreSQL"); + this.setStatus("type", databaseStatus.databaseType ? "PASS" : "WARN", reason); + this.setValue("connectivity", databaseStatus.connectivity, databaseStatus.message || "not configured"); + this.setStatus("connectivity", databaseStatus.connectivityStatus || databaseStatus.status, reason); + this.setValue("responseTime", responseTime); + this.setStatus("responseTime", Number.isFinite(databaseStatus.responseTimeMs) ? "PASS" : "WARN", reason); + this.setValue("version", databaseStatus.version, "not available"); + this.setStatus("version", databaseStatus.versionStatus, reason); + this.setValue("lastChecked", databaseStatus.lastChecked, "not available"); + this.setStatus("lastChecked", databaseStatus.lastChecked ? "PASS" : "WARN", reason); } renderStorageStatus(storageStatus = {}) { diff --git a/docs_build/dev/reports/PR_26175_CHARLIE_008-system-health-current-database-health.md b/docs_build/dev/reports/PR_26175_CHARLIE_008-system-health-current-database-health.md new file mode 100644 index 000000000..48883304b --- /dev/null +++ b/docs_build/dev/reports/PR_26175_CHARLIE_008-system-health-current-database-health.md @@ -0,0 +1,37 @@ +# PR_26175_CHARLIE_008 System Health Current Database Health + +## Scope + +Team: Charlie + +Purpose: Add current-environment database health only to Admin System Health. + +## Changes + +- Replaced the Database Health table body with current-environment fields: + - database type + - connectivity + - response time + - version + - last checked +- Added safe server-owned database status fields to the Admin System Health API payload. +- Database type follows the current environment identity: + - Local, DEV, and IST: Local Docker PostgreSQL + - UAT and PRD: Supabase PostgreSQL +- Updated focused API and Playwright tests. + +## Architecture Constraint + +PASS. Database health reads only the currently configured deployment database. No database checks are made for peer 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` + +## Artifact + +- `tmp/PR_26175_CHARLIE_008-system-health-current-database-health_delta.zip` diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt index 833366a34..e8e235569 100644 --- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt +++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt @@ -7,7 +7,7 @@ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. Changed runtime JS files considered: (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 +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 284/284; executed functions 33/37 Guardrail warnings: (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 c5c648f78..b83e6a3d3 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -18,7 +18,7 @@ Exercised tool entry points detected: Changed runtime JS files covered: (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 +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 284/284; executed functions 33/37 Files with executed line/function counts where available: (36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14 @@ -28,7 +28,7 @@ Files with executed line/function counts where available: (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 +(89%) assets/theme-v2/js/admin-system-health.js - executed lines 284/284; 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 diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs index b632d31aa..7305d14c0 100644 --- a/src/dev-runtime/server/local-api-router.mjs +++ b/src/dev-runtime/server/local-api-router.mjs @@ -3653,9 +3653,14 @@ class ApiRuntimeDataSource { }; } - async ownerDatabaseStatus() { + async ownerDatabaseStatus(environmentIdentity = systemHealthEnvironmentIdentity()) { + const startedAt = Date.now(); const databaseStatus = { ...databaseConfigStatus(), + connectivity: "not configured", + connectivityStatus: "WARN", + databaseType: environmentIdentity.databaseModel || "PostgreSQL", + lastChecked: new Date().toISOString(), lastMigration: { appliedAt: "", name: "", @@ -3667,10 +3672,14 @@ class ApiRuntimeDataSource { DML: 0, }, migrationStatus: "WARN", + responseTimeMs: null, status: "WARN", + version: "", + versionStatus: "WARN", }; try { const adapter = this.supabaseDatabaseAdapter("Reading Admin System Health migration history"); + const versionRows = await adapter.databaseClient().query("SELECT version() AS version;"); const countRows = await adapter.databaseClient().query(` SELECT "migrationType", count(*)::int AS count FROM schema_migrations @@ -3685,8 +3694,11 @@ LIMIT 1; `); const counts = new Map(countRows.map((row) => [String(row.migrationType || ""), Number(row.count || 0)])); const lastRow = lastRows[0] || {}; + const version = String(versionRows[0]?.version || "").trim(); return { ...databaseStatus, + connectivity: "connected", + connectivityStatus: "PASS", lastMigration: { appliedAt: String(lastRow.appliedAt || ""), name: String(lastRow.migrationName || ""), @@ -3698,12 +3710,20 @@ LIMIT 1; DML: counts.get("DML") || 0, }, migrationStatus: "PASS", + message: "Current environment database connection responded through the safe Admin System Health API.", + responseTimeMs: Date.now() - startedAt, status: databaseStatus.configured === true ? "PASS" : "WARN", + version: version || "not available", + versionStatus: version ? "PASS" : "WARN", }; } catch (error) { return { ...databaseStatus, - message: `Migration history read failed: ${error instanceof Error ? error.message : String(error || "Unknown database error.")}`, + connectivity: "failed", + connectivityStatus: "FAIL", + message: `Current environment database health read failed: ${error instanceof Error ? error.message : String(error || "Unknown database error.")}`, + responseTimeMs: Date.now() - startedAt, + status: "FAIL", }; } } @@ -3756,7 +3776,7 @@ LIMIT 1; const checkedAt = new Date().toISOString(); const environmentIdentity = systemHealthEnvironmentIdentity(process.env, checkedAt); const environmentMap = systemHealthEnvironmentMap(); - const databaseStatus = await this.ownerDatabaseStatus(); + const databaseStatus = await this.ownerDatabaseStatus(environmentIdentity); const storageStatus = this.ownerStorageStatus(); const environmentStatus = storageProjectsPrefixStatus(); const localApiStartup = systemHealthLocalApiStartupDiagnostics(); diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs index e3669cced..e7863ac4e 100644 --- a/tests/dev-runtime/AdminHealthOperations.test.mjs +++ b/tests/dev-runtime/AdminHealthOperations.test.mjs @@ -137,6 +137,11 @@ test("Admin can view operational health while Creator sessions are blocked", asy 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.equal(health.databaseStatus.databaseType, "Local Docker PostgreSQL"); + assert.ok(["connected", "failed", "not configured"].includes(health.databaseStatus.connectivity)); + assert.equal(typeof health.databaseStatus.lastChecked, "string"); + assert.equal(typeof health.databaseStatus.responseTimeMs === "number" || health.databaseStatus.responseTimeMs === null, true); + assert.equal(typeof health.databaseStatus.version, "string"); assert.deepEqual( health.environmentMap.map((row) => row.name), ["Local", "DEV", "IST", "UAT", "PRD"], diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs index 10120dbba..76c78b53e 100644 --- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs @@ -151,10 +151,11 @@ test("Admin System Health renders Postgres diagnostics through the safe status A await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("deferred/cancelled"); await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).not.toContainText("secret"); await expect(page.getByRole("table", { name: "Database health" })).toContainText("Postgres"); - await expect(page.locator("[data-admin-system-health-db-value='provider']")).toHaveText("Postgres"); - await expect(page.locator("[data-admin-system-health-db-value='host']")).not.toHaveText("Configured host placeholder"); - await expect(page.locator("[data-admin-system-health-db-value='database']")).not.toHaveText("Configured database placeholder"); - await expect(page.locator("[data-admin-system-health-db-value='connection']")).not.toHaveText("Connection check pending"); + await expect(page.locator("[data-admin-system-health-db-value='type']")).toHaveText("Local Docker PostgreSQL"); + await expect(page.locator("[data-admin-system-health-db-value='connectivity']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-db-value='responseTime']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-db-value='version']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-db-value='lastChecked']")).not.toHaveText("Loading"); await expect(page.getByRole("table", { name: "Database health" })).not.toContainText("postgres://"); await expect(page.getByRole("table", { name: "Database health" })).not.toContainText("postgresql://"); await expect(page.getByRole("table", { name: "Storage health" })).toContainText("Cloudflare R2");