feat: add safe miner control foundation

This commit is contained in:
lussierj committed 2026-08-24 20:49:28 +00:00
1 parent 6cd876b4b3
commit f5965af914
39 files changed
+2952 -491

No files matched your search

+144 -2
View File
@@ -5,6 +5,7 @@
const minerId = document.body.dataset.minerId;
const returnTo = document.body.dataset.returnTo || "";
const canControl = document.body.dataset.canControlMiners === "true";
const el = {
loading: document.getElementById("minerDetailLoading"),
error: document.getElementById("minerDetailError"),
@@ -32,7 +33,17 @@
identityHistory: document.getElementById("minerIdentityHistory"),
identityHistoryBody: document.getElementById("minerIdentityHistoryBody"),
observations: document.getElementById("minerObservations"),
observationsBody: document.getElementById("minerObservationsBody")
observationsBody: document.getElementById("minerObservationsBody"),
controlsRefresh: document.getElementById("minerControlsRefresh"),
controlsSummary: document.getElementById("minerControlsSummary"),
controlCapabilities: document.getElementById("minerControlCapabilities"),
controlActions: document.getElementById("minerControlActions"),
controlPermission: document.getElementById("minerControlPermission"),
controlResult: document.getElementById("minerControlResult"),
serviceControl: document.getElementById("minerServiceControl"),
performanceForm: document.getElementById("minerPerformanceControl"),
performanceMode: document.getElementById("minerPerformanceMode"),
controlHistory: document.getElementById("minerControlHistory")
};
function create(tag, className, text) {
@@ -121,6 +132,123 @@
return payload ? payload.data : null;
}
async function mutate(path, body) {
const response = await fetch(path, {
method: "POST",
credentials: "same-origin",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (response.status === 401) {
window.location.assign("/login?next=" + encodeURIComponent(
window.location.pathname + window.location.search
));
throw new Error("Login required");
}
let payload = null;
try { payload = await response.json(); } catch (_error) { payload = null; }
if (!response.ok) {
throw new Error(payload && payload.detail
? String(payload.detail) : "Request failed (" + response.status + ")");
}
return payload ? payload.data : null;
}
function capabilityText(capability) {
if (!capability) return "Unknown";
const reason = capability.reason ? " / " + words(capability.reason) : "";
return words(capability.state) + reason;
}
function operationStatus(operation) {
if (operation.status === "pending") return "Pending";
if (operation.status === "claimed") return "Running";
if (operation.status === "failed") return "Failed";
if (operation.status === "cancelled") return "Cancelled";
if (operation.verification_status === "confirmed") return "Confirmed";
if (operation.verification_status === "unconfirmed") return "Unconfirmed";
if (operation.verification_status === "verification_failed") return "Verification failed";
if (operation.verification_status === "unavailable") return "Verification unavailable";
return "Completed";
}
function renderControls(data) {
el.controlsSummary.textContent = "Runtime: " + words(data.runtime_state) +
" / Known mode: " + words(data.performance_mode);
el.controlCapabilities.replaceChildren();
[["Agent", data.agent], ["Service control", data.service_control],
["Performance mode", data.performance_mode_control]].forEach(function (item) {
const block = create("div");
block.append(create("span", "secondary", item[0]),
create("strong", "", capabilityText(item[1])));
el.controlCapabilities.appendChild(block);
});
el.controlPermission.hidden = canControl;
el.controlActions.hidden = !canControl;
el.serviceControl.hidden = true;
el.performanceForm.hidden = true;
if (canControl && data.available_service_action) {
el.serviceControl.dataset.action = data.available_service_action;
el.serviceControl.textContent = data.available_service_action === "suspend"
? "Suspend miner" : "Resume miner";
el.serviceControl.hidden = false;
}
if (canControl && data.performance_mode_control.state === "supported") {
if (data.performance_mode) el.performanceMode.value = data.performance_mode;
el.performanceForm.hidden = false;
}
el.controlHistory.replaceChildren();
(data.recent_operations || []).forEach(function (operation) {
const row = create("tr");
[words(operation.action), display(operation.requested_value, "None"),
operationStatus(operation), words(operation.verification_status),
dateTime(operation.created_at)].forEach(function (value) {
row.appendChild(create("td", "", value));
});
el.controlHistory.appendChild(row);
});
if (!(data.recent_operations || []).length) {
const row = create("tr");
const cell = create("td", "empty-copy", "No control operations recorded.");
cell.colSpan = 5;
row.appendChild(cell);
el.controlHistory.appendChild(row);
}
}
async function loadControls() {
el.controlsSummary.textContent = "Loading control capability and history.";
try {
const data = await api("/api/miners/" + encodeURIComponent(minerId) + "/controls");
renderControls(data);
} catch (error) {
el.controlsSummary.textContent = "Controls unavailable: " + error.message;
}
}
async function queueControl(action, requestedValue) {
const description = action === "performance_mode"
? "set performance mode to " + requestedValue : action + " this miner";
if (!window.confirm("Confirm: " + description + "?")) return;
const pathAction = action === "performance_mode" ? "performance-mode" : action;
const body = action === "performance_mode" ? { mode: requestedValue } : {};
el.controlResult.hidden = false;
el.controlResult.textContent = "Queueing control operation.";
try {
const operation = await mutate(
"/api/miners/" + encodeURIComponent(minerId) + "/controls/" + pathAction,
body
);
el.controlResult.textContent = "Pending: " + words(operation.action) +
" operation " + operation.operation_id + ".";
await loadControls();
} catch (error) {
el.controlResult.textContent = "Control was not queued: " + error.message;
}
}
function table(headers, rows) {
const wrap = create("div", "table-wrap compact-history-table");
const tableNode = create("table");
@@ -413,8 +541,21 @@
}
}
function refreshAll() {
loadOverview();
loadControls();
}
el.retry.addEventListener("click", loadOverview);
el.refresh.addEventListener("click", loadOverview);
el.refresh.addEventListener("click", refreshAll);
el.controlsRefresh.addEventListener("click", loadControls);
el.serviceControl.addEventListener("click", function () {
queueControl(el.serviceControl.dataset.action, null);
});
el.performanceForm.addEventListener("submit", function (event) {
event.preventDefault();
queueControl("performance_mode", el.performanceMode.value);
});
el.identityHistory.addEventListener("toggle", function () {
if (el.identityHistory.open) loadIdentityHistory();
});
@@ -422,4 +563,5 @@
if (el.observations.open) loadObservations();
});
loadOverview();
loadControls();
})();