feat: add internal alpha frontend shell
This commit is contained in:
1 parent
eeb3c7634e
commit
b741fd799b
10 files changed
+1982
-15
No files matched your search
@@ -0,0 +1,301 @@
|
||||
"""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() -> str:
|
||||
return """
|
||||
<div class="notice" role="status">Live layout and telemetry data are not
|
||||
connected to this Internal Alpha canvas. No production state is represented
|
||||
below.</div>
|
||||
<section class="map-layout" aria-label="Mine Map placeholder">
|
||||
<aside class="map-controls" aria-labelledby="mapHierarchyTitle">
|
||||
<h2 id="mapHierarchyTitle">Location hierarchy</h2>
|
||||
<ul class="hierarchy">
|
||||
<li>Site</li>
|
||||
<li>Room</li>
|
||||
<li>Rack</li>
|
||||
<li>Shelf / position</li>
|
||||
</ul>
|
||||
<div class="control-stack">
|
||||
<div class="field">
|
||||
<label for="statusLayer">Status layer</label>
|
||||
<select id="statusLayer" disabled>
|
||||
<option>Live status unavailable</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mapFilter">Filters</label>
|
||||
<input id="mapFilter" type="search"
|
||||
placeholder="Filters not connected" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="map-viewport" aria-label="Operational map viewport">
|
||||
<div class="map-empty">
|
||||
<strong>Operational canvas</strong>
|
||||
<span>Site, room, rack, miner, and status overlays will render here
|
||||
when authoritative layout and telemetry sources are connected.</span>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="map-inspector" aria-labelledby="selectionTitle">
|
||||
<h2 id="selectionTitle">Selection details</h2>
|
||||
<dl class="definition-list">
|
||||
<dt>Selection</dt><dd>None</dd>
|
||||
<dt>Status</dt><dd>Not connected</dd>
|
||||
<dt>Freshness</dt><dd>Unavailable</dd>
|
||||
</dl>
|
||||
</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 = CONTENT[view]()
|
||||
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>
|
||||
</body>
|
||||
</html>"""
|
||||
Reference in new issue
Block a user