467 lines
22 KiB
Python
467 lines
22 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(can_view_locations: bool, can_view_miners: bool) -> str:
|
|
location_filters = """
|
|
<div class="field"><label for="repairSite">Site</label>
|
|
<select id="repairSite"><option value="">All sites</option></select></div>
|
|
<div class="field"><label for="repairRoom">Room</label>
|
|
<select id="repairRoom"><option value="">All rooms</option></select></div>
|
|
""" if can_view_locations else ""
|
|
search_placeholder = (
|
|
"Miner, model, status, reason, or context"
|
|
if can_view_miners else "Repair status, reason, or context"
|
|
)
|
|
model_filter = """
|
|
<div class="field"><label for="repairModel">Model</label>
|
|
<input id="repairModel" type="text" autocomplete="off" placeholder="Any model"></div>
|
|
""" if can_view_miners else ""
|
|
return f"""
|
|
<section class="panel repair-center" aria-labelledby="repairQueueTitle">
|
|
<div class="panel-head">
|
|
<div><h2 id="repairQueueTitle">Repair Center</h2>
|
|
<p class="panel-copy">Active work and historical modular repairs, with legacy queue records clearly identified.</p></div>
|
|
<span class="freshness" id="repairFreshness">Loading repair work</span>
|
|
</div>
|
|
<div class="repair-summary" aria-label="Repair summary">
|
|
<div><span>Total results</span><strong id="repairTotal">-</strong></div>
|
|
<div><span>Active</span><strong id="repairActive">-</strong></div>
|
|
<div><span>Historical</span><strong id="repairHistorical">-</strong></div>
|
|
</div>
|
|
<form class="repair-filters" id="repairFilters" role="search">
|
|
<div class="field repair-search"><label for="repairQuery">Search repairs</label>
|
|
<input id="repairQuery" type="search" autocomplete="off"
|
|
placeholder="{search_placeholder}"></div>
|
|
<div class="field"><label for="repairScope">Scope</label>
|
|
<select id="repairScope"><option value="active">Active</option>
|
|
<option value="historical">Historical</option><option value="all">All</option></select></div>
|
|
<div class="field"><label for="repairStatus">Repair status</label>
|
|
<select id="repairStatus"><option value="">All statuses</option></select></div>
|
|
{model_filter}
|
|
<div class="field"><label for="repairLifecycle">Miner lifecycle</label>
|
|
<select id="repairLifecycle"><option value="">All lifecycle states</option>
|
|
<option value="active">Active</option><option value="spare">Spare</option>
|
|
<option value="in_repair">In repair</option><option value="retired">Retired</option></select></div>
|
|
{location_filters}
|
|
<div class="repair-filter-actions">
|
|
<button class="command-button" type="submit">Apply</button>
|
|
<button class="quiet-button" id="repairReset" type="button">Reset</button>
|
|
</div>
|
|
</form>
|
|
{_state_messages("repair records")}
|
|
<div data-view-state="ready" hidden>
|
|
<p class="repair-result-summary" id="repairResultSummary" aria-live="polite"></p>
|
|
<div class="table-wrap repair-table-wrap">
|
|
<table class="repair-table">
|
|
<thead><tr><th>Miner</th><th>Repair state</th><th>Reported problem</th>
|
|
<th>Latest context</th><th>Location</th><th>Opened / age</th>
|
|
<th>Parts</th><th>Actions</th></tr></thead>
|
|
<tbody id="repairRows"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<nav class="repair-pagination" aria-label="Repair result pages">
|
|
<button class="quiet-button" id="repairPrevious" type="button" disabled>Previous</button>
|
|
<label for="repairLimit">Rows per page</label>
|
|
<select id="repairLimit"><option value="25">25</option><option value="50">50</option>
|
|
<option value="100">100</option><option value="250">250</option></select>
|
|
<button class="quiet-button" id="repairNext" type="button" disabled>Next</button>
|
|
</nav>
|
|
</section>
|
|
<aside class="repair-inspector" id="repairInspector" aria-labelledby="repairInspectorTitle" hidden>
|
|
<div class="inspector-head"><div><span class="eyebrow">Repair context</span>
|
|
<h2 id="repairInspectorTitle">Repair details</h2></div>
|
|
<button class="icon-button" id="repairInspectorClose" type="button" aria-label="Close repair details">X</button></div>
|
|
<div id="repairInspectorBody"></div>
|
|
</aside>
|
|
"""
|
|
|
|
|
|
def _miners_content(can_view_locations: bool) -> str:
|
|
location_filters = """
|
|
<div class="field"><label for="fleetSite">Site</label>
|
|
<select id="fleetSite"><option value="">All sites</option></select></div>
|
|
<div class="field"><label for="fleetRoom">Room</label>
|
|
<select id="fleetRoom"><option value="">All rooms</option></select></div>
|
|
""" if can_view_locations else ""
|
|
return f"""
|
|
<section class="panel fleet-panel" aria-labelledby="minerListTitle">
|
|
<div class="panel-head">
|
|
<div><h2 id="minerListTitle">Fleet</h2>
|
|
<p class="panel-copy">Current miner identity, operating state, telemetry, and repair context.</p></div>
|
|
<span class="freshness" id="fleetFreshness">Loading current fleet</span>
|
|
</div>
|
|
<form class="fleet-filters" id="fleetFilters" role="search">
|
|
<div class="field fleet-search"><label for="fleetQuery">Search miners</label>
|
|
<input id="fleetQuery" type="search" autocomplete="off"
|
|
placeholder="Asset ID, MAC, IP, model, or serial"></div>
|
|
<div class="field"><label for="fleetLifecycle">Lifecycle</label>
|
|
<select id="fleetLifecycle"><option value="">All lifecycle states</option>
|
|
<option value="active">Active</option><option value="spare">Spare</option>
|
|
<option value="in_repair">In repair</option><option value="retired">Retired</option></select></div>
|
|
<div class="field"><label for="fleetModel">Model</label>
|
|
<input id="fleetModel" type="text" autocomplete="off" placeholder="Any model"></div>
|
|
<div class="field"><label for="fleetRuntime">Runtime</label>
|
|
<select id="fleetRuntime"><option value="">All runtime states</option>
|
|
<option value="online">Online</option><option value="offline">Offline</option>
|
|
<option value="unknown">Unknown</option></select></div>
|
|
<div class="field"><label for="fleetRepair">Repair</label>
|
|
<select id="fleetRepair"><option value="">Any repair state</option>
|
|
<option value="active">Active repair</option><option value="none">No active repair</option></select></div>
|
|
<div class="field"><label for="fleetOperational">Operational state</label>
|
|
<select id="fleetOperational"><option value="">All operational states</option>
|
|
<option value="online">Online</option><option value="offline">Offline</option>
|
|
<option value="unstable">Hash unstable</option><option value="repair">In repair</option>
|
|
<option value="spare">Spare</option><option value="retired">Retired</option>
|
|
<option value="unknown">Unknown</option></select></div>
|
|
{location_filters}
|
|
<div class="field"><label for="fleetTags">Tags</label>
|
|
<input id="fleetTags" type="text" autocomplete="off" placeholder="Comma separated"></div>
|
|
<label class="check-control" for="fleetProblems">
|
|
<input id="fleetProblems" type="checkbox"> Problems only</label>
|
|
<div class="fleet-filter-actions">
|
|
<button class="command-button" type="submit">Apply</button>
|
|
<button class="quiet-button" id="fleetReset" type="button">Reset</button>
|
|
</div>
|
|
</form>
|
|
{_state_messages("miners")}
|
|
<div data-view-state="ready" hidden>
|
|
<p class="fleet-result-summary" id="fleetResultSummary" aria-live="polite"></p>
|
|
<div class="table-wrap fleet-table-wrap">
|
|
<table class="fleet-table">
|
|
<thead><tr><th>Miner</th><th>Operational</th><th>Model / Serial</th>
|
|
<th>Network</th><th>Location</th><th>Hashrate</th><th>Power</th>
|
|
<th>Max chip temp</th><th>Lifecycle / Repair</th><th>Actions</th></tr></thead>
|
|
<tbody id="fleetRows"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<nav class="fleet-pagination" aria-label="Fleet pages">
|
|
<button class="quiet-button" id="fleetPrevious" type="button" disabled>Previous</button>
|
|
<label for="fleetLimit">Rows per page</label>
|
|
<select id="fleetLimit"><option value="25">25</option><option value="50">50</option>
|
|
<option value="100">100</option><option value="250">250</option></select>
|
|
<button class="quiet-button" id="fleetNext" type="button" disabled>Next</button>
|
|
</nav>
|
|
</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 _miners_content(has_permission("locations.view"))
|
|
if view == "miners"
|
|
else _repairs_content(
|
|
has_permission("locations.view"), has_permission("miners.view")
|
|
)
|
|
if view == "repairs"
|
|
else CONTENT[view]()
|
|
)
|
|
page_script = (
|
|
'<script src="/assets/minerops-mine-map.js" defer></script>'
|
|
if view == "map"
|
|
else '<script src="/assets/minerops-fleet.js" defer></script>'
|
|
if view == "miners"
|
|
else '<script src="/assets/minerops-repair-center.js" defer></script>'
|
|
if view == "repairs"
|
|
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()}"
|
|
data-can-view-locations="{str(has_permission("locations.view")).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>
|
|
{page_script}
|
|
</body>
|
|
</html>"""
|