Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions admin/system-health.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,11 @@ <h2 id="admin-system-health-title">System Health Tables</h2>
</tr>
</thead>
<tbody>
<tr><td>Provider</td><td data-admin-system-health-db-value="provider">Postgres</td><td data-health-status="PASS" data-admin-system-health-db-status="provider">PASS</td></tr>
<tr><td>Host</td><td data-admin-system-health-db-value="host">Configured host placeholder</td><td data-health-status="PENDING" data-admin-system-health-db-status="host" title="Reason: host reader is intentionally not wired in this foundation PR." aria-label="PENDING: host reader is intentionally not wired in this foundation PR.">PENDING</td></tr>
<tr><td>Port</td><td data-admin-system-health-db-value="port">5432</td><td data-health-status="PASS" data-admin-system-health-db-status="port">PASS</td></tr>
<tr><td>Database</td><td data-admin-system-health-db-value="database">Configured database placeholder</td><td data-health-status="PENDING" data-admin-system-health-db-status="database" title="Reason: database name reader is intentionally not wired in this foundation PR." aria-label="PENDING: database name reader is intentionally not wired in this foundation PR.">PENDING</td></tr>
<tr><td>Migration Version</td><td data-admin-system-health-db-value="migration">Pending migration reader</td><td data-health-status="PENDING" data-admin-system-health-db-status="migration" title="Reason: migration version reader is intentionally not wired in this foundation PR." aria-label="PENDING: migration version reader is intentionally not wired in this foundation PR.">PENDING</td></tr>
<tr><td>Status</td><td data-admin-system-health-db-value="connection">Connection check pending</td><td data-health-status="PENDING" data-admin-system-health-db-status="connection" title="Reason: live Postgres connection checks are intentionally not wired in this foundation PR." aria-label="PENDING: live Postgres connection checks are intentionally not wired in this foundation PR.">PENDING</td></tr>
<tr><td>Database type</td><td data-admin-system-health-db-value="type">Loading</td><td data-health-status="PENDING" data-admin-system-health-db-status="type" title="Reason: database health has not loaded yet." aria-label="PENDING: database health has not loaded yet.">PENDING</td></tr>
<tr><td>Connectivity</td><td data-admin-system-health-db-value="connectivity">Loading</td><td data-health-status="PENDING" data-admin-system-health-db-status="connectivity" title="Reason: database health has not loaded yet." aria-label="PENDING: database health has not loaded yet.">PENDING</td></tr>
<tr><td>Response time</td><td data-admin-system-health-db-value="responseTime">Loading</td><td data-health-status="PENDING" data-admin-system-health-db-status="responseTime" title="Reason: database health has not loaded yet." aria-label="PENDING: database health has not loaded yet.">PENDING</td></tr>
<tr><td>Version</td><td data-admin-system-health-db-value="version">Loading</td><td data-health-status="PENDING" data-admin-system-health-db-status="version" title="Reason: database health has not loaded yet." aria-label="PENDING: database health has not loaded yet.">PENDING</td></tr>
<tr><td>Last checked</td><td data-admin-system-health-db-value="lastChecked">Loading</td><td data-health-status="PENDING" data-admin-system-health-db-status="lastChecked" title="Reason: database health has not loaded yet." aria-label="PENDING: database health has not loaded yet.">PENDING</td></tr>
</tbody>
</table>
</div>
Expand Down
34 changes: 15 additions & 19 deletions assets/theme-v2/js/admin-system-health.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = {}) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 1 addition & 1 deletion docs_build/dev/reports/coverage_changed_js_guardrail.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions docs_build/dev/reports/playwright_v8_coverage_report.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
26 changes: 23 additions & 3 deletions src/dev-runtime/server/local-api-router.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand All @@ -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
Expand All @@ -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 || ""),
Expand All @@ -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",
};
}
}
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions tests/dev-runtime/AdminHealthOperations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
9 changes: 5 additions & 4 deletions tests/playwright/tools/AdminHealthOperationsPage.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading