Skip to content

Commit 95f37c3

Browse files
committed
Consolidate operations health and storage connectivity into Admin System Health - PR_26168_233-through-235-system-health-operations-stack
1 parent 7a42c64 commit 95f37c3

16 files changed

Lines changed: 1599 additions & 876 deletions

admin/system-health.html

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<div class="container">
1919
<div class="kicker">Admin</div>
2020
<h1>System Health</h1>
21-
<p class="lede">Review safe runtime readiness across account, Local DB, R2 storage, migrations, package planning, and promotion safety.</p>
21+
<p class="lede">Review safe runtime readiness across account, Local DB, R2 storage, migrations, package planning, promotion safety, and storage connectivity.</p>
2222
</div>
2323
</section>
2424
<section class="section">
@@ -48,6 +48,15 @@ <h2>Admin</h2>
4848
<p>Project package readiness</p>
4949
</div>
5050
</details>
51+
<details class="vertical-accordion" open>
52+
<summary>Storage Connectivity</summary>
53+
<div class="accordion-body content-stack">
54+
<button class="btn btn--compact" type="button" data-admin-system-health-storage-action="storage-list">List</button>
55+
<button class="btn btn--compact" type="button" data-admin-system-health-storage-action="storage-write-test-object">Write Test Object</button>
56+
<button class="btn btn--compact" type="button" data-admin-system-health-storage-action="storage-read-test-object">Read Test Object</button>
57+
<button class="btn btn--compact" type="button" data-admin-system-health-storage-action="storage-delete-test-object">Delete Test Object</button>
58+
</div>
59+
</details>
5160
</div>
5261
</aside>
5362
<div data-tool-display-mode data-asset-root="assets/theme-v2/images" data-tool-slug="admin-system-health" data-tool-icon-src="assets/theme-v2/images/image-missing.svg" data-tool-character-src="assets/theme-v2/images/image-missing.svg"></div>
@@ -89,6 +98,51 @@ <h2 id="admin-system-health-title">System Health Overview</h2>
8998
</tbody>
9099
</table>
91100
</div>
101+
<div class="table-wrapper">
102+
<table class="data-table" aria-label="Configured connection summary">
103+
<caption>Configured Connections</caption>
104+
<thead>
105+
<tr>
106+
<th scope="col">Connection</th>
107+
<th scope="col">Status</th>
108+
<th scope="col">Mode</th>
109+
</tr>
110+
</thead>
111+
<tbody data-admin-system-health-connection-summary>
112+
<tr><td>Account</td><td>Loading</td><td>Loading configured connection summary.</td></tr>
113+
</tbody>
114+
</table>
115+
</div>
116+
<div class="table-wrapper">
117+
<table class="data-table" aria-label="System health database status">
118+
<caption>Database Status</caption>
119+
<thead>
120+
<tr>
121+
<th scope="col">Field</th>
122+
<th scope="col">Status</th>
123+
<th scope="col">Value</th>
124+
</tr>
125+
</thead>
126+
<tbody data-admin-system-health-database-status-rows>
127+
<tr><td>Connection Configured</td><td>Loading</td><td>Loading database status.</td></tr>
128+
</tbody>
129+
</table>
130+
</div>
131+
<div class="table-wrapper">
132+
<table class="data-table" aria-label="System health storage status">
133+
<caption>Project Asset Storage Status</caption>
134+
<thead>
135+
<tr>
136+
<th scope="col">Field</th>
137+
<th scope="col">Status</th>
138+
<th scope="col">Value</th>
139+
</tr>
140+
</thead>
141+
<tbody data-admin-system-health-storage-status-rows>
142+
<tr><td>Storage Configured</td><td>Loading</td><td>Loading storage status.</td></tr>
143+
</tbody>
144+
</table>
145+
</div>
92146
<div class="table-wrapper">
93147
<table class="data-table" aria-label="R2 operational readiness">
94148
<caption>R2 Operational Readiness</caption>
@@ -106,6 +160,22 @@ <h2 id="admin-system-health-title">System Health Overview</h2>
106160
</tbody>
107161
</table>
108162
</div>
163+
<div class="table-wrapper">
164+
<table class="data-table" aria-label="System health storage connectivity results">
165+
<caption>Storage Connectivity Results</caption>
166+
<thead>
167+
<tr>
168+
<th scope="col">Action</th>
169+
<th scope="col">Status</th>
170+
<th scope="col">Executed</th>
171+
<th scope="col">Message</th>
172+
</tr>
173+
</thead>
174+
<tbody data-admin-system-health-storage-connectivity-result-rows>
175+
<tr><td>startup</td><td>SKIP</td><td>no</td><td>Storage connectivity startup has not run yet.</td></tr>
176+
</tbody>
177+
</table>
178+
</div>
109179
<div class="table-wrapper">
110180
<table class="data-table" aria-label="System health details">
111181
<caption>System Health Details</caption>
@@ -151,6 +221,12 @@ <h2>Inspector</h2>
151221
<p class="status" role="status" data-admin-system-health-status>Loading Admin System Health.</p>
152222
</div>
153223
</details>
224+
<details class="vertical-accordion" open>
225+
<summary>Storage Connectivity Status</summary>
226+
<div class="accordion-body content-stack">
227+
<p class="status" role="status" data-admin-system-health-storage-connectivity-status>Storage connectivity startup pending.</p>
228+
</div>
229+
</details>
154230
<details class="vertical-accordion" open>
155231
<summary>Related Pages</summary>
156232
<div class="accordion-body content-stack">

assets/theme-v2/js/admin-system-health.js

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,50 @@
11
import {
2-
readAdminSystemHealthStatus
2+
readAdminSystemHealthStatus,
3+
runAdminSystemHealthStorageConnectivityAction
34
} from "../../../src/engine/api/admin-system-health-api-client.js";
45

6+
const STORAGE_CONNECTIVITY_ACTIONS = Object.freeze([
7+
"storage-list",
8+
"storage-write-test-object",
9+
"storage-read-test-object",
10+
"storage-delete-test-object",
11+
]);
12+
513
class AdminSystemHealthController {
614
constructor(root) {
715
this.root = root;
16+
this.connectionSummary = root.querySelector("[data-admin-system-health-connection-summary]");
17+
this.databaseStatusRows = root.querySelector("[data-admin-system-health-database-status-rows]");
818
this.details = root.querySelector("[data-admin-system-health-detail-rows]");
919
this.limits = root.querySelector("[data-admin-system-health-limit-rows]");
1020
this.overview = root.querySelector("[data-admin-system-health-overview-rows]");
1121
this.r2Readiness = root.querySelector("[data-admin-system-health-r2-readiness-rows]");
1222
this.status = root.querySelector("[data-admin-system-health-status]");
23+
this.storageActionButtons = Array.from(root.querySelectorAll("[data-admin-system-health-storage-action]"));
24+
this.storageConnectivityRows = root.querySelector("[data-admin-system-health-storage-connectivity-result-rows]");
25+
this.storageConnectivityStatus = root.querySelector("[data-admin-system-health-storage-connectivity-status]");
26+
this.storageStatusRows = root.querySelector("[data-admin-system-health-storage-status-rows]");
1327
this.summary = root.querySelector("[data-admin-system-health-summary-rows]");
1428
}
1529

1630
init() {
17-
if (!this.details || !this.limits || !this.overview || !this.r2Readiness || !this.status || !this.summary) {
31+
if (!this.connectionSummary || !this.databaseStatusRows || !this.details || !this.limits || !this.overview || !this.r2Readiness || !this.status || !this.storageConnectivityRows || !this.storageConnectivityStatus || !this.storageStatusRows || !this.summary) {
1832
return;
1933
}
34+
this.storageActionButtons.forEach((button) => {
35+
button.addEventListener("click", () => this.runStorageConnectivityAction(button.dataset.adminSystemHealthStorageAction));
36+
});
2037
this.load();
2138
}
2239

2340
setStatus(status, message) {
2441
this.status.textContent = `${status}: ${message}`;
2542
}
2643

44+
setStorageConnectivityStatus(status, message) {
45+
this.storageConnectivityStatus.textContent = `${status}: ${message}`;
46+
}
47+
2748
createRow(values) {
2849
const row = document.createElement("tr");
2950
values.forEach((value) => {
@@ -57,6 +78,36 @@ class AdminSystemHealthController {
5778
]));
5879
}
5980

81+
renderConnectionSummary(summary = {}) {
82+
this.connectionSummary.replaceChildren();
83+
[
84+
["Account", summary.account?.status || "unknown", summary.account?.configured === true ? "configured" : "not configured"],
85+
["Product Data / Local DB", summary.productData?.status || "unknown", summary.productData?.configured === true ? "configured" : "not configured"],
86+
["Project Asset Storage / R2", summary.projectAssetStorage?.status || "unknown", summary.projectAssetStorage?.configured === true ? "configured" : "not configured"],
87+
["Environment Switching", summary.environmentSwitching || "manual-env-change-and-server-restart", "manual"],
88+
["Secrets", summary.secretsExposed === true ? "exposed" : "not exposed", "read-only summary"],
89+
].forEach((row) => {
90+
this.connectionSummary.append(this.createRow(row));
91+
});
92+
}
93+
94+
renderDatabaseStatus(databaseStatus = {}) {
95+
const migrationCounts = databaseStatus.migrationCounts || {};
96+
const lastMigration = databaseStatus.lastMigration || {};
97+
this.databaseStatusRows.replaceChildren();
98+
[
99+
["Connection Configured", databaseStatus.configured === true ? "PASS" : "WARN", databaseStatus.configured === true ? "yes" : "no"],
100+
["Database Host", databaseStatus.hostStatus || "WARN", databaseStatus.host || "not configured"],
101+
["Database Port", databaseStatus.portStatus || "WARN", databaseStatus.port ? String(databaseStatus.port) : "not configured"],
102+
["Database Name", databaseStatus.databaseNameStatus || "WARN", databaseStatus.databaseName || "not configured"],
103+
["SSL Mode", databaseStatus.sslModeStatus || "WARN", databaseStatus.sslMode || "not configured"],
104+
["Migration Counts", databaseStatus.migrationStatus || "WARN", `DDL=${migrationCounts.DDL || 0}; DML=${migrationCounts.DML || 0}`],
105+
["Last Migration", databaseStatus.lastMigrationStatus || "WARN", lastMigration.name && lastMigration.appliedAt ? `${lastMigration.type || "unknown"} ${lastMigration.name} at ${lastMigration.appliedAt}` : "not recorded"],
106+
].forEach((row) => {
107+
this.databaseStatusRows.append(this.createRow(row));
108+
});
109+
}
110+
60111
renderDetails(rows) {
61112
this.details.replaceChildren();
62113
rows.forEach((row) => {
@@ -69,6 +120,20 @@ class AdminSystemHealthController {
69120
});
70121
}
71122

123+
renderStorageStatus(storageStatus = {}) {
124+
this.storageStatusRows.replaceChildren();
125+
[
126+
["Storage Configured", storageStatus.configured === true ? "PASS" : "WARN", storageStatus.configured === true ? "yes" : "no"],
127+
["Storage Endpoint", storageStatus.endpointStatus || "WARN", storageStatus.endpoint || "not configured"],
128+
["Storage Bucket", storageStatus.bucketStatus || "WARN", storageStatus.bucket || "not configured"],
129+
["Projects Prefix", storageStatus.projectsPrefixStatus || "WARN", storageStatus.projectsPrefix || "not configured"],
130+
["Access Key", storageStatus.accessKeyStatus || "WARN", storageStatus.accessKeyConfigured === true ? "configured; value hidden" : "not configured"],
131+
["Secret Key", storageStatus.secretKeyStatus || "WARN", storageStatus.secretKeyConfigured === true ? "configured; value hidden" : "not configured"],
132+
].forEach((row) => {
133+
this.storageStatusRows.append(this.createRow(row));
134+
});
135+
}
136+
72137
renderLimits(rows) {
73138
this.limits.replaceChildren();
74139
rows.forEach((row) => {
@@ -96,28 +161,87 @@ class AdminSystemHealthController {
96161
});
97162
}
98163

164+
appendStorageConnectivityResult(result = {}) {
165+
this.storageConnectivityRows.prepend(this.createRow([
166+
result.actionId || "storage-connectivity",
167+
result.status || "WARN",
168+
result.executed === true ? "yes" : "no",
169+
result.message || "No storage connectivity message returned.",
170+
]));
171+
}
172+
173+
clearStorageConnectivityResults() {
174+
this.storageConnectivityRows.replaceChildren();
175+
}
176+
177+
runStorageConnectivityAction(actionId) {
178+
try {
179+
const result = runAdminSystemHealthStorageConnectivityAction(actionId);
180+
this.appendStorageConnectivityResult(result);
181+
this.setStorageConnectivityStatus(result.status || "WARN", result.message || "Storage connectivity action returned no message.");
182+
return result;
183+
} catch (error) {
184+
const result = {
185+
actionId: actionId || "storage-connectivity",
186+
executed: false,
187+
message: error instanceof Error ? error.message : "Storage connectivity action failed.",
188+
status: "FAIL",
189+
};
190+
this.appendStorageConnectivityResult(result);
191+
this.setStorageConnectivityStatus("FAIL", result.message);
192+
return result;
193+
}
194+
}
195+
196+
runStartupStorageConnectivity() {
197+
this.clearStorageConnectivityResults();
198+
this.setStorageConnectivityStatus("SKIP", "Storage connectivity startup running.");
199+
const results = STORAGE_CONNECTIVITY_ACTIONS.map((actionId) => this.runStorageConnectivityAction(actionId));
200+
const failed = results.find((result) => result.status === "FAIL");
201+
const warned = results.find((result) => result.status === "WARN");
202+
const skipped = results.find((result) => result.status === "SKIP");
203+
if (failed) {
204+
this.setStorageConnectivityStatus("FAIL", "Storage connectivity startup found one or more failures. Review result diagnostics.");
205+
} else if (warned) {
206+
this.setStorageConnectivityStatus("WARN", "Storage connectivity startup found one or more warnings. Review result diagnostics.");
207+
} else if (skipped) {
208+
this.setStorageConnectivityStatus("SKIP", "Storage connectivity startup skipped one or more actions. Review result diagnostics.");
209+
} else {
210+
this.setStorageConnectivityStatus("PASS", "Storage connectivity startup completed List, Write Test Object, Read Test Object, and Delete Test Object.");
211+
}
212+
}
213+
99214
load() {
100215
try {
101216
const payload = readAdminSystemHealthStatus();
102217
this.renderSummary(payload.summary || {});
103218
this.renderOverview(Array.isArray(payload.overview) ? payload.overview : []);
219+
this.renderConnectionSummary(payload.connectionSummary || {});
220+
this.renderDatabaseStatus(payload.databaseStatus || {});
221+
this.renderStorageStatus(payload.storageStatus || {});
104222
this.renderDetails(Array.isArray(payload.details) ? payload.details : []);
105223
this.renderLimits(Array.isArray(payload.limits) ? payload.limits : []);
106224
this.renderR2Readiness(payload.r2Readiness || {});
107225
this.setStatus(payload.status || "WARN", payload.message || "Admin System Health loaded.");
226+
this.runStartupStorageConnectivity();
108227
} catch (error) {
109228
this.renderSummary({ counts: { FAIL: 1, PASS: 0, WARN: 0 }, lastRefreshAt: "not available", score: 0, status: "FAIL" });
229+
this.renderConnectionSummary({});
230+
this.renderDatabaseStatus({});
110231
this.renderOverview([
111232
{
112233
area: "System Health",
113234
status: "FAIL",
114235
summary: error instanceof Error ? error.message : "Admin System Health unavailable.",
115236
},
116237
]);
238+
this.renderStorageStatus({});
117239
this.renderDetails([]);
118240
this.renderLimits([]);
119241
this.renderR2Readiness({ rows: [] });
242+
this.clearStorageConnectivityResults();
120243
this.setStatus("FAIL", error instanceof Error ? error.message : "Admin System Health unavailable.");
244+
this.setStorageConnectivityStatus("SKIP", "Storage connectivity startup skipped because Admin System Health failed to load.");
121245
}
122246
}
123247
}

0 commit comments

Comments
 (0)