feat: add agent management
This commit is contained in:
1 parent
d9f0da694b
commit
b9b5e5e36f
17 files changed
+1644
-1
No files matched your search
@@ -0,0 +1,268 @@
|
||||
"""Semantic HTML for the Internal Alpha Agent Management page."""
|
||||
|
||||
|
||||
def agent_management_content(can_manage_agents: bool) -> str:
|
||||
management = """
|
||||
<section class="panel agent-enrollment-panel"
|
||||
aria-labelledby="agentEnrollmentCreateTitle">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow">Agent deployment</p>
|
||||
<h2 id="agentEnrollmentCreateTitle">Add Agent</h2>
|
||||
<p class="panel-copy">
|
||||
Create a short-lived one-time enrollment for an Agent Zone.
|
||||
Windows is the currently supported deployment platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="agentCreateForm" class="agent-create-form">
|
||||
<div class="field">
|
||||
<label for="agentSite">Site</label>
|
||||
<select id="agentSite" required>
|
||||
<option value="">Choose a site</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="agentZone">Agent Zone</label>
|
||||
<select id="agentZone" required disabled>
|
||||
<option value="">Choose a site first</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="agentDisplayName">Agent name</label>
|
||||
<input id="agentDisplayName"
|
||||
type="text"
|
||||
maxlength="120"
|
||||
autocomplete="off"
|
||||
placeholder="Example: Plexus Unit 4 Agent"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="agentPlatform">Platform</label>
|
||||
<select id="agentPlatform">
|
||||
<option value="windows">Windows</option>
|
||||
<option value="linux-x64" disabled>Linux x64 — coming later</option>
|
||||
<option value="linux-arm64" disabled>Linux ARM64 / Raspberry Pi — coming later</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="agent-form-actions">
|
||||
<button id="agentCreateSubmit"
|
||||
class="command-button"
|
||||
type="submit">
|
||||
Create Windows enrollment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p id="agentCreateStatus"
|
||||
class="freshness"
|
||||
aria-live="polite"></p>
|
||||
</form>
|
||||
|
||||
<section id="agentEnrollmentResult"
|
||||
class="agent-enrollment-result"
|
||||
aria-labelledby="agentEnrollmentResultTitle"
|
||||
hidden>
|
||||
<div>
|
||||
<p class="eyebrow">One-time credential</p>
|
||||
<h3 id="agentEnrollmentResultTitle">
|
||||
Enrollment created
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
This token is displayed only for this enrollment step.
|
||||
Keep it private. MinerOps does not store the plaintext token.
|
||||
</p>
|
||||
|
||||
<div class="agent-token-box">
|
||||
<code id="agentEnrollmentToken"></code>
|
||||
</div>
|
||||
|
||||
<p id="agentEnrollmentExpires" class="freshness"></p>
|
||||
|
||||
<div class="agent-form-actions">
|
||||
<button id="agentEnrollmentCopy"
|
||||
class="quiet-button"
|
||||
type="button">
|
||||
Copy token
|
||||
</button>
|
||||
<button id="agentEnrollmentDismiss"
|
||||
class="quiet-button"
|
||||
type="button">
|
||||
Clear token from screen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p id="agentEnrollmentCopyStatus"
|
||||
class="freshness"
|
||||
aria-live="polite"></p>
|
||||
|
||||
<p class="panel-copy">
|
||||
The self-enrolling Windows installer is the next deployment
|
||||
milestone. For now this verifies the complete secure
|
||||
enrollment workflow.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
""" if can_manage_agents else """
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow">Agent deployment</p>
|
||||
<h2>Agent enrollment</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="permission-note">
|
||||
Your account can view Agent status but cannot create Agent
|
||||
enrollments.
|
||||
</p>
|
||||
</section>
|
||||
"""
|
||||
|
||||
return f"""
|
||||
<div id="agentManagementLoading" class="state-message" role="status">
|
||||
<strong>Loading Agents</strong>
|
||||
<span>Requesting Agent registry and enrollment information.</span>
|
||||
</div>
|
||||
|
||||
<div id="agentManagementError"
|
||||
class="state-message"
|
||||
data-kind="error"
|
||||
role="alert"
|
||||
hidden>
|
||||
<strong>Agent Management unavailable</strong>
|
||||
<span id="agentManagementErrorText">
|
||||
Agent information could not be loaded.
|
||||
</span>
|
||||
<button id="agentManagementRetry"
|
||||
class="command-button"
|
||||
type="button">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="agentManagementContent" hidden>
|
||||
<section class="agent-summary-grid" aria-label="Agent Management summary">
|
||||
<div class="panel agent-summary-card">
|
||||
<span>Registered Agents</span>
|
||||
<strong id="agentCount">-</strong>
|
||||
</div>
|
||||
<div class="panel agent-summary-card">
|
||||
<span>Agent Zones</span>
|
||||
<strong id="agentZoneCount">-</strong>
|
||||
</div>
|
||||
<div class="panel agent-summary-card">
|
||||
<span>Waiting enrollments</span>
|
||||
<strong id="agentWaitingEnrollmentCount">-</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="agent-management-columns">
|
||||
<section class="panel" aria-labelledby="registeredAgentsTitle">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow">Durable identities</p>
|
||||
<h2 id="registeredAgentsTitle">Agents</h2>
|
||||
<p class="panel-copy">
|
||||
Registered Agent identities and their latest factual
|
||||
check-in information.
|
||||
</p>
|
||||
</div>
|
||||
<button id="agentManagementRefresh"
|
||||
class="quiet-button"
|
||||
type="button">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="agent-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<th>Site / Zone</th>
|
||||
<th>Host</th>
|
||||
<th>Version</th>
|
||||
<th>Last check-in</th>
|
||||
<th>Credential</th>
|
||||
<th>Routing</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="agentRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="agentEmpty" class="panel-copy" hidden>
|
||||
No registered Agents.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="agentZonesTitle">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow">Network routing boundaries</p>
|
||||
<h2 id="agentZonesTitle">Agent Zones</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="agent-zone-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Site</th>
|
||||
<th>Zone</th>
|
||||
<th>Networks</th>
|
||||
<th>Assigned Agent</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="agentZoneRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="agentZoneEmpty" class="panel-copy" hidden>
|
||||
No Agent Zones.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{management}
|
||||
|
||||
<section class="panel" aria-labelledby="agentEnrollmentHistoryTitle">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow">Bootstrap history</p>
|
||||
<h2 id="agentEnrollmentHistoryTitle">Enrollment history</h2>
|
||||
<p class="panel-copy">
|
||||
Metadata only. Plaintext enrollment tokens are never
|
||||
returned by this history view.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="agent-enrollment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent name</th>
|
||||
<th>Site / Zone</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Enrolled Agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="agentEnrollmentRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="agentEnrollmentEmpty" class="panel-copy" hidden>
|
||||
No Agent enrollment records.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/assets/minerops-agent-management.js" defer></script>
|
||||
"""
|
||||
@@ -18,6 +18,7 @@ DashboardUser = Annotated[dict, Depends(require_permission("dashboard.view"))]
|
||||
MapUser = Annotated[dict, Depends(require_permission("locations.view"))]
|
||||
RepairUser = Annotated[dict, Depends(require_permission("repairs.view"))]
|
||||
MinerUser = Annotated[dict, Depends(require_permission("miners.view"))]
|
||||
AgentUser = Annotated[dict, Depends(require_permission("agents.view"))]
|
||||
|
||||
|
||||
def render_page(view: str, current_user: dict, **context) -> str:
|
||||
@@ -92,6 +93,27 @@ def miner_detail_script() -> Response:
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/assets/minerops-agent-management.js",
|
||||
include_in_schema=False,
|
||||
)
|
||||
def agent_management_script() -> Response:
|
||||
return Response(
|
||||
(STATIC_ROOT / "agent-management.js").read_text(encoding="utf-8"),
|
||||
media_type="text/javascript",
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent/management",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
def agent_management_page(current_user: AgentUser) -> str:
|
||||
return render_page("agent_management", current_user)
|
||||
|
||||
|
||||
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||
def dashboard_page(current_user: DashboardUser) -> str:
|
||||
return render_page("dashboard", current_user)
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
if (document.body.dataset.view !== "agent_management") return;
|
||||
|
||||
const ids = [
|
||||
"agentManagementLoading",
|
||||
"agentManagementError",
|
||||
"agentManagementErrorText",
|
||||
"agentManagementRetry",
|
||||
"agentManagementContent",
|
||||
"agentManagementRefresh",
|
||||
"agentCount",
|
||||
"agentZoneCount",
|
||||
"agentWaitingEnrollmentCount",
|
||||
"agentRows",
|
||||
"agentEmpty",
|
||||
"agentZoneRows",
|
||||
"agentZoneEmpty",
|
||||
"agentEnrollmentRows",
|
||||
"agentEnrollmentEmpty",
|
||||
"agentCreateForm",
|
||||
"agentSite",
|
||||
"agentZone",
|
||||
"agentDisplayName",
|
||||
"agentPlatform",
|
||||
"agentCreateSubmit",
|
||||
"agentCreateStatus",
|
||||
"agentEnrollmentResult",
|
||||
"agentEnrollmentToken",
|
||||
"agentEnrollmentExpires",
|
||||
"agentEnrollmentCopy",
|
||||
"agentEnrollmentDismiss",
|
||||
"agentEnrollmentCopyStatus"
|
||||
];
|
||||
|
||||
const el = {};
|
||||
|
||||
ids.forEach(function (id) {
|
||||
el[id] = document.getElementById(id);
|
||||
});
|
||||
|
||||
let registry = {
|
||||
agents: [],
|
||||
sites: [],
|
||||
zones: []
|
||||
};
|
||||
|
||||
let enrollments = [];
|
||||
|
||||
function text(value, fallback) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return fallback || "Unavailable";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function dateTime(value) {
|
||||
if (!value) return "Never";
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.valueOf())) return "Unavailable";
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
|
||||
function words(value) {
|
||||
return text(value, "Unknown")
|
||||
.replaceAll("_", " ")
|
||||
.replace(/\b\w/g, function (letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function cell(value, className) {
|
||||
const node = document.createElement("td");
|
||||
if (className) node.className = className;
|
||||
node.textContent = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
function setState(name) {
|
||||
el.agentManagementLoading.hidden = name !== "loading";
|
||||
el.agentManagementError.hidden = name !== "error";
|
||||
el.agentManagementContent.hidden = name !== "ready";
|
||||
}
|
||||
|
||||
async function api(path, options) {
|
||||
const response = await fetch(path, Object.assign({
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
}, options || {}));
|
||||
|
||||
const payload = await response.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = payload && payload.detail
|
||||
? String(payload.detail)
|
||||
: "Request failed with status " + response.status + ".";
|
||||
throw new Error(detail);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderAgents() {
|
||||
el.agentRows.replaceChildren();
|
||||
|
||||
registry.agents.forEach(function (agent) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const identity = document.createElement("td");
|
||||
const name = document.createElement("strong");
|
||||
const id = document.createElement("span");
|
||||
|
||||
name.textContent = text(agent.display_name, "Unnamed Agent");
|
||||
id.className = "secondary";
|
||||
id.textContent = agent.agent_id;
|
||||
|
||||
identity.append(name, id);
|
||||
|
||||
const location = [
|
||||
text(agent.site_name, "Unknown site"),
|
||||
text(agent.agent_zone_name, "No Agent Zone")
|
||||
].join(" / ");
|
||||
|
||||
const host = [
|
||||
text(agent.hostname, "Unknown host"),
|
||||
text(agent.ip_address, "No IP")
|
||||
].join(" / ");
|
||||
|
||||
row.append(
|
||||
identity,
|
||||
cell(location),
|
||||
cell(host),
|
||||
cell(text(agent.version, "Unknown")),
|
||||
cell(dateTime(agent.last_checkin_at)),
|
||||
cell(words(agent.credential_state)),
|
||||
cell(words(agent.routing_state))
|
||||
);
|
||||
|
||||
el.agentRows.appendChild(row);
|
||||
});
|
||||
|
||||
el.agentEmpty.hidden = registry.agents.length !== 0;
|
||||
}
|
||||
|
||||
function renderZones() {
|
||||
el.agentZoneRows.replaceChildren();
|
||||
|
||||
registry.zones.forEach(function (zone) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const networks = Array.isArray(zone.networks) && zone.networks.length
|
||||
? zone.networks.join(", ")
|
||||
: "No networks recorded";
|
||||
|
||||
row.append(
|
||||
cell(zone.site_name),
|
||||
cell(zone.name),
|
||||
cell(networks),
|
||||
cell(text(zone.assigned_agent_id, "Unassigned")),
|
||||
cell(zone.is_enabled ? "Enabled" : "Disabled")
|
||||
);
|
||||
|
||||
el.agentZoneRows.appendChild(row);
|
||||
});
|
||||
|
||||
el.agentZoneEmpty.hidden = registry.zones.length !== 0;
|
||||
}
|
||||
|
||||
function renderEnrollments() {
|
||||
el.agentEnrollmentRows.replaceChildren();
|
||||
|
||||
enrollments.forEach(function (enrollment) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
row.append(
|
||||
cell(enrollment.display_name),
|
||||
cell(
|
||||
enrollment.site_name + " / " +
|
||||
enrollment.agent_zone_name
|
||||
),
|
||||
cell(words(enrollment.status)),
|
||||
cell(dateTime(enrollment.created_at)),
|
||||
cell(dateTime(enrollment.expires_at)),
|
||||
cell(text(enrollment.enrolled_agent_id, "Not enrolled"))
|
||||
);
|
||||
|
||||
el.agentEnrollmentRows.appendChild(row);
|
||||
});
|
||||
|
||||
el.agentEnrollmentEmpty.hidden = enrollments.length !== 0;
|
||||
}
|
||||
|
||||
function populateSites() {
|
||||
if (!el.agentSite) return;
|
||||
|
||||
const selected = el.agentSite.value;
|
||||
|
||||
el.agentSite.replaceChildren();
|
||||
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Choose a site";
|
||||
el.agentSite.appendChild(placeholder);
|
||||
|
||||
registry.sites.forEach(function (site) {
|
||||
const option = document.createElement("option");
|
||||
option.value = site.id;
|
||||
option.textContent = site.name;
|
||||
el.agentSite.appendChild(option);
|
||||
});
|
||||
|
||||
if ([...el.agentSite.options].some(function (option) {
|
||||
return option.value === selected;
|
||||
})) {
|
||||
el.agentSite.value = selected;
|
||||
}
|
||||
|
||||
populateZones();
|
||||
}
|
||||
|
||||
function populateZones() {
|
||||
if (!el.agentZone || !el.agentSite) return;
|
||||
|
||||
const siteId = el.agentSite.value;
|
||||
const selected = el.agentZone.value;
|
||||
|
||||
el.agentZone.replaceChildren();
|
||||
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
|
||||
if (!siteId) {
|
||||
placeholder.textContent = "Choose a site first";
|
||||
el.agentZone.disabled = true;
|
||||
el.agentZone.appendChild(placeholder);
|
||||
return;
|
||||
}
|
||||
|
||||
placeholder.textContent = "Choose an Agent Zone";
|
||||
el.agentZone.appendChild(placeholder);
|
||||
|
||||
const available = registry.zones.filter(function (zone) {
|
||||
return (
|
||||
zone.site_id === siteId &&
|
||||
zone.is_enabled === true &&
|
||||
!zone.assigned_agent_id
|
||||
);
|
||||
});
|
||||
|
||||
available.forEach(function (zone) {
|
||||
const option = document.createElement("option");
|
||||
option.value = zone.id;
|
||||
option.textContent = zone.name;
|
||||
el.agentZone.appendChild(option);
|
||||
});
|
||||
|
||||
el.agentZone.disabled = available.length === 0;
|
||||
|
||||
if ([...el.agentZone.options].some(function (option) {
|
||||
return option.value === selected;
|
||||
})) {
|
||||
el.agentZone.value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
el.agentCount.textContent = String(registry.agents.length);
|
||||
el.agentZoneCount.textContent = String(registry.zones.length);
|
||||
|
||||
const waiting = enrollments.filter(function (item) {
|
||||
return item.status === "waiting";
|
||||
}).length;
|
||||
|
||||
el.agentWaitingEnrollmentCount.textContent = String(waiting);
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSummary();
|
||||
renderAgents();
|
||||
renderZones();
|
||||
renderEnrollments();
|
||||
populateSites();
|
||||
}
|
||||
|
||||
async function refreshData(showLoading) {
|
||||
if (showLoading !== false) setState("loading");
|
||||
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
api("/api/agents"),
|
||||
api("/api/agent-enrollments")
|
||||
]);
|
||||
|
||||
registry = results[0] && results[0].data
|
||||
? results[0].data
|
||||
: { agents: [], sites: [], zones: [] };
|
||||
|
||||
enrollments = results[1] && Array.isArray(results[1].data)
|
||||
? results[1].data
|
||||
: [];
|
||||
|
||||
render();
|
||||
setState("ready");
|
||||
} catch (error) {
|
||||
el.agentManagementErrorText.textContent =
|
||||
error.message || "Agent information could not be loaded.";
|
||||
setState("error");
|
||||
}
|
||||
}
|
||||
|
||||
async function createEnrollment(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!el.agentCreateForm) return;
|
||||
|
||||
el.agentCreateSubmit.disabled = true;
|
||||
el.agentCreateStatus.textContent = "Creating enrollment...";
|
||||
|
||||
try {
|
||||
const payload = await api("/api/agent-enrollments", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
site_id: el.agentSite.value,
|
||||
agent_zone_id: el.agentZone.value,
|
||||
display_name: el.agentDisplayName.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const enrollment = payload.data;
|
||||
|
||||
el.agentEnrollmentToken.textContent =
|
||||
enrollment.enrollment_token;
|
||||
|
||||
el.agentEnrollmentExpires.textContent =
|
||||
"Expires " + dateTime(enrollment.expires_at);
|
||||
|
||||
el.agentEnrollmentCopyStatus.textContent = "";
|
||||
el.agentEnrollmentResult.hidden = false;
|
||||
el.agentCreateStatus.textContent = "Enrollment created.";
|
||||
|
||||
el.agentDisplayName.value = "";
|
||||
|
||||
await refreshData(false);
|
||||
} catch (error) {
|
||||
el.agentCreateStatus.textContent =
|
||||
error.message || "Enrollment could not be created.";
|
||||
} finally {
|
||||
el.agentCreateSubmit.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToken() {
|
||||
const token = el.agentEnrollmentToken.textContent;
|
||||
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(token);
|
||||
el.agentEnrollmentCopyStatus.textContent =
|
||||
"Enrollment token copied.";
|
||||
} catch (error) {
|
||||
el.agentEnrollmentCopyStatus.textContent =
|
||||
"Copy was unavailable. Select and copy the token manually.";
|
||||
}
|
||||
}
|
||||
|
||||
function dismissToken() {
|
||||
el.agentEnrollmentToken.textContent = "";
|
||||
el.agentEnrollmentExpires.textContent = "";
|
||||
el.agentEnrollmentCopyStatus.textContent = "";
|
||||
el.agentEnrollmentResult.hidden = true;
|
||||
}
|
||||
|
||||
el.agentManagementRetry.addEventListener("click", function () {
|
||||
refreshData(true);
|
||||
});
|
||||
|
||||
if (el.agentManagementRefresh) {
|
||||
el.agentManagementRefresh.addEventListener("click", function () {
|
||||
refreshData(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (el.agentSite) {
|
||||
el.agentSite.addEventListener("change", populateZones);
|
||||
}
|
||||
|
||||
if (el.agentCreateForm) {
|
||||
el.agentCreateForm.addEventListener("submit", createEnrollment);
|
||||
}
|
||||
|
||||
if (el.agentEnrollmentCopy) {
|
||||
el.agentEnrollmentCopy.addEventListener("click", copyToken);
|
||||
}
|
||||
|
||||
if (el.agentEnrollmentDismiss) {
|
||||
el.agentEnrollmentDismiss.addEventListener(
|
||||
"click",
|
||||
dismissToken
|
||||
);
|
||||
}
|
||||
|
||||
refreshData(true);
|
||||
}());
|
||||
@@ -1771,3 +1771,86 @@ button.map-position:hover {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Agent Management v1 */
|
||||
|
||||
.agent-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.agent-summary-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.agent-summary-card span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.agent-summary-card strong {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.agent-management-columns {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.agent-table td:first-child {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.agent-create-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.agent-create-form .agent-form-actions,
|
||||
.agent-create-form #agentCreateStatus {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.agent-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-enrollment-result {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.agent-token-box {
|
||||
overflow-wrap: anywhere;
|
||||
padding: 14px;
|
||||
margin: 12px 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.agent-token-box code {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.agent-summary-grid,
|
||||
.agent-create-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Agent Management action visibility */
|
||||
.agent-management-columns .quiet-button,
|
||||
.agent-enrollment-panel .quiet-button {
|
||||
color: var(--ink);
|
||||
}
|
||||
@@ -6,6 +6,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
|
||||
from app.frontend.agent_management_view import agent_management_content
|
||||
|
||||
|
||||
NAVIGATION = (
|
||||
@@ -15,6 +16,7 @@ NAVIGATION = (
|
||||
("repairs", "Repair Center", "/operations/repairs", "repairs.view"),
|
||||
("miners", "Miners", "/assets/miners", "miners.view"),
|
||||
("system_health", "System Health", "/system-health", "system_health.view"),
|
||||
("agent_management", "Agents", "/agent/management", "agents.view"),
|
||||
)
|
||||
|
||||
PAGE_DETAILS = {
|
||||
@@ -53,6 +55,11 @@ PAGE_DETAILS = {
|
||||
"System Health",
|
||||
"Inspect application services, operational data, and maintenance facts.",
|
||||
),
|
||||
"agent_management": (
|
||||
"Administration / Agents",
|
||||
"Agent Management",
|
||||
"Inspect Agent identities, routing boundaries, and secure enrollment.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +469,9 @@ def render_app_page(
|
||||
else '<div class="global-search-unavailable" aria-hidden="true"></div>'
|
||||
)
|
||||
content = (
|
||||
system_health_content()
|
||||
agent_management_content(has_permission("agents.manage"))
|
||||
if view == "agent_management"
|
||||
else system_health_content()
|
||||
if view == "system_health"
|
||||
else miner_detail_content()
|
||||
if view == "miner_detail"
|
||||
|
||||
Reference in new issue
Block a user