Files
minerops/backend/app/frontend/views.py
T
2026-08-11 14:27:09 +00:00

364 lines
15 KiB
Python

"""Shared semantic HTML for the MinerOps Internal Alpha shell."""
from html import escape
from typing import Callable
NAVIGATION = (
("dashboard", "Dashboard", "/dashboard", "dashboard.view"),
("map", "Mine Map", "/monitoring/map", "locations.view"),
("repairs", "Repair Center", "/operations/repairs", "repairs.view"),
("miners", "Miners", "/assets/miners", "miners.view"),
)
PAGE_DETAILS = {
"dashboard": (
"Fleet overview",
"Dashboard",
"A concise starting point for operational state and recent activity.",
),
"map": (
"Monitoring / Spatial operations",
"Mine Map",
"Inspect where miners and operational conditions are concentrated.",
),
"repairs": (
"Operations",
"Repair Center",
"Review miners currently represented in the backend repair queue.",
),
"miners": (
"Assets",
"Miners",
"Search durable miner inventory and inspect current identity and state.",
),
}
def _dashboard_content() -> str:
metrics = (
("Site health", "--", "Live health summary not yet connected"),
("Total miners", "--", "Inventory summary not yet connected"),
("Miners in repair", "--", "Repair summary not yet connected"),
("Current hashrate", "-- TH/s", "Telemetry not yet connected"),
("Current power", "-- kW", "Telemetry not yet connected"),
)
metric_html = "".join(
"<article class='metric'>"
f"<span class='metric-label'>{escape(label)}</span>"
f"<strong class='metric-value'>{escape(value)}</strong>"
f"<span class='metric-state'>{escape(state)}</span>"
"</article>"
for label, value, state in metrics
)
return f"""
<section class="metrics" aria-label="Fleet summary placeholders">
{metric_html}
</section>
<div class="content-grid">
<section class="panel" aria-labelledby="activityTitle">
<div class="panel-head">
<div>
<h2 id="activityTitle">Recent activity</h2>
<p class="panel-copy">Operational events will appear here when
the dashboard activity feed is connected.</p>
</div>
</div>
<div class="state-message">
<strong>No activity feed connected</strong>
<span>No events are fabricated for this placeholder.</span>
</div>
</section>
<aside class="panel" aria-labelledby="attentionTitle">
<h2 id="attentionTitle">Data boundary</h2>
<p class="panel-copy">Summary cards intentionally show unavailable
values until live APIs are connected and freshness can be represented
accurately.</p>
</aside>
</div>
"""
def _map_content(can_manage_locations: bool = False) -> str:
manage_link = (
'<a class="object-link" href="/locations">Manage locations</a>'
if can_manage_locations
else ""
)
return f"""
<section class="map-context" aria-labelledby="mapControlsTitle">
<div class="map-context-head">
<div>
<h2 id="mapControlsTitle">Map context</h2>
<p id="mapScope" class="panel-copy">Select a site and room.</p>
</div>
{manage_link}
</div>
<div class="map-control-grid">
<div class="field"><label for="mapSite">Site</label>
<select id="mapSite" aria-describedby="mapScope"><option>Loading sites...</option></select></div>
<div class="field"><label for="mapRoom">Room</label>
<select id="mapRoom"><option>Loading rooms...</option></select></div>
<div class="field"><label for="mapLayer">Layer</label>
<select id="mapLayer">
<option value="status">Operational Status</option>
<option value="temperature">Temperature</option>
<option value="hashrate">Hashrate</option>
<option value="power">Power</option>
<option value="repair">Repair State</option>
</select></div>
<div class="field map-search"><label for="mapSearch">Find miner</label>
<input id="mapSearch" type="search" autocomplete="off"
placeholder="Asset ID, serial, MAC, or IP"></div>
<label class="map-problems-control" for="mapProblemsOnly">
<input id="mapProblemsOnly" type="checkbox">
<span>Problems only</span>
</label>
</div>
<details class="map-filters" id="mapFilters">
<summary>Filters <span id="mapFilterCount" aria-live="polite"></span></summary>
<div class="map-filter-grid">
<div class="field"><label for="mapLifecycle">Lifecycle</label>
<select id="mapLifecycle"><option value="">All lifecycle states</option></select></div>
<div class="field"><label for="mapModel">Model</label>
<select id="mapModel"><option value="">All models</option></select></div>
<div class="field"><label for="mapRepair">Repair</label>
<select id="mapRepair"><option value="">All repair states</option><option value="active">Active repair</option><option value="none">No active repair</option><option value="legacy">Legacy repair location</option></select></div>
<div class="field"><label for="mapStatus">Operational status</label>
<select id="mapStatus"><option value="">All operational states</option></select></div>
<button id="mapClearFilters" class="quiet-button" type="button">Clear filters</button>
</div>
</details>
</section>
<section class="map-status-band" aria-label="Map summary">
<div id="mapSummary" class="map-summary" aria-live="polite"></div>
<div id="mapFreshness" class="freshness">Waiting for map data</div>
</section>
<div id="mapCriticalState" class="state-message" data-kind="error" role="alert" hidden>
<strong id="mapCriticalTitle">Map unavailable</strong>
<span id="mapCriticalDetail">The map could not be loaded.</span>
<button id="mapRetry" class="command-button" type="button">Retry</button>
</div>
<div id="mapLoading" class="state-message" role="status">
<strong>Loading Mine Map</strong>
<span>Requesting the current physical layout and operational observations.</span>
</div>
<section class="mine-map-workspace" id="mapWorkspace" hidden>
<div class="map-main-column">
<div id="mapLegend" class="map-legend" aria-label="Map legend"></div>
<p id="mapPermissionNote" class="map-notice" hidden>
Miner details require inventory access. Layout and occupancy remain visible.
</p>
<div id="mapNotice" class="map-notice" role="status" hidden></div>
<div id="mapViewport" class="map-viewport-v1"
aria-label="Room racks, shelves, and positions"></div>
<section id="mapLegacySection" class="unplaced-section" hidden
aria-labelledby="mapLegacyTitle">
<div class="panel-head"><div><h2 id="mapLegacyTitle">Unscoped legacy positions</h2>
<p class="panel-copy">Stored positions without a durable room relationship. They are not assigned to the selected site or room.</p></div></div>
<div id="mapLegacyPositions" class="legacy-position-list"></div>
</section>
<section id="mapUnplacedSection" class="unplaced-section" hidden
aria-labelledby="unplacedTitle">
<div class="panel-head"><div><h2 id="unplacedTitle">Unplaced and operational areas</h2>
<p class="panel-copy">Miner records without a configured rack position.</p></div></div>
<div id="mapUnplaced"></div>
</section>
</div>
<aside id="mapInspector" class="map-inspector-v1" aria-labelledby="mapInspectorTitle" hidden>
<div class="inspector-head">
<div><p class="eyebrow">Selected position</p><h2 id="mapInspectorTitle">Miner details</h2></div>
<button id="mapInspectorClose" class="icon-button" type="button" aria-label="Close miner details" title="Close miner details">X</button>
</div>
<p id="mapInspectorStatus" class="status-callout"></p>
<dl id="mapInspectorDetails" class="definition-list inspector-details"></dl>
<a id="mapOpenMiner" class="command-link" href="/assets/miners">Open Miner</a>
</aside>
</section>
"""
def _state_messages(noun: str) -> str:
return f"""
<div class="state-message" data-view-state="loading" role="status">
<strong>Loading {noun}</strong>
<span data-state-detail>Requesting current information from MinerOps.</span>
</div>
<div class="state-message" data-view-state="empty" hidden>
<strong>No {noun} found</strong>
<span data-state-detail>There is no information to show in this view.</span>
</div>
<div class="state-message" data-view-state="denied" data-kind="error" hidden>
<strong>Access unavailable</strong>
<span data-state-detail>Your account cannot view this information.</span>
</div>
<div class="state-message" data-view-state="error" data-kind="error" hidden>
<strong>Could not load {noun}</strong>
<span data-state-detail>Retry after checking the MinerOps connection.</span>
</div>
"""
def _repairs_content() -> str:
return f"""
<section class="panel" aria-labelledby="repairQueueTitle">
<div class="panel-head">
<div>
<h2 id="repairQueueTitle">Current repair queue</h2>
<p class="panel-copy">Real records from the existing repair queue
API. Open a miner to continue in its current detail view.</p>
</div>
<span class="freshness">Loaded on request</span>
</div>
{_state_messages("repair records")}
<div class="table-wrap" data-view-state="ready" hidden>
<table>
<thead><tr><th>Miner</th><th>Model</th><th>Repair state</th>
<th>Reported issue</th><th>Last update</th></tr></thead>
<tbody id="repairRows"></tbody>
</table>
</div>
</section>
"""
def _miners_content() -> str:
return f"""
<section class="panel" aria-labelledby="minerListTitle">
<div class="panel-head">
<div>
<h2 id="minerListTitle">Miner inventory</h2>
<p class="panel-copy">Real inventory records. Search matches the
fields supported by the Inventory API.</p>
</div>
<span class="freshness">Up to 100 results</span>
</div>
<form class="filter-bar" id="minerSearchForm" role="search">
<div class="field">
<label for="minerSearchInput">Search miners</label>
<input id="minerSearchInput" type="search" autocomplete="off"
placeholder="Asset ID, MAC, serial, model, or IP">
</div>
<button class="command-button" type="submit">Search</button>
</form>
{_state_messages("miners")}
<div class="table-wrap" data-view-state="ready" hidden>
<table>
<thead><tr><th>Asset ID</th><th>Current MAC</th><th>Model</th>
<th>Lifecycle</th><th>Runtime</th><th>Location</th></tr></thead>
<tbody id="minerRows"></tbody>
</table>
</div>
</section>
"""
CONTENT = {
"dashboard": _dashboard_content,
"map": _map_content,
"repairs": _repairs_content,
"miners": _miners_content,
}
def render_app_page(
view: str,
current_user: dict,
has_permission: Callable[[str], bool],
) -> str:
context, title, summary = PAGE_DETAILS[view]
display_name = escape(
str(
current_user.get("display_name")
or current_user.get("username")
or "MinerOps user"
)
)
role = escape(str(current_user.get("role") or "Authenticated user"))
can_view_miners = has_permission("miners.view")
navigation = "".join(
(
f'<li><a href="{href}" aria-current="page">{escape(label)}</a></li>'
if key == view
else f'<li><a href="{href}">{escape(label)}</a></li>'
)
for key, label, href, permission in NAVIGATION
if has_permission(permission)
)
global_search = (
'<form class="global-search" id="globalSearchForm" role="search">'
'<label for="globalSearchInput">Search miners</label>'
'<input id="globalSearchInput" type="search" autocomplete="off" '
'placeholder="Search miners (more sources planned)">'
'</form>'
if can_view_miners
else '<div class="global-search-unavailable" aria-hidden="true"></div>'
)
content = (
_map_content(has_permission("locations.manage"))
if view == "map"
else CONTENT[view]()
)
map_script = (
'<script src="/assets/minerops-mine-map.js" defer></script>'
if view == "map"
else ""
)
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(title)} | MinerOps</title>
<link rel="stylesheet" href="/assets/minerops-app.css">
</head>
<body data-view="{escape(view)}"
data-can-view-miners="{str(can_view_miners).lower()}">
<a class="skip-link" href="#mainContent">Skip to main content</a>
<div class="app-shell">
<header class="topbar">
<div class="brand-area">
<button class="menu-button" id="menuButton" type="button"
aria-controls="primaryNav" aria-expanded="false">Menu</button>
<a class="brand" href="/dashboard">
<span class="brand-name">MinerOps</span>
<span class="brand-tagline">Operational Intelligence for Mining</span>
</a>
</div>
{global_search}
<div class="session-tools">
<button class="quiet-button" type="button"
aria-label="Alerts are not yet connected"
title="Alerts are not yet connected"
disabled>Alerts: unavailable</button>
<div class="session-copy">
<span class="session-user">{display_name}</span>
<span class="session-role">{role}</span>
</div>
<button class="quiet-button" id="logoutButton"
type="button">Sign out</button>
</div>
</header>
<nav class="primary-nav" id="primaryNav"
aria-label="Primary navigation" data-open="false">
<ul>{navigation}</ul>
<p class="nav-note">Internal Alpha navigation. Additional
destinations appear only as working, authorized views.</p>
</nav>
<main class="main-content" id="mainContent" tabindex="-1">
<header class="page-head">
<div>
<p class="eyebrow">{escape(context)}</p>
<h1>{escape(title)}</h1>
<p class="page-summary">{escape(summary)}</p>
</div>
<p class="freshness">Current view: {escape(title)}</p>
</header>
{content}
</main>
</div>
<script src="/assets/minerops-app.js" defer></script>
{map_script}
</body>
</html>"""