246 lines
8.3 KiB
JavaScript
246 lines
8.3 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const view = document.body.dataset.view || "";
|
|
const canViewMiners =
|
|
document.body.dataset.canViewMiners === "true";
|
|
|
|
function text(value, fallback) {
|
|
if (value === null || value === undefined || String(value).trim() === "") {
|
|
return fallback || "Not available";
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function statusName(value) {
|
|
return text(value, "unknown").toLowerCase().replaceAll(" ", "_");
|
|
}
|
|
|
|
function create(tag, options) {
|
|
const element = document.createElement(tag);
|
|
const values = options || {};
|
|
if (values.className) element.className = values.className;
|
|
if (values.text !== undefined) element.textContent = values.text;
|
|
if (values.href) element.href = values.href;
|
|
return element;
|
|
}
|
|
|
|
function showState(name, message) {
|
|
for (const state of document.querySelectorAll("[data-view-state]")) {
|
|
state.hidden = state.dataset.viewState !== name;
|
|
}
|
|
const active = document.querySelector(
|
|
'[data-view-state="' + name + '"]'
|
|
);
|
|
const detail = active && active.querySelector("[data-state-detail]");
|
|
if (detail && message) detail.textContent = message;
|
|
}
|
|
|
|
async function api(path) {
|
|
const controller = new AbortController();
|
|
const timeout = window.setTimeout(function () {
|
|
controller.abort();
|
|
}, 15000);
|
|
let response;
|
|
try {
|
|
response = await fetch(path, {
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" },
|
|
signal: controller.signal
|
|
});
|
|
} catch (error) {
|
|
if (error && error.name === "AbortError") {
|
|
throw new Error(
|
|
"MinerOps took too long to respond. Retry in a moment."
|
|
);
|
|
}
|
|
throw new Error(
|
|
"MinerOps could not reach the backend. Check the connection and retry."
|
|
);
|
|
} finally {
|
|
window.clearTimeout(timeout);
|
|
}
|
|
if (response.status === 401) {
|
|
window.location.replace(
|
|
"/login?next=" +
|
|
encodeURIComponent(window.location.pathname + window.location.search)
|
|
);
|
|
throw new Error("Your session expired. Sign in again to continue.");
|
|
}
|
|
if (response.status === 403) {
|
|
const denied = new Error(
|
|
"Your account does not have permission to view this information."
|
|
);
|
|
denied.permissionDenied = true;
|
|
throw denied;
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
"MinerOps could not load this information. Retry in a moment."
|
|
);
|
|
}
|
|
try {
|
|
return await response.json();
|
|
} catch (error) {
|
|
throw new Error(
|
|
"MinerOps returned an invalid response. Retry in a moment."
|
|
);
|
|
}
|
|
}
|
|
|
|
function setupShell() {
|
|
const menu = document.getElementById("menuButton");
|
|
const nav = document.getElementById("primaryNav");
|
|
if (menu && nav) {
|
|
menu.addEventListener("click", function () {
|
|
const open = nav.dataset.open !== "true";
|
|
nav.dataset.open = String(open);
|
|
menu.setAttribute("aria-expanded", String(open));
|
|
});
|
|
}
|
|
|
|
const search = document.getElementById("globalSearchForm");
|
|
const searchInput = document.getElementById("globalSearchInput");
|
|
if (search && searchInput) {
|
|
search.addEventListener("submit", function (event) {
|
|
event.preventDefault();
|
|
const query = searchInput.value.trim();
|
|
const target = query
|
|
? "/assets/miners?query=" + encodeURIComponent(query)
|
|
: "/assets/miners";
|
|
window.location.assign(target);
|
|
});
|
|
}
|
|
|
|
const logout = document.getElementById("logoutButton");
|
|
if (logout) {
|
|
logout.addEventListener("click", async function () {
|
|
logout.disabled = true;
|
|
try {
|
|
await fetch("/api/auth/logout", {
|
|
method: "POST",
|
|
credentials: "same-origin"
|
|
});
|
|
} finally {
|
|
window.location.replace("/login");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function addCell(row, label, content) {
|
|
const cell = create("td");
|
|
cell.dataset.label = label;
|
|
if (content instanceof Node) cell.appendChild(content);
|
|
else cell.textContent = text(content);
|
|
row.appendChild(cell);
|
|
}
|
|
|
|
function status(value) {
|
|
const label = create("span", {
|
|
className: "status-text",
|
|
text: text(value, "Unknown").replaceAll("_", " ")
|
|
});
|
|
label.dataset.status = statusName(value);
|
|
return label;
|
|
}
|
|
|
|
function dateTime(value) {
|
|
if (!value) return "Not available";
|
|
const parsed = new Date(value);
|
|
return Number.isNaN(parsed.valueOf())
|
|
? String(value)
|
|
: parsed.toLocaleString();
|
|
}
|
|
|
|
async function loadRepairs() {
|
|
const rows = document.getElementById("repairRows");
|
|
if (!rows) return;
|
|
showState("loading");
|
|
try {
|
|
const envelope = await api("/api/repairs/queue");
|
|
if (
|
|
!envelope ||
|
|
!envelope.data ||
|
|
typeof envelope.data !== "object" ||
|
|
!Array.isArray(envelope.data.items)
|
|
) {
|
|
throw new Error(
|
|
"MinerOps returned an invalid repair response."
|
|
);
|
|
}
|
|
const items = envelope.data.items;
|
|
if (!items.every(function (item) {
|
|
return item &&
|
|
typeof item === "object" &&
|
|
typeof item.miner_id === "string" &&
|
|
item.miner_id.trim() !== "";
|
|
})) {
|
|
throw new Error(
|
|
"MinerOps returned an incomplete repair response."
|
|
);
|
|
}
|
|
rows.replaceChildren();
|
|
if (!items.length) {
|
|
showState("empty");
|
|
return;
|
|
}
|
|
for (const repair of items) {
|
|
const row = create("tr");
|
|
const identity = create("div");
|
|
const minerLabel = text(
|
|
repair.current_mac,
|
|
text(repair.miner_id, "Unknown miner")
|
|
);
|
|
if (canViewMiners) {
|
|
identity.appendChild(create("a", {
|
|
className: "object-link",
|
|
text: minerLabel,
|
|
href: "/miners/" +
|
|
encodeURIComponent(repair.miner_id) +
|
|
"/detail"
|
|
}));
|
|
} else {
|
|
identity.appendChild(create("strong", {
|
|
text: minerLabel
|
|
}));
|
|
}
|
|
identity.appendChild(create("span", {
|
|
className: "secondary",
|
|
text: text(repair.current_ip, "No current IP")
|
|
}));
|
|
addCell(row, "Miner", identity);
|
|
addCell(row, "Model", repair.model);
|
|
addCell(
|
|
row,
|
|
"Repair state",
|
|
status(repair.latest_event_status || repair.asset_state)
|
|
);
|
|
addCell(
|
|
row,
|
|
"Reported issue",
|
|
repair.reported_error ||
|
|
repair.latest_event_title ||
|
|
"No reported issue"
|
|
);
|
|
addCell(
|
|
row,
|
|
"Last update",
|
|
dateTime(repair.latest_event_at || repair.state_updated_at)
|
|
);
|
|
rows.appendChild(row);
|
|
}
|
|
showState("ready");
|
|
} catch (error) {
|
|
showState(
|
|
error.permissionDenied ? "denied" : "error",
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
setupShell();
|
|
if (view === "repairs") loadRepairs();
|
|
})();
|