From 8455fb6d2da7686865e1e970f38d65cfbbba55b6 Mon Sep 17 00:00:00 2001 From: Charlie Team <97194984+ToolboxAid@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:14:44 -0400 Subject: [PATCH] Expand System Health R2 validation --- admin/system-health.html | 13 +- assets/theme-v2/js/admin-system-health.js | 41 +- ...0-r2-storage-health-expanded-validation.md | 35 + ...h-expanded-validation_branch-validation.md | 17 + ...nded-validation_manual-validation-notes.md | 9 + ...anded-validation_requirements-checklist.md | 13 + ...lth-expanded-validation_validation-lane.md | 22 + .../dev/reports/codex_changed_files.txt | 59 +- docs_build/dev/reports/codex_review.diff | 988 ++++++++++-------- .../reports/coverage_changed_js_guardrail.txt | 6 +- .../reports/playwright_v8_coverage_report.txt | 17 +- src/api/admin-system-health-api-client.js | 4 + src/dev-runtime/server/local-api-router.mjs | 105 +- .../api/admin-system-health/contract.test.mjs | 19 + .../AdminHealthOperations.test.mjs | 6 + .../tools/AdminHealthOperationsPage.spec.mjs | 8 +- 16 files changed, 851 insertions(+), 511 deletions(-) create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md diff --git a/admin/system-health.html b/admin/system-health.html index 5aa9fa80a..b62f232d7 100644 --- a/admin/system-health.html +++ b/admin/system-health.html @@ -316,16 +316,17 @@

Manual Health Actions

Check Target + Timing Status - Bucket connectivityLoadingPENDING - ListLoadingPENDING - UploadLoadingPENDING - ReadLoadingPENDING - DeleteLoadingPENDING - Last checkedLoadingPENDING + Bucket connectivityLoadingLoadingPENDING + ListLoadingLoadingPENDING + UploadLoadingLoadingPENDING + ReadLoadingLoadingPENDING + DeleteLoadingLoadingPENDING + Last checkedLoadingLoadingPENDING diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js index 746d47753..21bc50a63 100644 --- a/assets/theme-v2/js/admin-system-health.js +++ b/assets/theme-v2/js/admin-system-health.js @@ -1,7 +1,7 @@ import { readAdminSystemHealthStatus, runAdminSystemHealthAction, - runAdminSystemHealthStorageConnectivityAction, + runAdminSystemHealthStorageExpandedValidation, } from "../../../src/api/admin-system-health-api-client.js"; import { applyStatusNode, @@ -54,6 +54,10 @@ class AdminSystemHealthController { node.dataset.adminSystemHealthStorageStatus, node, ])); + this.storageTimings = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-storage-timing]")).map((node) => [ + node.dataset.adminSystemHealthStorageTiming, + node, + ])); this.runtimeHealthValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-runtime-health-value]")).map((node) => [ node.dataset.adminSystemHealthRuntimeHealthValue, node, @@ -122,6 +126,13 @@ class AdminSystemHealthController { this.setStatusNode(node, status, reason); } + setStorageTiming(key, value, fallback) { + const node = this.storageTimings.get(key); + if (node) { + node.textContent = asText(value, fallback); + } + } + setRuntimeHealthValue(key, value, fallback) { const node = this.runtimeHealthValues.get(key); if (node) { @@ -195,6 +206,7 @@ class AdminSystemHealthController { renderStoragePending(reason) { ["bucket", "list", "upload", "read", "delete", "lastChecked"].forEach((key) => { this.setStorageStatus(key, "PENDING", reason); + this.setStorageTiming(key, "not available"); }); this.renderRuntimePending(reason); } @@ -261,8 +273,10 @@ class AdminSystemHealthController { const reason = storageStatus.message || "Cloudflare R2 configuration status returned by the safe Admin System Health API."; this.setStorageValue("bucket", storageStatus.environmentFolder ? `${storageStatus.bucket || "not configured"} ${storageStatus.environmentFolder}` : storageStatus.bucket, "not configured"); this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); + this.setStorageTiming("bucket", storageStatus.lastChecked ? "configuration status" : "not available"); this.setStorageValue("lastChecked", storageStatus.lastChecked, "not available"); this.setStorageStatus("lastChecked", storageStatus.lastChecked ? "PASS" : "WARN", reason); + this.setStorageTiming("lastChecked", storageStatus.lastChecked ? "configuration status" : "not available"); } renderRuntimeHealthPending(reason) { @@ -684,9 +698,11 @@ class AdminSystemHealthController { } this.setStorageValue(key, this.storageResultTarget(result)); this.setStorageStatus(key, result.status, result.message || "R2 diagnostic returned without a message."); + this.setStorageTiming(key, Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); if (result.lastChecked) { this.setStorageValue("lastChecked", result.lastChecked); this.setStorageStatus("lastChecked", "PASS", "Most recent current-environment R2 health check timestamp."); + this.setStorageTiming("lastChecked", Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); } } @@ -732,15 +748,24 @@ class AdminSystemHealthController { runStorageDiagnostics() { STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { this.setStorageStatus(key, "PENDING", "R2 diagnostic is running through the safe Admin System Health API."); + this.setStorageTiming(key, "running"); }); - STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ actionId, key }) => { - try { - this.renderStorageResult(key, runAdminSystemHealthStorageConnectivityAction(actionId)); - } catch (error) { - const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; + try { + const validation = runAdminSystemHealthStorageExpandedValidation(); + const diagnostics = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; + diagnostics.forEach((storageResult) => { + const key = STORAGE_DIAGNOSTIC_ACTION_KEY_BY_ID.get(storageResult.actionId); + if (key) { + this.renderStorageResult(key, storageResult); + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; + STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { this.setStorageStatus(key, "PENDING", message); - } - }); + this.setStorageTiming(key, "not available"); + }); + } } createCell(text) { diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md new file mode 100644 index 000000000..61eba9efb --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md @@ -0,0 +1,35 @@ +# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation + +Team: Charlie +Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +Base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +Lifecycle: Build / Validation +Repair: Rebased onto repaired PR_26177_CHARLIE_029 branch on 2026-06-25. + +## Scope +- Expanded System Health R2 validation into an API-owned current-environment validation run. +- Added safe list/upload/read/delete diagnostics with duration, operation label, cleanup status, and permanent-object metadata. +- Updated the System Health UI to show storage timing without adding inline styles, script blocks, or page-local CSS. + +## Changed Files +- admin/system-health.html +- assets/theme-v2/js/admin-system-health.js +- src/api/admin-system-health-api-client.js +- src/dev-runtime/server/local-api-router.mjs +- tests/api/admin-system-health/contract.test.mjs +- tests/dev-runtime/AdminHealthOperations.test.mjs +- tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +- docs_build/dev/reports/coverage_changed_js_guardrail.txt +- docs_build/dev/reports/playwright_v8_coverage_report.txt + +## Validation +- PASS: node --check src/dev-runtime/server/local-api-router.mjs +- PASS: node --check assets/theme-v2/js/admin-system-health.js +- PASS: node --check src/api/admin-system-health-api-client.js +- PASS: node --test tests/api/admin-system-health/contract.test.mjs +- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +- PASS: git diff --check + +## ZIP +- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_delta.zip diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md new file mode 100644 index 000000000..0c0387294 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md @@ -0,0 +1,17 @@ +# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Branch Validation + +Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +Expected stack base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +Current status at validation: +## pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +Updated onto repaired PR_26177_CHARLIE_029 stack base. + +Result: PASS + +Checks: +- PASS: Branch created from PR 029 branch for the approved stacked chain. +- PASS: Active branch matches PR identity. +- PASS: Worktree contained only scoped PR changes and generated validation reports. +- PASS: Branch is based on repaired PR 029 branch. +- PASS: Rebase conflict scope was generated report artifacts only. +- PASS: No start_of_day files modified. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md new file mode 100644 index 000000000..36806ff77 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md @@ -0,0 +1,9 @@ +# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Manual Validation Notes + +- Confirmed the browser calls one expanded R2 validation action instead of owning the list/upload/read/delete sequence itself. +- Confirmed Storage Health displays timing values for list, upload, read, and delete rows. +- Confirmed storage validation payload includes cleanup status and permanent-object metadata. +- Confirmed storage diagnostics stay scoped to the current environment folder. +- Confirmed no storage credentials or secrets are displayed. +- Confirmed branch repair conflict was limited to generated report artifacts. +- Confirmed no start_of_day files changed. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md new file mode 100644 index 000000000..2dda36cc8 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md @@ -0,0 +1,13 @@ +# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Requirement Checklist + +- PASS: Expanded Cloud/R2 storage health validation. +- PASS: Included safe bucket/list/write/read/delete validation through API-owned action contract. +- PASS: Included timing/status fields where available. +- PASS: Does not create browser-owned storage health state. +- PASS: Test object cleanup is attempted in the same expanded validation run. +- PASS: Does not expose secrets. +- PASS: Current environment folder only. +- PASS: No unrelated files modified. +- PASS: No start_of_day files modified. +- PASS: Rebased onto repaired PR_26177_CHARLIE_029 branch. +- PASS: Shows unavailable/not configured states explicitly. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md new file mode 100644 index 000000000..0dbf85ec0 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md @@ -0,0 +1,22 @@ +# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Validation Lane Report + +Impacted lanes: +- runtime: Local API Admin System Health R2 action contract. +- contract: Admin System Health API contract tests. +- UI: Admin System Health Theme V2 page controller and markup. +- Playwright: targeted Admin System Health page spec. + +Commands: +- PASS: node --check src/dev-runtime/server/local-api-router.mjs +- PASS: node --check assets/theme-v2/js/admin-system-health.js +- PASS: node --check src/api/admin-system-health-api-client.js +- PASS: node --test tests/api/admin-system-health/contract.test.mjs +- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +- PASS: git diff --check + +Skipped lanes: +- Full samples smoke skipped; not impacted by System Health storage diagnostics. +- Full Project Workspace suite skipped; targeted Admin/System Health coverage was sufficient for this scoped page/API change. + +Result: PASS diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 79c9ad1c4..ae2d25d52 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,44 +1,45 @@ -# git diff --name-only origin/main -- +# git diff --name-only pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- admin/system-health.html assets/theme-v2/js/admin-system-health.js -docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel.md -docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_branch-validation.md -docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_manual-validation-notes.md -docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_requirements-checklist.md -docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_validation-lane.md +docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md +docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md +docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff docs_build/dev/reports/coverage_changed_js_guardrail.txt docs_build/dev/reports/playwright_v8_coverage_report.txt +src/api/admin-system-health-api-client.js src/dev-runtime/server/local-api-router.mjs tests/api/admin-system-health/contract.test.mjs tests/dev-runtime/AdminHealthOperations.test.mjs tests/playwright/tools/AdminHealthOperationsPage.spec.mjs # git status --short - M docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel.md - M docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_branch-validation.md - M docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_manual-validation-notes.md - M docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_requirements-checklist.md - M docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_validation-lane.md + M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md + M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md + M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md + M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md + M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md M docs_build/dev/reports/codex_review.diff - M docs_build/dev/reports/coverage_changed_js_guardrail.txt M docs_build/dev/reports/playwright_v8_coverage_report.txt -# git diff --stat origin/main -- - admin/system-health.html | 16 + - assets/theme-v2/js/admin-system-health.js | 42 ++ - ...LIE_029-system-health-postgres-metrics-panel.md | 39 + - ...lth-postgres-metrics-panel_branch-validation.md | 17 + - ...stgres-metrics-panel_manual-validation-notes.md | 9 + - ...ostgres-metrics-panel_requirements-checklist.md | 16 + - ...ealth-postgres-metrics-panel_validation-lane.md | 21 + - docs_build/dev/reports/codex_changed_files.txt | 42 +- - docs_build/dev/reports/codex_review.diff | 794 +++++++++++++++------ - .../dev/reports/coverage_changed_js_guardrail.txt | 4 +- - .../dev/reports/playwright_v8_coverage_report.txt | 24 +- - src/dev-runtime/server/local-api-router.mjs | 115 ++- - tests/api/admin-system-health/contract.test.mjs | 15 + - tests/dev-runtime/AdminHealthOperations.test.mjs | 20 + - .../tools/AdminHealthOperationsPage.spec.mjs | 11 + - 15 files changed, 924 insertions(+), 261 deletions(-) +# git diff --stat pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- + admin/system-health.html | 13 +- + assets/theme-v2/js/admin-system-health.js | 41 +- + ...IE_030-r2-storage-health-expanded-validation.md | 35 + + ...health-expanded-validation_branch-validation.md | 17 + + ...-expanded-validation_manual-validation-notes.md | 9 + + ...h-expanded-validation_requirements-checklist.md | 13 + + ...e-health-expanded-validation_validation-lane.md | 22 + + docs_build/dev/reports/codex_changed_files.txt | 70 +- + docs_build/dev/reports/codex_review.diff | 988 +++++++++++---------- + .../dev/reports/coverage_changed_js_guardrail.txt | 6 +- + .../dev/reports/playwright_v8_coverage_report.txt | 17 +- + src/api/admin-system-health-api-client.js | 4 + + src/dev-runtime/server/local-api-router.mjs | 105 ++- + tests/api/admin-system-health/contract.test.mjs | 19 + + tests/dev-runtime/AdminHealthOperations.test.mjs | 6 + + .../tools/AdminHealthOperationsPage.spec.mjs | 8 +- + 16 files changed, 851 insertions(+), 522 deletions(-) diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 1f8c7b688..8ce24fdc9 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,126 +1,155 @@ diff --git a/admin/system-health.html b/admin/system-health.html -index abe5c4f97..5aa9fa80a 100644 +index 5aa9fa80a..b62f232d7 100644 --- a/admin/system-health.html +++ b/admin/system-health.html -@@ -51,6 +51,7 @@ -

Notifications & Alerts

-

Local API Startup

-

Database Health

-+

Postgres Metrics

-

Storage Health

-

Runtime Health

-

Health Check History

-@@ -293,6 +294,21 @@ +@@ -316,16 +316,17 @@ + + Check + Target ++ Timing + Status + + + +- Bucket connectivityLoadingPENDING +- ListLoadingPENDING +- UploadLoadingPENDING +- ReadLoadingPENDING +- DeleteLoadingPENDING +- Last checkedLoadingPENDING ++ Bucket connectivityLoadingLoadingPENDING ++ ListLoadingLoadingPENDING ++ UploadLoadingLoadingPENDING ++ ReadLoadingLoadingPENDING ++ DeleteLoadingLoadingPENDING ++ Last checkedLoadingLoadingPENDING -+
-+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
Postgres Metrics
MetricSafe ValueStatus
Postgres metricsUnavailable until safe API status loadsPENDING
-+
-
- - diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js -index 656394104..746d47753 100644 +index 746d47753..21bc50a63 100644 --- a/assets/theme-v2/js/admin-system-health.js +++ b/assets/theme-v2/js/admin-system-health.js -@@ -74,6 +74,7 @@ class AdminSystemHealthController { - this.notificationRows = root.querySelector("[data-admin-system-health-notification-rows]"); - this.serviceCards = root.querySelector("[data-admin-system-health-service-cards]"); - this.startupRows = root.querySelector("[data-admin-system-health-startup-rows]"); -+ this.postgresMetricRows = root.querySelector("[data-admin-system-health-postgres-metric-rows]"); - this.runtimeRows = root.querySelector("[data-admin-system-health-runtime-rows]"); +@@ -1,7 +1,7 @@ + import { + readAdminSystemHealthStatus, + runAdminSystemHealthAction, +- runAdminSystemHealthStorageConnectivityAction, ++ runAdminSystemHealthStorageExpandedValidation, + } from "../../../src/api/admin-system-health-api-client.js"; + import { + applyStatusNode, +@@ -54,6 +54,10 @@ class AdminSystemHealthController { + node.dataset.adminSystemHealthStorageStatus, + node, + ])); ++ this.storageTimings = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-storage-timing]")).map((node) => [ ++ node.dataset.adminSystemHealthStorageTiming, ++ node, ++ ])); + this.runtimeHealthValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-runtime-health-value]")).map((node) => [ + node.dataset.adminSystemHealthRuntimeHealthValue, + node, +@@ -122,6 +126,13 @@ class AdminSystemHealthController { + this.setStatusNode(node, status, reason); } -@@ -145,6 +146,7 @@ class AdminSystemHealthController { - this.setStatus(key, "PENDING", reason); - }); - this.renderStartupPending(reason); -+ this.renderPostgresMetricsPending(reason); - this.renderStoragePending(reason); - this.renderRuntimeHealthPending(reason); - this.renderEnvironmentCapabilitiesPending(reason); -@@ -213,6 +215,46 @@ class AdminSystemHealthController { - this.setStatus("version", databaseStatus.versionStatus, reason); - this.setValue("lastChecked", databaseStatus.lastChecked, "not available"); - this.setStatus("lastChecked", databaseStatus.lastChecked ? "PASS" : "WARN", reason); -+ this.renderPostgresMetrics(databaseStatus.postgresMetrics || {}); -+ } -+ -+ renderPostgresMetricsPending(reason) { -+ if (!this.postgresMetricRows) { -+ return; ++ setStorageTiming(key, value, fallback) { ++ const node = this.storageTimings.get(key); ++ if (node) { ++ node.textContent = asText(value, fallback); + } -+ const row = document.createElement("tr"); -+ row.append( -+ this.createCell("Postgres metrics"), -+ this.createCell("Unavailable"), -+ this.createStatusCell("PENDING", reason), -+ ); -+ this.postgresMetricRows.replaceChildren(row); + } + -+ renderPostgresMetrics(postgresMetrics = {}) { -+ if (!this.postgresMetricRows) { -+ return; -+ } -+ if (postgresMetrics?.secretsExposed === true || postgresMetrics?.secretEditingAllowed === true) { -+ this.renderPostgresMetricsPending("Safe Postgres metrics response was blocked because it exposed secret controls."); -+ return; -+ } -+ const rows = Array.isArray(postgresMetrics.rows) ? postgresMetrics.rows : []; -+ if (!rows.length) { -+ this.renderPostgresMetricsPending("Safe Admin System Health API returned no Postgres metric rows."); -+ return; + setRuntimeHealthValue(key, value, fallback) { + const node = this.runtimeHealthValues.get(key); + if (node) { +@@ -195,6 +206,7 @@ class AdminSystemHealthController { + renderStoragePending(reason) { + ["bucket", "list", "upload", "read", "delete", "lastChecked"].forEach((key) => { + this.setStorageStatus(key, "PENDING", reason); ++ this.setStorageTiming(key, "not available"); + }); + this.renderRuntimePending(reason); + } +@@ -261,8 +273,10 @@ class AdminSystemHealthController { + const reason = storageStatus.message || "Cloudflare R2 configuration status returned by the safe Admin System Health API."; + this.setStorageValue("bucket", storageStatus.environmentFolder ? `${storageStatus.bucket || "not configured"} ${storageStatus.environmentFolder}` : storageStatus.bucket, "not configured"); + this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); ++ this.setStorageTiming("bucket", storageStatus.lastChecked ? "configuration status" : "not available"); + this.setStorageValue("lastChecked", storageStatus.lastChecked, "not available"); + this.setStorageStatus("lastChecked", storageStatus.lastChecked ? "PASS" : "WARN", reason); ++ this.setStorageTiming("lastChecked", storageStatus.lastChecked ? "configuration status" : "not available"); + } + + renderRuntimeHealthPending(reason) { +@@ -684,9 +698,11 @@ class AdminSystemHealthController { + } + this.setStorageValue(key, this.storageResultTarget(result)); + this.setStorageStatus(key, result.status, result.message || "R2 diagnostic returned without a message."); ++ this.setStorageTiming(key, Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); + if (result.lastChecked) { + this.setStorageValue("lastChecked", result.lastChecked); + this.setStorageStatus("lastChecked", "PASS", "Most recent current-environment R2 health check timestamp."); ++ this.setStorageTiming("lastChecked", Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); + } + } + +@@ -732,15 +748,24 @@ class AdminSystemHealthController { + runStorageDiagnostics() { + STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { + this.setStorageStatus(key, "PENDING", "R2 diagnostic is running through the safe Admin System Health API."); ++ this.setStorageTiming(key, "running"); + }); +- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ actionId, key }) => { +- try { +- this.renderStorageResult(key, runAdminSystemHealthStorageConnectivityAction(actionId)); +- } catch (error) { +- const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; ++ try { ++ const validation = runAdminSystemHealthStorageExpandedValidation(); ++ const diagnostics = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; ++ diagnostics.forEach((storageResult) => { ++ const key = STORAGE_DIAGNOSTIC_ACTION_KEY_BY_ID.get(storageResult.actionId); ++ if (key) { ++ this.renderStorageResult(key, storageResult); ++ } ++ }); ++ } catch (error) { ++ const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; ++ STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { + this.setStorageStatus(key, "PENDING", message); +- } +- }); ++ this.setStorageTiming(key, "not available"); ++ }); + } -+ const fragment = document.createDocumentFragment(); -+ rows.forEach((metricRow) => { -+ const row = document.createElement("tr"); -+ row.append( -+ this.createCell(metricRow.metric), -+ this.createCell(metricRow.value), -+ this.createStatusCell(metricRow.status, postgresMetrics.message), -+ ); -+ fragment.append(row); -+ }); -+ this.postgresMetricRows.replaceChildren(fragment); } - renderStorageStatus(storageStatus = {}) { -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel.md b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel.md + createCell(text) { +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md new file mode 100644 -index 000000000..7ff7fe32a +index 000000000..61eba9efb --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel.md -@@ -0,0 +1,39 @@ -+# PR_26177_CHARLIE_029-system-health-postgres-metrics-panel ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +@@ -0,0 +1,35 @@ ++# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation + +Team: Charlie -+Branch: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -+Base: main ++Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++Base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +Lifecycle: Build / Validation -+Repair: Updated from origin/main on 2026-06-25 after PR #177 reported draft=true and mergeable=false. ++Repair: Rebased onto repaired PR_26177_CHARLIE_029 branch on 2026-06-25. + +## Scope -+- Added a System Health Postgres Metrics panel backed by the server-owned Admin System Health API. -+- Added safe current-environment metrics for connection status, database name, current schema, migration status, last migration, table count, database size, and last checked. -+- Preserved current-environment-only behavior and did not add cross-environment database checks. ++- Expanded System Health R2 validation into an API-owned current-environment validation run. ++- Added safe list/upload/read/delete diagnostics with duration, operation label, cleanup status, and permanent-object metadata. ++- Updated the System Health UI to show storage timing without adding inline styles, script blocks, or page-local CSS. + +## Changed Files +- admin/system-health.html +- assets/theme-v2/js/admin-system-health.js ++- src/api/admin-system-health-api-client.js +- src/dev-runtime/server/local-api-router.mjs +- tests/api/admin-system-health/contract.test.mjs +- tests/dev-runtime/AdminHealthOperations.test.mjs @@ -131,89 +160,81 @@ index 000000000..7ff7fe32a +## Validation +- PASS: node --check src/dev-runtime/server/local-api-router.mjs +- PASS: node --check assets/theme-v2/js/admin-system-health.js ++- PASS: node --check src/api/admin-system-health-api-client.js +- PASS: node --test tests/api/admin-system-health/contract.test.mjs +- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +- PASS: git diff --check + -+## Repair Notes -+- PASS: Merged current origin/main into the PR branch. -+- PASS: Merge conflict was limited to generated report artifacts: codex_changed_files.txt and codex_review.diff. -+- PASS: No runtime, UI, API, database, or product-data conflict required a product decision. -+- PASS: Scope remains Postgres metrics panel only. -+ +## ZIP -+- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_delta.zip -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_branch-validation.md ++- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_delta.zip +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md new file mode 100644 -index 000000000..9364d3f7c +index 000000000..0c0387294 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_branch-validation.md ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md @@ -0,0 +1,17 @@ -+# PR_26177_CHARLIE_029-system-health-postgres-metrics-panel Branch Validation ++# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Branch Validation + -+Branch: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -+Expected start branch: main, then PR branch for build ++Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++Expected stack base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +Current status at validation: -+## pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -+Updated from origin/main for PR #177 repair. ++## pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++Updated onto repaired PR_26177_CHARLIE_029 stack base. + +Result: PASS + +Checks: -+- PASS: Started from synchronized main. ++- PASS: Branch created from PR 029 branch for the approved stacked chain. +- PASS: Active branch matches PR identity. -+- PASS: Current origin/main merged into branch. -+- PASS: Merge conflict scope was generated report artifacts only. -+- PASS: Worktree contains only scoped PR repair/report changes after report refresh. ++- PASS: Worktree contained only scoped PR changes and generated validation reports. ++- PASS: Branch is based on repaired PR 029 branch. ++- PASS: Rebase conflict scope was generated report artifacts only. +- PASS: No start_of_day files modified. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_manual-validation-notes.md +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md new file mode 100644 -index 000000000..1c30d0c93 +index 000000000..36806ff77 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md @@ -0,0 +1,9 @@ -+# PR_26177_CHARLIE_029-system-health-postgres-metrics-panel Manual Validation Notes ++# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Manual Validation Notes + -+- Confirmed Postgres Metrics appears as a separate System Health table. -+- Confirmed metric values come from the server-owned Admin System Health API response. -+- Confirmed unavailable metrics render visibly as Unavailable/WARN rather than silently falling back. -+- Confirmed page retains external scripts/styles only; no inline style/script/handler additions. -+- Confirmed no secrets or database URLs are shown in the Database Health or Postgres Metrics tables. -+- Confirmed current main merge conflict was limited to generated report artifacts. ++- Confirmed the browser calls one expanded R2 validation action instead of owning the list/upload/read/delete sequence itself. ++- Confirmed Storage Health displays timing values for list, upload, read, and delete rows. ++- Confirmed storage validation payload includes cleanup status and permanent-object metadata. ++- Confirmed storage diagnostics stay scoped to the current environment folder. ++- Confirmed no storage credentials or secrets are displayed. ++- Confirmed branch repair conflict was limited to generated report artifacts. +- Confirmed no start_of_day files changed. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_requirements-checklist.md +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md new file mode 100644 -index 000000000..f99698e88 +index 000000000..2dda36cc8 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_requirements-checklist.md -@@ -0,0 +1,16 @@ -+# PR_26177_CHARLIE_029-system-health-postgres-metrics-panel Requirement Checklis ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +@@ -0,0 +1,13 @@ ++# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Requirement Checklis + -+- PASS: Add/extend System Health Postgres metrics panel. -+- PASS: Include connection status. -+- PASS: Include database name. -+- PASS: Include current schema/migration status when available. -+- PASS: Include table count when available. -+- PASS: Include database size when available. -+- PASS: Do not add expensive queries. -+- PASS: Show explicit Unavailable status when a metric is unavailable. -+- PASS: Do not expose secrets. -+- PASS: Keep browser UI consuming API/service contract. -+- PASS: No cross-environment checks added. ++- PASS: Expanded Cloud/R2 storage health validation. ++- PASS: Included safe bucket/list/write/read/delete validation through API-owned action contract. ++- PASS: Included timing/status fields where available. ++- PASS: Does not create browser-owned storage health state. ++- PASS: Test object cleanup is attempted in the same expanded validation run. ++- PASS: Does not expose secrets. ++- PASS: Current environment folder only. ++- PASS: No unrelated files modified. +- PASS: No start_of_day files modified. -+- PASS: Repaired PR #177 branch from current main. -+- PASS: Mark ready for review after validation/reporting is complete. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_validation-lane.md ++- PASS: Rebased onto repaired PR_26177_CHARLIE_029 branch. ++- PASS: Shows unavailable/not configured states explicitly. +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md new file mode 100644 -index 000000000..e1ca3d673 +index 000000000..0dbf85ec0 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_029-system-health-postgres-metrics-panel_validation-lane.md -@@ -0,0 +1,21 @@ -+# PR_26177_CHARLIE_029-system-health-postgres-metrics-panel Validation Lane Repor ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md +@@ -0,0 +1,22 @@ ++# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Validation Lane Repor + +Impacted lanes: -+- runtime: Local API Admin System Health status contract. ++- runtime: Local API Admin System Health R2 action contract. +- contract: Admin System Health API contract tests. +- UI: Admin System Health Theme V2 page controller and markup. +- Playwright: targeted Admin System Health page spec. @@ -221,358 +242,421 @@ index 000000000..e1ca3d673 +Commands: +- PASS: node --check src/dev-runtime/server/local-api-router.mjs +- PASS: node --check assets/theme-v2/js/admin-system-health.js ++- PASS: node --check src/api/admin-system-health-api-client.js +- PASS: node --test tests/api/admin-system-health/contract.test.mjs +- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +- PASS: git diff --check + +Skipped lanes: -+- Full samples smoke skipped; not impacted by System Health API/UI panel change. -+- Full workspace suite skipped; targeted Project Workspace coverage was sufficient for this scoped Admin page. ++- Full samples smoke skipped; not impacted by System Health storage diagnostics. ++- Full Project Workspace suite skipped; targeted Admin/System Health coverage was sufficient for this scoped page/API change. + +Result: PASS diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.tx -index 4cf3859da..7b1c51f19 100644 +index 7b1c51f19..7211f2375 100644 --- a/docs_build/dev/reports/coverage_changed_js_guardrail.tx +++ b/docs_build/dev/reports/coverage_changed_js_guardrail.tx -@@ -6,7 +6,7 @@ Missing changed runtime JS files are WARN, not FAIL. +@@ -6,7 +6,9 @@ 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: --(63%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 62/98 -+(100%) none changed - no changed runtime JS files +-(100%) none changed - no changed runtime JS files ++(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only ++(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 ++(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Guardrail warnings: --(100%) none - no changed runtime JS coverage warnings -+(100%) none changed - no changed runtime JS files +-(100%) none changed - no changed runtime JS files ++(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.tx -index 8f1bdb531..627ed631f 100644 +index 627ed631f..2a2982e6a 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.tx +++ b/docs_build/dev/reports/playwright_v8_coverage_report.tx -@@ -12,22 +12,28 @@ 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 --(64%) Theme V2 Shared JS - exercised 2 runtime JS files -+(78%) Theme V2 Shared JS - exercised 5 runtime JS files +@@ -17,7 +17,9 @@ Exercised tool entry points detected: + (78%) Theme V2 Shared JS - exercised 5 runtime JS files Changed runtime JS files covered: --(63%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 62/98 -+(100%) none changed - no changed runtime JS files +-(100%) none changed - no changed runtime JS files ++(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only ++(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 ++(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Files with executed line/function counts where available: --(63%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 62/98 -+(36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 -+(46%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 12/26 -+(65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26 -+(75%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 76/102 -+(77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 + (36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 +@@ -27,13 +29,18 @@ Files with executed line/function counts where available: + (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 --(100%) legal/legal-nav.js - executed lines 34/34; executed functions 3/3 -+(80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 -+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 -+(83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 -+(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 28/28; executed functions 4/4 + (80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 +-(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 ++(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 + (83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 + (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 28/28; executed functions 4/4 ++(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Uncovered or low-coverage changed JS files: --(100%) none - no low-coverage changed runtime JS files -+(100%) none changed - no changed runtime JS files +-(100%) none changed - no changed runtime JS files ++(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only Changed JS files considered: --(0%) tests/playwright/tools/RemainingLegalPages.spec.mjs - changed JS file not collected as browser runtime coverage --(63%) assets/theme-v2/js/gamefoundry-partials.js - changed JS file with browser V8 coverage --(100%) legal/legal-nav.js - changed JS file with browser V8 coverage -+(100%) none - no changed JS files +-(100%) none - no changed JS files ++(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage ++(0%) tests/api/admin-system-health/contract.test.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 ++(81%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage ++(100%) src/api/admin-system-health-api-client.js - changed JS file with browser V8 coverage +diff --git a/src/api/admin-system-health-api-client.js b/src/api/admin-system-health-api-client.js +index 9b166d097..49101c59f 100644 +--- a/src/api/admin-system-health-api-client.js ++++ b/src/api/admin-system-health-api-client.js +@@ -20,6 +20,10 @@ export function runAdminSystemHealthStorageConnectivityAction(actionId) { + ); + } + ++export function runAdminSystemHealthStorageExpandedValidation() { ++ return runAdminSystemHealthStorageConnectivityAction("storage-expanded-validation"); ++} ++ + export function runAdminSystemHealthAction(actionId) { + return requireServerApiData( + safeRequestServerApi("/admin/system-health/action", { diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs -index 993e14a59..462d8b7a0 100644 +index 462d8b7a0..6dbb8a106 100644 --- a/src/dev-runtime/server/local-api-router.mjs +++ b/src/dev-runtime/server/local-api-router.mjs -@@ -650,6 +650,63 @@ function databaseConfigStatus(env = process.env) { +@@ -356,6 +356,7 @@ const SYSTEM_HEALTH_STORAGE_ACTION_IDS = Object.freeze([ + "storage-read-test-object", + "storage-delete-test-object", + ]); ++const SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID = "storage-expanded-validation"; + const SYSTEM_HEALTH_MANUAL_ACTION_LABELS = Object.freeze({ + "database-check": "Run Database Check", + "full-health-check": "Run Full Health Check", +@@ -4046,43 +4047,75 @@ class ApiRuntimeDataSource { + + storageConnectivityConfigFailure(actionId, storage, scope = "project-prefix") { + const missing = storage.config?.missingKeys?.join(", ") || storage.config?.validationError || "storage configuration incomplete"; ++ const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); + return { + actionId, ++ cleanupStatus: "not-run", ++ durationMs: 0, + environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", + executed: false, + lastChecked: new Date().toISOString(), + message: `Storage connectivity requires configured storage and GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX before this action can run. Missing or invalid: ${missing}.`, ++ operation: action?.operation || "configuration", ++ operationLabel: action?.label || "Storage configuration", ++ permanentObjectCreated: false, + projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), + secretEditingAllowed: false, + secretsExposed: false, + status: "FAIL", + storageStatus: this.ownerStorageStatus(), ++ targetPrefix: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : String(storage.config?.projectsPrefix || "").trim(), + }; } - } -+function systemHealthPostgresMetrics(databaseStatus = {}, checkedAt = new Date().toISOString()) { -+ const reason = databaseStatus.message || "Postgres metrics are reported only when the current environment database reader returns safe values."; -+ const tableCount = Number(databaseStatus.tableCount); -+ const metricRows = [ -+ { -+ metric: "Connection status", -+ status: databaseStatus.connectivityStatus || databaseStatus.status || "WARN", -+ value: databaseStatus.connectivity || "Unavailable", -+ }, -+ { -+ metric: "Database name", -+ status: databaseStatus.currentDatabaseNameStatus || databaseStatus.databaseNameStatus || "WARN", -+ value: databaseStatus.currentDatabaseName || databaseStatus.databaseName || "Unavailable", -+ }, -+ { -+ metric: "Current schema", -+ status: databaseStatus.currentSchemaStatus || "WARN", -+ value: databaseStatus.currentSchema || "Unavailable", -+ }, -+ { -+ metric: "Migration status", -+ status: databaseStatus.migrationStatus || "WARN", -+ value: databaseStatus.migrationStatus === "PASS" -+ ? `DDL=${databaseStatus.migrationCounts?.DDL || 0}; DML=${databaseStatus.migrationCounts?.DML || 0} -+ : "Unavailable", -+ }, -+ { -+ metric: "Last migration", -+ status: databaseStatus.lastMigrationStatus || "WARN", -+ value: databaseStatus.lastMigration?.name || "Unavailable", -+ }, -+ { -+ metric: "Table count", -+ status: Number.isFinite(tableCount) ? "PASS" : "WARN", -+ value: Number.isFinite(tableCount) ? String(tableCount) : "Unavailable", -+ }, -+ { -+ metric: "Database size", -+ status: databaseStatus.databaseSizeStatus || "WARN", -+ value: databaseStatus.databaseSize || "Unavailable", -+ }, -+ { -+ metric: "Last checked", -+ status: databaseStatus.lastChecked ? "PASS" : "WARN", -+ value: databaseStatus.lastChecked || checkedAt || "Unavailable", -+ }, -+ ]; -+ return { -+ lastChecked: databaseStatus.lastChecked || checkedAt, -+ message: reason, -+ rows: metricRows, -+ secretEditingAllowed: false, -+ secretsExposed: false, -+ status: overallHealthStatus(metricRows), -+ }; -+} -+ - function projectPackageReadinessStatus() { - const decisionPath = path.join(process.cwd(), "docs_build", "codex", "decisions", "project-packages.md"); - const contract = projectPackageReadinessContract(); -@@ -4232,31 +4289,60 @@ class ApiRuntimeDataSource { - migrationStatus: "WARN", - responseTimeMs: null, - status: "WARN", -+ currentDatabaseName: "", -+ currentDatabaseNameStatus: "WARN", -+ currentSchema: "", -+ currentSchemaStatus: "WARN", -+ databaseSize: "", -+ databaseSizeBytes: null, -+ databaseSizeStatus: "WARN", -+ tableCount: null, - version: "", - versionStatus: "WARN", +- storageConnectivityResult({ actionId, environmentFolder = "", executed = true, keysListed = 0, message, objectKey, projectsPrefix, status }) { ++ storageConnectivityResult({ ++ action, ++ actionId, ++ cleanupStatus = "not-applicable", ++ durationMs = 0, ++ environmentFolder = "", ++ executed = true, ++ keysListed = 0, ++ message, ++ objectKey, ++ permanentObjectCreated = false, ++ projectsPrefix, ++ status, ++ targetPrefix = "", ++ }) { ++ const resolvedAction = action || STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId) || {}; + return { + actionId, ++ cleanupStatus, ++ durationMs, + environmentFolder, + executed, + keysListed, + lastChecked: new Date().toISOString(), + message, ++ operation: resolvedAction.operation || "", ++ operationLabel: resolvedAction.label || actionId, ++ permanentObjectCreated, + projectsPrefix, + secretEditingAllowed: false, + secretsExposed: false, + status, + storageStatus: this.ownerStorageStatus(), ++ targetPrefix, + testObjectKey: objectKey, }; - 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( -+ const databaseClient = adapter.databaseClient(); -+ const versionRows = await databaseClient.query("SELECT version() AS version;"); -+ const currentRows = await databaseClient.query("SELECT current_database() AS database_name, current_schema() AS schema_name;"); -+ const countRows = await databaseClient.query( - SELECT "migrationType", count(*)::int AS coun - FROM schema_migrations - GROUP BY "migrationType" - ORDER BY "migrationType"; - `); -- const lastRows = await adapter.databaseClient().query( -+ const lastRows = await databaseClient.query( - SELECT "migrationType", "migrationName", "appliedAt" - FROM schema_migrations - ORDER BY "appliedAt" DESC, key DESC - LIMIT 1; -+`); -+ const tableCountRows = await databaseClient.query( -+SELECT count(*)::int AS table_coun -+FROM information_schema.tables -+WHERE table_schema NOT IN ('pg_catalog', 'information_schema') -+ AND table_type = 'BASE TABLE'; -+`); -+ const databaseSizeRows = await databaseClient.query( -+SELECT pg_database_size(current_database()) AS database_size_bytes, -+ pg_size_pretty(pg_database_size(current_database())) AS database_size; - `); - const counts = new Map(countRows.map((row) => [String(row.migrationType || ""), Number(row.count || 0)])); -+ const currentRow = currentRows[0] || {}; - const lastRow = lastRows[0] || {}; -- const version = String(versionRows[0]?.version || "").trim(); -- return { -+ const tableCount = Number(tableCountRows[0]?.table_count); -+ const databaseSizeBytes = Number(databaseSizeRows[0]?.database_size_bytes); -+ const connectedStatus = { - ...databaseStatus, - connectivity: "connected", - connectivityStatus: "PASS", -+ currentDatabaseName: String(currentRow.database_name || ""), -+ currentDatabaseNameStatus: currentRow.database_name ? "PASS" : "WARN", -+ currentSchema: String(currentRow.schema_name || ""), -+ currentSchemaStatus: currentRow.schema_name ? "PASS" : "WARN", -+ databaseSize: String(databaseSizeRows[0]?.database_size || ""), -+ databaseSizeBytes: Number.isFinite(databaseSizeBytes) ? databaseSizeBytes : null, -+ databaseSizeStatus: databaseSizeRows[0]?.database_size ? "PASS" : "WARN", - lastMigration: { - appliedAt: String(lastRow.appliedAt || ""), - name: String(lastRow.migrationName || ""), -@@ -4268,14 +4354,19 @@ LIMIT 1; - DML: counts.get("DML") || 0, - }, - migrationStatus: "PASS", -- message: "Current environment database connection responded through the safe Admin System Health API.", -+ message: "Current environment database connection and safe Postgres metrics responded through the Admin System Health API.", - responseTimeMs: Date.now() - startedAt, - status: databaseStatus.configured === true ? "PASS" : "WARN", -- version: version || "not available", -- versionStatus: version ? "PASS" : "WARN", -+ tableCount: Number.isFinite(tableCount) ? tableCount : null, -+ version: String(versionRows[0]?.version || "").trim() || "not available", -+ versionStatus: versionRows[0]?.version ? "PASS" : "WARN", - }; -- } catch (error) { - return { -+ ...connectedStatus, -+ postgresMetrics: systemHealthPostgresMetrics(connectedStatus, connectedStatus.lastChecked), -+ }; -+ } catch (error) { -+ const failedStatus = { - ...databaseStatus, - connectivity: "failed", - connectivityStatus: "FAIL", -@@ -4283,6 +4374,10 @@ LIMIT 1; - responseTimeMs: Date.now() - startedAt, + } + + async runStorageConnectivityAction(actionId, options = {}) { ++ if (actionId === SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID) { ++ return this.runStorageExpandedValidation(options); ++ } + const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); + if (!action) { + throw new Error(`Unknown storage connectivity action: ${actionId || "missing actionId"}.`); + } + ++ const startedAt = Date.now(); + const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; + const storage = createConfiguredProjectAssetStorage(); + const projectsPrefix = String(storage.config?.projectsPrefix || "").trim(); +@@ -4098,7 +4131,9 @@ class ApiRuntimeDataSource { + const result = await storage.listObjects(targetPrefix); + const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ durationMs: Date.now() - startedAt, + environmentFolder, + keysListed, + message: result.ok +@@ -4107,6 +4142,7 @@ class ApiRuntimeDataSource { + objectKey, + projectsPrefix, + status: result.ok ? "PASS" : "FAIL", ++ targetPrefix, + }); + } + +@@ -4116,7 +4152,9 @@ class ApiRuntimeDataSource { + : await storage.listProjectObjects(); + const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ durationMs: Date.now() - startedAt, + environmentFolder, + keysListed, + message: result.ok +@@ -4125,6 +4163,7 @@ class ApiRuntimeDataSource { + objectKey, + projectsPrefix, + status: result.ok ? "PASS" : "FAIL", ++ targetPrefix, + }); + } + +@@ -4135,14 +4174,19 @@ class ApiRuntimeDataSource { + objectKey, + }); + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ cleanupStatus: result.ok ? "requires-delete-action" : "not-created", ++ durationMs: Date.now() - startedAt, + environmentFolder, + message: result.ok + ? `Upload test object completed at ${objectKey}. + : `${result.message || "Storage upload failed."} Verify upload permission for ${targetPrefix}.`, + objectKey, ++ permanentObjectCreated: result.ok === true, + projectsPrefix, + status: result.ok ? "PASS" : "FAIL", ++ targetPrefix, + }); + } + +@@ -4151,7 +4195,9 @@ class ApiRuntimeDataSource { + const text = result.ok ? Buffer.from(result.bytes || []).toString("utf8") : ""; + const contentMatches = text === STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT; + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ durationMs: Date.now() - startedAt, + environmentFolder, + message: result.ok && contentMatches + ? `Read test object completed from ${objectKey}. +@@ -4159,32 +4205,77 @@ class ApiRuntimeDataSource { + objectKey, + projectsPrefix, + status: result.ok && contentMatches ? "PASS" : "FAIL", ++ targetPrefix, + }); + } + + const result = await storage.deleteObject(objectKey); + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ cleanupStatus: result.ok ? "completed" : "failed", ++ durationMs: Date.now() - startedAt, + environmentFolder, + message: result.ok + ? `Delete test object completed at ${objectKey}. + : `${result.message || "Storage delete failed."} Verify delete permission for ${targetPrefix}.`, + objectKey, ++ permanentObjectCreated: false, + projectsPrefix, + status: result.ok ? "PASS" : "FAIL", ++ targetPrefix, + }); + } catch (error) { + return this.storageConnectivityResult({ ++ action, + actionId: action.id, ++ durationMs: Date.now() - startedAt, + environmentFolder, + message: `Storage connectivity action failed: ${error instanceof Error ? error.message : String(error || "Unknown storage error.")}`, + objectKey, + projectsPrefix, status: "FAIL", - }; -+ return { -+ ...failedStatus, -+ postgresMetrics: systemHealthPostgresMetrics(failedStatus, failedStatus.lastChecked), -+ }; ++ targetPrefix, + }); } } -@@ -4410,6 +4505,7 @@ LIMIT 1; - const environmentIdentity = systemHealthEnvironmentIdentity(process.env, checkedAt); - const environmentMap = systemHealthEnvironmentMap(); - const databaseStatus = await this.ownerDatabaseStatus(environmentIdentity); -+ const postgresMetrics = databaseStatus.postgresMetrics || systemHealthPostgresMetrics(databaseStatus, checkedAt); - const storageStatus = this.ownerStorageStatus(); - const environmentStatus = storageProjectsPrefixStatus(); - const localApiStartup = systemHealthLocalApiStartupDiagnostics(); -@@ -4581,6 +4677,7 @@ LIMIT 1; - notificationsFoundation, - operationsHealth, - overview, -+ postgresMetrics, - pressureLabels: SYSTEM_HEALTH_LIMIT_PRESSURE_LABELS, - connectionSummary: this.ownerConnectionSummary(), - databaseStatus, ++ async runStorageExpandedValidation(options = {}) { ++ const startedAt = Date.now(); ++ const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; ++ const storage = createConfiguredProjectAssetStorage(); ++ const targetPrefix = this.storageConnectivityTargetPrefix(storage, scope); ++ const objectKey = this.storageConnectivityTestObjectKey(storage, scope); ++ const results = []; ++ for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { ++ results.push(await this.runStorageConnectivityAction(actionId, { scope })); ++ } ++ const uploadResult = results.find((result) => result.actionId === "storage-upload-test-object"); ++ const deleteResult = results.find((result) => result.actionId === "storage-delete-test-object"); ++ const permanentObjectCreated = uploadResult?.status === "PASS" && deleteResult?.status !== "PASS"; ++ const status = overallHealthStatus(results); ++ return { ++ actionId: SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID, ++ cleanupStatus: permanentObjectCreated ? "failed" : "completed", ++ durationMs: Date.now() - startedAt, ++ environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", ++ executed: true, ++ lastChecked: new Date().toISOString(), ++ message: permanentObjectCreated ++ ? `Expanded R2 validation detected a cleanup failure for ${objectKey}; review storage credentials and delete permission. ++ : `Expanded R2 validation completed against ${targetPrefix}; test object cleanup was attempted in the same validation run.`, ++ permanentObjectCreated, ++ projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), ++ secretEditingAllowed: false, ++ secretsExposed: false, ++ status, ++ storageDiagnostics: results, ++ storageStatus: this.ownerStorageStatus(), ++ targetPrefix, ++ testObjectKey: objectKey, ++ }; ++ } ++ + ownerPromotionFoundation() { + const steps = [ + { +@@ -4424,20 +4515,20 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, + } + + async adminSystemHealthStorageHealthCheck() { +- const results = []; +- for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { +- results.push(await this.runStorageConnectivityAction(actionId, { scope: "environment-folder" })); +- } ++ const validation = await this.runStorageExpandedValidation({ scope: "environment-folder" }); ++ const results = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; + return { + actionId: "storage-check", + checkedAt: new Date().toISOString(), + label: SYSTEM_HEALTH_MANUAL_ACTION_LABELS["storage-check"], +- message: "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", ++ message: validation.message || "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", ++ permanentObjectCreated: validation.permanentObjectCreated === true, + secretEditingAllowed: false, + secretsExposed: false, +- status: overallHealthStatus(results.map((result) => ({ status: result.status }))), ++ status: validation.status || overallHealthStatus(results.map((result) => ({ status: result.status }))), + storageDiagnostics: results, + storageStatus: this.ownerStorageStatus(), ++ validationDurationMs: validation.durationMs, + }; + } + diff --git a/tests/api/admin-system-health/contract.test.mjs b/tests/api/admin-system-health/contract.test.mjs -index e9e366882..25aa84b34 100644 +index 25aa84b34..54e821f13 100644 --- a/tests/api/admin-system-health/contract.test.mjs +++ b/tests/api/admin-system-health/contract.test.mjs -@@ -111,6 +111,7 @@ test("Admin System Health completion contract remains server-owned and current-e - "runtimeFeatureFlags", - "serviceHealth", - "configurationSummary", -+ "postgresMetrics", - "scheduledMonitoring", - "notificationsFoundation", - ].filter((key) => Object.hasOwn(health, key)), -@@ -121,10 +122,24 @@ test("Admin System Health completion contract remains server-owned and current-e - "runtimeFeatureFlags", - "serviceHealth", - "configurationSummary", -+ "postgresMetrics", - "scheduledMonitoring", - "notificationsFoundation", - ], - ); +@@ -146,6 +146,25 @@ test("Admin System Health completion contract remains server-owned and current-e + assert.equal(healthText.includes("/uat/projects"), false); + assert.equal(health.secretEditingAllowed, false); + assert.equal(health.secretsExposed, false); ++ const expandedStorage = await apiJson(server.baseUrl, "/api/admin/system-health/storage-connectivity-action", { ++ body: { actionId: "storage-expanded-validation" }, ++ method: "POST", ++ }); + assert.deepEqual( -+ health.postgresMetrics.rows.map((row) => row.metric), ++ expandedStorage.storageDiagnostics.map((row) => row.actionId), + [ -+ "Connection status", -+ "Database name", -+ "Current schema", -+ "Migration status", -+ "Last migration", -+ "Table count", -+ "Database size", -+ "Last checked", ++ "storage-bucket-connectivity", ++ "storage-list", ++ "storage-upload-test-object", ++ "storage-read-test-object", ++ "storage-delete-test-object", + ], + ); - const healthText = JSON.stringify(health); - assert.equal(healthText.includes("api-secret"), false); - assert.equal(healthText.includes("site-secret"), false); ++ assert.equal(expandedStorage.storageDiagnostics.every((row) => row.environmentFolder === "/dev"), true); ++ assert.equal(expandedStorage.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); ++ assert.equal(expandedStorage.permanentObjectCreated, false); ++ assert.equal(JSON.stringify(expandedStorage).includes("api-secret"), false); ++ assert.equal(JSON.stringify(expandedStorage).includes("site-secret"), false); + } finally { + await server.close(); + } diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs -index e682005b2..99fef0757 100644 +index 99fef0757..9ea40988a 100644 --- a/tests/dev-runtime/AdminHealthOperations.test.mjs +++ b/tests/dev-runtime/AdminHealthOperations.test.mjs -@@ -142,6 +142,24 @@ test("Admin can view operational health while Creator sessions are blocked", asy - 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.equal(health.postgresMetrics.secretEditingAllowed, false); -+ assert.equal(health.postgresMetrics.secretsExposed, false); -+ assert.deepEqual( -+ health.postgresMetrics.rows.map((row) => row.metric), -+ [ -+ "Connection status", -+ "Database name", -+ "Current schema", -+ "Migration status", -+ "Last migration", -+ "Table count", -+ "Database size", -+ "Last checked", -+ ], -+ ); -+ assert.equal(health.postgresMetrics.rows.every((row) => typeof row.value === "string"), true); -+ assert.equal(health.postgresMetrics.rows.some((row) => row.value === "Unavailable"), true); -+ assert.equal(health.databaseStatus.postgresMetrics.rows.length, health.postgresMetrics.rows.length); - assert.equal(health.runtimeHealth.environmentName, "Local"); - assert.equal(health.runtimeHealth.appVersion, "1.0.0"); - assert.equal(health.runtimeHealth.apiVersion, "1.0.0"); -@@ -318,6 +336,8 @@ test("Admin can view operational health while Creator sessions are blocked", asy - const healthText = JSON.stringify(health.operationsHealth); - assert.equal(healthText.includes("monthlyPriceCents"), false); - assert.equal(healthText.includes("priceCents"), false); -+ assert.equal(JSON.stringify(health.postgresMetrics).includes("postgres://"), false); -+ assert.equal(JSON.stringify(health.postgresMetrics).includes("postgresql://"), false); - assert.equal(health.secretEditingAllowed, false); - } finally { - await server.close(); +@@ -311,6 +311,12 @@ test("Admin can view operational health while Creator sessions are blocked", asy + ], + ); + assert.equal(storageAction.storageDiagnostics.every((row) => row.environmentFolder === "/local"), true); ++ assert.equal(storageAction.permanentObjectCreated, false); ++ assert.equal(typeof storageAction.validationDurationMs, "number"); ++ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); ++ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.operationLabel === "string"), true); ++ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.cleanupStatus === "string"), true); ++ assert.equal(storageAction.storageDiagnostics.every((row) => row.permanentObjectCreated === false), true); + const refreshAction = await apiJson(server.baseUrl, "/api/admin/system-health/action", { + body: { actionId: "refresh" }, + method: "POST", diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -index 52d7acf50..56733ce6a 100644 +index 56733ce6a..713344ac0 100644 --- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -@@ -217,6 +217,16 @@ test("Admin System Health renders Postgres diagnostics through the safe status A - 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://"); -+ const postgresMetricsTable = page.getByRole("table", { name: "Postgres metrics" }); -+ await expect(postgresMetricsTable).toContainText("Connection status"); -+ await expect(postgresMetricsTable).toContainText("Database name"); -+ await expect(postgresMetricsTable).toContainText("Current schema"); -+ await expect(postgresMetricsTable).toContainText("Migration status"); -+ await expect(postgresMetricsTable).toContainText("Last migration"); -+ await expect(postgresMetricsTable).toContainText("Table count"); -+ await expect(postgresMetricsTable).toContainText("Database size"); -+ await expect(postgresMetricsTable).not.toContainText("postgres://"); -+ await expect(postgresMetricsTable).not.toContainText("postgresql://"); - await expect(page.getByRole("table", { name: "Storage health" })).toContainText("Cloudflare R2"); - await expect(page.locator("[data-admin-system-health-storage-value='bucket']")).toContainText("/dev"); - await expect(page.locator("[data-admin-system-health-storage-value='list']")).toContainText("/dev"); -@@ -333,6 +343,7 @@ test("Admin System Health operations page keeps scripts and styles external", as - expect(pageSource).toContain("Runtime Health"); - expect(pageSource).toContain("Diagnostics Plan"); - expect(pageSource).toContain("Local API Startup Diagnostics"); -+ expect(pageSource).toContain("Postgres Metrics"); - expect(pageSource).toContain("Server-owned Postgres health reader"); - expect(pageSource).toContain("Server-owned Cloudflare R2 storage diagnostic"); - expect(pageSource).toContain("assets/theme-v2/js/admin-system-health.js"); +@@ -233,6 +233,10 @@ test("Admin System Health renders Postgres diagnostics through the safe status A + await expect(page.locator("[data-admin-system-health-storage-value='upload']")).toContainText("/dev"); + await expect(page.locator("[data-admin-system-health-storage-value='read']")).not.toHaveText("Health object"); + await expect(page.locator("[data-admin-system-health-storage-value='delete']")).not.toHaveText("Health object"); ++ await expect(page.locator("[data-admin-system-health-storage-timing='list']")).not.toHaveText("Loading"); ++ await expect(page.locator("[data-admin-system-health-storage-timing='upload']")).not.toHaveText("Loading"); ++ await expect(page.locator("[data-admin-system-health-storage-timing='read']")).not.toHaveText("Loading"); ++ await expect(page.locator("[data-admin-system-health-storage-timing='delete']")).not.toHaveText("Loading"); + await expect(page.getByRole("table", { name: "Runtime health" })).toContainText("Runtime Health"); + await expect(page.locator("[data-admin-system-health-runtime-health-value='environment']")).toHaveText("DEV"); + await expect(page.locator("[data-admin-system-health-runtime-health-value='appVersion']")).toHaveText("1.0.0"); +@@ -286,7 +290,7 @@ test("Admin System Health renders Postgres diagnostics through the safe status A + expect((title || ariaLabel || "").trim()).not.toEqual(""); + } + expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(true); +- expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(5); ++ expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(1); + await page.getByRole("button", { name: "Run Runtime Check" }).click(); + await expect(page.getByRole("table", { name: "Manual health action results" })).toContainText("Run Runtime Check"); + expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/action"))).toBe(true); +@@ -353,5 +357,5 @@ test("Admin System Health operations page keeps scripts and styles external", as + expect(runtimeSource).not.toContain("localStorage"); + expect(runtimeSource).not.toContain("sessionStorage"); + expect(runtimeSource).toContain("runAdminSystemHealthAction"); +- expect(runtimeSource).toContain("runAdminSystemHealthStorageConnectivityAction"); ++ expect(runtimeSource).toContain("runAdminSystemHealthStorageExpandedValidation"); + }); diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt index 7b1c51f19..7211f2375 100644 --- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt +++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt @@ -6,7 +6,9 @@ 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: -(100%) none changed - no changed runtime JS files +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 +(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Guardrail warnings: -(100%) none changed - no changed runtime JS files +(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 627ed631f..2a2982e6a 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -17,7 +17,9 @@ Exercised tool entry points detected: (78%) Theme V2 Shared JS - exercised 5 runtime JS files Changed runtime JS files covered: -(100%) none changed - no changed runtime JS files +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 +(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Files with executed line/function counts where available: (36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 @@ -27,13 +29,18 @@ Files with executed line/function counts where available: (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 (80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 -(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 +(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 (83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 (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 28/28; executed functions 4/4 +(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 Uncovered or low-coverage changed JS files: -(100%) none changed - no changed runtime JS files +(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only Changed JS files considered: -(100%) none - no changed JS files +(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage +(0%) tests/api/admin-system-health/contract.test.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 +(81%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage +(100%) src/api/admin-system-health-api-client.js - changed JS file with browser V8 coverage diff --git a/src/api/admin-system-health-api-client.js b/src/api/admin-system-health-api-client.js index 9b166d097..49101c59f 100644 --- a/src/api/admin-system-health-api-client.js +++ b/src/api/admin-system-health-api-client.js @@ -20,6 +20,10 @@ export function runAdminSystemHealthStorageConnectivityAction(actionId) { ); } +export function runAdminSystemHealthStorageExpandedValidation() { + return runAdminSystemHealthStorageConnectivityAction("storage-expanded-validation"); +} + export function runAdminSystemHealthAction(actionId) { return requireServerApiData( safeRequestServerApi("/admin/system-health/action", { diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs index 462d8b7a0..6dbb8a106 100644 --- a/src/dev-runtime/server/local-api-router.mjs +++ b/src/dev-runtime/server/local-api-router.mjs @@ -356,6 +356,7 @@ const SYSTEM_HEALTH_STORAGE_ACTION_IDS = Object.freeze([ "storage-read-test-object", "storage-delete-test-object", ]); +const SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID = "storage-expanded-validation"; const SYSTEM_HEALTH_MANUAL_ACTION_LABELS = Object.freeze({ "database-check": "Run Database Check", "full-health-check": "Run Full Health Check", @@ -4046,43 +4047,75 @@ class ApiRuntimeDataSource { storageConnectivityConfigFailure(actionId, storage, scope = "project-prefix") { const missing = storage.config?.missingKeys?.join(", ") || storage.config?.validationError || "storage configuration incomplete"; + const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); return { actionId, + cleanupStatus: "not-run", + durationMs: 0, environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", executed: false, lastChecked: new Date().toISOString(), message: `Storage connectivity requires configured storage and GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX before this action can run. Missing or invalid: ${missing}.`, + operation: action?.operation || "configuration", + operationLabel: action?.label || "Storage configuration", + permanentObjectCreated: false, projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), secretEditingAllowed: false, secretsExposed: false, status: "FAIL", storageStatus: this.ownerStorageStatus(), + targetPrefix: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : String(storage.config?.projectsPrefix || "").trim(), }; } - storageConnectivityResult({ actionId, environmentFolder = "", executed = true, keysListed = 0, message, objectKey, projectsPrefix, status }) { + storageConnectivityResult({ + action, + actionId, + cleanupStatus = "not-applicable", + durationMs = 0, + environmentFolder = "", + executed = true, + keysListed = 0, + message, + objectKey, + permanentObjectCreated = false, + projectsPrefix, + status, + targetPrefix = "", + }) { + const resolvedAction = action || STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId) || {}; return { actionId, + cleanupStatus, + durationMs, environmentFolder, executed, keysListed, lastChecked: new Date().toISOString(), message, + operation: resolvedAction.operation || "", + operationLabel: resolvedAction.label || actionId, + permanentObjectCreated, projectsPrefix, secretEditingAllowed: false, secretsExposed: false, status, storageStatus: this.ownerStorageStatus(), + targetPrefix, testObjectKey: objectKey, }; } async runStorageConnectivityAction(actionId, options = {}) { + if (actionId === SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID) { + return this.runStorageExpandedValidation(options); + } const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); if (!action) { throw new Error(`Unknown storage connectivity action: ${actionId || "missing actionId"}.`); } + const startedAt = Date.now(); const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; const storage = createConfiguredProjectAssetStorage(); const projectsPrefix = String(storage.config?.projectsPrefix || "").trim(); @@ -4098,7 +4131,9 @@ class ApiRuntimeDataSource { const result = await storage.listObjects(targetPrefix); const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; return this.storageConnectivityResult({ + action, actionId: action.id, + durationMs: Date.now() - startedAt, environmentFolder, keysListed, message: result.ok @@ -4107,6 +4142,7 @@ class ApiRuntimeDataSource { objectKey, projectsPrefix, status: result.ok ? "PASS" : "FAIL", + targetPrefix, }); } @@ -4116,7 +4152,9 @@ class ApiRuntimeDataSource { : await storage.listProjectObjects(); const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; return this.storageConnectivityResult({ + action, actionId: action.id, + durationMs: Date.now() - startedAt, environmentFolder, keysListed, message: result.ok @@ -4125,6 +4163,7 @@ class ApiRuntimeDataSource { objectKey, projectsPrefix, status: result.ok ? "PASS" : "FAIL", + targetPrefix, }); } @@ -4135,14 +4174,19 @@ class ApiRuntimeDataSource { objectKey, }); return this.storageConnectivityResult({ + action, actionId: action.id, + cleanupStatus: result.ok ? "requires-delete-action" : "not-created", + durationMs: Date.now() - startedAt, environmentFolder, message: result.ok ? `Upload test object completed at ${objectKey}.` : `${result.message || "Storage upload failed."} Verify upload permission for ${targetPrefix}.`, objectKey, + permanentObjectCreated: result.ok === true, projectsPrefix, status: result.ok ? "PASS" : "FAIL", + targetPrefix, }); } @@ -4151,7 +4195,9 @@ class ApiRuntimeDataSource { const text = result.ok ? Buffer.from(result.bytes || []).toString("utf8") : ""; const contentMatches = text === STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT; return this.storageConnectivityResult({ + action, actionId: action.id, + durationMs: Date.now() - startedAt, environmentFolder, message: result.ok && contentMatches ? `Read test object completed from ${objectKey}.` @@ -4159,32 +4205,77 @@ class ApiRuntimeDataSource { objectKey, projectsPrefix, status: result.ok && contentMatches ? "PASS" : "FAIL", + targetPrefix, }); } const result = await storage.deleteObject(objectKey); return this.storageConnectivityResult({ + action, actionId: action.id, + cleanupStatus: result.ok ? "completed" : "failed", + durationMs: Date.now() - startedAt, environmentFolder, message: result.ok ? `Delete test object completed at ${objectKey}.` : `${result.message || "Storage delete failed."} Verify delete permission for ${targetPrefix}.`, objectKey, + permanentObjectCreated: false, projectsPrefix, status: result.ok ? "PASS" : "FAIL", + targetPrefix, }); } catch (error) { return this.storageConnectivityResult({ + action, actionId: action.id, + durationMs: Date.now() - startedAt, environmentFolder, message: `Storage connectivity action failed: ${error instanceof Error ? error.message : String(error || "Unknown storage error.")}`, objectKey, projectsPrefix, status: "FAIL", + targetPrefix, }); } } + async runStorageExpandedValidation(options = {}) { + const startedAt = Date.now(); + const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; + const storage = createConfiguredProjectAssetStorage(); + const targetPrefix = this.storageConnectivityTargetPrefix(storage, scope); + const objectKey = this.storageConnectivityTestObjectKey(storage, scope); + const results = []; + for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { + results.push(await this.runStorageConnectivityAction(actionId, { scope })); + } + const uploadResult = results.find((result) => result.actionId === "storage-upload-test-object"); + const deleteResult = results.find((result) => result.actionId === "storage-delete-test-object"); + const permanentObjectCreated = uploadResult?.status === "PASS" && deleteResult?.status !== "PASS"; + const status = overallHealthStatus(results); + return { + actionId: SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID, + cleanupStatus: permanentObjectCreated ? "failed" : "completed", + durationMs: Date.now() - startedAt, + environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", + executed: true, + lastChecked: new Date().toISOString(), + message: permanentObjectCreated + ? `Expanded R2 validation detected a cleanup failure for ${objectKey}; review storage credentials and delete permission.` + : `Expanded R2 validation completed against ${targetPrefix}; test object cleanup was attempted in the same validation run.`, + permanentObjectCreated, + projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), + secretEditingAllowed: false, + secretsExposed: false, + status, + storageDiagnostics: results, + storageStatus: this.ownerStorageStatus(), + targetPrefix, + testObjectKey: objectKey, + }; + } + ownerPromotionFoundation() { const steps = [ { @@ -4424,20 +4515,20 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, } async adminSystemHealthStorageHealthCheck() { - const results = []; - for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { - results.push(await this.runStorageConnectivityAction(actionId, { scope: "environment-folder" })); - } + const validation = await this.runStorageExpandedValidation({ scope: "environment-folder" }); + const results = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; return { actionId: "storage-check", checkedAt: new Date().toISOString(), label: SYSTEM_HEALTH_MANUAL_ACTION_LABELS["storage-check"], - message: "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", + message: validation.message || "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", + permanentObjectCreated: validation.permanentObjectCreated === true, secretEditingAllowed: false, secretsExposed: false, - status: overallHealthStatus(results.map((result) => ({ status: result.status }))), + status: validation.status || overallHealthStatus(results.map((result) => ({ status: result.status }))), storageDiagnostics: results, storageStatus: this.ownerStorageStatus(), + validationDurationMs: validation.durationMs, }; } diff --git a/tests/api/admin-system-health/contract.test.mjs b/tests/api/admin-system-health/contract.test.mjs index 25aa84b34..54e821f13 100644 --- a/tests/api/admin-system-health/contract.test.mjs +++ b/tests/api/admin-system-health/contract.test.mjs @@ -146,6 +146,25 @@ test("Admin System Health completion contract remains server-owned and current-e assert.equal(healthText.includes("/uat/projects"), false); assert.equal(health.secretEditingAllowed, false); assert.equal(health.secretsExposed, false); + const expandedStorage = await apiJson(server.baseUrl, "/api/admin/system-health/storage-connectivity-action", { + body: { actionId: "storage-expanded-validation" }, + method: "POST", + }); + assert.deepEqual( + expandedStorage.storageDiagnostics.map((row) => row.actionId), + [ + "storage-bucket-connectivity", + "storage-list", + "storage-upload-test-object", + "storage-read-test-object", + "storage-delete-test-object", + ], + ); + assert.equal(expandedStorage.storageDiagnostics.every((row) => row.environmentFolder === "/dev"), true); + assert.equal(expandedStorage.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); + assert.equal(expandedStorage.permanentObjectCreated, false); + assert.equal(JSON.stringify(expandedStorage).includes("api-secret"), false); + assert.equal(JSON.stringify(expandedStorage).includes("site-secret"), false); } finally { await server.close(); } diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs index 99fef0757..9ea40988a 100644 --- a/tests/dev-runtime/AdminHealthOperations.test.mjs +++ b/tests/dev-runtime/AdminHealthOperations.test.mjs @@ -311,6 +311,12 @@ test("Admin can view operational health while Creator sessions are blocked", asy ], ); assert.equal(storageAction.storageDiagnostics.every((row) => row.environmentFolder === "/local"), true); + assert.equal(storageAction.permanentObjectCreated, false); + assert.equal(typeof storageAction.validationDurationMs, "number"); + assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); + assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.operationLabel === "string"), true); + assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.cleanupStatus === "string"), true); + assert.equal(storageAction.storageDiagnostics.every((row) => row.permanentObjectCreated === false), true); const refreshAction = await apiJson(server.baseUrl, "/api/admin/system-health/action", { body: { actionId: "refresh" }, method: "POST", diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs index 56733ce6a..713344ac0 100644 --- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs @@ -233,6 +233,10 @@ test("Admin System Health renders Postgres diagnostics through the safe status A await expect(page.locator("[data-admin-system-health-storage-value='upload']")).toContainText("/dev"); await expect(page.locator("[data-admin-system-health-storage-value='read']")).not.toHaveText("Health object"); await expect(page.locator("[data-admin-system-health-storage-value='delete']")).not.toHaveText("Health object"); + await expect(page.locator("[data-admin-system-health-storage-timing='list']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-storage-timing='upload']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-storage-timing='read']")).not.toHaveText("Loading"); + await expect(page.locator("[data-admin-system-health-storage-timing='delete']")).not.toHaveText("Loading"); await expect(page.getByRole("table", { name: "Runtime health" })).toContainText("Runtime Health"); await expect(page.locator("[data-admin-system-health-runtime-health-value='environment']")).toHaveText("DEV"); await expect(page.locator("[data-admin-system-health-runtime-health-value='appVersion']")).toHaveText("1.0.0"); @@ -286,7 +290,7 @@ test("Admin System Health renders Postgres diagnostics through the safe status A expect((title || ariaLabel || "").trim()).not.toEqual(""); } expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(true); - expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(5); + expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(1); await page.getByRole("button", { name: "Run Runtime Check" }).click(); await expect(page.getByRole("table", { name: "Manual health action results" })).toContainText("Run Runtime Check"); expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/action"))).toBe(true); @@ -353,5 +357,5 @@ test("Admin System Health operations page keeps scripts and styles external", as expect(runtimeSource).not.toContain("localStorage"); expect(runtimeSource).not.toContain("sessionStorage"); expect(runtimeSource).toContain("runAdminSystemHealthAction"); - expect(runtimeSource).toContain("runAdminSystemHealthStorageConnectivityAction"); + expect(runtimeSource).toContain("runAdminSystemHealthStorageExpandedValidation"); });
Storage Health - Cloudflare R2