FHIRFLARE-IG-Toolkit/templates/retrieve_split_data.html
Sudo-JHare ca8668e5e5 V2.0 release.
Hapi Config Page.
retrieve bundles from server
split bundles to resources.
new upload page, and Package Registry CACHE
2025-05-04 22:55:36 +10:00

580 lines
32 KiB
HTML

{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<!-- <img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192"> -->
<h1 class="display-5 fw-bold text-body-emphasis">Retrieve & Split Data</h1>
<div class="col-lg-8 mx-auto">
<p class="lead mb-4">
Retrieve FHIR bundles from a server, then download as a ZIP file. Split uploaded or retrieved bundle ZIPs into individual resources, downloaded as a ZIP file.
</p>
</div>
</div>
<div class="container mt-4">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-cloud-download me-2"></i>Retrieve Bundles</h4>
</div>
<div class="card-body">
{% if form.errors %}
<div class="alert alert-danger">
<p><strong>Please correct the following errors:</strong></p>
<ul>
{% for field, errors in form.errors.items() %}
<li>{{ form[field].label.text }}: {{ errors|join(', ') }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form id="retrieveForm" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label fw-bold">FHIR Server</label>
<div class="input-group">
<button type="button" class="btn btn-outline-primary" id="toggleServer">
<span id="toggleLabel">Use Local HAPI</span>
</button>
{# Ensure the input field uses the form object's data #}
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
</div>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (/fhir proxy) or enter a custom FHIR server URL.</small>
</div>
{# --- Checkbox Row --- #}
<div class="row g-3 mb-3 align-items-center">
<div class="col-md-6">
{# Render Fetch Referenced Resources checkbox using the macro #}
{{ render_field(form.validate_references, id='validate_references_checkbox') }}
</div>
<div class="col-md-6" id="fetchReferenceBundlesGroup" style="display: none;"> {# Initially hidden #}
{# Render NEW Fetch Full Reference Bundles checkbox using the macro #}
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
</div>
</div>
{# --- End Checkbox Row --- #}
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
<h5>Resource Types</h5>
<div class="d-flex flex-wrap gap-2" id="resourceButtons"></div>
</div>
{# Render SubmitField using the form object #}
<button type="submit" class="btn btn-primary btn-lg w-100" id="retrieveButton" name="submit_retrieve">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
<i class="bi bi-arrow-down-circle me-2"></i>{{ form.submit_retrieve.label.text }}
</button>
<button type="button" class="btn btn-success btn-lg w-100 mt-2" id="downloadRetrieveButton" style="display: none;">
<i class="bi bi-download me-2"></i>Download Retrieved Bundles
</button>
</form>
<div class="card mt-4">
<div class="card-header">Retrieval Log</div>
<div class="card-body">
<div id="retrieveConsole" class="border p-3 rounded bg-dark text-light" style="height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
<span class="text-muted">Retrieval output will appear here...</span>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
</div>
<div class="card-body">
<form id="splitForm" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label fw-bold">Bundle Source</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="bundle_source" id="uploadBundles" value="upload" checked>
<label class="form-check-label" for="uploadBundles">Upload Bundle ZIP</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="bundle_source" id="useRetrievedBundles" value="retrieved" {% if not session.get('retrieve_params', {}).get('bundle_zip_path', None) %} disabled {% endif %}>
<label class="form-check-label" for="useRetrievedBundles">Use Retrieved Bundles{% if not session.get('retrieve_params', {}).get('bundle_zip_path', None) %} (No bundles retrieved in this session){% endif %}</label>
</div>
</div>
{# Render FileField using the macro #}
{{ render_field(form.split_bundle_zip, class="form-control") }}
{# Render SubmitField using the form object #}
<button type="submit" class="btn btn-primary btn-lg w-100" id="splitButton" name="submit_split">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
<i class="bi bi-arrow-right-circle me-2"></i>{{ form.submit_split.label.text }}
</button>
<button type="button" class="btn btn-success btn-lg w-100 mt-2" id="downloadSplitButton" style="display: none;">
<i class="bi bi-download me-2"></i>Download Split Resources
</button>
</form>
<div class="card mt-4">
<div class="card-header">Splitting Log</div>
<div class="card-body">
<div id="splitConsole" class="border p-3 rounded bg-dark text-light" style="height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
<span class="text-muted">Splitting output will appear here...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
const retrieveForm = document.getElementById('retrieveForm');
const splitForm = document.getElementById('splitForm');
const retrieveButton = document.getElementById('retrieveButton');
const splitButton = document.getElementById('splitButton');
const downloadRetrieveButton = document.getElementById('downloadRetrieveButton');
const downloadSplitButton = document.getElementById('downloadSplitButton');
const fetchMetadataButton = document.getElementById('fetchMetadata');
const toggleServerButton = document.getElementById('toggleServer');
const toggleLabel = document.getElementById('toggleLabel');
const fhirServerUrlInput = document.getElementById('fhirServerUrl');
const resourceTypesDiv = document.getElementById('resourceTypes');
const resourceButtonsContainer = document.getElementById('resourceButtons');
const retrieveConsole = document.getElementById('retrieveConsole');
const splitConsole = document.getElementById('splitConsole');
const bundleSourceRadios = document.getElementsByName('bundle_source');
const splitBundleZipInput = document.getElementById('split_bundle_zip');
const splitBundleZipGroup = splitBundleZipInput ? splitBundleZipInput.closest('.form-group') : null;
const validateReferencesCheckbox = document.getElementById('validate_references_checkbox');
const fetchReferenceBundlesGroup = document.getElementById('fetchReferenceBundlesGroup');
const fetchReferenceBundlesCheckbox = document.getElementById('fetch_reference_bundles_checkbox');
// --- State & Config Variables ---
const appMode = '{{ app_mode | default("standalone") | lower }}';
const apiKey = '{{ api_key }}';
const sessionZipPath = '{{ session.get("retrieve_params", {}).get("bundle_zip_path", "") }}';
let useLocalHapi = (appMode !== 'lite');
let retrieveZipPath = null;
let splitZipPath = null;
let fetchedMetadataCache = null;
// --- Helper Functions ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
// --- UI Update Functions ---
function updateBundleSourceUI() {
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
if (splitBundleZipGroup) {
splitBundleZipGroup.style.display = selectedSource === 'upload' ? 'block' : 'none';
}
if (splitBundleZipInput) {
splitBundleZipInput.required = selectedSource === 'upload';
}
}
function updateServerToggleUI() {
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton) return;
if (appMode === 'lite') {
useLocalHapi = false;
toggleServerButton.disabled = true;
toggleServerButton.classList.add('disabled');
toggleServerButton.style.pointerEvents = 'none !important';
toggleServerButton.setAttribute('aria-disabled', 'true');
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
toggleLabel.textContent = 'Use Custom URL';
fhirServerUrlInput.style.display = 'block';
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
fhirServerUrlInput.required = true;
} else {
toggleServerButton.disabled = false;
toggleServerButton.classList.remove('disabled');
toggleServerButton.style.pointerEvents = 'auto';
toggleServerButton.removeAttribute('aria-disabled');
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
fhirServerUrlInput.required = !useLocalHapi;
}
fhirServerUrlInput.classList.remove('is-invalid');
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
}
function toggleFetchReferenceBundles() {
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
if (!validateReferencesCheckbox.checked && fetchReferenceBundlesCheckbox) {
fetchReferenceBundlesCheckbox.checked = false;
}
} else {
console.warn("Could not find checkbox elements needed for toggling.");
}
}
// --- Event Listeners ---
bundleSourceRadios.forEach(radio => {
radio.addEventListener('change', updateBundleSourceUI);
});
toggleServerButton.addEventListener('click', () => {
if (appMode === 'lite') return;
useLocalHapi = !useLocalHapi;
if (useLocalHapi) fhirServerUrlInput.value = '';
updateServerToggleUI();
resourceTypesDiv.style.display = 'none';
fetchedMetadataCache = null;
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
});
if (validateReferencesCheckbox) {
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
} else {
console.warn("Validate references checkbox not found.");
}
fetchMetadataButton.addEventListener('click', async () => {
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
resourceTypesDiv.style.display = 'block';
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
if (!useLocalHapi && !customUrl) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
return;
}
if (!useLocalHapi) {
try { new URL(customUrl); } catch (_) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Invalid custom URL format.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Invalid URL format.</span>';
return;
}
}
fhirServerUrlInput.classList.remove('is-invalid');
fetchMetadataButton.disabled = true;
fetchMetadataButton.textContent = 'Fetching...';
try {
const fetchUrl = '/fhir/metadata'; // Always target proxy
const headers = { 'Accept': 'application/fhir+json' };
if (!useLocalHapi && customUrl) {
headers['X-Target-FHIR-Server'] = customUrl;
console.log(`Workspaceing metadata via proxy with X-Target-FHIR-Server: ${customUrl}`);
} else {
console.log("Fetching metadata via proxy for local HAPI server");
}
console.log(`Proxy Fetch URL: ${fetchUrl}`);
console.log(`Request Headers sent TO PROXY: ${JSON.stringify(headers)}`);
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
if (!response.ok) {
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
console.error(`Metadata fetch failed: ${errorText}`);
throw new Error(errorText);
}
const data = await response.json();
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
fetchedMetadataCache = data;
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
resourceButtonsContainer.innerHTML = '';
if (!resourceTypes.length) {
resourceButtonsContainer.innerHTML = '<span class="text-warning">No resource types found in metadata.</span>';
} else {
resourceTypes.sort().forEach(type => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-outline-primary btn-sm me-2 mb-2';
button.textContent = type;
button.dataset.resource = type;
button.addEventListener('click', () => {
button.classList.toggle('active');
button.classList.toggle('btn-primary');
button.classList.toggle('btn-outline-primary');
});
resourceButtonsContainer.appendChild(button);
});
}
} catch (e) {
console.error('Metadata fetch error:', e);
resourceButtonsContainer.innerHTML = `<span class="text-danger">Error: ${e.message}</span>`;
resourceTypesDiv.style.display = 'block';
alert(`Error fetching metadata: ${e.message}`);
fetchedMetadataCache = null;
} finally {
fetchMetadataButton.disabled = false;
fetchMetadataButton.textContent = 'Fetch Metadata';
}
});
retrieveForm.addEventListener('submit', async (e) => {
e.preventDefault();
const spinner = retrieveButton.querySelector('.spinner-border');
const icon = retrieveButton.querySelector('i');
retrieveButton.disabled = true;
if (spinner) spinner.style.display = 'inline-block';
if (icon) icon.style.display = 'none';
downloadRetrieveButton.style.display = 'none';
retrieveZipPath = null;
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
if (!selectedResources.length) {
alert('Please select at least one resource type.');
retrieveButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
}
const formData = new FormData();
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
selectedResources.forEach(res => formData.append('resources', res));
if (validateReferencesCheckbox) {
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
}
if (fetchReferenceBundlesCheckbox) {
if (validateReferencesCheckbox && validateReferencesCheckbox.checked) {
formData.append('fetch_reference_bundles', fetchReferenceBundlesCheckbox.checked ? 'true' : 'false');
} else {
formData.append('fetch_reference_bundles', 'false');
}
} else {
formData.append('fetch_reference_bundles', 'false');
}
const currentFhirServerUrl = useLocalHapi ? '/fhir' : fhirServerUrlInput.value.trim();
if (!useLocalHapi && !currentFhirServerUrl) {
alert('Custom FHIR Server URL is required.'); fhirServerUrlInput.classList.add('is-invalid');
retrieveButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
}
formData.append('fhir_server_url', currentFhirServerUrl);
const headers = {
'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey
};
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}`);
try {
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
}
retrieveZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line);
const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-info'; let prefix = '[INFO]';
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
const messageDiv = document.createElement('div');
messageDiv.className = messageClass;
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
retrieveConsole.appendChild(messageDiv);
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
if (data.type === 'complete' && retrieveZipPath) {
const completeData = data.data || {};
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
if (!bundlesFound) {
retrieveConsole.innerHTML += `<div class="text-warning">${timestamp} [WARNING] No bundles were retrieved. Check server data or resource selection.</div>`;
} else {
downloadRetrieveButton.style.display = 'block';
const useRetrievedRadio = document.getElementById('useRetrievedBundles');
const useRetrievedLabel = useRetrievedRadio?.nextElementSibling;
if(useRetrievedRadio) useRetrievedRadio.disabled = false;
if(useRetrievedLabel) useRetrievedLabel.textContent = 'Use Retrieved Bundles';
}
}
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
}
}
if (buffer.trim()) { /* Handle final buffer if necessary */ }
} catch (e) {
console.error('Retrieval error:', e);
const ts = new Date().toLocaleTimeString();
retrieveConsole.innerHTML += `<div class="text-danger">${ts} [ERROR] ${sanitizeText(e.message)}</div>`;
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
} finally {
retrieveButton.disabled = false;
if (spinner) spinner.style.display = 'none';
if (icon) icon.style.display = 'inline-block';
}
});
downloadRetrieveButton.addEventListener('click', () => {
if (retrieveZipPath) {
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
const downloadUrl = `/tmp/${filename}`;
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = 'retrieved_bundles.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
.then(() => console.log("Session clear requested."))
.catch(err => console.error("Session clear failed:", err));
downloadRetrieveButton.style.display = 'none';
retrieveZipPath = null;
const useRetrievedRadio = document.getElementById('useRetrievedBundles');
const useRetrievedLabel = useRetrievedRadio?.nextElementSibling;
if(useRetrievedRadio) useRetrievedRadio.disabled = true;
if(useRetrievedLabel) useRetrievedLabel.textContent = 'Use Retrieved Bundles (No bundles retrieved in this session)';
}, 500);
} else {
console.error("No retrieve ZIP path available for download");
alert("Download error: No file path available.");
}
});
splitForm.addEventListener('submit', async (e) => {
e.preventDefault();
const spinner = splitButton.querySelector('.spinner-border');
const icon = splitButton.querySelector('i');
splitButton.disabled = true;
if (spinner) spinner.style.display = 'inline-block';
if (icon) icon.style.display = 'none';
downloadSplitButton.style.display = 'none';
splitZipPath = null;
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
const formData = new FormData();
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
if (bundleSource === 'upload') {
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
alert('Please select a ZIP file to upload for splitting.');
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
}
formData.append('split_bundle_zip', splitBundleZipInput.files[0]);
console.log("Splitting uploaded file:", splitBundleZipInput.files[0].name);
} else if (bundleSource === 'retrieved' && sessionZipPath) {
formData.append('split_bundle_zip_path', sessionZipPath);
console.log("Splitting retrieved bundle path:", sessionZipPath);
} else if (bundleSource === 'retrieved' && !sessionZipPath){
// Check if the retrieveZipPath from the *current* run exists
if (retrieveZipPath) {
formData.append('split_bundle_zip_path', retrieveZipPath);
console.log("Splitting retrieve bundle path from current run:", retrieveZipPath);
} else {
alert('No bundle source available. Please retrieve bundles first or upload a ZIP.');
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
}
} else {
alert('No bundle source selected.');
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
}
const headers = {
'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey
};
try {
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) {
const errorMsg = await response.text();
let detail = errorMsg; try { const errorJson = JSON.parse(errorMsg); detail = errorJson.message || errorJson.error || JSON.stringify(errorJson); } catch(e) {}
throw new Error(`HTTP ${response.status}: ${detail}`);
}
splitZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line);
const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-info'; let prefix = '[INFO]';
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
const messageDiv = document.createElement('div'); messageDiv.className = messageClass; messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`; splitConsole.appendChild(messageDiv); splitConsole.scrollTop = splitConsole.scrollHeight;
if (data.type === 'complete' && splitZipPath) { downloadSplitButton.style.display = 'block'; }
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
}
}
if (buffer.trim()) { /* Handle final buffer */ }
} catch (e) {
console.error('Splitting error:', e);
const ts = new Date().toLocaleTimeString();
splitConsole.innerHTML += `<div class="text-danger">${ts} [ERROR] ${sanitizeText(e.message)}</div>`;
splitConsole.scrollTop = splitConsole.scrollHeight;
} finally {
splitButton.disabled = false;
if (spinner) spinner.style.display = 'none';
if (icon) icon.style.display = 'inline-block';
}
});
downloadSplitButton.addEventListener('click', () => {
if (splitZipPath) {
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
const downloadUrl = `/tmp/${filename}`;
console.log(`Attempting to download split ZIP from Flask endpoint: ${downloadUrl}`);
const link = document.createElement('a'); link.href = downloadUrl; link.download = 'split_resources.zip'; document.body.appendChild(link); link.click(); document.body.removeChild(link);
setTimeout(() => {
const csrfToken = splitForm.querySelector('input[name="csrf_token"]')?.value || '';
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
.then(() => console.log("Session clear requested after split download."))
.catch(err => console.error("Session clear failed:", err));
downloadSplitButton.style.display = 'none';
splitZipPath = null;
}, 500);
} else {
console.error("No split ZIP path available for download");
alert("Download error: No file path available.");
}
});
// --- Initial Setup Calls ---
updateBundleSourceUI();
updateServerToggleUI();
toggleFetchReferenceBundles(); // Initial call for the new checkbox
}); // End DOMContentLoaded
</script>
{% endblock %}