(function () { "use strict"; if (document.body.dataset.view !== "miners") return; const canViewLocations = document.body.dataset.canViewLocations === "true"; const ids = [ "fleetFilters", "fleetQuery", "fleetLifecycle", "fleetModel", "fleetRuntime", "fleetRepair", "fleetOperational", "fleetTags", "fleetProblems", "fleetReset", "fleetRows", "fleetResultSummary", "fleetFreshness", "fleetPrevious", "fleetNext", "fleetLimit" ]; const el = {}; for (const id of ids) el[id] = document.getElementById(id); if (canViewLocations) { el.fleetSite = document.getElementById("fleetSite"); el.fleetRoom = document.getElementById("fleetRoom"); } const limits = new Set([25, 50, 100, 250]); const state = { offset: 0, limit: 100, returned: 0, locationOptions: null, siteId: "", roomId: "" }; function display(value, fallback) { if (value === null || value === undefined || String(value).trim() === "") { return fallback || "Unavailable"; } return String(value); } function words(value) { return display(value, "unknown").replaceAll("_", " ").toLowerCase(); } function create(tag, className, value) { const node = document.createElement(tag); if (className) node.className = className; if (value !== undefined) node.textContent = value; return node; } function showState(name, message) { for (const node of document.querySelectorAll("[data-view-state]")) { node.hidden = node.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; } function finite(value) { return typeof value === "number" && Number.isFinite(value); } function metric(value, suffix) { return finite(value) ? String(value) + suffix : "Unavailable"; } function age(value) { if (!value) return "Never observed"; const time = new Date(value).valueOf(); if (Number.isNaN(time)) return "Observation time unavailable"; const seconds = Math.max(0, Math.floor((Date.now() - time) / 1000)); if (seconds < 60) return seconds + "s ago"; if (seconds < 3600) return Math.floor(seconds / 60) + "m ago"; if (seconds < 86400) return Math.floor(seconds / 3600) + "h ago"; return Math.floor(seconds / 86400) + "d ago"; } function operational(miner) { if (miner.active_repair || miner.lifecycle_status === "in_repair") { return { key: "repair", label: "In repair", problem: true }; } if (miner.lifecycle_status === "retired") { return { key: "retired", label: "Retired", problem: false }; } if (miner.lifecycle_status === "spare") { return { key: "spare", label: "Spare", problem: false }; } const runtime = words(miner.runtime_status); if (runtime === "offline") { return { key: "offline", label: "Offline", problem: true }; } if (miner.hash_stable === false) { return { key: "unstable", label: "Hash unstable", problem: true }; } if (runtime === "online") { return { key: "online", label: "Online", problem: false }; } return { key: "unknown", label: "Operational state unknown", problem: true }; } function prefixedLabel(value, prefix) { const label = display(value, ""); if (!label) return ""; return new RegExp("^" + prefix + "\\b", "i").test(label) ? label : prefix + " " + label; } function locationText(miner) { if (!canViewLocations || !miner.location_position_id) return "Not mapped"; const parts = []; if (miner.site_name) parts.push(miner.site_name); if (miner.room_name) parts.push(miner.room_name); if (miner.rack_label) parts.push(prefixedLabel(miner.rack_label, "Rack")); if (miner.shelf_label) parts.push(prefixedLabel(miner.shelf_label, "Shelf")); parts.push(display( miner.position_label, miner.position_index === null ? "Position unavailable" : "Position " + miner.position_index )); return parts.join(" / "); } function detailLink(miner) { return "/miners/" + encodeURIComponent(miner.id) + "/detail"; } function mapLink(miner) { if (!canViewLocations || !miner.location_position_id) return null; const params = new URLSearchParams({ miner_id: miner.id }); if (miner.site_id && miner.room_id) { params.set("site_id", miner.site_id); params.set("room_id", miner.room_id); } return "/monitoring/map?" + params.toString(); } function addCell(row, label, content, className) { const cell = create("td", className || ""); cell.dataset.label = label; if (content instanceof Node) cell.appendChild(content); else cell.textContent = display(content); row.appendChild(cell); } function stack(primary, secondary) { const box = create("div", "fleet-stack"); box.append(create("span", "fleet-primary", display(primary))); if (secondary) box.append(create("span", "secondary", secondary)); return box; } function renderRow(miner) { const condition = operational(miner); const row = create("tr", "fleet-row"); row.dataset.status = condition.key; const identity = create("div", "fleet-stack"); const link = create("a", "object-link", display(miner.asset_id, "No Asset ID")); link.href = detailLink(miner); identity.append(link, create("span", "secondary fleet-uuid", miner.id)); addCell(row, "Miner", identity, "fleet-identity"); const status = create("div", "fleet-stack"); const marker = create("span", "fleet-status", condition.label); marker.dataset.status = condition.key; status.append(marker, create("span", "secondary", age(miner.observed_at))); addCell(row, "Operational", status); addCell(row, "Model / Serial", stack(miner.model, display(miner.miner_sn))); addCell(row, "Network", stack(miner.current_mac, display(miner.current_ip))); addCell(row, "Location", locationText(miner), "fleet-location"); addCell(row, "Hashrate", metric(miner.hashrate_ths, " TH/s")); addCell(row, "Power", metric(miner.power_w, " W")); addCell(row, "Max chip temp", metric(miner.chip_temp_max_c, " C")); const repair = miner.active_repair ? words(miner.active_repair.status) + " / " + display(miner.active_repair.reason) : "No active repair"; addCell(row, "Lifecycle / Repair", stack(words(miner.lifecycle_status), repair)); const actions = create("div", "fleet-actions"); const open = create("a", "command-link", "Open miner"); open.href = detailLink(miner); actions.appendChild(open); const mapHref = mapLink(miner); if (mapHref) { const map = create("a", "quiet-link", "View on Mine Map"); map.href = mapHref; actions.appendChild(map); } addCell(row, "Actions", actions); return row; } function option(value, label) { const node = document.createElement("option"); node.value = value; node.textContent = label; return node; } function syncLocationOptions(selectedSite, selectedRoom) { if (!canViewLocations || !state.locationOptions) return; el.fleetSite.replaceChildren(option("", "All sites")); for (const site of state.locationOptions.sites || []) { el.fleetSite.appendChild(option(String(site.id), site.name)); } el.fleetSite.value = Array.from(el.fleetSite.options).some(function (item) { return item.value === selectedSite; }) ? selectedSite : ""; el.fleetRoom.replaceChildren(option("", "All rooms")); for (const room of state.locationOptions.rooms || []) { if (!el.fleetSite.value || String(room.site_id) === el.fleetSite.value) { el.fleetRoom.appendChild(option(String(room.id), room.name)); } } el.fleetRoom.value = Array.from(el.fleetRoom.options).some(function (item) { return item.value === selectedRoom; }) ? selectedRoom : ""; state.siteId = el.fleetSite.value; state.roomId = el.fleetRoom.value; } function readUrl() { const params = new URLSearchParams(window.location.search); el.fleetQuery.value = (params.get("query") || "").slice(0, 255); el.fleetLifecycle.value = params.get("lifecycle_status") || ""; el.fleetModel.value = (params.get("model") || "").slice(0, 255); el.fleetRuntime.value = params.get("runtime_status") || ""; el.fleetRepair.value = params.get("repair") || ""; el.fleetOperational.value = params.get("operational_state") || ""; el.fleetTags.value = params.getAll("tags").join(", "); el.fleetProblems.checked = params.get("problems") === "1"; const requestedLimit = Number(params.get("limit")); state.limit = limits.has(requestedLimit) ? requestedLimit : 100; state.offset = Math.max(0, Number.parseInt(params.get("offset") || "0", 10) || 0); el.fleetLimit.value = String(state.limit); if (canViewLocations) { state.siteId = params.get("site_id") || ""; state.roomId = params.get("room_id") || ""; syncLocationOptions(state.siteId, state.roomId); } } function requestParams() { const params = new URLSearchParams(); const values = { query: el.fleetQuery.value.trim(), lifecycle_status: el.fleetLifecycle.value, model: el.fleetModel.value.trim(), runtime_status: el.fleetRuntime.value, repair: el.fleetRepair.value, operational_state: el.fleetOperational.value, problems: el.fleetProblems.checked ? "1" : "", site_id: canViewLocations ? state.siteId : "", room_id: canViewLocations ? state.roomId : "", limit: String(state.limit), offset: String(state.offset) }; for (const pair of Object.entries(values)) { if (pair[1]) params.set(pair[0], pair[1]); } for (const tag of el.fleetTags.value.split(",")) { const clean = tag.trim(); if (clean) params.append("tags", clean); } return params; } function setUrl(mode) { const params = requestParams(); window.history[mode === "push" ? "pushState" : "replaceState"]( {}, "", "/assets/miners?" + params.toString() ); } async function load() { showState("loading"); const controller = new AbortController(); const timer = window.setTimeout(function () { controller.abort(); }, 15000); try { const response = await fetch("/api/inventory/miners?" + requestParams().toString(), { credentials: "same-origin", headers: { Accept: "application/json" }, signal: controller.signal }); if (response.status === 401) { window.location.replace("/login?next=" + encodeURIComponent( window.location.pathname + window.location.search )); return; } if (response.status === 403) { const denied = new Error("Your account cannot view this fleet information."); denied.permissionDenied = true; throw denied; } if (!response.ok) throw new Error("The fleet service returned " + response.status + "."); const envelope = await response.json(); if (!envelope || !Array.isArray(envelope.data) || !envelope.meta) { throw new Error("MinerOps returned an incomplete fleet response."); } state.returned = envelope.data.length; state.locationOptions = envelope.meta.location_options || null; const selectedSite = canViewLocations ? state.siteId : ""; const selectedRoom = canViewLocations ? state.roomId : ""; syncLocationOptions(selectedSite, selectedRoom); el.fleetRows.replaceChildren(); for (const miner of envelope.data) el.fleetRows.appendChild(renderRow(miner)); if (!envelope.data.length) { el.fleetPrevious.disabled = state.offset === 0; el.fleetNext.disabled = true; showState("empty", state.offset ? "No miners are present on this page. Return to the previous page." : "No authorized miners match the current filters."); return; } el.fleetPrevious.disabled = state.offset === 0; el.fleetNext.disabled = state.returned < state.limit; const first = state.offset + 1; const last = state.offset + state.returned; el.fleetResultSummary.textContent = "Showing miners " + first + "-" + last + ". Total fleet size is not calculated."; el.fleetFreshness.textContent = state.returned + " miners on this page"; showState("ready"); } catch (error) { showState(error.permissionDenied ? "denied" : "error", error && error.name === "AbortError" ? "MinerOps took too long to respond. Retry in a moment." : display(error && error.message, "The fleet could not be loaded.")); } finally { window.clearTimeout(timer); } } el.fleetFilters.addEventListener("submit", function (event) { event.preventDefault(); if (canViewLocations) { state.siteId = el.fleetSite.value; state.roomId = el.fleetRoom.value; } state.offset = 0; setUrl("push"); load(); }); el.fleetReset.addEventListener("click", function () { el.fleetFilters.reset(); state.offset = 0; state.limit = 100; state.siteId = ""; state.roomId = ""; syncLocationOptions("", ""); setUrl("push"); load(); }); el.fleetPrevious.addEventListener("click", function () { state.offset = Math.max(0, state.offset - state.limit); setUrl("push"); load(); }); el.fleetNext.addEventListener("click", function () { state.offset += state.limit; setUrl("push"); load(); }); el.fleetLimit.addEventListener("change", function () { state.limit = Number(el.fleetLimit.value); state.offset = 0; setUrl("push"); load(); }); if (canViewLocations) { el.fleetSite.addEventListener("change", function () { state.siteId = el.fleetSite.value; state.roomId = ""; syncLocationOptions(state.siteId, ""); }); el.fleetRoom.addEventListener("change", function () { state.roomId = el.fleetRoom.value; }); } window.addEventListener("popstate", function () { readUrl(); load(); }); readUrl(); load(); }());