(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 ); } } function locationText(miner) { if (miner.room_id) return "Room " + miner.room_id; if (miner.site_id) return "Site " + miner.site_id; return "Not assigned"; } async function loadMiners(query) { const rows = document.getElementById("minerRows"); if (!rows) return; showState("loading"); const params = new URLSearchParams({ limit: "100", offset: "0" }); if (query) params.set("query", query); try { const envelope = await api( "/api/inventory/miners?" + params.toString() ); if (!envelope || !Array.isArray(envelope.data)) { throw new Error( "MinerOps returned an invalid inventory response." ); } const miners = envelope.data; if (!miners.every(function (miner) { return miner && typeof miner === "object" && typeof miner.id === "string" && miner.id.trim() !== ""; })) { throw new Error( "MinerOps returned an incomplete inventory response." ); } rows.replaceChildren(); if (!miners.length) { showState( "empty", query ? "No authorized miners match this search." : "No miners have been added or discovered yet." ); return; } for (const miner of miners) { const row = create("tr"); const identity = create("div"); const link = create("a", { className: "object-link", text: text(miner.asset_id, "No Asset ID"), href: "/miners/" + encodeURIComponent(miner.id) + "/detail" }); identity.appendChild(link); identity.appendChild(create("span", { className: "secondary", text: miner.id })); addCell(row, "Asset ID", identity); addCell( row, "Current MAC", miner.current_mac || "Not observed" ); addCell(row, "Model", miner.model); addCell(row, "Lifecycle", status(miner.lifecycle_status)); addCell(row, "Runtime", status(miner.runtime_status)); addCell(row, "Location", locationText(miner)); rows.appendChild(row); } showState("ready"); } catch (error) { showState( error.permissionDenied ? "denied" : "error", error.message ); } } function setupMiners() { const form = document.getElementById("minerSearchForm"); const input = document.getElementById("minerSearchInput"); if (!form || !input) return; const initial = new URLSearchParams(window.location.search).get("query") || ""; input.value = initial; form.addEventListener("submit", function (event) { event.preventDefault(); const query = input.value.trim(); const url = query ? "/assets/miners?query=" + encodeURIComponent(query) : "/assets/miners"; window.history.pushState({ query: query }, "", url); loadMiners(query); }); window.addEventListener("popstate", function () { const query = new URLSearchParams(window.location.search).get("query") || ""; input.value = query; loadMiners(query); }); loadMiners(initial); } setupShell(); if (view === "repairs") loadRepairs(); if (view === "miners") setupMiners(); })();