568 lines
27 KiB
JavaScript
568 lines
27 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
if (document.body.dataset.view !== "miner_detail") return;
|
|
|
|
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"),
|
|
errorText: document.getElementById("minerDetailErrorText"),
|
|
content: document.getElementById("minerDetailContent"),
|
|
retry: document.getElementById("minerDetailRetry"),
|
|
refresh: document.getElementById("minerDetailRefresh"),
|
|
title: document.getElementById("minerIdentityTitle"),
|
|
model: document.getElementById("minerDetailModel"),
|
|
uuid: document.getElementById("minerDetailUuid"),
|
|
operational: document.getElementById("minerDetailOperational"),
|
|
lifecycle: document.getElementById("minerDetailLifecycle"),
|
|
locationSummary: document.getElementById("minerDetailLocationSummary"),
|
|
returnLink: document.getElementById("minerDetailReturn"),
|
|
mapLink: document.getElementById("minerDetailMap"),
|
|
trendsLink: document.getElementById("minerDetailTrends"),
|
|
repairsLink: document.getElementById("minerDetailRepairs"),
|
|
identity: document.getElementById("minerIdentityDetails"),
|
|
tags: document.getElementById("minerTags"),
|
|
telemetry: document.getElementById("minerTelemetryDetails"),
|
|
telemetryAge: document.getElementById("minerTelemetryAge"),
|
|
location: document.getElementById("minerLocationBody"),
|
|
repair: document.getElementById("minerRepairBody"),
|
|
episodes: document.getElementById("minerRepairEpisodes"),
|
|
identityHistory: document.getElementById("minerIdentityHistory"),
|
|
identityHistoryBody: document.getElementById("minerIdentityHistoryBody"),
|
|
observations: document.getElementById("minerObservations"),
|
|
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) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function display(value, fallback) {
|
|
if (value === null || value === undefined || value === "") {
|
|
return fallback === undefined ? "Unavailable" : fallback;
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function words(value) {
|
|
const text = display(value, "unknown").replaceAll("_", " ").toLowerCase();
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
}
|
|
|
|
function dateTime(value) {
|
|
if (!value) return "Unavailable";
|
|
const parsed = new Date(value);
|
|
return Number.isNaN(parsed.getTime()) ? "Unavailable" : parsed.toLocaleString();
|
|
}
|
|
|
|
function age(value) {
|
|
if (!value) return "No observation available";
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) return "Observation time unavailable";
|
|
const seconds = Math.max(0, Math.floor((Date.now() - parsed.getTime()) / 1000));
|
|
if (seconds < 60) return seconds + " seconds ago";
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + " minutes ago";
|
|
if (seconds < 86400) return Math.floor(seconds / 3600) + " hours ago";
|
|
return Math.floor(seconds / 86400) + " days ago";
|
|
}
|
|
|
|
function metric(value, suffix) {
|
|
if (value === null || value === undefined || value === "") return "Unavailable";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return "Unavailable";
|
|
return String(number) + suffix;
|
|
}
|
|
|
|
function addDefinition(list, label, value) {
|
|
const row = create("div");
|
|
row.append(create("dt", "", label), create("dd", "", display(value)));
|
|
list.appendChild(row);
|
|
}
|
|
|
|
function prefixed(value, prefix) {
|
|
const label = display(value, "");
|
|
if (!label) return "";
|
|
return new RegExp("^" + prefix + "\\b", "i").test(label)
|
|
? label : prefix + " " + label;
|
|
}
|
|
|
|
function locationText(location) {
|
|
if (!location) return "Not mapped";
|
|
const parts = [location.site_name, location.room_name,
|
|
prefixed(location.rack_label, "Rack"),
|
|
prefixed(location.shelf_label, "Shelf"),
|
|
location.position_label || (location.position_index === null
|
|
? "Position unavailable" : "Position " + location.position_index)];
|
|
return parts.filter(Boolean).join(" / ");
|
|
}
|
|
|
|
async function api(path) {
|
|
const response = await fetch(path, {
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
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;
|
|
}
|
|
|
|
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");
|
|
const head = create("thead");
|
|
const headRow = create("tr");
|
|
headers.forEach(function (header) { headRow.appendChild(create("th", "", header)); });
|
|
head.appendChild(headRow);
|
|
const body = create("tbody");
|
|
rows.forEach(function (values) {
|
|
const row = create("tr");
|
|
values.forEach(function (value) { row.appendChild(create("td", "", display(value))); });
|
|
body.appendChild(row);
|
|
});
|
|
tableNode.append(head, body);
|
|
wrap.appendChild(tableNode);
|
|
return wrap;
|
|
}
|
|
|
|
function renderIdentity(miner) {
|
|
const identity = miner.asset_id || miner.current_mac || "Miner " + miner.id.slice(0, 8);
|
|
el.title.textContent = identity;
|
|
el.model.textContent = display(miner.model, "Model unavailable");
|
|
el.uuid.textContent = "Miner UUID: " + miner.id;
|
|
const pageTitle = document.querySelector(".page-head h1");
|
|
if (pageTitle) pageTitle.textContent = identity;
|
|
document.title = identity + " | MinerOps";
|
|
el.identity.replaceChildren();
|
|
[
|
|
["Asset ID", miner.asset_id], ["Model", miner.model],
|
|
["Miner serial", miner.miner_sn], ["Control-board serial", miner.control_board_sn],
|
|
["PSU serial", miner.psu_serial], ["Current MAC", miner.current_mac],
|
|
["Current IP", miner.current_ip], ["Hostname", miner.hostname],
|
|
["Firmware", miner.firmware_version], ["Created", dateTime(miner.created_at)],
|
|
["First seen", dateTime(miner.first_seen_at)], ["Last seen", dateTime(miner.last_seen_at)]
|
|
].forEach(function (item) { addDefinition(el.identity, item[0], item[1]); });
|
|
el.tags.replaceChildren();
|
|
(miner.tags || []).forEach(function (tag) {
|
|
el.tags.appendChild(create("span", "tag", tag.name));
|
|
});
|
|
}
|
|
|
|
function renderTelemetry(miner) {
|
|
el.telemetry.replaceChildren();
|
|
[
|
|
["Runtime", words(miner.runtime_status)],
|
|
["Observed", dateTime(miner.observed_at)],
|
|
["Hashrate", metric(miner.hashrate_ths, " TH/s")],
|
|
["Power", metric(miner.power_w, " W")],
|
|
["Max chip temperature", metric(miner.chip_temp_max_c, " C")],
|
|
["Hash stability", miner.hash_stable === null || miner.hash_stable === undefined
|
|
? "Unavailable" : miner.hash_stable ? "Stable" : "Unstable"]
|
|
].forEach(function (item) { addDefinition(el.telemetry, item[0], item[1]); });
|
|
el.telemetryAge.textContent = age(miner.observed_at);
|
|
}
|
|
|
|
function renderLocation(section, minerIdValue, canViewMap) {
|
|
el.location.replaceChildren();
|
|
if (section.restricted) {
|
|
el.location.appendChild(create("p", "permission-note", "Location information restricted."));
|
|
el.locationSummary.textContent = "Location restricted";
|
|
return;
|
|
}
|
|
if (!section.location) {
|
|
el.location.appendChild(create("p", "empty-copy", "Not mapped"));
|
|
el.locationSummary.textContent = "Not mapped";
|
|
return;
|
|
}
|
|
const location = section.location;
|
|
const list = create("dl", "definition-list");
|
|
[["Site", location.site_name], ["Room", location.room_name],
|
|
["Rack", prefixed(location.rack_label, "Rack")],
|
|
["Shelf", prefixed(location.shelf_label, "Shelf")],
|
|
["Position", location.position_label || (location.position_index === null
|
|
? null : "Position " + location.position_index)]
|
|
].forEach(function (item) { addDefinition(list, item[0], item[1]); });
|
|
el.location.appendChild(list);
|
|
const summary = locationText(location);
|
|
el.locationSummary.textContent = summary;
|
|
if (canViewMap) {
|
|
const params = new URLSearchParams({ miner_id: minerIdValue });
|
|
params.set("site_id", location.site_id);
|
|
params.set("room_id", location.room_id);
|
|
el.mapLink.href = "/monitoring/map?" + params.toString();
|
|
el.mapLink.hidden = false;
|
|
}
|
|
}
|
|
|
|
function eventPayload(payload) {
|
|
const list = create("dl", "definition-list event-payload");
|
|
Object.entries(payload || {}).forEach(function (entry) {
|
|
const value = typeof entry[1] === "object"
|
|
? JSON.stringify(entry[1]) : entry[1];
|
|
addDefinition(list, words(entry[0]), value);
|
|
});
|
|
return list;
|
|
}
|
|
|
|
function renderRepairDetails(target, timeline, parts, canReadComponents) {
|
|
target.replaceChildren();
|
|
const events = timeline && timeline.events ? timeline.events : [];
|
|
const heading = create("h4", "", "Timeline");
|
|
const timelineList = create("ol", "repair-timeline-list");
|
|
events.forEach(function (event) {
|
|
const item = create("li");
|
|
item.append(create("strong", "", words(event.event_type)),
|
|
create("span", "secondary", dateTime(event.occurred_at) + " / " +
|
|
display(event.actor && event.actor.display_name, "Unknown actor")),
|
|
eventPayload(event.payload));
|
|
timelineList.appendChild(item);
|
|
});
|
|
if (!events.length) timelineList.appendChild(create("li", "empty-copy", "No timeline events recorded."));
|
|
target.append(heading, timelineList, create("h4", "", "Parts and components"));
|
|
const entries = parts && parts.entries ? parts.entries : [];
|
|
if (!entries.length) {
|
|
target.appendChild(create("p", "empty-copy", "No parts recorded for this repair."));
|
|
return;
|
|
}
|
|
const rows = entries.map(function (entry) {
|
|
let description = "Tracked component";
|
|
if (entry.consumable) {
|
|
description = entry.consumable.description + " / " +
|
|
entry.consumable.quantity + " " + entry.consumable.unit;
|
|
} else if (entry.component_snapshot) {
|
|
const snapshot = entry.component_snapshot;
|
|
description = [words(snapshot.component_type), snapshot.asset_id,
|
|
snapshot.manufacturer, snapshot.model, snapshot.manufacturer_serial]
|
|
.filter(Boolean).join(" / ");
|
|
if (canReadComponents && entry.component_id) {
|
|
description += " / Component UUID " + entry.component_id;
|
|
}
|
|
}
|
|
return [description, entry.notes, dateTime(entry.recorded_at),
|
|
entry.voided_at ? "Voided: " + display(entry.void_reason) : "Recorded"];
|
|
});
|
|
target.appendChild(table(["Part or component", "Notes", "Recorded", "State"], rows));
|
|
}
|
|
|
|
function repairEpisode(episode, canReadComponents) {
|
|
const details = create("details");
|
|
const summary = create("summary", "repair-episode-summary");
|
|
summary.append(create("strong", "", (episode.source === "legacy" ? "Legacy compatibility" : words(episode.status))),
|
|
create("span", "secondary", [words(episode.reason), dateTime(episode.opened_at)]
|
|
.filter(function (value) { return value !== "Unavailable"; }).join(" / ")));
|
|
details.appendChild(summary);
|
|
const overview = create("dl", "definition-list");
|
|
[["Status", words(episode.status)], ["Reason", words(episode.reason)],
|
|
["Opened", dateTime(episode.opened_at)], ["Closed", dateTime(episode.closed_at)],
|
|
["Latest context", episode.latest_context], ["Parts", episode.parts_count]
|
|
].forEach(function (item) { addDefinition(overview, item[0], item[1]); });
|
|
details.appendChild(overview);
|
|
if (episode.source === "modular" && episode.repair_id) {
|
|
const lazy = create("div", "lazy-content", "Open to load timeline and parts.");
|
|
details.appendChild(lazy);
|
|
details.addEventListener("toggle", async function () {
|
|
if (!details.open || details.dataset.loaded || details.dataset.loading) return;
|
|
details.dataset.loading = "true";
|
|
lazy.textContent = "Loading repair timeline and parts...";
|
|
try {
|
|
const results = await Promise.all([
|
|
api("/api/repairs/" + encodeURIComponent(episode.repair_id) + "/events"),
|
|
api("/api/repairs/" + encodeURIComponent(episode.repair_id) + "/parts")
|
|
]);
|
|
renderRepairDetails(lazy, results[0], results[1], canReadComponents);
|
|
details.dataset.loaded = "true";
|
|
} catch (error) {
|
|
lazy.textContent = "Repair history unavailable: " + error.message;
|
|
} finally { delete details.dataset.loading; }
|
|
});
|
|
} else {
|
|
details.appendChild(create("p", "permission-note",
|
|
"Legacy compatibility context has no modular repair timeline."));
|
|
}
|
|
return details;
|
|
}
|
|
|
|
function renderRepair(section, capabilities) {
|
|
el.repair.replaceChildren();
|
|
el.episodes.replaceChildren();
|
|
if (section.restricted) {
|
|
el.repair.appendChild(create("p", "permission-note", "Repair information restricted."));
|
|
return;
|
|
}
|
|
if (section.active_repair) {
|
|
const active = section.active_repair;
|
|
const list = create("dl", "definition-list");
|
|
[["Status", words(active.status)], ["Reason", words(active.reason)],
|
|
["Opened", dateTime(active.opened_at)], ["Source", "Modular repair workflow"],
|
|
["Latest context", active.latest_context], ["Parts", active.parts_count]
|
|
].forEach(function (item) { addDefinition(list, item[0], item[1]); });
|
|
el.repair.appendChild(list);
|
|
} else if ((section.episodes || []).some(function (item) { return item.source === "legacy" && item.active; })) {
|
|
el.repair.appendChild(create("p", "permission-note", "Legacy compatibility repair context is recorded. No modular repair UUID exists."));
|
|
} else {
|
|
el.repair.appendChild(create("p", "empty-copy", "No active repair"));
|
|
}
|
|
(section.episodes || []).forEach(function (episode) {
|
|
el.episodes.appendChild(repairEpisode(episode, capabilities.can_read_components));
|
|
});
|
|
}
|
|
|
|
async function loadIdentityHistory() {
|
|
if (el.identityHistory.dataset.loaded || el.identityHistory.dataset.loading) return;
|
|
el.identityHistory.dataset.loading = "true";
|
|
el.identityHistoryBody.textContent = "Loading identity history...";
|
|
try {
|
|
const results = await Promise.all([
|
|
api("/api/inventory/miners/" + encodeURIComponent(minerId) + "/asset-id-history"),
|
|
api("/api/miners/" + encodeURIComponent(minerId) + "/network-identities")
|
|
]);
|
|
el.identityHistoryBody.replaceChildren(create("h3", "", "Asset ID history"));
|
|
const assetRows = (results[0] || []).map(function (entry) {
|
|
return [entry.old_asset_id, entry.new_asset_id, dateTime(entry.changed_at),
|
|
entry.changed_by_display_name];
|
|
});
|
|
el.identityHistoryBody.appendChild(assetRows.length
|
|
? table(["Previous", "New", "Changed", "Actor"], assetRows)
|
|
: create("p", "empty-copy", "No Asset ID changes recorded."));
|
|
el.identityHistoryBody.appendChild(create("h3", "", "Network identity history"));
|
|
const networkRows = (results[1] || []).map(function (entry) {
|
|
return [entry.mac_address, entry.ip_address, entry.hostname,
|
|
entry.is_current ? "Current" : "Historical", dateTime(entry.last_seen_at)];
|
|
});
|
|
el.identityHistoryBody.appendChild(networkRows.length
|
|
? table(["MAC", "IP", "Hostname", "State", "Last seen"], networkRows)
|
|
: create("p", "empty-copy", "No network identity history recorded."));
|
|
el.identityHistory.dataset.loaded = "true";
|
|
} catch (error) {
|
|
el.identityHistoryBody.textContent = "Identity history unavailable: " + error.message;
|
|
} finally { delete el.identityHistory.dataset.loading; }
|
|
}
|
|
|
|
async function loadObservations() {
|
|
if (el.observations.dataset.loaded || el.observations.dataset.loading) return;
|
|
el.observations.dataset.loading = "true";
|
|
el.observationsBody.textContent = "Loading recent observations...";
|
|
try {
|
|
const observations = await api("/api/miners/" + encodeURIComponent(minerId) + "/status-snapshots");
|
|
const rows = (observations || []).map(function (item) {
|
|
return [dateTime(item.observed_at), metric(item.mhs_av === null ? null : Number(item.mhs_av) / 1000000, " TH/s"),
|
|
metric(item.power_w, " W"), metric(item.chip_temp_max_c, " C"),
|
|
item.hash_stable === null || item.hash_stable === undefined
|
|
? "Unavailable" : item.hash_stable ? "Stable" : "Unstable"];
|
|
});
|
|
el.observationsBody.replaceChildren(rows.length
|
|
? table(["Observed", "Hashrate", "Power", "Max chip temp", "Hash stability"], rows)
|
|
: create("p", "empty-copy", "No observations recorded."));
|
|
el.observations.dataset.loaded = "true";
|
|
} catch (error) {
|
|
el.observationsBody.textContent = "Recent observations unavailable: " + error.message;
|
|
} finally { delete el.observations.dataset.loading; }
|
|
}
|
|
|
|
function renderOverview(data) {
|
|
renderIdentity(data.miner);
|
|
renderTelemetry(data.miner);
|
|
el.operational.textContent = words(data.operational_state);
|
|
el.operational.dataset.status = data.operational_state;
|
|
el.lifecycle.textContent = "Lifecycle: " + words(data.miner.lifecycle_status);
|
|
renderLocation(data.location, data.miner.id, data.capabilities.can_view_map);
|
|
renderRepair(data.repair, data.capabilities);
|
|
el.trendsLink.href = "/monitoring/analytics?" + new URLSearchParams({
|
|
scope: "miner", scope_id: data.miner.id, range: "24h"
|
|
}).toString();
|
|
if (data.capabilities.can_open_repair_center) {
|
|
const params = new URLSearchParams({ scope: "all", q: data.miner.id });
|
|
el.repairsLink.href = "/operations/repairs?" + params.toString();
|
|
el.repairsLink.hidden = false;
|
|
}
|
|
if (returnTo) {
|
|
el.returnLink.href = returnTo;
|
|
el.returnLink.textContent = returnTo.startsWith("/monitoring/map")
|
|
? "Return to Mine Map" : "Return to previous view";
|
|
el.returnLink.hidden = false;
|
|
}
|
|
}
|
|
|
|
async function loadOverview() {
|
|
el.loading.hidden = false;
|
|
el.error.hidden = true;
|
|
el.content.hidden = true;
|
|
try {
|
|
const data = await api("/api/miners/" + encodeURIComponent(minerId) + "/overview");
|
|
renderOverview(data);
|
|
el.loading.hidden = true;
|
|
el.content.hidden = false;
|
|
} catch (error) {
|
|
el.loading.hidden = true;
|
|
el.error.hidden = false;
|
|
el.errorText.textContent = error.message;
|
|
}
|
|
}
|
|
|
|
function refreshAll() {
|
|
loadOverview();
|
|
loadControls();
|
|
}
|
|
|
|
el.retry.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();
|
|
});
|
|
el.observations.addEventListener("toggle", function () {
|
|
if (el.observations.open) loadObservations();
|
|
});
|
|
loadOverview();
|
|
loadControls();
|
|
})();
|