// Part 5 of 15 of the operator console app script.
//
// Queue list, import status, and the case review shell.
//
// 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.
// Shown wherever a retained case's image would render. `assetMissing` is set by
// the reload sweep when a decided case's source file has been moved out of the
// queue folder: the decision is kept, the file is gone, and the operator needs
// to be told which of the two happened.
const MISSING_ASSET_NOTE = "원본 파일 없음 (판정 기록 보존)";
function renderQueue() {
const rows = filteredSubmissions();
const queueLabel = state.submissionQueue?.label || "";
const queueMeta = queueLabel ? ` (${escapeHtml(queueLabel)})` : "";
const body = document.getElementById("queue-body");
document.getElementById("queue-health").innerHTML = state.apiError
? `
${escapeHtml(state.apiError)}
`
: `
${rows.length}건 / 고위험 ${submissions.filter((item) => item.riskBand === "high").length}건${queueMeta}
`;
if (!rows.length) {
body.innerHTML = `
`;
document.getElementById("recommendation-box").innerHTML = `실제 API 데이터가 로드될 때까지 판정하지 않습니다.`;
document.getElementById("derived-preview").innerHTML = `자동 후보 생성 없음`;
// Clear the evidence-gap recommendations too: they belong to the case that was
// just deselected, and leaving them rendered invited running suggested queries
// against nothing.
const nextActions = document.getElementById("evidence-next-actions");
if (nextActions) nextActions.innerHTML = "";
const llmDraft = document.getElementById("llm-draft");
if (llmDraft) {
llmDraft.hidden = true;
llmDraft.innerHTML = "";
}
}
function renderCaseReview() {
const submission = getSelectedCase();
if (!submission) {
renderNoSelectedCase();
return;
}
renderLlmDraft(submission);
document.getElementById("case-title").textContent = `${submission.id} · ${submission.title}`;
const caseImageElement = document.getElementById("case-image");
caseImageElement.src = safeUrl(submission.asset);
caseImageElement.alt = submission.assetMissing
? `${submission.id} 제출 이미지 - ${MISSING_ASSET_NOTE}`
: `${submission.id} 제출 이미지`;
caseImageElement.classList.toggle("case-image-missing", Boolean(submission.assetMissing));
// Reset the view controls (rotate / fit) so a new case starts unrotated.
caseImageElement.dataset.rotation = "0";
caseImageElement.style.transform = "";
caseImageElement.style.objectFit = "";
// Without this the retained case is a broken with no explanation, and
// the operator cannot tell "file archived after the decision" from "upload
// corrupted". The decision survives in the DB either way; this is what makes
// it readable in the console.
document.getElementById("image-derivative-note").textContent = submission.assetMissing
? `${submission.derivativeNote} · ${MISSING_ASSET_NOTE}`
: submission.derivativeNote;
document.getElementById("case-score").className = `score-pill ${riskClass(submission.riskBand)}`;
document.getElementById("case-score").textContent = `${riskLabels[submission.riskBand]} ${submission.riskScore}`;
document.getElementById("floating-case-score").className = `score-pill ${riskClass(submission.riskBand)}`;
document.getElementById("floating-case-score").textContent = `${riskLabels[submission.riskBand]} ${submission.riskScore}`;
document.getElementById("file-facts").innerHTML = Object.entries(submission.fileFacts)
.map(([label, value]) => `