// Part 2 of 15 of the operator console app script. // // Auth token prompt, fetch wrapper, and bootstrap/refresh of client state. // // Load order is significant: these are classic (non-module) scripts sharing one // global scope, and index.html must keep them in the order listed above/below. // Splitting is a pure line-range extraction of the former single app.js. function promptForAuthToken(message = "") { const gate = document.getElementById("auth-gate"); const form = document.getElementById("auth-gate-form"); const input = document.getElementById("auth-gate-input"); const error = document.getElementById("auth-gate-error"); if (!gate || !form || !input) { return Promise.resolve(""); } // Collapse concurrent 401s onto one prompt so a burst of parallel requests // cannot stack multiple dialogs. if (pendingAuthPrompt) { return pendingAuthPrompt; } if (error) { error.hidden = !message; error.textContent = message || ""; } gate.hidden = false; input.value = ""; input.focus(); pendingAuthPrompt = new Promise((resolve) => { const onSubmit = (event) => { event.preventDefault(); const value = input.value.trim(); if (!value) { return; } form.removeEventListener("submit", onSubmit); gate.hidden = true; input.value = ""; pendingAuthPrompt = null; resolve(value); }; form.addEventListener("submit", onSubmit); }); return pendingAuthPrompt; } function authHeaders(extra) { const headers = { "Content-Type": "application/json", ...(extra || {}) }; if (authToken) { headers.Authorization = `Bearer ${authToken}`; } return headers; } async function apiJson(path, options = {}, allowAuthRetry = true) { const response = await fetch(path, { ...options, headers: authHeaders(options.headers), }); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch (error) { payload = null; } } if (response.status === 401 && allowAuthRetry) { // Ask for the token, then replay this request once. A second 401 falls // through to the normal error path instead of looping. const token = await promptForAuthToken( authToken ? "토큰이 올바르지 않습니다. 다시 입력하세요." : "", ); if (token) { authToken = token; return apiJson(path, options, false); } } if (!response.ok) { const message = (payload && payload.error) || `API 요청 실패: ${response.status}`; throw new Error(message); } return payload; } function replaceCollection(target, next) { target.splice(0, target.length, ...(next || [])); } function clearRuntimeData() { replaceCollection(submissions, []); replaceCollection(providers, []); replaceCollection(knowledgeEntries, []); replaceCollection(collectionCandidates, []); replaceCollection(corrections, []); replaceCollection(auditEvents, []); Object.assign(coverageThresholds, DEFAULT_COVERAGE_THRESHOLDS); Object.keys(searchCoverage).forEach((key) => delete searchCoverage[key]); state.submissionQueue = null; state.selectedCaseId = ""; } function applyBootstrap(payload) { // Any successful payload proves the API is reachable, so clear a stale error // banner here (not only in refreshFromApi) — actions that applyBootstrap // directly, e.g. promote/dismiss, otherwise leave the old error visible. state.apiError = ""; replaceCollection(submissions, payload.submissions); replaceCollection(providers, payload.providers); replaceCollection(knowledgeEntries, payload.knowledgeEntries); state.submissionQueue = payload.submissionQueue || null; Object.assign( coverageThresholds, normalizeCoverageThresholds(payload.coverageThresholds || (payload.searchCoverage && payload.searchCoverage.coverageThresholds)), ); Object.keys(searchCoverage).forEach((key) => delete searchCoverage[key]); Object.assign(searchCoverage, payload.searchCoverage || {}); replaceCollection(collectionCandidates, payload.collectionCandidates); replaceCollection(corrections, payload.corrections); replaceCollection(auditEvents, payload.auditEvents); replaceCollection(decisionMemoPresets, payload.decisionMemoPresets || []); if (!submissions.some((item) => item.id === state.selectedCaseId)) { state.selectedCaseId = submissions[0]?.id || ""; } } async function refreshFromApi() { const payload = await apiJson("/api/bootstrap"); state.apiError = ""; applyBootstrap(payload); renderAll(); } function setImportStatus(message, tone = "idle") { state.importStatus = { message, tone }; renderImportStatus(); }