mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
Hapi Config Page. retrieve bundles from server split bundles to resources. new upload page, and Package Registry CACHE
580 lines
32 KiB
HTML
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, "<").replace(/>/g, ">") : "";
|
|
|
|
// --- 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 %} |