const config = window.MASTERMAP_CONFIG || {}; const cleanPath = config.cleanPath || ""; let applyWorkbookPath = config.applyWorkbookPath || ""; let applyBlueprintPath = config.applyBlueprintPath || ""; const defaultOutputSheet = config.defaultOutputSheet || "Cleaned_Data"; const sheetSelect = document.getElementById("sheetSelect"); const applySheetSelect = document.getElementById("applySheetSelect"); const outputSheet = document.getElementById("outputSheet"); const models = document.getElementById("models"); const fetchModels = document.getElementById("fetchModels"); const runButton = document.getElementById("runButton"); const applyButton = document.getElementById("applyButton"); const saveReferencesButton = document.getElementById("saveReferencesButton"); const applyWorkbookForm = document.getElementById("applyWorkbookForm"); const applyBlueprintForm = document.getElementById("applyBlueprintForm"); const applyWorkbookInput = document.getElementById("applyWorkbookInput"); const applyBlueprintInput = document.getElementById("applyBlueprintInput"); const runStatus = document.getElementById("runStatus"); const applyStatus = document.getElementById("applyStatus"); const referencesStatus = document.getElementById("referencesStatus"); const applyWorkbookFile = document.getElementById("applyWorkbookFile"); const applyBlueprintFile = document.getElementById("applyBlueprintFile"); const cleanLogs = document.getElementById("cleanLogs"); const applyLogs = document.getElementById("applyLogs"); const cleanProgressPanel = document.getElementById("cleanProgressPanel"); const cleanProgressSummary = document.getElementById("cleanProgressSummary"); const cleanProgressList = document.getElementById("cleanProgressList"); const cleanResult = document.getElementById("cleanResult"); const applyResult = document.getElementById("applyResult"); let cleanRawLogText = ""; let cleanLiveLine = ""; let cleanProgressOrder = []; let cleanProgressByColumn = {}; let applyRawLogText = ""; let applyLiveLine = ""; let activeRunStream = null; let activeRunJobId = ""; let stopRequested = false; function submitUploadForm(formId, statusId, event) { if (event) { event.preventDefault(); } const form = document.getElementById(formId); const status = document.getElementById(statusId); [ ["clean_selected_sheet", sheetSelect ? sheetSelect.value : ""], ["output_sheet", outputSheet ? outputSheet.value : ""], ["models", models ? models.value : ""], ["apply_selected_sheet", applySheetSelect ? applySheetSelect.value : ""] ].forEach(([name, value]) => { let input = form.querySelector(`input[name="${name}"]`); if (!input) { input = document.createElement("input"); input.type = "hidden"; input.name = name; form.appendChild(input); } input.value = value; }); status.textContent = "Loading..."; if (formId === "applyWorkbookForm" || formId === "applyBlueprintForm") { uploadApplyFile(form, status, formId); return; } form.submit(); } async function uploadApplyFile(form, status, formId) { try { const res = await fetch(form.action, { method: "POST", body: new FormData(form), headers: { "Accept": "application/json" } }); const data = await res.json(); if (!res.ok) { status.textContent = data.error || "Upload failed."; status.classList.add("error"); return; } status.classList.remove("error"); status.textContent = data.message || "Loaded."; if (formId === "applyWorkbookForm") { applyWorkbookPath = data.apply_workbook_path || ""; applyWorkbookFile.innerHTML = data.apply_workbook_filename ? `
${escapeHtml(data.apply_workbook_filename)}
` : ""; } if (formId === "applyBlueprintForm") { applyBlueprintPath = data.apply_blueprint_path || ""; applyBlueprintFile.innerHTML = data.apply_blueprint_filename ? `
${escapeHtml(data.apply_blueprint_filename)}
` : ""; } if (data.apply_sheets) { setApplySheets(data.apply_sheets, data.apply_selected_sheet); } applyButton.disabled = !(applyWorkbookPath && applyBlueprintPath && applySheetSelect.value); } catch (error) { status.textContent = "Upload failed."; status.classList.add("error"); } } function clearCleanOutput() { cleanRawLogText = ""; cleanLiveLine = ""; cleanProgressOrder = []; cleanProgressByColumn = {}; cleanLogs.textContent = ""; cleanProgressPanel.classList.remove("active"); cleanProgressSummary.textContent = "Waiting..."; cleanProgressList.innerHTML = ""; cleanResult.classList.remove("active"); cleanResult.innerHTML = ""; } function clearApplyOutput() { applyRawLogText = ""; applyLiveLine = ""; applyLogs.textContent = ""; applyResult.classList.remove("active"); applyResult.innerHTML = ""; } function parseProgressLine(line) { const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "").trim(); const match = cleanLine.match(/^Cleaning\s+(.+?):\s+(\d+)%\|.*?\|\s+(\d+)\/(\d+)\s+\[([^\]]*)\]/); if (!match) return null; const columnName = match[1].trim(); const percent = Number(match[2]); const current = match[3]; const total = match[4]; const bracketParts = match[5].split(",").map(part => part.trim()).filter(Boolean); const timingParts = bracketParts.slice(0, 2); const metricParts = bracketParts.slice(2); const timing = timingParts.join(", ").includes("?") ? "estimating..." : timingParts.join(", "); const metrics = metricParts.join(", "); return { columnName, percent, current, total, timing, metrics }; } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } function downloadUrl(path, filename, endpoint) { const params = new URLSearchParams({ path }); if (filename) { params.set("filename", filename); } return `${endpoint}?${params.toString()}`; } function renderCleanProgressRows() { cleanProgressPanel.classList.add("active"); cleanProgressSummary.textContent = `${cleanProgressOrder.length} column${cleanProgressOrder.length === 1 ? "" : "s"}`; cleanProgressList.innerHTML = cleanProgressOrder.map(columnName => { const item = cleanProgressByColumn[columnName]; const percent = Math.max(0, Math.min(100, item.percent)); const meta = `${item.current}/${item.total}${item.timing ? " | " + item.timing : ""}${item.metrics ? " | " + item.metrics : ""}`; return `
${escapeHtml(columnName)} ${percent}%
${escapeHtml(meta)}
`; }).join(""); } function renderCleanProgressLine(line) { const parsed = parseProgressLine(line); if (!parsed) return false; if (!cleanProgressByColumn[parsed.columnName]) { cleanProgressOrder.push(parsed.columnName); } cleanProgressByColumn[parsed.columnName] = parsed; renderCleanProgressRows(); return true; } function appendCleanLogChunk(chunk) { for (const char of chunk) { cleanRawLogText += char === "\r" ? "\n" : char; if (char === "\r") { renderCleanProgressLine(cleanLiveLine); cleanLiveLine = ""; } else if (char === "\n") { renderCleanProgressLine(cleanLiveLine); cleanLiveLine = ""; } else { cleanLiveLine += char; } } const isPartialProgress = cleanLiveLine.startsWith("Cleaning ") && cleanLiveLine.includes("|"); if (!isPartialProgress) { renderCleanProgressLine(cleanLiveLine); } cleanLogs.textContent = cleanRawLogText; cleanLogs.scrollTop = cleanLogs.scrollHeight; } function renderApplySummary() { const changed = applyRawLogText.match(/Success!\s+(\d+)\s+corrections injected/i); const added = applyRawLogText.match(/Memory updated:\s+(\d+)\s+new approved values added/i); if (!changed && !added) return; applyResult.classList.add("active"); applyResult.innerHTML = ` Blueprint applied
${changed ? changed[1] : "0"} workbook row value${changed && changed[1] === "1" ? "" : "s"} updated from human overrides.
${added ? added[1] : "0"} new unique reference value${added && added[1] === "1" ? "" : "s"} added to manual references.
Download Cleaned Workbook `; } function appendApplyLogChunk(chunk) { for (const char of chunk) { applyRawLogText += char === "\r" ? "\n" : char; if (char === "\r" || char === "\n") { applyLiveLine = ""; } else { applyLiveLine += char; } } applyLogs.textContent = applyRawLogText; applyLogs.scrollTop = applyLogs.scrollHeight; renderApplySummary(); } function clearPersistedOutputs() { [ "mastermap.cleanRawLogText", "mastermap.applyRawLogText", "mastermap.cleanResultHtml", "mastermap.applyResultHtml", "mastermap.cleanResultActive", "mastermap.applyResultActive" ].forEach(key => localStorage.removeItem(key)); } function setRunButtonIdle() { activeRunStream = null; activeRunJobId = ""; stopRequested = false; runButton.disabled = false; runButton.textContent = "Run Cleaning"; runButton.classList.remove("danger"); } async function stopActiveRun() { if (!activeRunJobId) return; stopRequested = true; runButton.disabled = true; runButton.textContent = "Stopping..."; runStatus.textContent = "Stopping run..."; await fetch(`/stop?job_id=${encodeURIComponent(activeRunJobId)}`, { method: "POST" }); } function selectApplySheet(sheetName) { if (!sheetName) return; const existing = Array.from(applySheetSelect.options).some(option => option.value === sheetName); if (!existing) { const option = document.createElement("option"); option.value = sheetName; option.textContent = sheetName; applySheetSelect.appendChild(option); } applySheetSelect.value = sheetName; applySheetSelect.disabled = false; } function setApplySheets(sheets, preferredSheet) { const selected = preferredSheet || applySheetSelect.value; applySheetSelect.innerHTML = ""; sheets.forEach(sheetName => { const option = document.createElement("option"); option.value = sheetName; option.textContent = sheetName; applySheetSelect.appendChild(option); }); if (selected && sheets.includes(selected)) { applySheetSelect.value = selected; } else if (sheets.length) { applySheetSelect.value = sheets[0]; } applySheetSelect.disabled = sheets.length === 0; } async function refreshApplySheets(preferredSheet) { if (!applyWorkbookPath) return selectApplySheet(preferredSheet); const params = new URLSearchParams({ path: applyWorkbookPath }); const res = await fetch(`/sheets?${params.toString()}`); if (!res.ok) { selectApplySheet(preferredSheet); return; } const data = await res.json(); setApplySheets(data.sheets || [], preferredSheet); } async function refreshReferenceSyncStatus(updateText = true) { try { const res = await fetch("/references/status"); const data = await res.json(); saveReferencesButton.disabled = !data.enabled; if (updateText) { referencesStatus.textContent = data.enabled ? `Ready to save manual references to ${data.space_id}.` : data.reason || "Reference sync is unavailable."; } } catch (error) { saveReferencesButton.disabled = true; if (updateText) { referencesStatus.textContent = "Reference sync status unavailable."; } } } clearPersistedOutputs(); refreshReferenceSyncStatus(); applyWorkbookForm.addEventListener("submit", event => { submitUploadForm("applyWorkbookForm", "applyWorkbookUploadStatus", event); }); applyBlueprintForm.addEventListener("submit", event => { submitUploadForm("applyBlueprintForm", "applyBlueprintUploadStatus", event); }); applyWorkbookInput.addEventListener("change", event => { submitUploadForm("applyWorkbookForm", "applyWorkbookUploadStatus", event); }); applyBlueprintInput.addEventListener("change", event => { submitUploadForm("applyBlueprintForm", "applyBlueprintUploadStatus", event); }); fetchModels.addEventListener("click", async () => { fetchModels.disabled = true; runStatus.textContent = "Fetching Groq models..."; const res = await fetch("/models"); const data = await res.json(); fetchModels.disabled = false; if (!res.ok) { runStatus.textContent = data.error || "Could not fetch models"; return; } models.value = data.models.join(","); runStatus.textContent = "Model list updated."; }); saveReferencesButton.addEventListener("click", async () => { saveReferencesButton.disabled = true; referencesStatus.textContent = "Saving manual references..."; try { const res = await fetch("/references/save", { method: "POST" }); const data = await res.json(); if (!res.ok) { referencesStatus.textContent = data.error || "Could not save manual references."; await refreshReferenceSyncStatus(); return; } referencesStatus.textContent = data.message || "Manual references saved."; } catch (error) { referencesStatus.textContent = "Could not save manual references."; } finally { await refreshReferenceSyncStatus(false); } }); runButton.addEventListener("click", () => { if (activeRunStream) { stopActiveRun(); return; } if (!cleanPath || !sheetSelect.value) return; clearCleanOutput(); activeRunJobId = window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : String(Date.now()); stopRequested = false; runButton.disabled = false; runButton.textContent = "Stop Cleaning"; runButton.classList.add("danger"); runStatus.textContent = "Running..."; const params = new URLSearchParams({ job_id: activeRunJobId, input: cleanPath, sheet: sheetSelect.value, output_sheet: outputSheet.value || defaultOutputSheet, models: models.value.trim() }); const generatedBlueprintPath = `data/uploads/Blueprint_${activeRunJobId}.xlsx`; const stream = new EventSource(`/run?${params.toString()}`); activeRunStream = stream; stream.onmessage = event => appendCleanLogChunk(JSON.parse(event.data)); stream.addEventListener("done", async () => { stream.close(); if (!stopRequested) { applyBlueprintPath = generatedBlueprintPath; const targetSheet = outputSheet.value || defaultOutputSheet; await refreshApplySheets(targetSheet); applyButton.disabled = !(applyWorkbookPath && applyBlueprintPath && applySheetSelect.value); applyStatus.textContent = "Generated blueprint is ready for Apply Blueprint."; cleanResult.classList.add("active"); cleanResult.innerHTML = ` Blueprint generated
Blueprint saved for this session.
Download Blueprint Download Cleaned Workbook `; runStatus.textContent = "Finished."; } else { runStatus.textContent = "Stopped."; } setRunButtonIdle(); }); stream.addEventListener("failed", () => { stream.close(); setRunButtonIdle(); runStatus.textContent = "Run failed. Check logs."; }); stream.addEventListener("error", () => { stream.close(); setRunButtonIdle(); runStatus.textContent = "Run stopped. Check logs."; }); }); applyButton.addEventListener("click", () => { if (!applyWorkbookPath || !applyBlueprintPath || !applySheetSelect.value) return; clearApplyOutput(); applyButton.disabled = true; applyStatus.textContent = "Applying blueprint..."; const params = new URLSearchParams({ input: applyWorkbookPath, blueprint: applyBlueprintPath, sheet: applySheetSelect.value }); const stream = new EventSource(`/apply?${params.toString()}`); stream.onmessage = event => appendApplyLogChunk(JSON.parse(event.data)); stream.addEventListener("done", () => { stream.close(); applyButton.disabled = false; applyStatus.textContent = "Finished."; renderApplySummary(); }); stream.addEventListener("failed", () => { stream.close(); applyButton.disabled = false; applyStatus.textContent = "Apply failed. Check logs."; }); stream.addEventListener("error", () => { stream.close(); applyButton.disabled = false; applyStatus.textContent = "Apply stopped. Check logs."; }); });