feat: add operational Dashboard
This commit is contained in:
1 parent
4a1959ecd6
commit
b8b741ccee
23 files changed
+1278
-59
No files matched your search
@@ -0,0 +1,347 @@
|
||||
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.dashboard.dependencies import get_dashboard_service
|
||||
from app.dashboard.models import (
|
||||
DashboardAccess,
|
||||
DashboardOverviewData,
|
||||
DashboardOverviewResponse,
|
||||
)
|
||||
from app.dashboard.router import router
|
||||
from app.dashboard.service import DashboardService
|
||||
from app.identity import get_current_user
|
||||
from app.inventory.models import (
|
||||
InventoryFleetAttentionSummary,
|
||||
InventoryFleetSummary,
|
||||
)
|
||||
from app.inventory.repository import InventoryRepository
|
||||
from app.locations.models import LocationSummary
|
||||
from app.locations.repository import LocationMapRepository
|
||||
from app.repairs.center_repository import RepairCenterRepository
|
||||
from app.repairs.models import ActiveRepairSummary
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 11, 12, 0, tzinfo=timezone.utc)
|
||||
APP_ROOT = Path(__file__).parents[1] / "app"
|
||||
|
||||
|
||||
def fleet_summary() -> InventoryFleetSummary:
|
||||
return InventoryFleetSummary(
|
||||
total=12,
|
||||
runtime_online=7,
|
||||
runtime_offline=3,
|
||||
runtime_unknown=2,
|
||||
lifecycle_in_repair=2,
|
||||
lifecycle_spare=1,
|
||||
lifecycle_retired=1,
|
||||
attention=InventoryFleetAttentionSummary(
|
||||
offline=2,
|
||||
unknown=1,
|
||||
unstable=1,
|
||||
repair=2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FakeInventoryService:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def summarize_fleet(self):
|
||||
self.calls += 1
|
||||
return fleet_summary()
|
||||
|
||||
|
||||
class FakeRepairService:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def summarize_active_repairs(self):
|
||||
self.calls += 1
|
||||
return ActiveRepairSummary(
|
||||
active=3,
|
||||
status_counts={"OPEN": 2, "waiting_parts": 1},
|
||||
)
|
||||
|
||||
|
||||
class FakeLocationService:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def summarize_locations(self, *, include_miner_counts):
|
||||
self.calls.append(include_miner_counts)
|
||||
return LocationSummary(
|
||||
sites=2,
|
||||
rooms=4,
|
||||
positions=20,
|
||||
occupied_positions=12,
|
||||
empty_positions=8,
|
||||
mapped_miners=12 if include_miner_counts else None,
|
||||
unmapped_miners=0 if include_miner_counts else None,
|
||||
)
|
||||
|
||||
|
||||
class DashboardServiceTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.inventory = FakeInventoryService()
|
||||
self.repairs = FakeRepairService()
|
||||
self.locations = FakeLocationService()
|
||||
self.service = DashboardService(
|
||||
self.inventory,
|
||||
self.repairs,
|
||||
self.locations,
|
||||
)
|
||||
|
||||
def test_composes_each_authorized_domain_once(self):
|
||||
result = self.service.get_overview(
|
||||
DashboardAccess(
|
||||
can_view_miners=True,
|
||||
can_view_repairs=True,
|
||||
can_view_locations=True,
|
||||
)
|
||||
)
|
||||
self.assertEqual(result.data.fleet.total, 12)
|
||||
self.assertEqual(result.data.repairs.active, 3)
|
||||
self.assertEqual(result.data.locations.mapped_miners, 12)
|
||||
self.assertEqual(self.inventory.calls, 1)
|
||||
self.assertEqual(self.repairs.calls, 1)
|
||||
self.assertEqual(self.locations.calls, [True])
|
||||
self.assertEqual(
|
||||
result.meta.available_sections,
|
||||
["fleet", "repairs", "locations"],
|
||||
)
|
||||
|
||||
def test_unauthorized_domains_are_not_called_or_returned(self):
|
||||
result = self.service.get_overview(DashboardAccess())
|
||||
self.assertIsNone(result.data.fleet)
|
||||
self.assertIsNone(result.data.repairs)
|
||||
self.assertIsNone(result.data.locations)
|
||||
self.assertEqual(result.meta.available_sections, [])
|
||||
self.assertEqual(self.inventory.calls, 0)
|
||||
self.assertEqual(self.repairs.calls, 0)
|
||||
self.assertEqual(self.locations.calls, [])
|
||||
|
||||
def test_location_only_summary_omits_miner_placement_counts(self):
|
||||
result = self.service.get_overview(
|
||||
DashboardAccess(can_view_locations=True)
|
||||
)
|
||||
self.assertEqual(self.locations.calls, [False])
|
||||
self.assertIsNone(result.data.locations.mapped_miners)
|
||||
self.assertIsNone(result.data.locations.unmapped_miners)
|
||||
self.assertIsNone(result.data.fleet)
|
||||
|
||||
|
||||
class StubDashboardService:
|
||||
def __init__(self):
|
||||
self.access = None
|
||||
|
||||
def get_overview(self, access):
|
||||
self.access = access
|
||||
return DashboardOverviewResponse(
|
||||
data=DashboardOverviewData(
|
||||
fleet=fleet_summary() if access.can_view_miners else None,
|
||||
repairs=(
|
||||
ActiveRepairSummary(active=1, status_counts={"OPEN": 1})
|
||||
if access.can_view_repairs else None
|
||||
),
|
||||
locations=(
|
||||
LocationSummary(
|
||||
sites=1,
|
||||
rooms=1,
|
||||
positions=2,
|
||||
occupied_positions=1,
|
||||
empty_positions=1,
|
||||
mapped_miners=1 if access.can_view_miners else None,
|
||||
unmapped_miners=11 if access.can_view_miners else None,
|
||||
)
|
||||
if access.can_view_locations else None
|
||||
),
|
||||
generated_at=NOW,
|
||||
),
|
||||
meta={
|
||||
"available_sections": [
|
||||
name
|
||||
for name, allowed in (
|
||||
("fleet", access.can_view_miners),
|
||||
("repairs", access.can_view_repairs),
|
||||
("locations", access.can_view_locations),
|
||||
)
|
||||
if allowed
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class DashboardRouterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.service = StubDashboardService()
|
||||
self.app = FastAPI()
|
||||
self.app.include_router(router)
|
||||
self.app.dependency_overrides[get_dashboard_service] = lambda: self.service
|
||||
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_dashboard_permission(self):
|
||||
self.assertEqual(self.client.get("/api/dashboard/overview").status_code, 401)
|
||||
self.authenticate(("miners.view",))
|
||||
self.assertEqual(self.client.get("/api/dashboard/overview").status_code, 403)
|
||||
|
||||
def test_returns_one_typed_permission_aware_envelope(self):
|
||||
self.authenticate(("dashboard.view", "miners.view", "locations.view"))
|
||||
response = self.client.get("/api/dashboard/overview")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["data"]["fleet"]["total"], 12)
|
||||
self.assertIsNone(payload["data"]["repairs"])
|
||||
self.assertEqual(payload["data"]["locations"]["mapped_miners"], 1)
|
||||
self.assertEqual(
|
||||
payload["meta"]["available_sections"],
|
||||
["fleet", "locations"],
|
||||
)
|
||||
self.assertEqual(response.headers["cache-control"], "no-store")
|
||||
self.assertFalse(self.service.access.can_view_repairs)
|
||||
|
||||
def test_location_only_response_contains_no_miner_counts(self):
|
||||
self.authenticate(("dashboard.view", "locations.view"))
|
||||
payload = self.client.get("/api/dashboard/overview").json()
|
||||
self.assertIsNone(payload["data"]["fleet"])
|
||||
self.assertIsNone(payload["data"]["locations"]["mapped_miners"])
|
||||
self.assertIsNone(payload["data"]["locations"]["unmapped_miners"])
|
||||
|
||||
|
||||
class DashboardRepositoryIntegrationTests(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,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.connection.rollback()
|
||||
self.connection.close()
|
||||
|
||||
def test_fleet_counts_are_exact_unbounded_and_preserve_overlap(self):
|
||||
repository = InventoryRepository(connection=self.connection)
|
||||
before = repository.summarize_fleet()
|
||||
rows = []
|
||||
states = (
|
||||
("offline", "in_repair"),
|
||||
("online", "spare"),
|
||||
("maintenance", "retired"),
|
||||
("online", "active"),
|
||||
("offline", "active"),
|
||||
("unknown", "active"),
|
||||
)
|
||||
for runtime, lifecycle in states:
|
||||
rows.append((uuid4(), runtime, lifecycle))
|
||||
bulk = [(uuid4(), "online", "active") for _ in range(501)]
|
||||
with self.connection.cursor() as cur:
|
||||
cur.executemany(
|
||||
"INSERT INTO miners (id, status, lifecycle_status, first_seen_at, last_seen_at) "
|
||||
"VALUES (%s, %s, %s, NULL, NULL)",
|
||||
[*rows, *bulk],
|
||||
)
|
||||
cur.execute(
|
||||
"INSERT INTO miner_status_snapshots (miner_id, hash_stable) VALUES (%s, false)",
|
||||
(rows[3][0],),
|
||||
)
|
||||
after = repository.summarize_fleet()
|
||||
self.assertEqual(after["total"] - before["total"], 507)
|
||||
self.assertEqual(after["runtime_online"] - before["runtime_online"], 503)
|
||||
self.assertEqual(after["runtime_offline"] - before["runtime_offline"], 2)
|
||||
self.assertEqual(after["runtime_unknown"] - before["runtime_unknown"], 2)
|
||||
self.assertEqual(after["lifecycle_in_repair"] - before["lifecycle_in_repair"], 1)
|
||||
self.assertEqual(after["lifecycle_spare"] - before["lifecycle_spare"], 1)
|
||||
self.assertEqual(after["lifecycle_retired"] - before["lifecycle_retired"], 1)
|
||||
self.assertEqual(after["attention_repair"] - before["attention_repair"], 1)
|
||||
self.assertEqual(after["attention_offline"] - before["attention_offline"], 1)
|
||||
self.assertEqual(after["attention_unknown"] - before["attention_unknown"], 1)
|
||||
self.assertEqual(after["attention_unstable"] - before["attention_unstable"], 1)
|
||||
|
||||
def test_location_summary_uses_current_positions_not_virtual_map_subset(self):
|
||||
repository = LocationMapRepository(connection=self.connection)
|
||||
before = repository.summarize_locations(include_miner_counts=True)
|
||||
site_id, room_id = uuid4(), uuid4()
|
||||
mapped_id, unmapped_id = uuid4(), uuid4()
|
||||
with self.connection.cursor() as cur:
|
||||
cur.execute("INSERT INTO sites (id, name) VALUES (%s, %s)", (site_id, str(site_id)))
|
||||
cur.execute(
|
||||
"INSERT INTO rooms (id, site_id, name) VALUES (%s, %s, %s)",
|
||||
(room_id, site_id, str(room_id)),
|
||||
)
|
||||
cur.executemany(
|
||||
"INSERT INTO miners (id, lifecycle_status, first_seen_at, last_seen_at) "
|
||||
"VALUES (%s, 'active', NULL, NULL)",
|
||||
[(mapped_id,), (unmapped_id,)],
|
||||
)
|
||||
cur.execute(
|
||||
"INSERT INTO location_positions (room_id, room_name, rack_name, shelf_name, "
|
||||
"position_index, miner_id) VALUES (%s, 'Room', 'A', '1', 1, %s), "
|
||||
"(%s, 'Room', 'A', '1', 2, NULL)",
|
||||
(room_id, mapped_id, room_id),
|
||||
)
|
||||
after = repository.summarize_locations(include_miner_counts=True)
|
||||
self.assertEqual(after["sites"] - before["sites"], 1)
|
||||
self.assertEqual(after["rooms"] - before["rooms"], 1)
|
||||
self.assertEqual(after["positions"] - before["positions"], 2)
|
||||
self.assertEqual(after["occupied_positions"] - before["occupied_positions"], 1)
|
||||
self.assertEqual(after["empty_positions"] - before["empty_positions"], 1)
|
||||
self.assertEqual(after["mapped_miners"] - before["mapped_miners"], 1)
|
||||
self.assertEqual(after["unmapped_miners"] - before["unmapped_miners"], 1)
|
||||
|
||||
def test_repair_summary_executes_without_loading_queue_items(self):
|
||||
summary = RepairCenterRepository().summarize_active_repairs()
|
||||
self.assertIn("active", summary)
|
||||
self.assertIn("status_counts", summary)
|
||||
|
||||
|
||||
class DashboardArchitectureTests(unittest.TestCase):
|
||||
def test_router_and_service_have_no_sql_or_database_driver_imports(self):
|
||||
for relative in ("dashboard/router.py", "dashboard/service.py"):
|
||||
source = (APP_ROOT / relative).read_text(encoding="utf-8")
|
||||
modules = []
|
||||
for node in ast.walk(ast.parse(source)):
|
||||
if isinstance(node, ast.Import):
|
||||
modules.extend(alias.name for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
modules.append(node.module)
|
||||
self.assertFalse(
|
||||
any(name.startswith(("psycopg", "sqlalchemy", "asyncpg")) for name in modules)
|
||||
)
|
||||
for keyword in ("SELECT ", "INSERT ", "UPDATE ", "DELETE "):
|
||||
self.assertNotIn(keyword, source.upper())
|
||||
|
||||
def test_dashboard_frontend_has_one_read_only_request_and_safe_dom_writes(self):
|
||||
source = (APP_ROOT / "frontend" / "static" / "dashboard.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertEqual(source.count('fetch("/api/dashboard/overview"'), 1)
|
||||
self.assertNotIn("innerHTML", source)
|
||||
self.assertNotIn("setInterval", source)
|
||||
self.assertNotIn('method: "POST"', source)
|
||||
self.assertNotIn('method: "PATCH"', source)
|
||||
self.assertNotIn('method: "DELETE"', source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user