feat: add system health diagnostics
This commit is contained in:
1 parent
0dd26a3f24
commit
c3501b987e
17 files changed
+1335
-741
No files matched your search
@@ -0,0 +1,164 @@
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
const notice = document.getElementById("healthNotice");
|
||||
const content = document.getElementById("healthContent");
|
||||
const refresh = document.getElementById("healthRefresh");
|
||||
|
||||
function setText(id, value, fallback) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = value === null || value === undefined || value === ""
|
||||
? (fallback || "Unavailable") : String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
return typeof value === "number" ? value.toLocaleString() : "Unavailable";
|
||||
}
|
||||
|
||||
function timestamp(value) {
|
||||
if (!value) return "Unavailable";
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.valueOf()) ? "Unavailable" : date.toLocaleString();
|
||||
}
|
||||
|
||||
function factualAge(value) {
|
||||
if (!value) return "Unavailable";
|
||||
const observed = new Date(value);
|
||||
if (Number.isNaN(observed.valueOf())) return "Unavailable";
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - observed.valueOf()) / 1000));
|
||||
if (seconds < 60) return seconds + " seconds ago";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return minutes + (minutes === 1 ? " minute ago" : " minutes ago");
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return hours + (hours === 1 ? " hour ago" : " hours ago");
|
||||
const days = Math.floor(hours / 24);
|
||||
return days + (days === 1 ? " day ago" : " days ago");
|
||||
}
|
||||
|
||||
function replaceFacts(id, facts) {
|
||||
const list = document.getElementById(id);
|
||||
list.replaceChildren();
|
||||
for (const [label, value] of facts) {
|
||||
const row = document.createElement("div");
|
||||
const term = document.createElement("dt");
|
||||
const detail = document.createElement("dd");
|
||||
term.textContent = label;
|
||||
detail.textContent = value;
|
||||
row.append(term, detail);
|
||||
list.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function render(payload) {
|
||||
const data = payload.data;
|
||||
const app = data.application;
|
||||
const postgres = data.postgresql;
|
||||
const migration = postgres.migration;
|
||||
const telemetry = postgres.telemetry;
|
||||
setText("healthAppStatus", app.status);
|
||||
setText("healthEnvironment", app.environment);
|
||||
setText("healthBuildCommit", app.build_commit);
|
||||
setText("healthBuildVersion", app.build_version);
|
||||
setText("healthServerTime", timestamp(app.server_time));
|
||||
setText("healthMigrationState", migration.state);
|
||||
setText("healthCurrentRevision", migration.current_revision);
|
||||
setText("healthExpectedRevision", migration.expected_revision);
|
||||
setText("healthPostgresqlStatus", postgres.status);
|
||||
setText("healthRedisStatus", data.redis.status);
|
||||
setText("healthSnapshotCount", telemetry ? number(telemetry.snapshot_count) : null);
|
||||
setText("healthOldestObservation", telemetry ? timestamp(telemetry.oldest_observed_at) : null);
|
||||
setText("healthLatestObservation", telemetry ? timestamp(telemetry.latest_observed_at) : null);
|
||||
setText("healthObservationAge", telemetry ? factualAge(telemetry.latest_observed_at) : null);
|
||||
|
||||
const fleet = postgres.fleet;
|
||||
replaceFacts("healthFleetFacts", fleet ? [
|
||||
["Total miners", number(fleet.total_miners)],
|
||||
["Without Asset ID", number(fleet.miners_without_asset_id)],
|
||||
["Unmapped", number(fleet.unmapped_miners)],
|
||||
["In multiple positions", number(fleet.miners_in_multiple_positions)],
|
||||
["Duplicate current MAC groups", number(fleet.duplicate_current_mac_groups)]
|
||||
] : [["Diagnostics", "Unavailable"]]);
|
||||
const locations = postgres.locations;
|
||||
replaceFacts("healthLocationFacts", locations ? [
|
||||
["Sites", number(locations.sites)], ["Rooms", number(locations.rooms)],
|
||||
["Positions", number(locations.positions)],
|
||||
["Occupied", number(locations.occupied_positions)],
|
||||
["Empty", number(locations.empty_positions)],
|
||||
["Legacy unscoped", number(locations.unscoped_legacy_positions)],
|
||||
["Expected MAC mismatches", number(locations.expected_mac_mismatches)]
|
||||
] : [["Diagnostics", "Unavailable"]]);
|
||||
const repairs = postgres.repairs;
|
||||
replaceFacts("healthRepairFacts", repairs ? [
|
||||
["Active modular repairs", number(repairs.active_modular_repairs)],
|
||||
["Duplicate active groups", number(repairs.duplicate_active_repair_groups)],
|
||||
["Legacy repair only", number(repairs.legacy_repair_only_miners)],
|
||||
["Lifecycle repair without active repair", number(repairs.lifecycle_repair_without_active_repair)],
|
||||
["Active repair lifecycle mismatches", number(repairs.active_repair_lifecycle_mismatches)],
|
||||
["Waiting placement", number(repairs.waiting_placement_records)],
|
||||
["Repair events", number(repairs.repair_events)],
|
||||
["Repair parts", number(repairs.repair_parts)]
|
||||
] : [["Diagnostics", "Unavailable"]]);
|
||||
const retention = postgres.retention;
|
||||
replaceFacts("healthRetentionFacts", retention ? [
|
||||
["Raw telemetry retention", retention.raw_retention_enabled ? "Enabled" : "Not enabled"],
|
||||
["Hourly rollups", retention.hourly.table_present ? "Present; manual population" : "Unavailable"],
|
||||
["Hourly latest bucket", timestamp(retention.hourly.latest_bucket)],
|
||||
["Daily rollups", retention.daily.table_present ? "Present; manual population" : "Unavailable"],
|
||||
["Daily latest bucket", timestamp(retention.daily.latest_bucket)],
|
||||
["Latest recorded run", timestamp(retention.latest_recorded_run_at)],
|
||||
["Latest run mode", retention.latest_recorded_run_mode || "None recorded"],
|
||||
["Completeness", "Not guaranteed"]
|
||||
] : [["Diagnostics", "Unavailable"]]);
|
||||
|
||||
const tableBody = document.getElementById("healthTableCounts");
|
||||
tableBody.replaceChildren();
|
||||
const rows = postgres.table_counts || [];
|
||||
for (const table of rows) {
|
||||
const row = document.createElement("tr");
|
||||
const name = document.createElement("th");
|
||||
const count = document.createElement("td");
|
||||
name.scope = "row";
|
||||
name.textContent = table.name;
|
||||
count.textContent = number(table.row_count);
|
||||
row.append(name, count);
|
||||
tableBody.append(row);
|
||||
}
|
||||
if (!rows.length) {
|
||||
const row = document.createElement("tr");
|
||||
const cell = document.createElement("td");
|
||||
cell.colSpan = 2;
|
||||
cell.textContent = "Table diagnostics unavailable.";
|
||||
row.append(cell);
|
||||
tableBody.append(row);
|
||||
}
|
||||
content.hidden = false;
|
||||
notice.dataset.state = payload.meta.partial ? "partial" : "ready";
|
||||
notice.textContent = payload.meta.partial
|
||||
? "Diagnostics loaded with unavailable sections: " + payload.meta.unavailable_sections.join(", ") + "."
|
||||
: "Diagnostics refreshed at " + timestamp(payload.meta.generated_at) + ".";
|
||||
}
|
||||
|
||||
async function load() {
|
||||
refresh.disabled = true;
|
||||
notice.dataset.state = "loading";
|
||||
notice.textContent = "Loading diagnostics.";
|
||||
try {
|
||||
const response = await fetch("/api/system/health-summary", {
|
||||
credentials: "same-origin", headers: {"Accept": "application/json"}
|
||||
});
|
||||
const payload = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) throw new Error("System Health request failed");
|
||||
render(payload);
|
||||
} catch (error) {
|
||||
notice.dataset.state = "error";
|
||||
notice.textContent = "System Health diagnostics could not be loaded.";
|
||||
} finally {
|
||||
refresh.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
refresh.addEventListener("click", load);
|
||||
load();
|
||||
}());
|
||||
Reference in new issue
Block a user