(() => { const menuController = window.WarpBoxUI?.bindMenuBar?.() || { close() { document.querySelectorAll(".menu-item.is-open").forEach((item) => { item.classList.remove("is-open"); item.querySelector(".menu-button")?.setAttribute("aria-expanded", "false"); }); } }; const toastTarget = document.getElementById("toast"); const dataNode = document.getElementById("boxes-data"); const tableBody = document.getElementById("boxes-table-body"); const emptyState = document.getElementById("boxes-empty-state"); const searchInput = document.getElementById("boxes-search"); const statusFilter = document.getElementById("boxes-status-filter"); const flagFilter = document.getElementById("boxes-flag-filter"); const sortFilter = document.getElementById("boxes-sort"); const pageSizeFilter = document.getElementById("boxes-page-size"); const selectAll = document.getElementById("boxes-select-all"); const prevPageButton = document.getElementById("boxes-prev-page"); const nextPageButton = document.getElementById("boxes-next-page"); const pageLabel = document.getElementById("boxes-page-label"); const rangeLabel = document.getElementById("boxes-range-label"); const selectedLabel = document.getElementById("boxes-selected-label"); const footerSummary = document.getElementById("boxes-footer-summary"); const detailFileList = document.getElementById("detail-file-list"); if (!dataNode || !tableBody || !searchInput || !detailFileList) return; const statEls = { total: document.querySelector("[data-stat-total]"), ready: document.querySelector("[data-stat-ready]"), uploading: document.querySelector("[data-stat-uploading]"), expired: document.querySelector("[data-stat-expired]") }; const detailEls = { boxId: document.getElementById("detail-box-id"), status: document.getElementById("detail-status"), created: document.getElementById("detail-created"), expires: document.getElementById("detail-expires"), retention: document.getElementById("detail-retention"), files: document.getElementById("detail-files"), size: document.getElementById("detail-size"), flags: document.getElementById("detail-flags"), open: document.getElementById("detail-open"), zip: document.getElementById("detail-zip") }; function showToast(message, type = "info", duration = 2200) { if (window.WarpBoxUI) { window.WarpBoxUI.toast(message, type, { target: toastTarget, duration }); return; } if (!toastTarget) return; toastTarget.textContent = message; toastTarget.classList.add("is-visible"); window.setTimeout(() => toastTarget.classList.remove("is-visible"), duration); } function parseData() { try { return JSON.parse(dataNode.textContent || "[]"); } catch (_) { return []; } } const state = { boxes: parseData(), selected: new Set(), activeId: null, page: 1 }; function pageSize() { return Number(pageSizeFilter.value || 10); } function allBoxes() { return state.boxes.slice(); } function sortBoxes(boxes) { const sorted = boxes.slice(); switch (sortFilter.value) { case "name": sorted.sort((a, b) => a.id.localeCompare(b.id)); break; case "largest": sorted.sort((a, b) => compareSizeLabel(a.total_size_label, b.total_size_label)); break; case "expires": sorted.sort((a, b) => compareExpiry(a.expires_at_iso, b.expires_at_iso)); break; default: sorted.sort((a, b) => (b.created_at_iso || "").localeCompare(a.created_at_iso || "")); } return sorted; } function compareSizeLabel(left, right) { return sizeLabelToBytes(right) - sizeLabelToBytes(left); } function sizeLabelToBytes(label) { const match = String(label || "").trim().match(/^([\d.]+)\s*([KMGT]?i?B|B)$/i); if (!match) return 0; const value = Number(match[1]); const unit = match[2].toUpperCase(); const map = { B: 1, KIB: 1024, MIB: 1024 ** 2, GIB: 1024 ** 3, TIB: 1024 ** 4 }; return value * (map[unit] || 1); } function compareExpiry(left, right) { if (!left && !right) return 0; if (!left) return 1; if (!right) return -1; return left.localeCompare(right); } function filteredBoxes() { const query = searchInput.value.trim().toLowerCase(); const status = statusFilter.value; const flag = flagFilter.value; return sortBoxes(allBoxes().filter((box) => { const matchesSearch = !query || String(box.search_text || "").includes(query); const matchesStatus = status === "all" || box.status === status; const matchesFlag = flag === "all" || (box.flags || []).includes(flag); return matchesSearch && matchesStatus && matchesFlag; })); } function pagedBoxes(boxes) { const size = pageSize(); const pages = Math.max(1, Math.ceil(boxes.length / size)); if (state.page > pages) state.page = pages; if (state.page < 1) state.page = 1; const start = (state.page - 1) * size; return { items: boxes.slice(start, start + size), start, pages }; } function selectedBoxes() { return allBoxes().filter((box) => state.selected.has(box.id)); } function currentActiveBox() { const boxes = allBoxes(); return boxes.find((box) => box.id === state.activeId) || null; } function ensureActiveBox(filtered) { if (filtered.length === 0) { state.activeId = null; return null; } if (!filtered.some((box) => box.id === state.activeId)) { state.activeId = filtered[0].id; } return filtered.find((box) => box.id === state.activeId) || filtered[0]; } function renderSummary(filtered) { const total = filtered.length; const ready = filtered.filter((box) => box.status === "ready").length; const uploading = filtered.filter((box) => box.status === "uploading").length; const expired = filtered.filter((box) => box.status === "expired" || box.status === "consumed").length; statEls.total.textContent = String(total); statEls.ready.textContent = String(ready); statEls.uploading.textContent = String(uploading); statEls.expired.textContent = String(expired); footerSummary.textContent = `${allBoxes().length} boxes loaded`; selectedLabel.textContent = `Selected: ${state.selected.size}`; } function renderTable() { const filtered = filteredBoxes(); const active = ensureActiveBox(filtered); const page = pagedBoxes(filtered); tableBody.innerHTML = ""; page.items.forEach((box) => tableBody.appendChild(buildRow(box))); emptyState.hidden = page.items.length !== 0; const startIndex = filtered.length ? page.start + 1 : 0; const endIndex = page.start + page.items.length; rangeLabel.textContent = `Showing ${startIndex}-${endIndex} of ${filtered.length}`; pageLabel.textContent = `Page ${state.page} / ${page.pages}`; prevPageButton.disabled = state.page <= 1; nextPageButton.disabled = state.page >= page.pages; selectAll.checked = page.items.length > 0 && page.items.every((box) => state.selected.has(box.id)); renderSummary(filtered); renderDetails(active); } function buildRow(box) { const row = document.createElement("tr"); if (box.id === state.activeId) row.classList.add("is-selected"); row.innerHTML = ` ${box.id} ${box.status_label} ${box.complete_files}/${box.file_count} ${box.total_size_label} ${box.retention_label || "Not set"} ${box.expires_at_label || "Not set"}
${renderFlags(box.flags)}
${renderRowActions(box)}
`; row.addEventListener("click", (event) => { if (event.target.closest("button") || event.target.closest("a") || event.target.closest("input")) return; state.activeId = box.id; renderTable(); }); row.querySelector(".boxes-row-check")?.addEventListener("change", (event) => { if (event.target.checked) { state.selected.add(box.id); } else { state.selected.delete(box.id); } selectedLabel.textContent = `Selected: ${state.selected.size}`; syncSelectAllForPage(); }); row.querySelector('[data-row-action="focus"]')?.addEventListener("click", () => { state.activeId = box.id; renderTable(); }); return row; } function renderFlags(flags) { if (!flags || !flags.length) return 'none'; return flags.map((flag) => `${escapeHtml(flag)}`).join(""); } function renderRowActions(box) { const parts = [ `Open`, `` ]; if (box.zip_available && box.zip_url) { parts.push(`ZIP`); } return parts.join(""); } function renderDetails(box) { if (!box) { detailEls.boxId.textContent = "-"; detailEls.status.textContent = "-"; detailEls.created.textContent = "-"; detailEls.expires.textContent = "-"; detailEls.retention.textContent = "-"; detailEls.files.textContent = "-"; detailEls.size.textContent = "-"; detailEls.flags.textContent = "-"; detailEls.open.href = "#"; detailEls.zip.href = "#"; detailEls.zip.setAttribute("aria-disabled", "true"); detailFileList.innerHTML = '
No box selected.
'; return; } detailEls.boxId.textContent = box.id; detailEls.status.textContent = box.status_label; detailEls.created.textContent = box.created_at_label || "Not set"; detailEls.expires.textContent = box.expires_at_label || "Not set"; detailEls.retention.textContent = box.retention_label || "Not set"; detailEls.files.textContent = `${box.complete_files}/${box.file_count} complete`; detailEls.size.textContent = box.total_size_label; detailEls.flags.textContent = (box.flags || []).join(", ") || "none"; detailEls.open.href = box.open_url || "#"; if (box.zip_available && box.zip_url) { detailEls.zip.href = box.zip_url; detailEls.zip.removeAttribute("aria-disabled"); detailEls.zip.style.pointerEvents = ""; detailEls.zip.style.opacity = ""; } else { detailEls.zip.href = "#"; detailEls.zip.setAttribute("aria-disabled", "true"); detailEls.zip.style.pointerEvents = "none"; detailEls.zip.style.opacity = ".55"; } renderFiles(box.files || []); } function renderFiles(files) { if (!files.length) { detailFileList.innerHTML = '
No file inventory available for this box.
'; return; } detailFileList.innerHTML = files.map((file) => `
${escapeHtml(file.name)}
${escapeHtml(file.status_label || file.status || "Unknown")}
${escapeHtml(file.size_label || "0 B")} ${escapeHtml(file.mime_type || "application/octet-stream")} ${file.is_complete && file.download_path ? `download` : "pending"}
`).join(""); } function syncSelectAllForPage() { const filtered = filteredBoxes(); const page = pagedBoxes(filtered); selectAll.checked = page.items.length > 0 && page.items.every((box) => state.selected.has(box.id)); selectedLabel.textContent = `Selected: ${state.selected.size}`; } function clearFilters() { searchInput.value = ""; statusFilter.value = "all"; flagFilter.value = "all"; sortFilter.value = "newest"; pageSizeFilter.value = "10"; state.page = 1; renderTable(); } async function runBulkAction(action, ids, deltaSeconds = 0) { if (!ids.length) { showToast("Select one or more boxes first", "warning"); return; } try { const response = await fetch("/admin/boxes/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, box_ids: ids, delta_seconds: deltaSeconds }) }); const payload = await response.json(); if (!response.ok) { const message = payload.error || payload.message || "Action failed"; const warning = Array.isArray(payload.warnings) && payload.warnings.length ? ` (${payload.warnings[0]})` : ""; showToast(`${message}${warning}`, "error", 3200); return; } state.boxes = Array.isArray(payload.boxes) ? payload.boxes : state.boxes; state.selected.clear(); if (state.activeId && !state.boxes.some((box) => box.id === state.activeId)) { state.activeId = null; } renderTable(); let message = payload.message || "Action complete"; if (Array.isArray(payload.warnings) && payload.warnings.length) { message += ` (${payload.warnings.length} warning${payload.warnings.length === 1 ? "" : "s"})`; } showToast(message, Array.isArray(payload.warnings) && payload.warnings.length ? "warning" : "success", 2800); } catch (_) { showToast("Network error while updating boxes", "error", 3200); } } function selectedIDsOrActive() { if (state.selected.size) return Array.from(state.selected); const active = currentActiveBox(); return active ? [active.id] : []; } async function runCommand(command) { switch (command) { case "refresh": window.location.reload(); return; case "export": exportVisibleCSV(); showToast("Visible boxes exported"); return; case "status-ready": statusFilter.value = "ready"; state.page = 1; renderTable(); return; case "status-expired": statusFilter.value = "expired"; state.page = 1; renderTable(); return; case "clear-filters": clearFilters(); showToast("Filters cleared"); return; case "expire": case "active-expire": await runBulkAction("expire", selectedIDsOrActive()); return; case "extend-day": case "active-extend-day": await runBulkAction("bump", selectedIDsOrActive(), 24 * 60 * 60); return; case "extend-week": case "active-extend-week": await runBulkAction("bump", selectedIDsOrActive(), 7 * 24 * 60 * 60); return; case "delete": case "active-delete": if (!window.confirm("Delete selected boxes? This removes stored files.")) return; await runBulkAction("delete", selectedIDsOrActive()); return; case "help-scope": showToast("Ownership filter waits for account + box owner data in backend", "info", 3400); return; case "help-flags": showToast("Flags: protected, one-time, zip off, legacy, consumed", "info", 3200); return; default: showToast(`Unknown command: ${command}`, "warning"); } } function exportVisibleCSV() { const rows = filteredBoxes().map((box) => ([ box.id, box.status_label, box.file_count, box.total_size_label, box.retention_label, box.expires_at_label, (box.flags || []).join("|") ])); const csv = [ ["box_id", "status", "files", "size", "retention", "expires", "flags"], ...rows ].map((row) => row.map(csvCell).join(",")).join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = "warpbox-boxes.csv"; anchor.click(); URL.revokeObjectURL(url); } function csvCell(value) { const text = String(value ?? ""); if (/[",\n]/.test(text)) return `"${text.replaceAll('"', '""')}"`; return text; } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function escapeAttr(value) { return escapeHtml(value).replaceAll("'", "'"); } [searchInput, statusFilter, flagFilter, sortFilter].forEach((control) => { control.addEventListener(control.tagName === "INPUT" ? "input" : "change", () => { state.page = 1; renderTable(); }); }); pageSizeFilter.addEventListener("change", () => { state.page = 1; renderTable(); }); selectAll?.addEventListener("change", () => { const filtered = filteredBoxes(); const page = pagedBoxes(filtered); page.items.forEach((box) => { if (selectAll.checked) state.selected.add(box.id); else state.selected.delete(box.id); }); renderTable(); }); prevPageButton?.addEventListener("click", () => { state.page -= 1; renderTable(); }); nextPageButton?.addEventListener("click", () => { state.page += 1; renderTable(); }); document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", async () => { menuController.close(); await runCommand(button.dataset.command); }); }); document.addEventListener("keydown", async (event) => { if (event.key === "Escape") menuController.close(); if (event.key === "F5") { event.preventDefault(); await runCommand("refresh"); } }); if (state.boxes.length > 0) { state.activeId = state.boxes[0].id; } renderTable(); })();