const form = document.querySelector("#capture-form"); const urlInput = document.querySelector("#capture-url"); const submitButton = document.querySelector("#capture-submit"); const formMessage = document.querySelector("#form-message"); const jobGrid = document.querySelector("#job-grid"); const emptyState = document.querySelector("#empty-state"); const refreshButton = document.querySelector("#refresh-jobs"); const serviceStatus = document.querySelector("#service-status"); const reportDialog = document.querySelector("#report-dialog"); const reportContent = document.querySelector("#report-content"); const closeReportButton = document.querySelector("#close-report"); const controls = { externalAssets: document.querySelector("#external-assets"), parallelConnections: document.querySelector("#parallel-connections"), parallelValue: document.querySelector("#parallel-value"), maxPages: document.querySelector("#max-pages"), maxDepth: document.querySelector("#max-depth"), maxBytes: document.querySelector("#max-bytes"), maxDuration: document.querySelector("#max-duration"), retentionDays: document.querySelector("#retention-days"), budgetSummary: document.querySelector("#budget-summary"), assetTypes: [...document.querySelectorAll("[data-asset-type]")], skipFileTypes: document.querySelector("#skip-file-types"), useProxy: document.querySelector("#use-proxy"), proxySummary: document.querySelector("#proxy-summary"), proxyDetail: document.querySelector("#proxy-detail"), proxyRow: document.querySelector(".proxy-row"), }; const publicStats = { websites: document.querySelector("#stat-websites"), data: document.querySelector("#stat-data"), active: document.querySelector("#stat-active"), files: document.querySelector("#stat-files"), }; const OWNER_STORAGE_KEY = "siteharbor.owner-token.v1"; const SESSION_STORAGE_KEY = "siteharbor.sessions.v1"; const OWNER_HEADER = "X-SiteHarbor-Session"; const MAX_STORED_SESSIONS = 40; const TOKEN_PATTERN = /^[A-Za-z0-9_-]{24,200}$/; const jobs = new Map(); const streams = new Map(); const jobLogs = new Map(); const jobLogIds = new Map(); const refreshTimers = new Map(); let storageAvailable = true; const ownerToken = getOwnerToken(); function createOwnerToken() { if (window.crypto?.randomUUID) return window.crypto.randomUUID(); if (window.crypto?.getRandomValues) { const bytes = new Uint8Array(24); window.crypto.getRandomValues(bytes); return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } return `browser-${Date.now()}-${Math.random().toString(36).slice(2).padEnd(16, "0")}`; } function getOwnerToken() { try { const existing = window.localStorage.getItem(OWNER_STORAGE_KEY); if (existing && TOKEN_PATTERN.test(existing)) return existing; const token = createOwnerToken(); window.localStorage.setItem(OWNER_STORAGE_KEY, token); return token; } catch { storageAvailable = false; return createOwnerToken(); } } function readStoredSessions() { if (!storageAvailable) return []; try { const raw = window.localStorage.getItem(SESSION_STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw); const sessions = Array.isArray(parsed) ? parsed : parsed.sessions; if (!Array.isArray(sessions)) return []; return sessions.filter((session) => typeof session?.id === "string" && session.id.length > 0); } catch { storageAvailable = false; return []; } } function writeStoredSessions(sessions) { if (!storageAvailable) return; try { window.localStorage.setItem( SESSION_STORAGE_KEY, JSON.stringify({ version: 1, sessions: sessions.slice(0, MAX_STORED_SESSIONS) }), ); } catch { storageAvailable = false; } } function snapshotJob(job) { return { id: job.id, url: job.url, state: job.state, phase: job.phase, message: job.message, error_message: job.error_message, warnings: Array.isArray(job.warnings) ? job.warnings.slice(0, 5) : [], created_at: job.created_at, updated_at: job.updated_at, started_at: job.started_at, completed_at: job.completed_at, expires_at: job.expires_at, cancel_requested: job.cancel_requested, stats: job.stats || {}, options: job.options || {}, archive: job.archive || {}, }; } function rememberJob(job) { const sessions = readStoredSessions(); const record = { id: job.id, created_at: job.created_at, job: snapshotJob(job) }; const existingIndex = sessions.findIndex((session) => session.id === job.id); if (existingIndex >= 0) sessions[existingIndex] = { ...sessions[existingIndex], ...record }; else sessions.unshift(record); sessions.sort((left, right) => sortByCreated(right.job || right, left.job || left)); writeStoredSessions(sessions); } function forgetJob(jobId) { jobs.delete(jobId); closeEvents(jobId); jobLogs.delete(jobId); jobLogIds.delete(jobId); writeStoredSessions(readStoredSessions().filter((session) => session.id !== jobId)); renderJobs(); } function sortByCreated(left, right) { return String(left.created_at || "").localeCompare(String(right.created_at || "")); } function formatBytes(value) { const amount = Number(value) || 0; if (!amount) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; const index = Math.min(Math.floor(Math.log(amount) / Math.log(1024)), units.length - 1); const rounded = amount / (1024 ** index); return `${rounded >= 10 || index === 0 ? rounded.toFixed(0) : rounded.toFixed(1)} ${units[index]}`; } function formatCount(value) { return new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format( Number(value) || 0, ); } function formatTime(value) { if (!value) return "--"; const date = new Date(value); return Number.isNaN(date.getTime()) ? "--" : date.toLocaleString([], { dateStyle: "medium", timeStyle: "short" }); } function formatLogTime(value) { if (!value) return "--:--:--"; const date = new Date(value); return Number.isNaN(date.getTime()) ? "--:--:--" : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function hostname(url) { try { return new URL(url).hostname; } catch { return url; } } function setMessage(message = "", kind = "") { formMessage.textContent = message; formMessage.className = `form-message ${kind}`.trim(); } function setServiceStatus(message, online = false) { serviceStatus.querySelector("span").textContent = message; serviceStatus.dataset.online = String(online); } function readNumber(control, fallback) { const value = Number.parseInt(control.value, 10); return Number.isFinite(value) ? value : fallback; } function formatDuration(seconds) { const value = Number(seconds) || 0; if (value >= 60 && value % 60 === 0) return `${value / 60} min`; return `${value} sec`; } function updateBudgetSummary() { controls.budgetSummary.textContent = [ `${readNumber(controls.maxPages, 150)} pages`, `${readNumber(controls.maxBytes, 100)} MB`, formatDuration(readNumber(controls.maxDuration, 300)), ].join(" / "); } function updateParallelValue() { controls.parallelValue.value = controls.parallelConnections.value; controls.parallelValue.textContent = controls.parallelConnections.value; } function updateProxySummary() { if (controls.useProxy.disabled) { controls.proxySummary.textContent = "Not configured"; } else if (controls.useProxy.checked) { controls.proxySummary.textContent = "Approved proxy"; } else { controls.proxySummary.textContent = "Direct connection"; } } function setControlLimits(capabilities) { const defaults = capabilities.defaults || {}; const limits = capabilities.limits || {}; const bindings = [ [controls.maxPages, "max_pages"], [controls.maxDepth, "max_depth"], [controls.maxBytes, "max_bytes_mb"], [controls.maxDuration, "max_duration_seconds"], [controls.retentionDays, "max_retention_days"], ]; bindings.forEach(([control, key]) => { if (limits[key] !== undefined) control.max = limits[key]; }); if (limits.max_parallel_connections !== undefined) { controls.parallelConnections.max = limits.max_parallel_connections; } controls.maxPages.value = defaults.max_pages ?? controls.maxPages.value; controls.maxDepth.value = defaults.max_depth ?? controls.maxDepth.value; controls.maxBytes.value = defaults.max_bytes_mb ?? controls.maxBytes.value; controls.maxDuration.value = defaults.max_duration_seconds ?? controls.maxDuration.value; controls.retentionDays.value = defaults.retention_days ?? controls.retentionDays.value; controls.externalAssets.checked = defaults.include_external_assets ?? true; controls.parallelConnections.value = defaults.parallel_connections ?? controls.parallelConnections.value; const selectedTypes = new Set(defaults.download_asset_types || []); controls.assetTypes.forEach((control) => { control.checked = selectedTypes.has(control.dataset.assetType); }); controls.skipFileTypes.value = (defaults.skip_file_types || []).join(", "); controls.useProxy.checked = Boolean(defaults.use_proxy); controls.useProxy.disabled = !capabilities.proxy_available; controls.proxyRow.classList.toggle("is-unavailable", !capabilities.proxy_available); controls.proxyDetail.textContent = capabilities.proxy_available ? "Use the operator-approved route for this capture." : "No operator-approved proxy route is available on this service."; updateBudgetSummary(); updateParallelValue(); updateProxySummary(); } async function request(path, options = {}, { session = true } = {}) { const headers = new Headers(options.headers || {}); if (session) headers.set(OWNER_HEADER, ownerToken); if (options.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json"); const response = await fetch(path, { cache: "no-store", ...options, headers }); const body = response.status === 204 ? null : await response.json().catch(() => null); if (!response.ok) { const error = new Error(body?.detail || "SiteHarbor could not complete that request."); error.status = response.status; throw error; } return body; } function isActive(job) { return ["queued", "running"].includes(job.state); } function upsertJob(job, { persist = true, connect = true } = {}) { jobs.set(job.id, job); if (persist) rememberJob(job); renderJobs(); if (connect && isActive(job)) connectEvents(job.id); else if (!isActive(job)) closeEvents(job.id); } function renderJobs() { const orderedJobs = [...jobs.values()].sort((left, right) => sortByCreated(right, left)); emptyState.hidden = orderedJobs.length > 0; jobGrid.replaceChildren(...orderedJobs.map(createJobCard)); } function createElement(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function jobProgress(job) { if (job.state === "ready") return 100; if (["failed", "cancelled", "expired"].includes(job.state)) return 100; const phase = String(job.phase || "").toLowerCase(); if (phase.includes("packag")) return 94; if (phase.includes("rewrit")) return 88; if (phase.includes("validat") || phase.includes("prepar")) return 6; const maximum = Number(job.options?.max_pages) || 0; const fetched = Number(job.stats?.pages_fetched) || 0; if (!maximum) return 16; return Math.max(10, Math.min(82, Math.round((fetched / maximum) * 82))); } function createJobCard(job) { const card = createElement("article", "job-card"); card.dataset.state = job.state; card.dataset.jobId = job.id; card.classList.toggle("is-active", isActive(job)); const topLine = createElement("div", "job-topline"); topLine.append( createElement("div", "job-host", hostname(job.url)), createElement("span", `state-pill ${job.state}`, job.phase || job.state), ); card.append(topLine); card.append(createElement("p", "job-url", job.url)); card.append(createElement("p", "job-phase", job.error_message || job.message || "Waiting for status")); const progress = createElement("div", "job-progress"); progress.append(createElement("span")); progress.firstElementChild.style.setProperty("--progress", `${jobProgress(job)}%`); card.append(progress); const latestPage = latestPageLog(job.id); const pagesFound = Number(job.stats?.pages_found) || 0; const pagesFetched = Number(job.stats?.pages_fetched) || 0; const queuedPages = Number(latestPage?.detail?.pages_remaining); const pagesRemaining = Number.isFinite(queuedPages) ? queuedPages : Math.max(0, pagesFound - pagesFetched); const stats = createElement("div", "job-stats"); const statItems = [ [pagesFetched, "pages fetched"], [pagesFound, "pages found"], [pagesRemaining, "pages remaining"], [formatBytes(job.stats?.bytes_downloaded || 0), "downloaded"], ]; statItems.forEach(([value, label]) => { const item = document.createElement("div"); item.append(createElement("strong", "", String(value)), createElement("span", "", label)); stats.append(item); }); card.append(stats); const meta = createElement("div", "job-meta"); meta.append( createElement("span", "", `Created ${formatTime(job.created_at)}`), createElement( "span", "", job.archive?.available ? `Archive ${formatBytes(job.archive.size_bytes)}` : `${job.options?.max_pages || "--"} page budget`, ), ); card.append(meta); const tags = createElement("div", "job-tags"); const parallel = job.options?.parallel_connections; if (parallel) tags.append(createElement("span", "", `${parallel} connections`)); const assetsFound = Number(job.stats?.assets_found) || 0; const assetsFetched = Number(job.stats?.assets_fetched) || 0; if (assetsFound || assetsFetched) { tags.append(createElement("span", "", `${assetsFetched}/${assetsFound} assets`)); } tags.append( createElement( "span", "", job.options?.include_external_assets ? "external assets" : "first-party only", ), ); if (job.options?.skip_file_types?.length) { tags.append(createElement("span", "", `skip .${job.options.skip_file_types[0]}`)); } card.append(tags); if (job.warnings?.length) card.append(createElement("p", "job-warning", job.warnings[0])); if (isActive(job)) card.append(createJobLog(job.id)); const actions = createElement("div", "job-actions"); if (job.archive?.available) { const download = createElement("button", "job-action primary", "Download ZIP"); download.type = "button"; download.addEventListener("click", () => downloadArchive(job, download)); actions.append(download); const report = createElement("button", "job-action", "View report"); report.type = "button"; report.addEventListener("click", () => showReport(job.id)); actions.append(report); const remove = createElement("button", "job-action danger", "Delete archive"); remove.type = "button"; remove.addEventListener("click", () => deleteJob(job.id)); actions.append(remove); } if (isActive(job)) { const cancel = createElement("button", "job-action danger", job.cancel_requested ? "Cancelling" : "Cancel"); cancel.type = "button"; cancel.disabled = Boolean(job.cancel_requested); cancel.addEventListener("click", () => cancelJob(job.id)); actions.append(cancel); } if (!isActive(job) && !job.archive?.available) { const forget = createElement("button", "job-action", "Forget session"); forget.type = "button"; forget.addEventListener("click", () => forgetJob(job.id)); actions.append(forget); } card.append(actions); return card; } function latestPageLog(jobId) { const entries = jobLogs.get(jobId) || []; return [...entries].reverse().find((entry) => entry.detail?.kind === "page") || null; } function createJobLog(jobId) { const section = createElement("section", "job-log"); const heading = createElement("div", "job-log-heading"); heading.append( createElement("span", "job-log-title", "Session log"), createElement("span", "job-log-count", `${(jobLogs.get(jobId) || []).length} events`), ); const list = createElement("ol", "job-log-list"); const entries = jobLogs.get(jobId) || []; if (entries.length) entries.forEach((entry) => list.append(createLogEntry(entry))); else list.append(createElement("li", "job-log-empty", "Waiting for worker activity...")); section.append(heading, list); return section; } function createLogEntry(entry) { const item = createElement("li", "job-log-entry"); const detail = entry.detail || {}; item.dataset.kind = detail.kind || entry.kind; item.append(createElement("time", "job-log-time", formatLogTime(entry.logged_at))); if (detail.kind === "page") { const action = createElement("span", "job-log-action", `page ${detail.action || "seen"}`); const url = createElement("span", "job-log-url", String(detail.url || "unknown page")); const queue = createElement( "span", "job-log-queue", `queue ${detail.pages_remaining ?? 0}p / ${detail.assets_remaining ?? 0}a`, ); item.append(action, url, queue); return item; } item.append( createElement("span", "job-log-action", String(entry.phase || entry.kind)), createElement("span", "job-log-url", String(entry.message || "Worker update")), ); return item; } function recordStreamEvent(jobId, kind, event) { let payload; try { payload = JSON.parse(event.data); } catch { return; } if (kind === "progress" && !payload.detail) return; const eventId = event.lastEventId || `${kind}:${payload.logged_at}:${payload.message}`; const seenIds = jobLogIds.get(jobId) || new Set(); if (seenIds.has(eventId)) return; seenIds.add(eventId); jobLogIds.set(jobId, seenIds); const entries = jobLogs.get(jobId) || []; const entry = { id: eventId, kind, ...payload }; entries.push(entry); jobLogs.set(jobId, entries); appendLogEntry(jobId, entry, entries.length); } function appendLogEntry(jobId, entry, count) { const card = jobGrid.querySelector(`[data-job-id="${jobId}"]`); const list = card?.querySelector(".job-log-list"); if (!list) return; const followTail = list.scrollTop + list.clientHeight >= list.scrollHeight - 16; list.querySelector(".job-log-empty")?.remove(); list.append(createLogEntry(entry)); const counter = card.querySelector(".job-log-count"); if (counter) counter.textContent = `${count} events`; if (followTail) list.scrollTop = list.scrollHeight; } function scheduleJobRefresh(jobId) { if (refreshTimers.has(jobId)) return; const timer = window.setTimeout(() => { refreshTimers.delete(jobId); void refreshJob(jobId); }, 350); refreshTimers.set(jobId, timer); } function connectEvents(jobId) { if (streams.has(jobId)) return; const eventUrl = `/api/captures/${encodeURIComponent(jobId)}/events?session=${encodeURIComponent(ownerToken)}`; const stream = new EventSource(eventUrl); streams.set(jobId, stream); stream.addEventListener("state", (event) => { recordStreamEvent(jobId, "state", event); void refreshJob(jobId); }); stream.addEventListener("progress", (event) => { recordStreamEvent(jobId, "progress", event); scheduleJobRefresh(jobId); }); stream.addEventListener("terminal", () => { closeEvents(jobId); void refreshJob(jobId); void loadPublicStats(); }); stream.onerror = () => { window.setTimeout(() => { if (streams.get(jobId) === stream) void refreshJob(jobId); }, 1_000); }; } function closeEvents(jobId) { const stream = streams.get(jobId); if (stream) stream.close(); streams.delete(jobId); const timer = refreshTimers.get(jobId); if (timer) window.clearTimeout(timer); refreshTimers.delete(jobId); } async function refreshJob(jobId) { try { upsertJob(await request(`/api/captures/${encodeURIComponent(jobId)}`)); } catch (error) { closeEvents(jobId); if (error.status === 404) forgetJob(jobId); } } async function refreshKnownJobs() { const storedIds = readStoredSessions().map((session) => session.id); const knownIds = [...new Set([...storedIds, ...jobs.keys()])]; await Promise.all(knownIds.map((jobId) => refreshJob(jobId))); } async function loadStoredJobs() { const sessions = readStoredSessions(); sessions.forEach((session) => { if (session.job?.id && !jobs.has(session.id)) jobs.set(session.id, session.job); }); renderJobs(); await refreshKnownJobs(); } async function loadPublicStats() { try { const stats = await request("/api/stats", {}, { session: false }); publicStats.websites.textContent = formatCount(stats.websites_cloned); publicStats.data.textContent = formatBytes(stats.bytes_scraped); publicStats.active.textContent = formatCount(stats.active_crawls); publicStats.files.textContent = formatCount(stats.files_archived); setServiceStatus("Capture service online", true); } catch { setServiceStatus("Service unavailable", false); } } async function cancelJob(jobId) { const current = jobs.get(jobId); if (current && !current.cancel_requested) { upsertJob( { ...current, cancel_requested: true, phase: "Cancelling", message: "Cancellation requested", }, { persist: false, connect: false }, ); } try { upsertJob(await request(`/api/captures/${encodeURIComponent(jobId)}/cancel`, { method: "POST" })); void loadPublicStats(); } catch (error) { if (current) upsertJob(current); setMessage(error.message); } } async function deleteJob(jobId) { try { const updated = await request(`/api/captures/${encodeURIComponent(jobId)}`, { method: "DELETE" }); if (updated.state === "expired") forgetJob(jobId); else upsertJob(updated); void loadPublicStats(); } catch (error) { setMessage(error.message); } } async function downloadArchive(job, button) { if (!job.archive?.download_url) return; const originalText = button.textContent; button.disabled = true; button.textContent = "Preparing download"; try { const response = await fetch(job.archive.download_url, { headers: { [OWNER_HEADER]: ownerToken }, }); if (!response.ok) { const body = await response.json().catch(() => null); const error = new Error(body?.detail || "The archive could not be downloaded."); error.status = response.status; throw error; } const objectUrl = URL.createObjectURL(await response.blob()); const anchor = document.createElement("a"); anchor.href = objectUrl; anchor.download = `siteharbor-${job.id}.zip`; document.body.append(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1_000); } catch (error) { setMessage(error.message); } finally { button.disabled = false; button.textContent = originalText; } } async function showReport(jobId) { try { const report = await request(`/api/captures/${encodeURIComponent(jobId)}/report`); renderReport(report); reportDialog.showModal(); } catch (error) { setMessage(error.message); } } function renderReport(report) { reportContent.replaceChildren(); const summary = createElement("div", "report-summary"); const stats = report.stats || {}; [ [stats.pages_fetched || 0, "pages"], [stats.assets_fetched || 0, "assets"], [formatBytes(stats.bytes_downloaded || 0), "downloaded"], [report.quality || "unknown", "capture quality"], ].forEach(([value, label]) => { const item = document.createElement("div"); item.append(createElement("strong", "", String(value)), createElement("span", "", label)); summary.append(item); }); reportContent.append(summary); const details = createElement("ul", "report-list"); [ `Entry point: ${report.entry_point || "not available"}`, `Capture duration: ${report.elapsed_seconds || 0} seconds`, `Captured resources: ${(report.resources || []).length}`, ].forEach((line) => details.append(createElement("li", "", line))); reportContent.append(details); if (report.warnings?.length) { reportContent.append(createElement("h3", "report-subhead", "Warnings")); const warnings = createElement("ul", "report-list"); report.warnings.forEach((warning) => warnings.append(createElement("li", "", warning))); reportContent.append(warnings); } if (report.errors?.length) { reportContent.append(createElement("h3", "report-subhead", "Resource failures")); const errors = createElement("ul", "report-list"); report.errors.slice(0, 20).forEach((error) => { errors.append(createElement("li", "", `${error.url}: ${error.message}`)); }); reportContent.append(errors); } } function selectedAssetTypes() { return controls.assetTypes .filter((control) => control.checked) .map((control) => control.dataset.assetType); } function skippedFileTypes() { return controls.skipFileTypes.value .split(",") .map((item) => item.trim()) .filter(Boolean); } form.addEventListener("submit", async (event) => { event.preventDefault(); const url = urlInput.value.trim(); if (!url) { setMessage("Enter a public website URL to begin."); urlInput.focus(); return; } setMessage(""); submitButton.disabled = true; submitButton.querySelector("span").textContent = "Preparing archive"; try { const job = await request("/api/captures", { method: "POST", body: JSON.stringify({ url, include_external_assets: controls.externalAssets.checked, parallel_connections: readNumber(controls.parallelConnections, 6), download_asset_types: selectedAssetTypes(), skip_file_types: skippedFileTypes(), use_proxy: controls.useProxy.checked && !controls.useProxy.disabled, max_pages: readNumber(controls.maxPages, 150), max_depth: readNumber(controls.maxDepth, 5), max_bytes_mb: readNumber(controls.maxBytes, 100), max_duration_seconds: readNumber(controls.maxDuration, 300), retention_days: readNumber(controls.retentionDays, 2), }), }); upsertJob(job); setMessage("Archive queued. This browser will reconnect to it automatically.", "success"); urlInput.value = ""; void loadPublicStats(); } catch (error) { setMessage(error.message); } finally { submitButton.disabled = false; submitButton.querySelector("span").textContent = "Start archive"; } }); [ controls.maxPages, controls.maxDepth, controls.maxBytes, controls.maxDuration, controls.retentionDays, ].forEach((control) => control.addEventListener("input", updateBudgetSummary)); controls.parallelConnections.addEventListener("input", updateParallelValue); controls.useProxy.addEventListener("change", updateProxySummary); refreshButton.addEventListener("click", async () => { refreshButton.disabled = true; try { await Promise.all([refreshKnownJobs(), loadPublicStats()]); } finally { refreshButton.disabled = false; } }); closeReportButton.addEventListener("click", () => reportDialog.close()); reportDialog.addEventListener("click", (event) => { if (event.target === reportDialog) reportDialog.close(); }); document.addEventListener("visibilitychange", () => { if (!document.hidden) { void refreshKnownJobs(); void loadPublicStats(); } }); window.addEventListener("online", () => { void refreshKnownJobs(); void loadPublicStats(); }); window.addEventListener("beforeunload", () => { streams.forEach((stream) => stream.close()); }); async function initialize() { if (!storageAvailable) { setMessage("Browser storage is unavailable, so this session cannot be restored after this tab closes."); } try { setControlLimits(await request("/api/capabilities", {}, { session: false })); } catch (error) { setMessage(error.message); } await Promise.all([loadStoredJobs(), loadPublicStats()]); window.setInterval(() => { void refreshKnownJobs(); void loadPublicStats(); }, 15_000); } void initialize();