Compare commits

...

2 Commits

Author SHA1 Message Date
f7063484d9 docker builds 2025-08-27 21:28:30 +10:00
b8014de7c3 Hotfix
added download validation links and fixed CORS issues
2025-08-27 21:08:01 +10:00
5 changed files with 28 additions and 69 deletions

View File

@ -3,7 +3,9 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0
# Install build dependencies, Node.js 18, and coreutils (for stdbuf) # Install build dependencies, Node.js 18, and coreutils (for stdbuf)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv curl coreutils git \ python3 python3-pip python3-venv \
default-jre-headless \
curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -1,8 +1,10 @@
# Base image with Python and Node.js # Base image with Python and Node.js
FROM python:3.9-slim FROM python:3.9-slim
# Install Node.js 18 and npm # Install JRE and other dependencies for the validator
# We need to install `default-jre-headless` to get a minimal Java Runtime Environment
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
default-jre-headless \
curl coreutils git \ curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \

17
app.py
View File

@ -3185,14 +3185,23 @@ def ig_configurator():
@app.route('/download/<path:filename>') @app.route('/download/<path:filename>')
def download_file(filename): def download_file(filename):
""" """
Serves a file from the temporary directory. Serves a file from the temporary directory created during validation.
This route is necessary to allow the user to download the validator reports. The filename includes the unique temporary directory name to prevent
unauthorized file access and to correctly locate the file.
""" """
# The temporary directory path is hardcoded for security # Use the stored temporary directory path from app.config
temp_dir = tempfile.gettempdir() temp_dir = current_app.config.get('VALIDATION_TEMP_DIR')
# If no temporary directory is set, or it's a security risk, return a 404.
if not temp_dir or not os.path.exists(temp_dir):
logger.error(f"Download request failed: Temporary directory not found or expired. Path: {temp_dir}")
return jsonify({"error": "File not found or validation session expired."}), 404
# Construct the full file path.
file_path = os.path.join(temp_dir, filename) file_path = os.path.join(temp_dir, filename)
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.error(f"File not found for download: {file_path}")
return jsonify({"error": "File not found."}), 404 return jsonify({"error": "File not found."}), 404
try: try:

View File

@ -10,7 +10,6 @@
</p> </p>
</div> </div>
</div> </div>
<div class="container mt-4"> <div class="container mt-4">
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-header"> <div class="card-header">
@ -28,7 +27,7 @@
</div> </div>
{% endif %} {% endif %}
<form id="retrieveForm" method="POST"> <form id="retrieveForm" method="POST">
{{ form.hidden_tag() }} {{ form.csrf_token(id='retrieve_csrf_token') }}
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold">FHIR Server</label> <label class="form-label fw-bold">FHIR Server</label>
<div class="input-group"> <div class="input-group">
@ -39,7 +38,6 @@
</div> </div>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local Fhir Server(/fhir proxy) or enter a custom FHIR server URL.</small> <small id="fhirServerHelp" class="form-text text-muted">Toggle to use local Fhir Server(/fhir proxy) or enter a custom FHIR server URL.</small>
</div> </div>
{# Authentication Section (Shown for Custom URL) #} {# Authentication Section (Shown for Custom URL) #}
<div class="mb-3" id="authSection" style="display: none;"> <div class="mb-3" id="authSection" style="display: none;">
<label class="form-label fw-bold">Authentication</label> <label class="form-label fw-bold">Authentication</label>
@ -68,7 +66,6 @@
</div> </div>
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small> <small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
</div> </div>
{# Checkbox Row #} {# Checkbox Row #}
<div class="row g-3 mb-3 align-items-center"> <div class="row g-3 mb-3 align-items-center">
<div class="col-md-6"> <div class="col-md-6">
@ -78,7 +75,6 @@
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }} {{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button> <button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
<div class="banner3 mt-3" id="resourceTypes" style="display: none;"> <div class="banner3 mt-3" id="resourceTypes" style="display: none;">
<h5>Resource Types</h5> <h5>Resource Types</h5>
@ -102,14 +98,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header"> <div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4> <h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form id="splitForm" method="POST" enctype="multipart/form-data"> <form id="splitForm" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }} {{ form.csrf_token(id='split_csrf_token') }}
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold">Bundle Source</label> <label class="form-label fw-bold">Bundle Source</label>
<div class="form-check"> <div class="form-check">
@ -141,7 +136,6 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References --- // --- DOM Element References ---
@ -172,7 +166,6 @@ document.addEventListener('DOMContentLoaded', () => {
const basicAuthInputs = document.getElementById('basicAuthInputs'); const basicAuthInputs = document.getElementById('basicAuthInputs');
const usernameInput = document.getElementById('username'); const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');
// --- State & Config Variables --- // --- State & Config Variables ---
const appMode = '{{ app_mode | default("standalone") | lower }}'; const appMode = '{{ app_mode | default("standalone") | lower }}';
const apiKey = '{{ api_key }}'; const apiKey = '{{ api_key }}';
@ -181,11 +174,9 @@ document.addEventListener('DOMContentLoaded', () => {
let retrieveZipPath = null; let retrieveZipPath = null;
let splitZipPath = null; let splitZipPath = null;
let fetchedMetadataCache = null; let fetchedMetadataCache = null;
let localHapiBaseUrl = ''; // NEW: To store the fetched URL let localHapiBaseUrl = ''; // To store the fetched URL
// --- Helper Functions --- // --- Helper Functions ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : ""; const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
// --- UI Update Functions --- // --- UI Update Functions ---
function updateBundleSourceUI() { function updateBundleSourceUI() {
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value; const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
@ -196,13 +187,12 @@ document.addEventListener('DOMContentLoaded', () => {
splitBundleZipInput.required = selectedSource === 'upload'; splitBundleZipInput.required = selectedSource === 'upload';
} }
} }
async function fetchLocalUrlAndSetUI() { async function fetchLocalUrlAndSetUI() {
try { try {
const response = await fetch('/api/get-local-server-url'); const response = await fetch('/api/get-local-server-url');
if (!response.ok) throw new Error('Failed to fetch local server URL.'); if (!response.ok) throw new Error('Failed to fetch local server URL.');
const data = await response.json(); const data = await response.json();
localHapiBaseUrl = data.url; localHapiBaseUrl = data.url.replace(/\/+$/, ''); // Normalize by removing trailing slashes
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`); console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -212,10 +202,8 @@ document.addEventListener('DOMContentLoaded', () => {
updateServerToggleUI(); updateServerToggleUI();
} }
} }
function updateServerToggleUI() { function updateServerToggleUI() {
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return; if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
if (appMode === 'lite') { if (appMode === 'lite') {
useLocalHapi = false; useLocalHapi = false;
toggleServerButton.disabled = true; toggleServerButton.disabled = true;
@ -243,7 +231,6 @@ document.addEventListener('DOMContentLoaded', () => {
updateAuthInputsUI(); updateAuthInputsUI();
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`); console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
} }
function updateAuthInputsUI() { function updateAuthInputsUI() {
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return; if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
const authType = authTypeSelect.value; const authType = authTypeSelect.value;
@ -254,7 +241,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (authType !== 'basic' && usernameInput) usernameInput.value = ''; if (authType !== 'basic' && usernameInput) usernameInput.value = '';
if (authType !== 'basic' && passwordInput) passwordInput.value = ''; if (authType !== 'basic' && passwordInput) passwordInput.value = '';
} }
function toggleFetchReferenceBundles() { function toggleFetchReferenceBundles() {
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) { if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none'; fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
@ -265,12 +251,10 @@ document.addEventListener('DOMContentLoaded', () => {
console.warn("Could not find checkbox elements needed for toggling."); console.warn("Could not find checkbox elements needed for toggling.");
} }
} }
// --- Event Listeners --- // --- Event Listeners ---
bundleSourceRadios.forEach(radio => { bundleSourceRadios.forEach(radio => {
radio.addEventListener('change', updateBundleSourceUI); radio.addEventListener('change', updateBundleSourceUI);
}); });
toggleServerButton.addEventListener('click', () => { toggleServerButton.addEventListener('click', () => {
if (appMode === 'lite') return; if (appMode === 'lite') return;
useLocalHapi = !useLocalHapi; useLocalHapi = !useLocalHapi;
@ -280,24 +264,20 @@ document.addEventListener('DOMContentLoaded', () => {
fetchedMetadataCache = null; fetchedMetadataCache = null;
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`); console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
}); });
if (authTypeSelect) { if (authTypeSelect) {
authTypeSelect.addEventListener('change', updateAuthInputsUI); authTypeSelect.addEventListener('change', updateAuthInputsUI);
} }
if (validateReferencesCheckbox) { if (validateReferencesCheckbox) {
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles); validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
} else { } else {
console.warn("Validate references checkbox not found."); console.warn("Validate references checkbox not found.");
} }
fetchMetadataButton.addEventListener('click', async () => { fetchMetadataButton.addEventListener('click', async () => {
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>'; resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
resourceTypesDiv.style.display = 'block'; resourceTypesDiv.style.display = 'block';
let customUrl = null; let customUrl = null;
if (!useLocalHapi) { if (!useLocalHapi) {
customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, ''); customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, ''); // Normalize custom URL
if (!customUrl) { if (!customUrl) {
fhirServerUrlInput.classList.add('is-invalid'); fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.'); alert('Please enter a valid FHIR server URL.');
@ -312,20 +292,17 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
fhirServerUrlInput.classList.remove('is-invalid'); fhirServerUrlInput.classList.remove('is-invalid');
fetchMetadataButton.disabled = true; fetchMetadataButton.disabled = true;
fetchMetadataButton.textContent = 'Fetching...'; fetchMetadataButton.textContent = 'Fetching...';
try { try {
// REVISED LOGIC: Build URL and headers based on the toggle state // Build URL and headers based on the toggle state
let fetchUrl; let fetchUrl;
const headers = { 'Accept': 'application/fhir+json' }; const headers = { 'Accept': 'application/fhir+json' };
if (useLocalHapi) { if (useLocalHapi) {
if (!localHapiBaseUrl) { if (!localHapiBaseUrl) {
throw new Error("Local HAPI URL not available. Please refresh."); throw new Error("Local HAPI URL not available. Please refresh.");
} }
fetchUrl = `${localHapiBaseUrl}/metadata`; fetchUrl = `${localHapiBaseUrl}/metadata`; // localHapiBaseUrl is already normalized
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`); console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
} else { } else {
fetchUrl = `${customUrl}/metadata`; fetchUrl = `${customUrl}/metadata`;
@ -339,11 +316,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`); console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`);
} }
console.log(`Request Headers sent: ${JSON.stringify(headers)}`); console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
const response = await fetch(fetchUrl, { method: 'GET', headers: headers }); const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
if (!response.ok) { if (!response.ok) {
let errorText = `Request failed with status ${response.status} ${response.statusText}`; let errorText = `Request failed with status ${response.status} ${response.statusText}`;
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {} try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
@ -353,7 +327,6 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await response.json(); const data = await response.json();
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500)); console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
fetchedMetadataCache = data; fetchedMetadataCache = data;
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || []; const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
resourceButtonsContainer.innerHTML = ''; resourceButtonsContainer.innerHTML = '';
if (!resourceTypes.length) { if (!resourceTypes.length) {
@ -384,7 +357,6 @@ document.addEventListener('DOMContentLoaded', () => {
fetchMetadataButton.textContent = 'Fetch Metadata'; fetchMetadataButton.textContent = 'Fetch Metadata';
} }
}); });
retrieveForm.addEventListener('submit', async (e) => { retrieveForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const spinner = retrieveButton.querySelector('.spinner-border'); const spinner = retrieveButton.querySelector('.spinner-border');
@ -395,7 +367,6 @@ document.addEventListener('DOMContentLoaded', () => {
downloadRetrieveButton.style.display = 'none'; downloadRetrieveButton.style.display = 'none';
retrieveZipPath = null; retrieveZipPath = null;
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`; retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource); const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
if (!selectedResources.length) { if (!selectedResources.length) {
alert('Please select at least one resource type.'); alert('Please select at least one resource type.');
@ -404,8 +375,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block'; if (icon) icon.style.display = 'inline-block';
return; return;
} }
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim();
if (!currentFhirServerUrl) { if (!currentFhirServerUrl) {
alert('FHIR Server URL is required.'); alert('FHIR Server URL is required.');
fhirServerUrlInput.classList.add('is-invalid'); fhirServerUrlInput.classList.add('is-invalid');
@ -414,14 +384,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block'; if (icon) icon.style.display = 'inline-block';
return; return;
} }
const formData = new FormData(); const formData = new FormData();
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]'); const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value); if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
selectedResources.forEach(res => formData.append('resources', res)); selectedResources.forEach(res => formData.append('resources', res));
formData.append('fhir_server_url', currentFhirServerUrl); formData.append('fhir_server_url', currentFhirServerUrl);
if (validateReferencesCheckbox) { if (validateReferencesCheckbox) {
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false'); formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
} }
@ -434,7 +401,6 @@ document.addEventListener('DOMContentLoaded', () => {
} else { } else {
formData.append('fetch_reference_bundles', 'false'); formData.append('fetch_reference_bundles', 'false');
} }
if (!useLocalHapi && authTypeSelect) { if (!useLocalHapi && authTypeSelect) {
const authType = authTypeSelect.value; const authType = authTypeSelect.value;
formData.append('auth_type', authType); formData.append('auth_type', authType);
@ -459,24 +425,20 @@ document.addEventListener('DOMContentLoaded', () => {
formData.append('password', passwordInput.value); formData.append('password', passwordInput.value);
} }
} }
const headers = { const headers = {
'Accept': 'application/x-ndjson', 'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '', 'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey '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')}`); console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}, AuthType: ${formData.get('auth_type')}`);
try { try {
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData }); const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' })); const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`); throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
} }
retrieveZipPath = response.headers.get('X-Zip-Path'); retrieveZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path: ${retrieveZipPath}`); console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
@ -498,13 +460,11 @@ document.addEventListener('DOMContentLoaded', () => {
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; } else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; } else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; } else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.className = messageClass; messageDiv.className = messageClass;
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`; messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
retrieveConsole.appendChild(messageDiv); retrieveConsole.appendChild(messageDiv);
retrieveConsole.scrollTop = retrieveConsole.scrollHeight; retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
if (data.type === 'complete' && retrieveZipPath) { if (data.type === 'complete' && retrieveZipPath) {
const completeData = data.data || {}; const completeData = data.data || {};
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0); const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
@ -522,7 +482,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
if (buffer.trim()) { /* Handle final buffer */ } if (buffer.trim()) { /* Handle final buffer */ }
} catch (e) { } catch (e) {
console.error('Retrieval error:', e); console.error('Retrieval error:', e);
const ts = new Date().toLocaleTimeString(); const ts = new Date().toLocaleTimeString();
@ -534,20 +493,17 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block'; if (icon) icon.style.display = 'inline-block';
} }
}); });
downloadRetrieveButton.addEventListener('click', () => { downloadRetrieveButton.addEventListener('click', () => {
if (retrieveZipPath) { if (retrieveZipPath) {
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip'; const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
const downloadUrl = `/tmp/${filename}`; const downloadUrl = `/tmp/${filename}`;
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`); console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = downloadUrl; link.href = downloadUrl;
link.download = 'retrieved_bundles.zip'; link.download = 'retrieved_bundles.zip';
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
setTimeout(() => { setTimeout(() => {
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || ''; const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }}) fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
@ -565,7 +521,6 @@ document.addEventListener('DOMContentLoaded', () => {
alert("Download error: No file path available."); alert("Download error: No file path available.");
} }
}); });
splitForm.addEventListener('submit', async (e) => { splitForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const spinner = splitButton.querySelector('.spinner-border'); const spinner = splitButton.querySelector('.spinner-border');
@ -576,13 +531,10 @@ document.addEventListener('DOMContentLoaded', () => {
downloadSplitButton.style.display = 'none'; downloadSplitButton.style.display = 'none';
splitZipPath = null; splitZipPath = null;
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`; splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
const formData = new FormData(); const formData = new FormData();
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]'); const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value); if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value; const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
if (bundleSource === 'upload') { if (bundleSource === 'upload') {
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) { if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
alert('Please select a ZIP file to upload for splitting.'); alert('Please select a ZIP file to upload for splitting.');
@ -614,16 +566,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block'; if (icon) icon.style.display = 'inline-block';
return; return;
} }
const headers = { const headers = {
'Accept': 'application/x-ndjson', 'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '', 'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey 'X-API-Key': apiKey
}; };
try { try {
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData }); const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) { if (!response.ok) {
const errorMsg = await response.text(); const errorMsg = await response.text();
let detail = errorMsg; let detail = errorMsg;
@ -632,7 +581,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
splitZipPath = response.headers.get('X-Zip-Path'); splitZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path for split: ${splitZipPath}`); console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
@ -665,7 +613,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
if (buffer.trim()) { /* Handle final buffer */ } if (buffer.trim()) { /* Handle final buffer */ }
} catch (e) { } catch (e) {
console.error('Splitting error:', e); console.error('Splitting error:', e);
const ts = new Date().toLocaleTimeString(); const ts = new Date().toLocaleTimeString();
@ -677,7 +624,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block'; if (icon) icon.style.display = 'inline-block';
} }
}); });
downloadSplitButton.addEventListener('click', () => { downloadSplitButton.addEventListener('click', () => {
if (splitZipPath) { if (splitZipPath) {
const filename = splitZipPath.split('/').pop() || 'split_resources.zip'; const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
@ -702,7 +648,6 @@ document.addEventListener('DOMContentLoaded', () => {
alert("Download error: No file path available."); alert("Download error: No file path available.");
} }
}); });
// --- Initial Setup Calls --- // --- Initial Setup Calls ---
updateBundleSourceUI(); updateBundleSourceUI();
fetchLocalUrlAndSetUI(); fetchLocalUrlAndSetUI();

View File

@ -275,6 +275,7 @@
clearResultsButton.classList.remove('d-none'); clearResultsButton.classList.remove('d-none');
if (data.file_paths) { if (data.file_paths) {
// Fix: Extract just the filename to construct the download URL
const jsonFileName = data.file_paths.json.split('/').pop(); const jsonFileName = data.file_paths.json.split('/').pop();
const htmlFileName = data.file_paths.html.split('/').pop(); const htmlFileName = data.file_paths.html.split('/').pop();
const downloadDiv = document.createElement('div'); const downloadDiv = document.createElement('div');