Files
minerops/backend/app/frontend/static/app.js
T

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();
})();