diff --git a/backend/app/config.py b/backend/app/config.py
index ba7459e..32348d6 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -5,6 +5,8 @@ class Settings(BaseSettings):
app_name: str = "MinerOps Console"
app_env: str = "development"
app_timezone: str = "America/Winnipeg"
+ minerops_build_commit: str | None = None
+ minerops_build_version: str | None = None
postgres_db: str = "minerops"
postgres_user: str = "minerops"
diff --git a/backend/app/frontend/static/system-health.css b/backend/app/frontend/static/system-health.css
new file mode 100644
index 0000000..3b5c45e
--- /dev/null
+++ b/backend/app/frontend/static/system-health.css
@@ -0,0 +1,139 @@
+.health-toolbar,
+.health-section-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.health-toolbar {
+ margin-bottom: 12px;
+}
+
+.health-toolbar h2,
+.health-toolbar p,
+.health-section h2,
+.health-section h3 {
+ margin-top: 0;
+}
+
+.health-toolbar p {
+ margin-bottom: 0;
+ color: var(--muted-text, #52606d);
+}
+
+.health-notice {
+ border-left: 4px solid #64748b;
+ background: #f1f5f9;
+ margin: 0 0 14px;
+ padding: 10px 12px;
+}
+
+.health-notice[data-state="partial"] {
+ border-left-color: #b45309;
+ background: #fff7ed;
+}
+
+.health-notice[data-state="error"] {
+ border-left-color: #b91c1c;
+ background: #fef2f2;
+}
+
+.health-section {
+ border-top: 1px solid #cbd5e1;
+ padding: 16px 0;
+}
+
+.health-columns,
+.health-integrity-grid,
+.health-facts {
+ display: grid;
+ gap: 14px;
+}
+
+.health-columns {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.health-integrity-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.health-facts {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin: 0;
+}
+
+.health-facts-compact {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.health-facts div {
+ min-width: 0;
+}
+
+.health-facts dt {
+ color: #52606d;
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.health-facts dd {
+ font-weight: 700;
+ margin: 3px 0 0;
+ overflow-wrap: anywhere;
+}
+
+.health-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.health-links a:focus-visible,
+#healthRefresh:focus-visible {
+ outline: 3px solid #0f766e;
+ outline-offset: 3px;
+}
+
+.table-scroll {
+ overflow-x: auto;
+}
+
+.table-scroll table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.table-scroll th,
+.table-scroll td {
+ border-bottom: 1px solid #dbe2e8;
+ padding: 7px 8px;
+ text-align: left;
+}
+
+@media (max-width: 900px) {
+ .health-integrity-grid,
+ .health-facts {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 640px) {
+ .health-toolbar,
+ .health-section-head {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .health-columns,
+ .health-integrity-grid,
+ .health-facts,
+ .health-facts-compact {
+ grid-template-columns: 1fr;
+ }
+
+ #healthRefresh {
+ width: 100%;
+ }
+}
diff --git a/backend/app/frontend/static/system-health.js b/backend/app/frontend/static/system-health.js
new file mode 100644
index 0000000..f14e8fa
--- /dev/null
+++ b/backend/app/frontend/static/system-health.js
@@ -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();
+}());
diff --git a/backend/app/frontend/system_health_view.py b/backend/app/frontend/system_health_view.py
new file mode 100644
index 0000000..c55c34e
--- /dev/null
+++ b/backend/app/frontend/system_health_view.py
@@ -0,0 +1,54 @@
+"""Semantic content for the Internal Alpha System Health page."""
+
+
+def system_health_content() -> str:
+ return """
+
+
Loading diagnostics.
+
+
+ Application
+
+ - Application
- -
+ - Environment
- -
+ - Build commit
- -
+ - Build version
- -
+ - Server time
- -
+ - Schema
- -
+ - Database revision
- -
+ - Expected revision
- -
+
+
+
+
Service availability
+ - PostgreSQL
- -
+ - Redis
- -
+
Telemetry
+ - Raw snapshots
- -
+ - Oldest observation
- -
+ - Latest observation
- -
+ - Latest age
- -
+
+
+ Operational integrity facts
+
+
+
+
+
+"""
diff --git a/backend/app/frontend/views.py b/backend/app/frontend/views.py
index 2d6a081..87fb8e3 100644
--- a/backend/app/frontend/views.py
+++ b/backend/app/frontend/views.py
@@ -5,6 +5,7 @@ from typing import Callable
from app.frontend.miner_detail_view import miner_detail_content
from app.frontend.monitoring_trends_view import monitoring_trends_content
+from app.frontend.system_health_view import system_health_content
NAVIGATION = (
@@ -13,6 +14,7 @@ NAVIGATION = (
("analytics", "Trends", "/monitoring/analytics", "miners.view"),
("repairs", "Repair Center", "/operations/repairs", "repairs.view"),
("miners", "Miners", "/assets/miners", "miners.view"),
+ ("system_health", "System Health", "/system-health", "system_health.view"),
)
PAGE_DETAILS = {
@@ -46,6 +48,11 @@ PAGE_DETAILS = {
"Miner Detail",
"Current identity, operating facts, location, and authorized history.",
),
+ "system_health": (
+ "Administration / Diagnostics",
+ "System Health",
+ "Inspect application services, operational data, and maintenance facts.",
+ ),
}
@@ -455,7 +462,9 @@ def render_app_page(
else ''
)
content = (
- miner_detail_content()
+ system_health_content()
+ if view == "system_health"
+ else miner_detail_content()
if view == "miner_detail"
else monitoring_trends_content(has_permission("locations.view"))
if view == "analytics"
@@ -474,7 +483,9 @@ def render_app_page(
)
)
page_script = (
- ''
+ ''
+ if view == "system_health"
+ else ''
if view == "miner_detail"
else ''
if view == "analytics"
@@ -489,7 +500,9 @@ def render_app_page(
else ""
)
page_style = (
- ''
+ ''
+ if view == "system_health"
+ else ''
if view == "analytics" else ""
)
object_attributes = ""
diff --git a/backend/app/main.py b/backend/app/main.py
index 21aa961..239462a 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -32,6 +32,8 @@ from app.components.router import router as components_router
from app.miner_detail.router import router as miner_detail_router
from app.monitoring.router import router as monitoring_router
from app.monitoring.frontend import router as monitoring_frontend_router
+from app.system_health.frontend import router as system_health_frontend_router
+from app.system_health.router import router as system_health_router
from app.repairs.exceptions import (
ControlBoardReplacementActorRequired,
InvalidMacAddress,
@@ -67,6 +69,8 @@ app.include_router(dashboard_router)
app.include_router(miner_detail_router)
app.include_router(monitoring_frontend_router)
app.include_router(monitoring_router)
+app.include_router(system_health_frontend_router)
+app.include_router(system_health_router)
install_identity_middleware(app)
@@ -34663,744 +34667,6 @@ async def minerops_inject_repair_history_modal(request, call_next):
-# MINEROPS_SYSTEM_HEALTH_V1
-from fastapi.responses import HTMLResponse as MinerOpsSystemHealthHTMLResponse
-from pathlib import Path as MinerOpsSystemPath
-
-
-@app.get("/api/system/health-summary")
-def minerops_system_health_summary(
- current_user: dict = Depends(get_current_user),
-):
- import re
- from collections import Counter
-
- ensure_location_tables()
- ensure_maintenance_tables()
-
- source_path = MinerOpsSystemPath(__file__)
- source_text = source_path.read_text(errors="ignore")
- source_lines = source_text.splitlines()
-
- route_re = re.compile(r'^\s*@app\.(get|post|patch|delete|put)\("([^"]+)"')
- routes = []
-
- for line_no, line in enumerate(source_lines, start=1):
- match = route_re.search(line)
-
- if match:
- routes.append(
- {
- "method": match.group(1).upper(),
- "path": match.group(2),
- "line": line_no,
- }
- )
-
- route_counter = Counter((r["method"], r["path"]) for r in routes)
- duplicate_routes = []
-
- for (method, route_path), count in sorted(route_counter.items(), key=lambda item: (item[0][1], item[0][0])):
- if count > 1:
- duplicate_routes.append(
- {
- "method": method,
- "path": route_path,
- "count": count,
- "lines": [
- r["line"]
- for r in routes
- if r["method"] == method and r["path"] == route_path
- ],
- }
- )
-
- middleware_lines = [
- line_no
- for line_no, line in enumerate(source_lines, start=1)
- if '@app.middleware("http")' in line
- ]
-
- asset_routes = [
- r
- for r in routes
- if str(r["path"]).startswith("/assets/")
- ]
-
- patch_markers = []
-
- for line_no, line in enumerate(source_lines, start=1):
- stripped = line.strip()
-
- if stripped.startswith("# MINEROPS_") or stripped.startswith("# Step "):
- patch_markers.append(
- {
- "line": line_no,
- "text": stripped,
- }
- )
-
- conn = None
-
- try:
- conn = get_db_connection()
-
- with conn.cursor(row_factory=psycopg.rows.dict_row) as cur:
- cur.execute(
- """
- SELECT
- relname AS table_name,
- pg_total_relation_size(relid) AS total_bytes,
- pg_size_pretty(pg_total_relation_size(relid)) AS total_size
- FROM pg_catalog.pg_statio_user_tables
- ORDER BY pg_total_relation_size(relid) DESC
- LIMIT 15;
- """
- )
- largest_tables = [dict(row) for row in cur.fetchall()]
-
- cur.execute(
- """
- SELECT
- relname AS table_name,
- n_live_tup::bigint AS estimated_rows,
- n_dead_tup::bigint AS estimated_dead_rows,
- last_vacuum,
- last_autovacuum,
- last_analyze,
- last_autoanalyze
- FROM pg_stat_user_tables
- ORDER BY n_live_tup DESC
- LIMIT 20;
- """
- )
- row_estimates = [dict(row) for row in cur.fetchall()]
-
- table_counts = {}
-
- for table_name in [
- "miners",
- "location_positions",
- "miner_location_states",
- "miner_maintenance_events",
- "miner_hashboards",
- "miner_network_identities",
- "minerops_agent_command_queue",
- ]:
- try:
- cur.execute(f"SELECT COUNT(*) AS row_count FROM {table_name};")
- table_counts[table_name] = cur.fetchone()["row_count"]
- except Exception as exc:
- table_counts[table_name] = f"error: {exc}"
-
- cur.execute(
- """
- SELECT
- COALESCE(status, 'unknown') AS status,
- COUNT(*) AS count
- FROM miners
- GROUP BY COALESCE(status, 'unknown')
- ORDER BY status;
- """
- )
- miner_status_counts = [dict(row) for row in cur.fetchall()]
-
- cur.execute(
- """
- SELECT
- COALESCE(asset_state, 'unknown') AS asset_state,
- COUNT(*) AS count
- FROM miner_location_states
- GROUP BY COALESCE(asset_state, 'unknown')
- ORDER BY asset_state;
- """
- )
- location_state_counts = [dict(row) for row in cur.fetchall()]
-
- cur.execute(
- """
- SELECT
- COALESCE(event_status, 'unknown') AS event_status,
- COUNT(*) AS count
- FROM miner_maintenance_events
- WHERE event_type = 'repair'
- GROUP BY COALESCE(event_status, 'unknown')
- ORDER BY event_status;
- """
- )
- repair_status_counts = [dict(row) for row in cur.fetchall()]
-
- cur.execute(
- """
- SELECT
- COUNT(*) FILTER (WHERE api3_supported IS TRUE) AS api3_supported,
- COUNT(*) FILTER (WHERE api3_supported IS FALSE) AS api3_failed,
- COUNT(*) FILTER (WHERE api3_supported IS NULL) AS api3_unknown,
- COUNT(*) FILTER (WHERE power_sn IS NOT NULL AND power_sn <> '') AS miners_with_power_sn,
- COUNT(*) FILTER (WHERE miner_sn IS NOT NULL AND miner_sn <> '') AS miners_with_miner_sn
- FROM miners;
- """
- )
- api3_summary = dict(cur.fetchone())
-
- cur.execute(
- """
- SELECT
- COUNT(*) AS repair_queue_count
- FROM miner_location_states
- WHERE asset_state = 'repair';
- """
- )
- repair_queue_count = cur.fetchone()["repair_queue_count"]
-
- cur.execute(
- """
- SELECT
- COUNT(*) AS raw_status_rows,
- MIN(observed_at) AS raw_oldest,
- MAX(observed_at) AS raw_newest,
- COUNT(*) FILTER (WHERE observed_at < now() - interval '30 days') AS raw_older_than_30d
- FROM miner_status_snapshots;
- """
- )
- raw_status_retention = dict(cur.fetchone())
-
- cur.execute(
- """
- SELECT
- COUNT(*) AS hourly_rows,
- MIN(bucket_at) AS hourly_oldest,
- MAX(bucket_at) AS hourly_newest
- FROM miner_status_hourly_rollups;
- """
- )
- hourly_retention = dict(cur.fetchone())
-
- cur.execute(
- """
- SELECT
- COUNT(*) AS daily_rows,
- MIN(bucket_date) AS daily_oldest,
- MAX(bucket_date) AS daily_newest
- FROM miner_status_daily_rollups;
- """
- )
- daily_retention = dict(cur.fetchone())
-
- cur.execute(
- """
- SELECT
- mode,
- raw_status_retention_days,
- raw_status_rows_eligible_delete,
- raw_status_rows_deleted,
- hourly_rollup_rows_after,
- daily_rollup_rows_after,
- notes,
- started_at,
- finished_at
- FROM minerops_retention_runs
- ORDER BY started_at DESC
- LIMIT 1;
- """
- )
- latest_retention_run = cur.fetchone()
- latest_retention_run = dict(latest_retention_run) if latest_retention_run else None
-
- return api_response(
- data={
- "app": {
- "status": "ok",
- "source_lines": len(source_lines),
- "source_size_bytes": source_path.stat().st_size,
- "route_count": len(routes),
- "duplicate_route_count": len(duplicate_routes),
- "middleware_count": len(middleware_lines),
- "asset_route_count": len(asset_routes),
- },
- "routes": {
- "duplicates": duplicate_routes,
- "middleware_lines": middleware_lines,
- "asset_routes": asset_routes,
- "recent_patch_markers": patch_markers[-40:],
- },
- "database": {
- "largest_tables": largest_tables,
- "row_estimates": row_estimates,
- "table_counts": table_counts,
- "miner_status_counts": miner_status_counts,
- "location_state_counts": location_state_counts,
- "repair_status_counts": repair_status_counts,
- "api3_summary": api3_summary,
- "repair_queue_count": repair_queue_count,
- "retention": {
- "raw_status": raw_status_retention,
- "hourly_rollups": hourly_retention,
- "daily_rollups": daily_retention,
- "latest_run": latest_retention_run,
- },
- },
- }
- )
-
- finally:
- if conn is not None:
- conn.close()
-
-
-MINEROPS_SYSTEM_HEALTH_HTML = r"""
-
-
-
- MinerOps System Health
-
-
-
-
-
-
-
-
-
System Health
-
Quick operational view of MinerOps app size, routes, database tables, repairs, and location state.
-
-
-
-
- Loading system health...
-
-
-
-
-
Fleet / Repair State
-
-
-
-
-
Status Retention
-
-
Loading retention details...
-
-
-
-
-
Largest Tables
-
-
- | Table | Size |
- | Loading... |
-
-
-
-
-
-
Repair Status Counts
-
-
- | Status | Count |
- | Loading... |
-
-
-
-
-
-
-
-
Duplicate Routes
-
-
- | Method | Path | Count | Lines |
- | Loading... |
-
-
-
-
-
-
Recent Patch Markers
-
-
- | Line | Marker |
- | Loading... |
-
-
-
-
-
-
-
-
-"""
-
-
-@app.get("/system-health", response_class=MinerOpsSystemHealthHTMLResponse)
-def minerops_system_health_page():
- return MINEROPS_SYSTEM_HEALTH_HTML
-
-
# MINEROPS_SYSTEM_HEALTH_RETENTION_V1
# MINEROPS_REPAIR_READY_TO_SHELF_STATUS_V1
diff --git a/backend/app/system_health/__init__.py b/backend/app/system_health/__init__.py
new file mode 100644
index 0000000..ba3bcfe
--- /dev/null
+++ b/backend/app/system_health/__init__.py
@@ -0,0 +1 @@
+"""System Health diagnostics domain."""
diff --git a/backend/app/system_health/adapters.py b/backend/app/system_health/adapters.py
new file mode 100644
index 0000000..57f1f2d
--- /dev/null
+++ b/backend/app/system_health/adapters.py
@@ -0,0 +1,29 @@
+"""External service adapters for System Health."""
+
+from collections.abc import Callable
+
+import redis
+
+from app.config import settings
+
+
+RedisFactory = Callable[[], redis.Redis]
+
+
+def default_redis_factory() -> redis.Redis:
+ return redis.Redis.from_url(
+ settings.redis_url,
+ socket_connect_timeout=2,
+ socket_timeout=2,
+ )
+
+
+class RedisHealthAdapter:
+ def __init__(self, factory: RedisFactory | None = None):
+ self._factory = factory or default_redis_factory
+
+ def ping(self) -> bool:
+ try:
+ return bool(self._factory().ping())
+ except Exception:
+ return False
diff --git a/backend/app/system_health/application.py b/backend/app/system_health/application.py
new file mode 100644
index 0000000..82f458b
--- /dev/null
+++ b/backend/app/system_health/application.py
@@ -0,0 +1,42 @@
+"""Startup-cached, non-secret application metadata."""
+
+from dataclasses import dataclass
+from pathlib import Path
+
+from alembic.config import Config
+from alembic.script import ScriptDirectory
+
+from app.config import settings
+
+
+def _clean_build_value(value: str | None) -> str | None:
+ if value is None:
+ return None
+ cleaned = value.strip()
+ return cleaned[:128] if cleaned else None
+
+
+def _expected_migration_head() -> str | None:
+ try:
+ backend_root = Path(__file__).resolve().parents[2]
+ config = Config(str(backend_root / "alembic.ini"))
+ config.set_main_option("script_location", str(backend_root / "alembic"))
+ return ScriptDirectory.from_config(config).get_current_head()
+ except Exception:
+ return None
+
+
+@dataclass(frozen=True)
+class ApplicationMetadata:
+ environment: str
+ build_commit: str | None
+ build_version: str | None
+ expected_migration_head: str | None
+
+
+APPLICATION_METADATA = ApplicationMetadata(
+ environment=settings.app_env,
+ build_commit=_clean_build_value(settings.minerops_build_commit),
+ build_version=_clean_build_value(settings.minerops_build_version),
+ expected_migration_head=_expected_migration_head(),
+)
diff --git a/backend/app/system_health/dependencies.py b/backend/app/system_health/dependencies.py
new file mode 100644
index 0000000..f9c49d8
--- /dev/null
+++ b/backend/app/system_health/dependencies.py
@@ -0,0 +1,9 @@
+"""Dependency construction for System Health."""
+
+from app.system_health.adapters import RedisHealthAdapter
+from app.system_health.repository import SystemHealthRepository
+from app.system_health.service import SystemHealthService
+
+
+def get_system_health_service() -> SystemHealthService:
+ return SystemHealthService(SystemHealthRepository(), RedisHealthAdapter())
diff --git a/backend/app/system_health/frontend.py b/backend/app/system_health/frontend.py
new file mode 100644
index 0000000..55336cc
--- /dev/null
+++ b/backend/app/system_health/frontend.py
@@ -0,0 +1,35 @@
+"""Authenticated frontend routes for System Health."""
+
+from pathlib import Path
+from typing import Annotated
+
+from fastapi import APIRouter, Depends
+from fastapi.responses import HTMLResponse, Response
+
+from app.frontend.views import render_app_page
+from app.identity import require_permission, user_has_permission
+
+
+router = APIRouter(tags=["frontend"])
+STATIC_ROOT = Path(__file__).parents[1] / "frontend" / "static"
+SystemHealthUser = Annotated[dict, Depends(require_permission("system_health.view"))]
+
+
+@router.get("/assets/minerops-system-health.css", include_in_schema=False)
+def system_health_styles() -> Response:
+ return Response((STATIC_ROOT / "system-health.css").read_text(encoding="utf-8"),
+ media_type="text/css", headers={"Cache-Control": "no-store"})
+
+
+@router.get("/assets/minerops-system-health.js", include_in_schema=False)
+def system_health_script() -> Response:
+ return Response((STATIC_ROOT / "system-health.js").read_text(encoding="utf-8"),
+ media_type="text/javascript", headers={"Cache-Control": "no-store"})
+
+
+@router.get("/system-health", response_class=HTMLResponse, include_in_schema=False)
+def system_health_page(current_user: SystemHealthUser) -> str:
+ return render_app_page(
+ "system_health", current_user,
+ lambda permission: user_has_permission(current_user, permission),
+ )
diff --git a/backend/app/system_health/models.py b/backend/app/system_health/models.py
new file mode 100644
index 0000000..4f18768
--- /dev/null
+++ b/backend/app/system_health/models.py
@@ -0,0 +1,124 @@
+"""Typed contracts for the System Health overview."""
+
+from datetime import date, datetime
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+
+Availability = Literal["available", "unavailable"]
+MigrationState = Literal["current", "mismatch", "unavailable"]
+
+
+class ApplicationDiagnostics(BaseModel):
+ status: Literal["available"] = "available"
+ environment: str
+ build_commit: str | None = None
+ build_version: str | None = None
+ server_time: datetime
+ expected_migration_head: str | None = None
+
+
+class MigrationDiagnostics(BaseModel):
+ state: MigrationState
+ current_revision: str | None = None
+ expected_revision: str | None = None
+
+
+class TableCount(BaseModel):
+ name: str
+ row_count: int
+
+
+class TableSize(BaseModel):
+ name: str
+ total_bytes: int
+
+
+class TelemetryDiagnostics(BaseModel):
+ snapshot_count: int
+ oldest_observed_at: datetime | None = None
+ latest_observed_at: datetime | None = None
+
+
+class FleetIntegrityDiagnostics(BaseModel):
+ total_miners: int
+ miners_without_asset_id: int
+ unmapped_miners: int
+ miners_in_multiple_positions: int
+ duplicate_current_mac_groups: int
+
+
+class LocationIntegrityDiagnostics(BaseModel):
+ sites: int
+ rooms: int
+ positions: int
+ occupied_positions: int
+ empty_positions: int
+ unscoped_legacy_positions: int
+ expected_mac_mismatches: int
+
+
+class RepairIntegrityDiagnostics(BaseModel):
+ active_modular_repairs: int
+ duplicate_active_repair_groups: int
+ legacy_repair_only_miners: int
+ lifecycle_repair_without_active_repair: int
+ active_repair_lifecycle_mismatches: int
+ waiting_placement_records: int
+ repair_events: int
+ repair_parts: int
+
+
+class RollupDiagnostics(BaseModel):
+ table_present: bool
+ row_count: int | None = None
+ latest_bucket: datetime | date | None = None
+ population: Literal["manual"] = "manual"
+
+
+class RetentionDiagnostics(BaseModel):
+ raw_retention_enabled: Literal[False] = False
+ raw_retention_mode: Literal["not_enabled"] = "not_enabled"
+ hourly: RollupDiagnostics
+ daily: RollupDiagnostics
+ latest_recorded_run_at: datetime | None = None
+ latest_recorded_run_mode: str | None = None
+ completeness: Literal["not_guaranteed"] = "not_guaranteed"
+
+
+class PostgreSQLDiagnostics(BaseModel):
+ status: Availability
+ migration: MigrationDiagnostics
+ table_counts: list[TableCount] = Field(default_factory=list)
+ largest_tables: list[TableSize] = Field(default_factory=list)
+ telemetry: TelemetryDiagnostics | None = None
+ fleet: FleetIntegrityDiagnostics | None = None
+ locations: LocationIntegrityDiagnostics | None = None
+ repairs: RepairIntegrityDiagnostics | None = None
+ retention: RetentionDiagnostics | None = None
+
+
+class RedisDiagnostics(BaseModel):
+ status: Availability
+
+
+class SystemHealthOverviewData(BaseModel):
+ application: ApplicationDiagnostics
+ postgresql: PostgreSQLDiagnostics
+ redis: RedisDiagnostics
+
+
+class SystemHealthOverviewMeta(BaseModel):
+ generated_at: datetime
+ partial: bool
+ unavailable_sections: list[str] = Field(default_factory=list)
+ frontend_request_count: Literal[1] = 1
+ postgresql_statement_count: Literal[3] = 3
+ redis_call_count: Literal[1] = 1
+
+
+class SystemHealthOverviewResponse(BaseModel):
+ data: SystemHealthOverviewData
+ meta: SystemHealthOverviewMeta
+ error: None = None
diff --git a/backend/app/system_health/repository.py b/backend/app/system_health/repository.py
new file mode 100644
index 0000000..d3edc40
--- /dev/null
+++ b/backend/app/system_health/repository.py
@@ -0,0 +1,179 @@
+"""PostgreSQL diagnostics for System Health."""
+
+from collections.abc import Callable, Iterator
+from contextlib import contextmanager
+from typing import Any
+
+import psycopg
+from psycopg.rows import dict_row
+
+from app.config import settings
+
+
+ConnectionFactory = Callable[[], psycopg.Connection]
+
+
+def default_connection_factory() -> psycopg.Connection:
+ return psycopg.connect(
+ host=settings.postgres_host,
+ port=settings.postgres_port,
+ dbname=settings.postgres_db,
+ user=settings.postgres_user,
+ password=settings.postgres_password,
+ connect_timeout=3,
+ )
+
+
+MIGRATION_SQL = """
+SELECT clock_timestamp() AS database_time,
+ (SELECT version_num FROM alembic_version LIMIT 1) AS current_revision
+"""
+
+OPERATIONS_SQL = """
+WITH telemetry AS (
+ SELECT count(*)::bigint AS snapshot_count,
+ min(observed_at) AS oldest_observed_at,
+ max(observed_at) AS latest_observed_at
+ FROM miner_status_snapshots
+),
+hourly AS (
+ SELECT count(*)::bigint AS row_count, max(bucket_at) AS latest_bucket
+ FROM miner_status_hourly_rollups
+),
+daily AS (
+ SELECT count(*)::bigint AS row_count, max(bucket_date) AS latest_bucket
+ FROM miner_status_daily_rollups
+),
+latest_retention AS (
+ SELECT mode, started_at FROM minerops_retention_runs
+ ORDER BY started_at DESC LIMIT 1
+),
+largest AS (
+ SELECT coalesce(jsonb_agg(to_jsonb(sized) ORDER BY sized.total_bytes DESC), '[]'::jsonb) AS rows
+ FROM (
+ SELECT relname AS name, pg_total_relation_size(relid)::bigint AS total_bytes
+ FROM pg_catalog.pg_statio_user_tables
+ WHERE relname = ANY(%s)
+ ORDER BY pg_total_relation_size(relid) DESC, relname
+ LIMIT 8
+ ) AS sized
+)
+SELECT
+ (SELECT count(*) FROM miners)::bigint AS miners,
+ (SELECT count(*) FROM sites)::bigint AS sites,
+ (SELECT count(*) FROM rooms)::bigint AS rooms,
+ (SELECT count(*) FROM location_positions)::bigint AS location_positions,
+ (SELECT count(*) FROM repairs)::bigint AS repairs,
+ (SELECT count(*) FROM repair_events)::bigint AS repair_events,
+ (SELECT count(*) FROM repair_parts)::bigint AS repair_parts,
+ telemetry.snapshot_count, telemetry.oldest_observed_at,
+ telemetry.latest_observed_at,
+ to_regclass('public.miner_status_hourly_rollups') IS NOT NULL AS hourly_present,
+ hourly.row_count AS hourly_rows, hourly.latest_bucket AS hourly_latest,
+ to_regclass('public.miner_status_daily_rollups') IS NOT NULL AS daily_present,
+ daily.row_count AS daily_rows, daily.latest_bucket AS daily_latest,
+ latest_retention.mode AS retention_mode,
+ latest_retention.started_at AS retention_started_at,
+ largest.rows AS largest_tables
+FROM telemetry CROSS JOIN hourly CROSS JOIN daily CROSS JOIN largest
+LEFT JOIN latest_retention ON true
+"""
+
+INTEGRITY_SQL = """
+WITH position_counts AS (
+ SELECT miner_id, count(*) AS position_count
+ FROM location_positions WHERE miner_id IS NOT NULL GROUP BY miner_id
+),
+active_repairs AS (
+ SELECT id, miner_id FROM repairs
+ WHERE status IN ('OPEN', 'IN_PROGRESS', 'WAITING_FOR_PARTS', 'READY_FOR_TEST')
+),
+active_repair_groups AS (
+ SELECT miner_id FROM active_repairs GROUP BY miner_id HAVING count(*) > 1
+),
+normalized_macs AS (
+ SELECT lower(regexp_replace(trim(current_mac), '[^0-9A-Fa-f]', '', 'g')) AS mac
+ FROM miners WHERE current_mac IS NOT NULL AND trim(current_mac) <> ''
+ GROUP BY lower(regexp_replace(trim(current_mac), '[^0-9A-Fa-f]', '', 'g'))
+ HAVING count(*) > 1
+)
+SELECT
+ (SELECT count(*) FROM miners WHERE asset_tag IS NULL OR trim(asset_tag) = '')::bigint
+ AS miners_without_asset_id,
+ (SELECT count(*) FROM miners m WHERE NOT EXISTS (
+ SELECT 1 FROM location_positions p WHERE p.miner_id = m.id
+ ))::bigint AS unmapped_miners,
+ (SELECT count(*) FROM position_counts WHERE position_count > 1)::bigint
+ AS miners_in_multiple_positions,
+ (SELECT count(*) FROM normalized_macs)::bigint AS duplicate_current_mac_groups,
+ (SELECT count(*) FROM location_positions WHERE room_id IS NULL)::bigint
+ AS unscoped_legacy_positions,
+ (SELECT count(*) FROM location_positions p JOIN miners m ON m.id = p.miner_id
+ WHERE p.expected_mac IS NOT NULL AND trim(p.expected_mac) <> ''
+ AND m.current_mac IS NOT NULL AND trim(m.current_mac) <> ''
+ AND lower(regexp_replace(trim(p.expected_mac), '[^0-9A-Fa-f]', '', 'g'))
+ <> lower(regexp_replace(trim(m.current_mac), '[^0-9A-Fa-f]', '', 'g'))
+ )::bigint AS expected_mac_mismatches,
+ (SELECT count(*) FROM location_positions WHERE miner_id IS NOT NULL)::bigint
+ AS occupied_positions,
+ (SELECT count(*) FROM location_positions WHERE miner_id IS NULL)::bigint
+ AS empty_positions,
+ (SELECT count(*) FROM active_repairs)::bigint AS active_modular_repairs,
+ (SELECT count(*) FROM active_repair_groups)::bigint AS duplicate_active_repair_groups,
+ (SELECT count(*) FROM miner_location_states state
+ WHERE state.asset_state = 'repair' AND NOT EXISTS (
+ SELECT 1 FROM active_repairs repair WHERE repair.miner_id = state.miner_id
+ ))::bigint AS legacy_repair_only_miners,
+ (SELECT count(*) FROM miners m WHERE m.lifecycle_status = 'in_repair'
+ AND NOT EXISTS (SELECT 1 FROM active_repairs r WHERE r.miner_id = m.id))::bigint
+ AS lifecycle_repair_without_active_repair,
+ (SELECT count(*) FROM active_repairs r JOIN miners m ON m.id = r.miner_id
+ WHERE m.lifecycle_status <> 'in_repair')::bigint
+ AS active_repair_lifecycle_mismatches,
+ (SELECT count(*) FROM miner_location_states WHERE asset_state = 'waiting_placement')::bigint
+ AS waiting_placement_records
+"""
+
+
+RELEVANT_TABLES = [
+ "miner_status_snapshots", "repair_events", "repair_parts", "repairs",
+ "location_positions", "miners", "miner_status_hourly_rollups",
+ "miner_status_daily_rollups",
+]
+
+
+class SystemHealthRepository:
+ def __init__(
+ self,
+ connection_factory: ConnectionFactory | None = None,
+ *,
+ connection: psycopg.Connection | None = None,
+ ):
+ self._connection_factory = connection_factory or default_connection_factory
+ self._connection = connection
+
+ @contextmanager
+ def _connection_scope(self) -> Iterator[psycopg.Connection]:
+ if self._connection is not None:
+ yield self._connection
+ return
+ connection = self._connection_factory()
+ try:
+ yield connection
+ finally:
+ connection.close()
+
+ def read_overview(self) -> dict[str, Any]:
+ with self._connection_scope() as connection:
+ with connection.cursor(row_factory=dict_row) as cur:
+ cur.execute(MIGRATION_SQL)
+ migration = dict(cur.fetchone())
+ cur.execute(OPERATIONS_SQL, (RELEVANT_TABLES,))
+ operations = dict(cur.fetchone())
+ cur.execute(INTEGRITY_SQL)
+ integrity = dict(cur.fetchone())
+ return {
+ "migration": migration,
+ "operations": operations,
+ "integrity": integrity,
+ }
diff --git a/backend/app/system_health/router.py b/backend/app/system_health/router.py
new file mode 100644
index 0000000..2be2d2c
--- /dev/null
+++ b/backend/app/system_health/router.py
@@ -0,0 +1,27 @@
+"""HTTP boundary for the System Health overview."""
+
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, Response
+
+from app.identity import require_permission
+from app.system_health.dependencies import get_system_health_service
+from app.system_health.models import SystemHealthOverviewResponse
+from app.system_health.service import SystemHealthService
+
+
+router = APIRouter(prefix="/api/system", tags=["system-health"])
+SystemHealthUser = Annotated[dict, Depends(require_permission("system_health.view"))]
+SystemHealthServiceDependency = Annotated[
+ SystemHealthService, Depends(get_system_health_service)
+]
+
+
+@router.get("/health-summary", response_model=SystemHealthOverviewResponse)
+def get_system_health_overview(
+ current_user: SystemHealthUser,
+ service: SystemHealthServiceDependency,
+ response: Response,
+) -> SystemHealthOverviewResponse:
+ response.headers["Cache-Control"] = "no-store"
+ return service.get_overview()
diff --git a/backend/app/system_health/service.py b/backend/app/system_health/service.py
new file mode 100644
index 0000000..11a419f
--- /dev/null
+++ b/backend/app/system_health/service.py
@@ -0,0 +1,136 @@
+"""Composition and failure semantics for System Health diagnostics."""
+
+from datetime import datetime, timezone
+
+from app.system_health.adapters import RedisHealthAdapter
+from app.system_health.application import APPLICATION_METADATA, ApplicationMetadata
+from app.system_health.models import (
+ ApplicationDiagnostics, FleetIntegrityDiagnostics, LocationIntegrityDiagnostics,
+ MigrationDiagnostics, PostgreSQLDiagnostics, RedisDiagnostics,
+ RepairIntegrityDiagnostics, RetentionDiagnostics, RollupDiagnostics,
+ SystemHealthOverviewData, SystemHealthOverviewMeta,
+ SystemHealthOverviewResponse, TableCount, TableSize, TelemetryDiagnostics,
+)
+from app.system_health.repository import SystemHealthRepository
+
+
+class SystemHealthService:
+ def __init__(self, repository: SystemHealthRepository,
+ redis_adapter: RedisHealthAdapter,
+ metadata: ApplicationMetadata = APPLICATION_METADATA):
+ self.repository = repository
+ self.redis_adapter = redis_adapter
+ self.metadata = metadata
+
+ def get_overview(self) -> SystemHealthOverviewResponse:
+ generated_at = datetime.now(timezone.utc)
+ unavailable = []
+ try:
+ postgresql = self._postgresql_diagnostics(
+ self.repository.read_overview()
+ )
+ except Exception:
+ unavailable.append("postgresql")
+ postgresql = PostgreSQLDiagnostics(
+ status="unavailable",
+ migration=MigrationDiagnostics(
+ state="unavailable",
+ expected_revision=self.metadata.expected_migration_head,
+ ),
+ )
+ redis_available = self.redis_adapter.ping()
+ if not redis_available:
+ unavailable.append("redis")
+ return SystemHealthOverviewResponse(
+ data=SystemHealthOverviewData(
+ application=ApplicationDiagnostics(
+ environment=self.metadata.environment,
+ build_commit=self.metadata.build_commit,
+ build_version=self.metadata.build_version,
+ server_time=generated_at,
+ expected_migration_head=self.metadata.expected_migration_head,
+ ),
+ postgresql=postgresql,
+ redis=RedisDiagnostics(
+ status="available" if redis_available else "unavailable"
+ ),
+ ),
+ meta=SystemHealthOverviewMeta(
+ generated_at=generated_at,
+ partial=bool(unavailable),
+ unavailable_sections=unavailable,
+ ),
+ )
+
+ def _postgresql_diagnostics(self, payload: dict) -> PostgreSQLDiagnostics:
+ migration = payload["migration"]
+ operations = payload["operations"]
+ integrity = payload["integrity"]
+ current = migration.get("current_revision")
+ expected = self.metadata.expected_migration_head
+ state = (
+ "unavailable" if not current or not expected
+ else "current" if current == expected else "mismatch"
+ )
+ counts = [
+ TableCount(name=name, row_count=int(operations[name]))
+ for name in (
+ "miners", "sites", "rooms", "location_positions", "repairs",
+ "repair_events", "repair_parts", "snapshot_count",
+ )
+ ]
+ return PostgreSQLDiagnostics(
+ status="available",
+ migration=MigrationDiagnostics(
+ state=state, current_revision=current, expected_revision=expected
+ ),
+ table_counts=counts,
+ largest_tables=[
+ TableSize(name=row["name"], total_bytes=int(row["total_bytes"]))
+ for row in operations.get("largest_tables") or []
+ ],
+ telemetry=TelemetryDiagnostics(
+ snapshot_count=int(operations["snapshot_count"]),
+ oldest_observed_at=operations.get("oldest_observed_at"),
+ latest_observed_at=operations.get("latest_observed_at"),
+ ),
+ fleet=FleetIntegrityDiagnostics(
+ total_miners=int(operations["miners"]),
+ miners_without_asset_id=int(integrity["miners_without_asset_id"]),
+ unmapped_miners=int(integrity["unmapped_miners"]),
+ miners_in_multiple_positions=int(integrity["miners_in_multiple_positions"]),
+ duplicate_current_mac_groups=int(integrity["duplicate_current_mac_groups"]),
+ ),
+ locations=LocationIntegrityDiagnostics(
+ sites=int(operations["sites"]), rooms=int(operations["rooms"]),
+ positions=int(operations["location_positions"]),
+ occupied_positions=int(integrity["occupied_positions"]),
+ empty_positions=int(integrity["empty_positions"]),
+ unscoped_legacy_positions=int(integrity["unscoped_legacy_positions"]),
+ expected_mac_mismatches=int(integrity["expected_mac_mismatches"]),
+ ),
+ repairs=RepairIntegrityDiagnostics(
+ active_modular_repairs=int(integrity["active_modular_repairs"]),
+ duplicate_active_repair_groups=int(integrity["duplicate_active_repair_groups"]),
+ legacy_repair_only_miners=int(integrity["legacy_repair_only_miners"]),
+ lifecycle_repair_without_active_repair=int(integrity["lifecycle_repair_without_active_repair"]),
+ active_repair_lifecycle_mismatches=int(integrity["active_repair_lifecycle_mismatches"]),
+ waiting_placement_records=int(integrity["waiting_placement_records"]),
+ repair_events=int(operations["repair_events"]),
+ repair_parts=int(operations["repair_parts"]),
+ ),
+ retention=RetentionDiagnostics(
+ hourly=RollupDiagnostics(
+ table_present=bool(operations["hourly_present"]),
+ row_count=int(operations["hourly_rows"]),
+ latest_bucket=operations.get("hourly_latest"),
+ ),
+ daily=RollupDiagnostics(
+ table_present=bool(operations["daily_present"]),
+ row_count=int(operations["daily_rows"]),
+ latest_bucket=operations.get("daily_latest"),
+ ),
+ latest_recorded_run_at=operations.get("retention_started_at"),
+ latest_recorded_run_mode=operations.get("retention_mode"),
+ ),
+ )
diff --git a/backend/tests/test_system_health.py b/backend/tests/test_system_health.py
new file mode 100644
index 0000000..8221afc
--- /dev/null
+++ b/backend/tests/test_system_health.py
@@ -0,0 +1,304 @@
+import ast
+import unittest
+from datetime import datetime, timezone
+from pathlib import Path
+from uuid import uuid4
+
+import psycopg
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from app.config import settings
+from app.identity import get_current_user
+from app.system_health.application import ApplicationMetadata
+from app.system_health.dependencies import get_system_health_service
+from app.system_health.frontend import router as frontend_router
+from app.system_health.repository import SystemHealthRepository
+from app.system_health.router import router as api_router
+from app.system_health.service import SystemHealthService
+
+
+APP_ROOT = Path(__file__).parents[1] / "app"
+EXPECTED_HEAD = "0014_repair_parts_tracking"
+
+
+def database_payload(revision=EXPECTED_HEAD):
+ return {
+ "migration": {"current_revision": revision},
+ "operations": {
+ "miners": 4, "sites": 1, "rooms": 2, "location_positions": 3,
+ "repairs": 1, "repair_events": 2, "repair_parts": 1,
+ "snapshot_count": 5,
+ "oldest_observed_at": datetime(2026, 8, 18, tzinfo=timezone.utc),
+ "latest_observed_at": datetime(2026, 8, 19, tzinfo=timezone.utc),
+ "hourly_present": True, "hourly_rows": 2,
+ "hourly_latest": datetime(2026, 8, 19, tzinfo=timezone.utc),
+ "daily_present": True, "daily_rows": 1,
+ "daily_latest": datetime(2026, 8, 19, tzinfo=timezone.utc).date(),
+ "retention_mode": "dry_run",
+ "retention_started_at": datetime(2026, 8, 19, tzinfo=timezone.utc),
+ "largest_tables": [{"name": "miners", "total_bytes": 8192}],
+ },
+ "integrity": {
+ "miners_without_asset_id": 1, "unmapped_miners": 1,
+ "miners_in_multiple_positions": 0, "duplicate_current_mac_groups": 0,
+ "unscoped_legacy_positions": 1, "expected_mac_mismatches": 1,
+ "occupied_positions": 2, "empty_positions": 1,
+ "active_modular_repairs": 1, "duplicate_active_repair_groups": 0,
+ "legacy_repair_only_miners": 0,
+ "lifecycle_repair_without_active_repair": 0,
+ "active_repair_lifecycle_mismatches": 0,
+ "waiting_placement_records": 1,
+ },
+ }
+
+
+class FakeRepository:
+ def __init__(self, payload=None, error=None):
+ self.payload = payload or database_payload()
+ self.error = error
+ self.calls = 0
+
+ def read_overview(self):
+ self.calls += 1
+ if self.error:
+ raise self.error
+ return self.payload
+
+
+class FakeRedis:
+ def __init__(self, available=True):
+ self.available = available
+ self.calls = 0
+
+ def ping(self):
+ self.calls += 1
+ return self.available
+
+
+def metadata(head=EXPECTED_HEAD):
+ return ApplicationMetadata(
+ environment="test", build_commit=None, build_version="0.1-test",
+ expected_migration_head=head,
+ )
+
+
+class SystemHealthServiceTests(unittest.TestCase):
+ def test_builds_typed_factual_overview_without_health_score(self):
+ repository, redis = FakeRepository(), FakeRedis()
+ result = SystemHealthService(repository, redis, metadata()).get_overview()
+ self.assertEqual(result.data.postgresql.migration.state, "current")
+ self.assertEqual(result.data.postgresql.telemetry.snapshot_count, 5)
+ self.assertEqual(result.data.postgresql.fleet.unmapped_miners, 1)
+ self.assertFalse(result.data.postgresql.retention.raw_retention_enabled)
+ self.assertEqual(result.data.postgresql.retention.hourly.population, "manual")
+ self.assertEqual(result.data.postgresql.retention.completeness, "not_guaranteed")
+ self.assertIsNone(result.data.application.build_commit)
+ self.assertFalse(result.meta.partial)
+ self.assertEqual((repository.calls, redis.calls), (1, 1))
+ self.assertNotIn("score", result.model_dump_json().lower())
+
+ def test_migration_mismatch_is_factual(self):
+ result = SystemHealthService(
+ FakeRepository(database_payload("0013_control_board_replacement")),
+ FakeRedis(), metadata(),
+ ).get_overview()
+ self.assertEqual(result.data.postgresql.migration.state, "mismatch")
+ self.assertEqual(
+ result.data.postgresql.migration.current_revision,
+ "0013_control_board_replacement",
+ )
+
+ def test_postgresql_failure_is_sanitized_and_redis_remains_available(self):
+ secret = "postgresql://user:password@private-host/minerops"
+ result = SystemHealthService(
+ FakeRepository(error=RuntimeError(secret)), FakeRedis(), metadata()
+ ).get_overview()
+ self.assertTrue(result.meta.partial)
+ self.assertEqual(result.meta.unavailable_sections, ["postgresql"])
+ self.assertEqual(result.data.postgresql.status, "unavailable")
+ self.assertEqual(result.data.redis.status, "available")
+ self.assertNotIn(secret, result.model_dump_json())
+
+ def test_redis_failure_preserves_database_diagnostics(self):
+ result = SystemHealthService(
+ FakeRepository(), FakeRedis(False), metadata()
+ ).get_overview()
+ self.assertEqual(result.data.postgresql.status, "available")
+ self.assertEqual(result.data.redis.status, "unavailable")
+ self.assertEqual(result.meta.unavailable_sections, ["redis"])
+
+
+class StubService:
+ def get_overview(self):
+ return SystemHealthService(
+ FakeRepository(), FakeRedis(), metadata()
+ ).get_overview()
+
+
+class SystemHealthRouterTests(unittest.TestCase):
+ def setUp(self):
+ self.app = FastAPI()
+ self.app.include_router(api_router)
+ self.app.dependency_overrides[get_system_health_service] = StubService
+ self.client = TestClient(self.app)
+
+ def tearDown(self):
+ self.client.close()
+
+ def authenticate(self, permissions):
+ self.app.dependency_overrides[get_current_user] = lambda: {
+ "id": str(uuid4()), "role": "Custom", "permissions": list(permissions)
+ }
+
+ def test_requires_authentication_and_system_health_permission(self):
+ self.assertEqual(self.client.get("/api/system/health-summary").status_code, 401)
+ self.authenticate(("dashboard.view",))
+ self.assertEqual(self.client.get("/api/system/health-summary").status_code, 403)
+
+ def test_returns_typed_no_store_envelope(self):
+ self.authenticate(("system_health.view",))
+ response = self.client.get("/api/system/health-summary")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.headers["cache-control"], "no-store")
+ payload = response.json()
+ self.assertEqual(payload["data"]["postgresql"]["status"], "available")
+ self.assertEqual(payload["meta"]["postgresql_statement_count"], 3)
+ self.assertIsNone(payload["error"])
+
+
+class CountingCursor(psycopg.Cursor):
+ statements = 0
+
+ def execute(self, query, params=None, *, prepare=None, binary=None):
+ type(self).statements += 1
+ return super().execute(
+ query, params, prepare=prepare, binary=binary
+ )
+
+
+class SystemHealthRepositoryIntegrationTests(unittest.TestCase):
+ def setUp(self):
+ self.connection = psycopg.connect(
+ host=settings.postgres_host, port=settings.postgres_port,
+ dbname=settings.postgres_db, user=settings.postgres_user,
+ password=settings.postgres_password, cursor_factory=CountingCursor,
+ )
+ CountingCursor.statements = 0
+ self.repository = SystemHealthRepository(connection=self.connection)
+
+ def tearDown(self):
+ self.connection.rollback()
+ self.connection.close()
+
+ def test_real_postgresql_overview_uses_exactly_three_statements(self):
+ site_id, room_id, mapped_id, unmapped_id = (
+ uuid4(), uuid4(), uuid4(), uuid4()
+ )
+ suffix = uuid4().hex[:10]
+ with self.connection.cursor() as cur:
+ cur.execute("INSERT INTO sites (id, name) VALUES (%s, %s)",
+ (site_id, "Health Site " + suffix))
+ cur.execute("INSERT INTO rooms (id, site_id, name) VALUES (%s, %s, %s)",
+ (room_id, site_id, "Health Room " + suffix))
+ cur.execute(
+ "INSERT INTO miners (id, asset_tag, lifecycle_status, current_mac) "
+ "VALUES (%s, %s, 'active', %s), (%s, NULL, 'active', NULL)",
+ (mapped_id, "HEALTH-" + suffix, "02:00:00:00:00:01", unmapped_id),
+ )
+ cur.execute(
+ "INSERT INTO location_positions "
+ "(room_id, room_name, rack_name, shelf_name, position_index, "
+ "miner_id, expected_mac) VALUES (%s, %s, %s, '1', 1, %s, %s), "
+ "(NULL, %s, %s, '1', 2, NULL, NULL)",
+ (room_id, "Health Room " + suffix, "Rack " + suffix, mapped_id,
+ "02:00:00:00:00:02", "Legacy " + suffix, "Rack " + suffix),
+ )
+ cur.execute(
+ "INSERT INTO miner_status_snapshots (miner_id, observed_at, mhs_av) "
+ "VALUES (%s, %s, 0)",
+ (mapped_id, datetime(2026, 8, 19, 12, tzinfo=timezone.utc)),
+ )
+ CountingCursor.statements = 0
+ result = self.repository.read_overview()
+ self.assertEqual(CountingCursor.statements, 3)
+ self.assertGreaterEqual(result["operations"]["miners"], 2)
+ self.assertGreaterEqual(result["operations"]["snapshot_count"], 1)
+ self.assertEqual(result["migration"]["current_revision"], EXPECTED_HEAD)
+ self.assertGreaterEqual(result["integrity"]["unmapped_miners"], 1)
+ self.assertGreaterEqual(result["integrity"]["unscoped_legacy_positions"], 1)
+ self.assertGreaterEqual(result["integrity"]["expected_mac_mismatches"], 1)
+ self.assertTrue(result["operations"]["hourly_present"])
+ self.assertTrue(result["operations"]["daily_present"])
+
+ def test_existing_role_matrix_preserves_read_only_grant(self):
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "SELECT roles.name FROM role_permissions "
+ "JOIN roles ON roles.id = role_permissions.role_id "
+ "JOIN permissions ON permissions.id = role_permissions.permission_id "
+ "WHERE permissions.key = 'system_health.view' ORDER BY roles.name"
+ )
+ roles = [row[0] for row in cur.fetchall()]
+ self.assertEqual(roles, ["Admin", "Manager", "Read Only"])
+
+
+class SystemHealthFrontendAndArchitectureTests(unittest.TestCase):
+ def test_page_is_semantic_safe_and_one_request(self):
+ app = FastAPI()
+ app.include_router(frontend_router)
+ app.dependency_overrides[get_current_user] = lambda: {
+ "id": str(uuid4()), "display_name": "Health ",
+ "role": "Manager", "permissions": ["system_health.view"],
+ }
+ client = TestClient(app)
+ response = client.get("/system-health")
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("System Health | MinerOps", response.text)
+ self.assertNotIn("Health ", response.text)
+ self.assertIn("aria-live=\"polite\"", response.text)
+ client.close()
+ script = (APP_ROOT / "frontend/static/system-health.js").read_text()
+ self.assertEqual(script.count('fetch("/api/system/health-summary"'), 1)
+ self.assertNotIn("setInterval", script)
+ self.assertNotIn("innerHTML", script)
+ self.assertIn("textContent", script)
+
+ def test_routes_are_unique_and_legacy_source_diagnostics_are_removed(self):
+ source = (APP_ROOT / "main.py").read_text()
+ self.assertNotIn("source_lines", source)
+ self.assertNotIn("recent_patch_markers", source)
+ from app.main import app
+ page = [r for r in app.routes if getattr(r, "path", None) == "/system-health"]
+ api = [r for r in app.routes if getattr(r, "path", None) == "/api/system/health-summary"]
+ self.assertEqual(len(page), 1)
+ self.assertEqual(len(api), 1)
+ self.assertEqual(page[0].methods, {"GET"})
+ self.assertEqual(api[0].methods, {"GET"})
+
+ def test_layers_and_security_boundaries(self):
+ router_source = (APP_ROOT / "system_health/router.py").read_text()
+ service_source = (APP_ROOT / "system_health/service.py").read_text()
+ repository_source = (APP_ROOT / "system_health/repository.py").read_text()
+ ast.parse(router_source)
+ ast.parse(service_source)
+ self.assertNotIn("SELECT ", router_source)
+ self.assertNotIn("SELECT ", service_source)
+ self.assertNotIn("fastapi", repository_source.lower())
+ self.assertNotIn("fastapi", service_source.lower())
+ static = "\n".join(
+ (APP_ROOT / path).read_text()
+ for path in (
+ "frontend/static/system-health.js",
+ "frontend/static/system-health.css",
+ )
+ ).lower()
+ for forbidden in (
+ "postgres_password", "redis_url", "jwt_secret", "connection string",
+ "stack trace", "password hash", "api key",
+ ):
+ self.assertNotIn(forbidden, static)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/test-system-health.sh b/scripts/test-system-health.sh
new file mode 100755
index 0000000..c8e4d3e
--- /dev/null
+++ b/scripts/test-system-health.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "${repo_root}"
+
+readonly source_files=(
+ backend/app/config.py
+ backend/app/frontend/static/system-health.css
+ backend/app/frontend/static/system-health.js
+ backend/app/frontend/system_health_view.py
+ backend/app/frontend/views.py
+ backend/app/main.py
+ backend/app/system_health/__init__.py
+ backend/app/system_health/adapters.py
+ backend/app/system_health/application.py
+ backend/app/system_health/dependencies.py
+ backend/app/system_health/frontend.py
+ backend/app/system_health/models.py
+ backend/app/system_health/repository.py
+ backend/app/system_health/router.py
+ backend/app/system_health/service.py
+ backend/tests/__init__.py
+ backend/tests/test_frontend_shell.py
+ backend/tests/test_system_health.py
+)
+
+for file in "${source_files[@]}"; do
+ [[ -r "${file}" ]] || { echo "Unreadable System Health input: ${file}" >&2; exit 1; }
+ [[ "$(stat -c '%a' "${file}")" == "644" ]] || {
+ echo "System Health source/test mode must be 0644: ${file}" >&2; exit 1;
+ }
+done
+[[ "$(stat -c '%a' scripts/test-system-health.sh)" == "755" ]] || {
+ echo "scripts/test-system-health.sh must have mode 0755." >&2; exit 1;
+}
+echo "System Health file-mode preflight passed."
+
+python3 -m py_compile backend/app/config.py backend/app/frontend/system_health_view.py backend/app/frontend/views.py backend/app/main.py backend/app/system_health/*.py backend/tests/__init__.py backend/tests/test_frontend_shell.py backend/tests/test_system_health.py
+node --check backend/app/frontend/static/system-health.js
+bash -n scripts/test-system-health.sh
+
+[[ -f .env.dev ]] || { echo "Missing .env.dev." >&2; exit 1; }
+project_name="${MINEROPS_SYSTEM_HEALTH_TEST_PROJECT:-minerops-system-health-$$}"
+compose=(docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml)
+cleanup() {
+ status=$?
+ if (( status != 0 )); then
+ "${compose[@]}" logs --no-color --tail 100 postgres redis || true
+ fi
+ "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
+ exit "${status}"
+}
+trap cleanup EXIT
+
+"${compose[@]}" up --detach postgres redis
+for attempt in $(seq 1 60); do
+ if "${compose[@]}" exec -T postgres sh -ec \
+ 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1 \
+ && "${compose[@]}" exec -T redis redis-cli ping 2>/dev/null | grep -qx PONG; then
+ break
+ fi
+ (( attempt < 60 )) || { echo "Dependencies did not become ready." >&2; exit 1; }
+ sleep 1
+done
+"${compose[@]}" build backend
+"${compose[@]}" run --rm --no-deps backend alembic upgrade head
+"${compose[@]}" run --rm --no-deps \
+ -v "${repo_root}/backend/tests:/app/tests:ro" \
+ backend python -m unittest -v tests.test_system_health tests.test_frontend_shell