mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
added Basic Auth options for custom server usage across multiple modules. new footer option - OpenAPI docs - integrated swagger UI for open API documentation
690 lines
36 KiB
HTML
690 lines
36 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_form_helpers.html" import render_field %}
|
|
|
|
{% block content %}
|
|
<div class="px-4 py-5 my-5 text-center">
|
|
<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>
|
|
{{ 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>
|
|
|
|
{# Authentication Section (Shown for Custom URL) #}
|
|
<div class="mb-3" id="authSection" style="display: none;">
|
|
<label class="form-label fw-bold">Authentication</label>
|
|
<div class="row g-3 align-items-end">
|
|
<div class="col-md-5">
|
|
<select class="form-select" id="authType" name="auth_type">
|
|
<option value="none" selected>None</option>
|
|
<option value="bearer">Bearer Token</option>
|
|
<option value="basic">Basic Authentication</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-7" id="authInputsGroup" style="display: none;">
|
|
{# Bearer Token Input #}
|
|
<div id="bearerTokenInput" style="display: none;">
|
|
<label for="bearerToken" class="form-label">Bearer Token</label>
|
|
<input type="password" class="form-control" id="bearerToken" name="bearer_token" placeholder="Enter Bearer Token">
|
|
</div>
|
|
{# Basic Auth Inputs #}
|
|
<div id="basicAuthInputs" style="display: none;">
|
|
<label for="username" class="form-label">Username</label>
|
|
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Username">
|
|
<label for="password" class="form-label">Password</label>
|
|
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Password">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
|
|
</div>
|
|
|
|
{# Checkbox Row #}
|
|
<div class="row g-3 mb-3 align-items-center">
|
|
<div class="col-md-6">
|
|
{{ render_field(form.validate_references, id='validate_references_checkbox') }}
|
|
</div>
|
|
<div class="col-md-6" id="fetchReferenceBundlesGroup" style="display: none;">
|
|
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
<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_field(form.split_bundle_zip, class="form-control") }}
|
|
<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');
|
|
const authSection = document.getElementById('authSection');
|
|
const authTypeSelect = document.getElementById('authType');
|
|
const authInputsGroup = document.getElementById('authInputsGroup');
|
|
const bearerTokenInput = document.getElementById('bearerToken');
|
|
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
// --- 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 || !authSection) return;
|
|
|
|
if (appMode === 'lite') {
|
|
useLocalHapi = false;
|
|
toggleServerButton.disabled = true;
|
|
toggleServerButton.classList.add('disabled');
|
|
toggleServerButton.style.pointerEvents = 'none';
|
|
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;
|
|
authSection.style.display = 'block';
|
|
} 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;
|
|
authSection.style.display = useLocalHapi ? 'none' : 'block';
|
|
}
|
|
fhirServerUrlInput.classList.remove('is-invalid');
|
|
updateAuthInputsUI();
|
|
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
|
|
}
|
|
|
|
function updateAuthInputsUI() {
|
|
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
|
|
const authType = authTypeSelect.value;
|
|
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
|
|
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
|
|
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
|
|
if (authType !== 'bearer' && bearerTokenInput) bearerTokenInput.value = '';
|
|
if (authType !== 'basic' && usernameInput) usernameInput.value = '';
|
|
if (authType !== 'basic' && passwordInput) passwordInput.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 (authTypeSelect) {
|
|
authTypeSelect.addEventListener('change', updateAuthInputsUI);
|
|
}
|
|
|
|
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';
|
|
const headers = { 'Accept': 'application/fhir+json' };
|
|
if (!useLocalHapi && customUrl) {
|
|
headers['X-Target-FHIR-Server'] = customUrl;
|
|
if (authTypeSelect && authTypeSelect.value !== 'none') {
|
|
const authType = authTypeSelect.value;
|
|
if (authType === 'bearer' && bearerTokenInput && bearerTokenInput.value) {
|
|
headers['Authorization'] = `Bearer ${bearerTokenInput.value}`;
|
|
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
|
|
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
|
|
headers['Authorization'] = `Basic ${credentials}`;
|
|
}
|
|
}
|
|
console.log(`Fetching 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);
|
|
|
|
// Add authentication fields for custom URL
|
|
if (!useLocalHapi && authTypeSelect) {
|
|
const authType = authTypeSelect.value;
|
|
formData.append('auth_type', authType);
|
|
if (authType === 'bearer' && bearerTokenInput) {
|
|
if (!bearerTokenInput.value) {
|
|
alert('Please enter a Bearer Token.');
|
|
retrieveButton.disabled = false;
|
|
if (spinner) spinner.style.display = 'none';
|
|
if (icon) icon.style.display = 'inline-block';
|
|
return;
|
|
}
|
|
formData.append('bearer_token', bearerTokenInput.value);
|
|
} else if (authType === 'basic' && usernameInput && passwordInput) {
|
|
if (!usernameInput.value || !passwordInput.value) {
|
|
alert('Please enter both Username and Password for Basic Authentication.');
|
|
retrieveButton.disabled = false;
|
|
if (spinner) spinner.style.display = 'none';
|
|
if (icon) icon.style.display = 'inline-block';
|
|
return;
|
|
}
|
|
formData.append('username', usernameInput.value);
|
|
formData.append('password', passwordInput.value);
|
|
}
|
|
}
|
|
|
|
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')}, AuthType: ${formData.get('auth_type')}`);
|
|
|
|
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 */ }
|
|
|
|
} 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) {
|
|
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();
|
|
});
|
|
</script>
|
|
{% endblock %} |