613 lines
29 KiB
JavaScript
613 lines
29 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const optionalFieldIds = ["repairModel", "repairSite", "repairRoom"];
|
|
const ids = [
|
|
"repairFilters", "repairQuery", "repairScope", "repairStatus",
|
|
"repairLifecycle", "repairReset", "repairRows",
|
|
"repairTotal", "repairActive", "repairHistorical", "repairFreshness",
|
|
"repairResultSummary", "repairPrevious", "repairNext", "repairLimit",
|
|
"repairInspector", "repairInspectorTitle", "repairInspectorBody",
|
|
"repairInspectorClose"
|
|
];
|
|
const el = {};
|
|
for (const id of ids) el[id] = document.getElementById(id);
|
|
for (const id of optionalFieldIds) el[id] = document.getElementById(id);
|
|
if (!el.repairFilters || !el.repairRows) return;
|
|
|
|
const STATUS_LABELS = {
|
|
OPEN: "Open", IN_PROGRESS: "In progress",
|
|
WAITING_FOR_PARTS: "Waiting for parts", READY_FOR_TEST: "Ready for test",
|
|
COMPLETED: "Completed", CANCELLED: "Cancelled",
|
|
open: "Open", diagnosing: "Diagnosing", waiting_parts: "Waiting parts",
|
|
in_progress: "In progress", testing: "Testing",
|
|
ready_to_shelf: "Ready to shelf", needs_followup: "Needs follow-up",
|
|
completed: "Completed", failed: "Failed", cancelled: "Cancelled",
|
|
repair: "In repair", unknown: "Unknown"
|
|
};
|
|
const state = { offset: 0, limit: 50, items: new Map(), rooms: [], opener: null };
|
|
let controller = null;
|
|
|
|
function text(value, fallback) {
|
|
if (value === null || value === undefined || String(value).trim() === "") {
|
|
return fallback || "Not available";
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function create(tag, className, value) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (value !== undefined) node.textContent = value;
|
|
return node;
|
|
}
|
|
|
|
function button(label, action, className) {
|
|
const node = create("button", className || "quiet-button", label);
|
|
node.type = "button";
|
|
node.dataset.action = action;
|
|
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 friendlyStatus(value) {
|
|
return STATUS_LABELS[value] || text(value, "Unknown").replaceAll("_", " ");
|
|
}
|
|
|
|
function dateTime(value) {
|
|
if (!value) return "Not available";
|
|
const parsed = new Date(value);
|
|
return Number.isNaN(parsed.valueOf()) ? String(value) : parsed.toLocaleString();
|
|
}
|
|
|
|
function age(value) {
|
|
if (!value) return "Age unavailable";
|
|
const milliseconds = Date.now() - new Date(value).valueOf();
|
|
if (!Number.isFinite(milliseconds) || milliseconds < 0) return "Age unavailable";
|
|
const hours = Math.floor(milliseconds / 3600000);
|
|
if (hours < 24) return hours + "h open";
|
|
return Math.floor(hours / 24) + "d open";
|
|
}
|
|
|
|
function labelPart(prefix, value) {
|
|
if (!value) return null;
|
|
const clean = String(value).trim();
|
|
return clean.toLowerCase().startsWith(prefix.toLowerCase() + " ")
|
|
? clean : prefix + " " + clean;
|
|
}
|
|
|
|
function locationText(item) {
|
|
const parts = [item.site_name, item.room_name,
|
|
labelPart("Rack", item.rack_label), labelPart("Shelf", item.shelf_label),
|
|
item.position_label || (item.position_index !== null ? "Position " + item.position_index : null)];
|
|
return parts.filter(Boolean).join(" / ") || "Unplaced";
|
|
}
|
|
|
|
async function api(path, options) {
|
|
const response = await fetch(path, Object.assign({
|
|
credentials: "same-origin", headers: { Accept: "application/json" }
|
|
}, options || {}));
|
|
if (response.status === 401) {
|
|
window.location.replace("/login?next=" + encodeURIComponent(
|
|
window.location.pathname + window.location.search));
|
|
throw new Error("Your session expired.");
|
|
}
|
|
const payload = await response.json().catch(function () { return null; });
|
|
if (!response.ok) {
|
|
const error = new Error(payload && payload.detail
|
|
? String(payload.detail) : "The repair request failed.");
|
|
error.permissionDenied = response.status === 403;
|
|
throw error;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function jsonOptions(method, body) {
|
|
return { method: method, headers: { Accept: "application/json",
|
|
"Content-Type": "application/json" }, body: JSON.stringify(body) };
|
|
}
|
|
|
|
function readUrl() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
el.repairQuery.value = params.get("query") || "";
|
|
el.repairScope.value = params.get("scope") || "active";
|
|
el.repairStatus.value = params.get("status") || "";
|
|
if (el.repairModel) el.repairModel.value = params.get("model") || "";
|
|
el.repairLifecycle.value = params.get("lifecycle_status") || "";
|
|
if (el.repairSite) el.repairSite.value = params.get("site_id") || "";
|
|
state.pendingRoom = params.get("room_id") || "";
|
|
state.offset = Math.max(0, Number(params.get("offset")) || 0);
|
|
state.limit = [25, 50, 100, 250].includes(Number(params.get("limit")))
|
|
? Number(params.get("limit")) : 50;
|
|
el.repairLimit.value = String(state.limit);
|
|
}
|
|
|
|
function currentParams(includePaging) {
|
|
const params = new URLSearchParams();
|
|
const values = {
|
|
query: el.repairQuery.value.trim(), scope: el.repairScope.value,
|
|
status: el.repairStatus.value,
|
|
model: el.repairModel ? el.repairModel.value.trim() : "",
|
|
lifecycle_status: el.repairLifecycle.value,
|
|
site_id: el.repairSite ? el.repairSite.value : "",
|
|
room_id: el.repairRoom ? el.repairRoom.value : ""
|
|
};
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (value && !(key === "scope" && value === "active")) params.set(key, value);
|
|
}
|
|
if (includePaging) {
|
|
params.set("limit", String(state.limit));
|
|
if (state.offset) params.set("offset", String(state.offset));
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function syncUrl(mode) {
|
|
const query = currentParams(true).toString();
|
|
const target = window.location.pathname + (query ? "?" + query : "");
|
|
window.history[mode === "replace" ? "replaceState" : "pushState"]({}, "", target);
|
|
}
|
|
|
|
function syncLocationOptions(options) {
|
|
if (!el.repairSite || !el.repairRoom) return;
|
|
const selectedSite = el.repairSite.value;
|
|
const selectedRoom = state.pendingRoom || el.repairRoom.value;
|
|
el.repairSite.replaceChildren(new Option("All sites", ""));
|
|
for (const site of options.sites || []) {
|
|
el.repairSite.appendChild(new Option(site.name, site.id));
|
|
}
|
|
if ([...el.repairSite.options].some(function (option) {
|
|
return option.value === selectedSite;
|
|
})) el.repairSite.value = selectedSite;
|
|
state.rooms = options.rooms || [];
|
|
el.repairRoom.replaceChildren(new Option("All rooms", ""));
|
|
for (const room of state.rooms) {
|
|
if (!el.repairSite.value || String(room.site_id) === el.repairSite.value) {
|
|
el.repairRoom.appendChild(new Option(room.name, room.id));
|
|
}
|
|
}
|
|
if ([...el.repairRoom.options].some(function (option) {
|
|
return option.value === selectedRoom;
|
|
})) el.repairRoom.value = selectedRoom;
|
|
state.pendingRoom = "";
|
|
}
|
|
|
|
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 identityCell(item) {
|
|
const wrap = create("div", "repair-miner");
|
|
const label = item.asset_id || item.current_mac || "Miner " + item.miner_id.slice(0, 8);
|
|
if (item.capabilities.can_open_miner) {
|
|
const link = create("a", "object-link", label);
|
|
const returnTo = window.location.pathname + window.location.search;
|
|
const path = "/miners/" + encodeURIComponent(item.miner_id) + "/detail";
|
|
link.href = path + "?return_to=" + encodeURIComponent(returnTo);
|
|
wrap.appendChild(link);
|
|
} else wrap.appendChild(create("strong", "", label));
|
|
const details = [item.model, item.miner_sn, item.current_mac, item.current_ip]
|
|
.filter(Boolean).join(" / ");
|
|
if (details) wrap.appendChild(create("span", "secondary", details));
|
|
wrap.appendChild(create("span", "source-label", item.source === "modular"
|
|
? "Repair domain" : "Legacy compatibility"));
|
|
return wrap;
|
|
}
|
|
|
|
function actionCell(item) {
|
|
const wrap = create("div", "repair-row-actions");
|
|
if (item.capabilities.can_open_map) {
|
|
const params = new URLSearchParams({ site_id: item.site_id,
|
|
room_id: item.room_id, miner_id: item.miner_id });
|
|
const link = create("a", "quiet-link", "Mine Map");
|
|
link.href = "/monitoring/map?" + params.toString();
|
|
wrap.appendChild(link);
|
|
}
|
|
if (Object.values(item.capabilities).some(Boolean)) {
|
|
const details = button("Details", "details");
|
|
details.dataset.key = item.source + ":" + (item.repair_id || item.miner_id);
|
|
wrap.appendChild(details);
|
|
}
|
|
return wrap;
|
|
}
|
|
|
|
function renderRows(items) {
|
|
el.repairRows.replaceChildren();
|
|
state.items.clear();
|
|
for (const item of items) {
|
|
const key = item.source + ":" + (item.repair_id || item.miner_id);
|
|
state.items.set(key, item);
|
|
const row = create("tr", "repair-row");
|
|
row.dataset.source = item.source;
|
|
row.dataset.active = String(item.active);
|
|
addCell(row, "Miner", identityCell(item));
|
|
const status = create("span", "repair-status", friendlyStatus(item.repair_status));
|
|
status.dataset.status = String(item.repair_status).toLowerCase();
|
|
const stateWrap = create("div");
|
|
stateWrap.append(status, create("span", "secondary", text(item.lifecycle_status)));
|
|
addCell(row, "Repair state", stateWrap);
|
|
addCell(row, "Reported problem", item.reported_problem || item.reason);
|
|
addCell(row, "Latest context", item.latest_context);
|
|
addCell(row, "Location", locationText(item));
|
|
const opened = create("div");
|
|
opened.append(create("span", "", dateTime(item.opened_at)),
|
|
create("span", "secondary", item.active ? age(item.opened_at) : "Historical"));
|
|
addCell(row, "Opened / age", opened);
|
|
addCell(row, "Parts", item.parts_count === null
|
|
? "Legacy record" : item.parts_count + " recorded");
|
|
addCell(row, "Actions", actionCell(item));
|
|
el.repairRows.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function section(title) {
|
|
const node = create("section", "repair-detail-section");
|
|
node.appendChild(create("h3", "", title));
|
|
return node;
|
|
}
|
|
|
|
function renderTimeline(events, legacy) {
|
|
const node = section(legacy ? "Legacy repair history" : "Repair timeline");
|
|
if (!events.length) {
|
|
node.appendChild(create("p", "secondary", "No history entries recorded."));
|
|
return node;
|
|
}
|
|
const list = create("ol", "repair-timeline");
|
|
for (const event of events) {
|
|
const entry = create("li");
|
|
const title = legacy
|
|
? text(event.event_title, event.event_type)
|
|
: friendlyStatus(event.event_type);
|
|
entry.append(create("strong", "", title),
|
|
create("span", "secondary", dateTime(event.occurred_at || event.event_at || event.created_at)));
|
|
const payload = legacy ? event.description : Object.entries(event.payload || {})
|
|
.map(function (pair) { return pair[0].replaceAll("_", " ") + ": " + pair[1]; })
|
|
.join(" / ");
|
|
if (payload) entry.appendChild(create("p", "", String(payload)));
|
|
list.appendChild(entry);
|
|
}
|
|
node.appendChild(list);
|
|
return node;
|
|
}
|
|
|
|
function partLabel(part) {
|
|
if (part.consumable) {
|
|
return part.consumable.quantity + " " + part.consumable.unit + " " +
|
|
part.consumable.description;
|
|
}
|
|
const snapshot = part.component_snapshot || {};
|
|
return [snapshot.component_type, snapshot.asset_id,
|
|
snapshot.manufacturer_serial].filter(Boolean).join(" / ") || "Tracked component";
|
|
}
|
|
|
|
function renderParts(item, parts) {
|
|
const node = section("Parts used");
|
|
if (!parts.length) node.appendChild(create("p", "secondary", "No parts recorded."));
|
|
const list = create("ul", "repair-parts-list");
|
|
for (const part of parts) {
|
|
const entry = create("li");
|
|
entry.appendChild(create("span", part.voided_at ? "voided-part" : "", partLabel(part)));
|
|
if (part.notes) entry.appendChild(create("span", "secondary", part.notes));
|
|
if (part.voided_at) {
|
|
entry.appendChild(create("span", "secondary", "Voided: " + text(part.void_reason)));
|
|
} else if (item.capabilities.can_manage_modular_parts) {
|
|
const voidButton = button("Void entry", "void-part", "danger-button");
|
|
voidButton.dataset.partId = part.repair_part_id;
|
|
entry.appendChild(voidButton);
|
|
}
|
|
list.appendChild(entry);
|
|
}
|
|
node.appendChild(list);
|
|
if (item.capabilities.can_manage_modular_parts) {
|
|
const form = create("form", "repair-action-form");
|
|
form.dataset.action = "record-part";
|
|
const kind = create("select");
|
|
kind.name = "kind";
|
|
kind.append(new Option("Consumable", "consumable"),
|
|
new Option("Tracked component", "tracked_component"));
|
|
const description = create("input");
|
|
description.name = "description"; description.placeholder = "Consumable description";
|
|
const quantity = create("input");
|
|
quantity.name = "quantity"; quantity.type = "number";
|
|
quantity.min = "0.001"; quantity.step = "0.001"; quantity.value = "1";
|
|
const unit = create("select"); unit.name = "unit";
|
|
for (const value of ["each", "gram", "millilitre", "metre", "centimetre"]) {
|
|
unit.appendChild(new Option(value, value));
|
|
}
|
|
const component = create("input");
|
|
component.name = "component"; component.placeholder = "Tracked component UUID";
|
|
const notes = create("input"); notes.name = "notes"; notes.placeholder = "Optional notes";
|
|
form.append(kind, description, quantity, unit, component, notes,
|
|
create("button", "command-button", "Record part"));
|
|
form.lastChild.type = "submit";
|
|
node.appendChild(form);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function modularActions(item) {
|
|
const node = section("Next action");
|
|
if (item.capabilities.can_complete_modular_repair) {
|
|
const form = create("form", "repair-action-form");
|
|
form.dataset.action = "complete-repair";
|
|
const target = create("select"); target.name = "target";
|
|
target.append(new Option("Return active", "active"), new Option("Return as spare", "spare"));
|
|
const summary = create("textarea"); summary.name = "summary";
|
|
summary.placeholder = "Optional completion summary";
|
|
const submit = create("button", "command-button", "Complete repair");
|
|
submit.type = "submit";
|
|
form.append(target, summary, submit);
|
|
node.appendChild(form);
|
|
} else node.appendChild(create("p", "secondary", "No repair action is available."));
|
|
return node;
|
|
}
|
|
|
|
function legacyActions(item) {
|
|
const node = section("Legacy actions");
|
|
if (!item.capabilities.can_manage_legacy) {
|
|
node.appendChild(create("p", "secondary", "Legacy repair actions are read-only."));
|
|
return node;
|
|
}
|
|
const form = create("form", "repair-action-form");
|
|
form.dataset.action = "legacy-update";
|
|
const status = create("select"); status.name = "status";
|
|
for (const value of ["open", "diagnosing", "waiting_parts", "in_progress",
|
|
"testing", "ready_to_shelf", "needs_followup", "completed", "failed", "cancelled"]) {
|
|
const option = new Option(friendlyStatus(value), value);
|
|
option.selected = value === item.repair_status;
|
|
status.appendChild(option);
|
|
}
|
|
const note = create("textarea"); note.name = "note"; note.placeholder = "Repair note";
|
|
const noteButton = create("button", "quiet-button", "Add note");
|
|
noteButton.type = "submit"; noteButton.value = "note"; noteButton.name = "intent";
|
|
const statusButton = create("button", "quiet-button", "Save status");
|
|
statusButton.type = "submit"; statusButton.value = "status"; statusButton.name = "intent";
|
|
form.append(status, note, noteButton, statusButton);
|
|
node.append(form, button("Mark repaired", "legacy-repaired", "command-button"),
|
|
button("Decommission", "legacy-decommission", "danger-button"));
|
|
return node;
|
|
}
|
|
|
|
function closeInspector() {
|
|
el.repairInspector.hidden = true;
|
|
el.repairInspectorBody.replaceChildren();
|
|
if (state.opener) state.opener.focus();
|
|
}
|
|
|
|
async function openInspector(item, opener) {
|
|
state.currentItem = item;
|
|
state.opener = opener;
|
|
el.repairInspector.hidden = false;
|
|
el.repairInspectorTitle.textContent = item.asset_id || item.current_mac || "Repair details";
|
|
el.repairInspectorBody.replaceChildren();
|
|
const overview = section("Repair overview");
|
|
overview.append(
|
|
create("p", "", friendlyStatus(item.repair_status) + " / " + text(item.lifecycle_status)),
|
|
create("p", "", text(item.reported_problem, "No reported problem")),
|
|
create("p", "secondary", locationText(item)),
|
|
create("p", "secondary", "Opened " + dateTime(item.opened_at))
|
|
);
|
|
el.repairInspectorBody.appendChild(overview);
|
|
el.repairInspectorClose.focus();
|
|
|
|
try {
|
|
if (item.source === "modular") {
|
|
const canRead = item.capabilities.can_read_modular_timeline;
|
|
const requests = canRead ? [
|
|
api("/api/repairs/" + encodeURIComponent(item.repair_id) + "/events"),
|
|
api("/api/repairs/" + encodeURIComponent(item.repair_id) + "/parts")
|
|
] : [];
|
|
const results = await Promise.all(requests);
|
|
if (canRead) {
|
|
el.repairInspectorBody.append(
|
|
renderTimeline(results[0].data.events || [], false),
|
|
renderParts(item, results[1].data.entries || [])
|
|
);
|
|
}
|
|
el.repairInspectorBody.appendChild(modularActions(item));
|
|
} else {
|
|
if (item.capabilities.can_read_legacy_history) {
|
|
const history = await api("/api/miners/" +
|
|
encodeURIComponent(item.miner_id) + "/maintenance-events");
|
|
const events = (history.data || []).filter(function (event) {
|
|
return String(event.event_type || "").toLowerCase() === "repair";
|
|
});
|
|
el.repairInspectorBody.appendChild(renderTimeline(events, true));
|
|
}
|
|
el.repairInspectorBody.appendChild(legacyActions(item));
|
|
}
|
|
} catch (error) {
|
|
el.repairInspectorBody.appendChild(create("p", "state-inline-error", error.message));
|
|
}
|
|
}
|
|
|
|
function selectedItem(target) {
|
|
const keyNode = target.closest("[data-key]");
|
|
return keyNode ? state.items.get(keyNode.dataset.key) : null;
|
|
}
|
|
|
|
async function reloadAfterMutation(message) {
|
|
closeInspector();
|
|
await load();
|
|
el.repairResultSummary.textContent = message + " " + el.repairResultSummary.textContent;
|
|
}
|
|
|
|
async function handleInspectorSubmit(event) {
|
|
event.preventDefault();
|
|
const form = event.target;
|
|
const item = state.currentItem;
|
|
if (!item) return;
|
|
const data = new FormData(form);
|
|
try {
|
|
if (form.dataset.action === "complete-repair") {
|
|
if (!window.confirm("Complete this repair and change the miner lifecycle?")) return;
|
|
await api("/api/repairs/" + encodeURIComponent(item.repair_id) + "/complete",
|
|
jsonOptions("POST", { target_lifecycle: data.get("target"),
|
|
completion_summary: String(data.get("summary") || "").trim() || null }));
|
|
await reloadAfterMutation("Repair completed.");
|
|
} else if (form.dataset.action === "record-part") {
|
|
const kind = data.get("kind");
|
|
const body = { notes: String(data.get("notes") || "").trim() || null };
|
|
if (kind === "consumable") {
|
|
body.consumable = { description: String(data.get("description") || "").trim(),
|
|
quantity: String(data.get("quantity") || ""), unit: data.get("unit") };
|
|
} else body.tracked_component_uuid = String(data.get("component") || "").trim();
|
|
await api("/api/repairs/" + encodeURIComponent(item.repair_id) + "/parts",
|
|
jsonOptions("POST", body));
|
|
await openInspector(item, state.opener);
|
|
} else if (form.dataset.action === "legacy-update") {
|
|
const intent = event.submitter && event.submitter.value;
|
|
const note = String(data.get("note") || "").trim();
|
|
const path = "/api/repairs/" + encodeURIComponent(item.miner_id) +
|
|
(intent === "note" ? "/note" : "/status");
|
|
const body = intent === "note"
|
|
? { note: note, event_status: data.get("status") }
|
|
: { event_status: data.get("status"), note: note || null };
|
|
await api(path, jsonOptions("POST", body));
|
|
await reloadAfterMutation(intent === "note" ? "Repair note added." : "Repair status updated.");
|
|
}
|
|
} catch (error) {
|
|
form.prepend(create("p", "state-inline-error", error.message));
|
|
}
|
|
}
|
|
|
|
async function handleInspectorClick(event) {
|
|
const action = event.target.closest("[data-action]");
|
|
const item = state.currentItem;
|
|
if (!action || !item) return;
|
|
try {
|
|
if (action.dataset.action === "void-part") {
|
|
const reason = window.prompt("Reason for voiding this repair-part entry:", "");
|
|
if (!reason || !reason.trim() || !window.confirm("Void this repair-part entry? Its history will remain recorded.")) return;
|
|
await api("/api/repairs/" + encodeURIComponent(item.repair_id) + "/parts/" +
|
|
encodeURIComponent(action.dataset.partId) + "/void",
|
|
jsonOptions("POST", { reason: reason.trim() }));
|
|
await openInspector(item, state.opener);
|
|
} else if (action.dataset.action === "legacy-repaired") {
|
|
if (!window.confirm("Mark this miner repaired and move it to Waiting Placement?")) return;
|
|
await api("/api/repairs/" + encodeURIComponent(item.miner_id) + "/state",
|
|
jsonOptions("POST", { asset_state: "waiting_placement",
|
|
note: "Repair marked complete. Miner is waiting for placement." }));
|
|
await reloadAfterMutation("Miner moved to Waiting Placement.");
|
|
} else if (action.dataset.action === "legacy-decommission") {
|
|
const typed = window.prompt("Type DECOMMISSION to confirm:", "");
|
|
if (typed !== "DECOMMISSION") return;
|
|
await api("/api/repairs/" + encodeURIComponent(item.miner_id) + "/state",
|
|
jsonOptions("POST", { asset_state: "decommissioned",
|
|
note: "Miner decommissioned from Repair Center." }));
|
|
await reloadAfterMutation("Miner decommissioned.");
|
|
}
|
|
} catch (error) {
|
|
el.repairInspectorBody.prepend(create("p", "state-inline-error", error.message));
|
|
}
|
|
}
|
|
|
|
function populateStatusOptions(counts) {
|
|
const selected = el.repairStatus.value;
|
|
const values = Object.keys(counts || {}).sort(function (left, right) {
|
|
return friendlyStatus(left).localeCompare(friendlyStatus(right));
|
|
});
|
|
el.repairStatus.replaceChildren(new Option("All statuses", ""));
|
|
for (const value of values) {
|
|
el.repairStatus.appendChild(new Option(
|
|
friendlyStatus(value) + " (" + counts[value] + ")", value));
|
|
}
|
|
if ([...el.repairStatus.options].some(function (option) {
|
|
return option.value === selected;
|
|
})) el.repairStatus.value = selected;
|
|
}
|
|
|
|
async function load() {
|
|
if (controller) controller.abort();
|
|
controller = new AbortController();
|
|
showState("loading");
|
|
el.repairFreshness.textContent = "Loading repair work";
|
|
try {
|
|
const params = currentParams(true);
|
|
const envelope = await api("/api/repairs/queue?" + params.toString(),
|
|
{ signal: controller.signal });
|
|
if (!envelope || !envelope.data || !Array.isArray(envelope.data.items) ||
|
|
!envelope.data.summary || !envelope.meta) {
|
|
throw new Error("MinerOps returned an invalid Repair Center response.");
|
|
}
|
|
const items = envelope.data.items;
|
|
if (!items.every(function (item) {
|
|
return item && item.miner_id && item.source && item.capabilities;
|
|
})) throw new Error("MinerOps returned incomplete repair records.");
|
|
const summary = envelope.data.summary;
|
|
el.repairTotal.textContent = String(summary.total);
|
|
el.repairActive.textContent = String(summary.active);
|
|
el.repairHistorical.textContent = String(summary.historical);
|
|
populateStatusOptions(summary.status_counts);
|
|
syncLocationOptions(envelope.meta.location_options || {});
|
|
renderRows(items);
|
|
const start = summary.total ? state.offset + 1 : 0;
|
|
const end = state.offset + items.length;
|
|
el.repairResultSummary.textContent = "Showing " + start + "-" + end +
|
|
" of " + summary.total + " matching repairs.";
|
|
el.repairPrevious.disabled = state.offset === 0;
|
|
el.repairNext.disabled = end >= summary.total;
|
|
el.repairFreshness.textContent = "Loaded " + new Date().toLocaleTimeString();
|
|
showState(items.length ? "ready" : "empty");
|
|
} catch (error) {
|
|
if (error.name === "AbortError") return;
|
|
showState(error.permissionDenied ? "denied" : "error", error.message);
|
|
el.repairFreshness.textContent = "Repair data unavailable";
|
|
}
|
|
}
|
|
|
|
el.repairRows.addEventListener("click", function (event) {
|
|
const action = event.target.closest('[data-action="details"]');
|
|
const item = selectedItem(event.target);
|
|
if (!action || !item) return;
|
|
state.currentItem = item;
|
|
openInspector(item, action);
|
|
});
|
|
el.repairInspectorClose.addEventListener("click", closeInspector);
|
|
el.repairInspector.addEventListener("submit", handleInspectorSubmit);
|
|
el.repairInspector.addEventListener("click", handleInspectorClick);
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.key === "Escape" && !el.repairInspector.hidden) closeInspector();
|
|
});
|
|
el.repairFilters.addEventListener("submit", function (event) {
|
|
event.preventDefault(); state.offset = 0; syncUrl("push"); load();
|
|
});
|
|
el.repairReset.addEventListener("click", function () {
|
|
el.repairFilters.reset(); state.offset = 0; state.limit = 50;
|
|
state.pendingRoom = ""; el.repairLimit.value = "50";
|
|
syncUrl("push"); load();
|
|
});
|
|
if (el.repairSite) el.repairSite.addEventListener("change", function () {
|
|
state.pendingRoom = "";
|
|
syncLocationOptions({ sites: [...el.repairSite.options].slice(1).map(function (option) {
|
|
return { id: option.value, name: option.textContent };
|
|
}), rooms: state.rooms });
|
|
});
|
|
el.repairPrevious.addEventListener("click", function () {
|
|
state.offset = Math.max(0, state.offset - state.limit); syncUrl("push"); load();
|
|
});
|
|
el.repairNext.addEventListener("click", function () {
|
|
state.offset += state.limit; syncUrl("push"); load();
|
|
});
|
|
el.repairLimit.addEventListener("change", function () {
|
|
state.limit = Number(el.repairLimit.value); state.offset = 0; syncUrl("push"); load();
|
|
});
|
|
window.addEventListener("popstate", function () { readUrl(); load(); });
|
|
|
|
readUrl();
|
|
syncUrl("replace");
|
|
load();
|
|
})();
|