FHIRFLARE-IG-Toolkit/templates/cp_push_igs.html
Sudo-JHare fa091db463 QOL Changes
added Basic Auth options for custom server  usage across multiple modules.

new footer option - OpenAPI docs - integrated swagger UI for open API documentation
2025-05-22 20:07:11 +10:00

501 lines
33 KiB
HTML

{% extends "base.html" %}
{# Import form helpers for CSRF token and field rendering #}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row">
{# Left Column: Downloaded IGs List & Report Area #}
<div class="col-md-6">
<h2><i class="bi bi-box-seam me-2"></i>Downloaded IGs</h2>
{% if packages %}
<div class="table-responsive mb-3">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Package Name</th>
<th>Version</th>
</tr>
</thead>
<tbody>
{% for pkg in packages %}
{% set name = pkg.name %}
{% set version = pkg.version %}
{% set duplicate_group = (duplicate_groups or {}).get(name) %}
{% set color_class = group_colors[name] if (duplicate_group and group_colors and name in group_colors) else '' %}
<tr {% if color_class %}class="{{ color_class }}"{% endif %}>
<td><code>{{ name }}</code></td>
<td>{{ version }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if duplicate_groups %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong><i class="bi bi-exclamation-triangle-fill me-2"></i>Duplicate Packages Detected:</strong>
<ul class="mb-0 mt-2">
{% for name, versions in duplicate_groups.items() %}
<li>{{ name }} (Versions: {{ versions | join(', ') }})</li>
{% endfor %}
</ul>
<p class="mt-2 mb-0"><small>Consider resolving duplicates by deleting unused versions.</small></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% else %}
<p class="text-muted">No packages downloaded yet. Use the "Import IG" tab.</p>
{% endif %}
{# Push Response Area #}
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h4><i class="bi bi-file-earmark-text me-2"></i>Push Report</h4>
<div id="reportActions" style="display: none;">
<button id="copyReportBtn" class="btn btn-sm btn-outline-secondary me-2" title="Copy Report Text">
<i class="bi bi-clipboard"></i> Copy
</button>
<button id="downloadReportBtn" class="btn btn-sm btn-outline-secondary" title="Download Report as Text File">
<i class="bi bi-download"></i> Download
</button>
</div>
</div>
<div id="pushResponse" class="border p-3 rounded bg-light" style="min-height: 100px;">
<span class="text-muted">Report summary will appear here after pushing...</span>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
</div>{# End Left Column #}
{# Right Column: Push IGs Form and Console #}
<div class="col-md-6">
<h2><i class="bi bi-upload me-2"></i>Push IGs to FHIR Server</h2>
<form id="pushIgForm">
{{ form.csrf_token if form else '' }}
{# Package Selection #}
<div class="mb-3">
<label for="packageSelect" class="form-label">Select Package to Push</label>
<select class="form-select" id="packageSelect" name="package_id" required>
<option value="" disabled selected>Select a package...</option>
{% for pkg in packages %}
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
{% endfor %}
</select>
</div>
{# Dependency Mode Display #}
<div class="mb-3">
<label for="dependencyMode" class="form-label">Dependency Mode Used During Import</label>
<input type="text" class="form-control" id="dependencyMode" readonly placeholder="Select package to view mode...">
</div>
{# FHIR Server URL #}
<div class="mb-3">
<label for="fhirServerUrl" class="form-label">Target FHIR Server URL</label>
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://localhost:8080/fhir" required>
</div>
{# Authentication Section #}
<div class="row g-3 mb-3 align-items-end">
<div class="col-md-5">
<label for="authType" class="form-label">Authentication</label>
<select class="form-select" id="authType" name="auth_type">
<option value="none" selected>None</option>
<option value="apiKey">Toolkit API Key (Internal)</option>
<option value="bearerToken">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="authToken" class="form-label">Bearer Token</label>
<input type="password" class="form-control" id="authToken" name="auth_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 Basic Auth Username">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Basic Auth Password">
</div>
</div>
</div>
{# Checkboxes Row #}
<div class="row g-3 mb-3">
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="includeDependencies" name="include_dependencies" checked>
<label class="form-check-label" for="includeDependencies">Include Dependencies</label>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="forceUpload" name="force_upload">
<label class="form-check-label" for="forceUpload">Force Upload</label>
<small class="form-text text-muted d-block">Force upload all resources.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="dryRun" name="dry_run">
<label class="form-check-label" for="dryRun">Dry Run</label>
<small class="form-text text-muted d-block">Simulate only.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="verbose" name="verbose">
<label class="form-check-label" for="verbose">Verbose Log</label>
<small class="form-text text-muted d-block">Show detailed log.</small>
</div>
</div>
</div>
{# Resource Type Filter #}
<div class="mb-3">
<label for="resourceTypesFilter" class="form-label">Filter Resource Types <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="resourceTypesFilter" name="resource_types_filter" rows="1" placeholder="Comma-separated, e.g., StructureDefinition, ValueSet"></textarea>
</div>
{# Skip Files Filter #}
<div class="mb-3">
<label for="skipFilesFilter" class="form-label">Skip Specific Files <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="skipFilesFilter" name="skip_files" rows="2" placeholder="Enter full file paths within package (e.g., package/examples/bad.json), one per line or comma-separated."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100" id="pushButton">
<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server
</button>
</form>
{# Live Console #}
<div class="mt-4">
<h4><i class="bi bi-terminal me-2"></i>Live Console</h4>
<div id="liveConsole" class="border p-3 rounded bg-dark text-light" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
<span class="text-muted">Console output will appear here...</span>
</div>
</div>
</div> {# End Right Column #}
</div> {# End row #}
</div> {# End container-fluid #}
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- DOM Element References ---
const packageSelect = document.getElementById('packageSelect');
const dependencyModeField = document.getElementById('dependencyMode');
const pushIgForm = document.getElementById('pushIgForm');
const pushButton = document.getElementById('pushButton');
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('pushResponse');
const reportActions = document.getElementById('reportActions');
const copyReportBtn = document.getElementById('copyReportBtn');
const downloadReportBtn = document.getElementById('downloadReportBtn');
const authTypeSelect = document.getElementById('authType');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const authTokenInput = document.getElementById('authToken');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const resourceTypesFilterInput = document.getElementById('resourceTypesFilter');
const skipFilesFilterInput = document.getElementById('skipFilesFilter');
const dryRunCheckbox = document.getElementById('dryRun');
const verboseCheckbox = document.getElementById('verbose');
const forceUploadCheckbox = document.getElementById('forceUpload');
const includeDependenciesCheckbox = document.getElementById('includeDependencies');
const fhirServerUrlInput = document.getElementById('fhirServerUrl');
// --- CSRF Token ---
const csrfTokenInput = pushIgForm ? pushIgForm.querySelector('input[name="csrf_token"]') : null;
const csrfToken = csrfTokenInput ? csrfTokenInput.value : "";
if (!csrfToken) { console.warn("CSRF token not found in form."); }
// --- Helper: Sanitize text for display ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
// --- Event Listeners ---
// Update dependency mode display
if (packageSelect && dependencyModeField) {
packageSelect.addEventListener('change', function() {
const packageId = this.value;
dependencyModeField.value = '';
if (packageId) {
const [packageName, version] = packageId.split('#');
fetch(`/get-package-metadata?package_name=${packageName}&version=${version}`)
.then(response => response.ok ? response.json() : Promise.reject(`Metadata fetch failed: ${response.status}`))
.then(data => {
dependencyModeField.value = (data && data.dependency_mode) ? data.dependency_mode : 'Unknown';
})
.catch(error => { console.error('Error fetching metadata:', error); dependencyModeField.value = 'Error'; });
}
});
if (packageSelect.value) { packageSelect.dispatchEvent(new Event('change')); }
}
// Show/Hide Auth Inputs
if (authTypeSelect && authInputsGroup && bearerTokenInput && basicAuthInputs) {
authTypeSelect.addEventListener('change', function() {
authInputsGroup.style.display = (this.value === 'bearerToken' || this.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = this.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = this.value === 'basic' ? 'block' : 'none';
// Clear inputs when switching
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
if (this.value !== 'basic' && usernameInput) usernameInput.value = '';
if (this.value !== 'basic' && passwordInput) passwordInput.value = '';
});
authInputsGroup.style.display = (authTypeSelect.value === 'bearerToken' || authTypeSelect.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = authTypeSelect.value === 'basic' ? 'block' : 'none';
} else {
console.error("Auth elements not found.");
}
// Report Action Button Listeners
if (copyReportBtn && responseDiv) {
copyReportBtn.addEventListener('click', () => {
const reportAlert = responseDiv.querySelector('.alert');
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : '';
if (reportText && navigator.clipboard) {
navigator.clipboard.writeText(reportText)
.then(() => {
const originalIcon = copyReportBtn.innerHTML;
copyReportBtn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
setTimeout(() => { copyReportBtn.innerHTML = originalIcon; }, 2000);
})
.catch(err => {
console.error('Failed to copy report text: ', err);
alert('Failed to copy report text.');
});
} else if (!navigator.clipboard) {
alert('Clipboard API not available in this browser.');
} else {
alert('No report content found to copy.');
}
});
}
if (downloadReportBtn && responseDiv) {
downloadReportBtn.addEventListener('click', () => {
const reportAlert = responseDiv.querySelector('.alert');
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : '';
const packageId = packageSelect ? packageSelect.value.replace('#','-') : 'fhir-ig';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `push-report-${packageId}-${timestamp}.txt`;
if (reportText) {
const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} else {
alert('No report content found to download.');
}
});
}
// Form Submission
if (pushIgForm) {
pushIgForm.addEventListener('submit', async function(event) {
event.preventDefault();
// Get form values
const packageId = packageSelect ? packageSelect.value : null;
const fhirServerUrl = fhirServerUrlInput ? fhirServerUrlInput.value.trim() : null;
if (!packageId || !fhirServerUrl) { alert('Please select package and enter FHIR Server URL.'); return; }
const [packageName, version] = packageId.split('#');
const auth_type = authTypeSelect ? authTypeSelect.value : 'none';
const auth_token = (auth_type === 'bearerToken' && authTokenInput) ? authTokenInput.value : null;
const username = (auth_type === 'basic' && usernameInput) ? usernameInput.value.trim() : null;
const password = (auth_type === 'basic' && passwordInput) ? passwordInput.value : null;
const resource_types_filter_raw = resourceTypesFilterInput ? resourceTypesFilterInput.value.trim() : '';
const resource_types_filter = resource_types_filter_raw ? resource_types_filter_raw.split(',').map(s => s.trim()).filter(s => s) : null;
const skip_files_raw = skipFilesFilterInput ? skipFilesFilterInput.value.trim() : '';
const skip_files = skip_files_raw ? skip_files_raw.split(/[\s,\n]+/).map(s => s.trim()).filter(s => s) : null;
const dry_run = dryRunCheckbox ? dryRunCheckbox.checked : false;
const isVerboseChecked = verboseCheckbox ? verboseCheckbox.checked : false;
const include_dependencies = includeDependenciesCheckbox ? includeDependenciesCheckbox.checked : true;
const force_upload = forceUploadCheckbox ? forceUploadCheckbox.checked : false;
// Validate Basic Auth inputs
if (auth_type === 'basic') {
if (!username) { alert('Please enter a username for Basic Authentication.'); return; }
if (!password) { alert('Please enter a password for Basic Authentication.'); return; }
}
// UI Updates
if (pushButton) {
pushButton.disabled = true;
pushButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
}
if (liveConsole) {
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting ${dry_run ? 'DRY RUN ' : ''}${force_upload ? 'FORCE ' : ''}push for ${packageName}#${version}...</div>`;
}
if (responseDiv) { responseDiv.innerHTML = '<span class="text-muted">Processing...</span>'; }
if (reportActions) { reportActions.style.display = 'none'; }
const internalApiKey = {{ api_key | default("") | tojson }};
try {
// API Fetch
const response = await fetch('/api/push-ig', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
body: JSON.stringify({
package_name: packageName,
version: version,
fhir_server_url: fhirServerUrl,
include_dependencies: include_dependencies,
auth_type: auth_type,
auth_token: auth_token,
username: username,
password: password,
resource_types_filter: resource_types_filter,
skip_files: skip_files,
dry_run: dry_run,
verbose: isVerboseChecked,
force_upload: force_upload
})
});
if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`); }
if (!response.body) { throw new Error("Response body is missing."); }
// Stream Processing
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-light'; let prefix = '[INFO]'; let shouldDisplay = false;
switch (data.type) {
case 'start': case 'error': case 'complete': shouldDisplay = true; break;
case 'success': case 'warning': case 'info': case 'progress': if (isVerboseChecked) { shouldDisplay = true; } break;
default: if (isVerboseChecked) { shouldDisplay = true; console.warn("Unknown type:", data.type); prefix = '[UNKNOWN]'; } break;
}
if (shouldDisplay && liveConsole) {
if(data.type==='error'){prefix='[ERROR]';messageClass='text-danger';}
else if(data.type==='complete'){const s=data.data?.status||'info';if(s==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(s==='partial'){prefix='[PARTIAL]';messageClass='text-warning';}else{prefix='[ERROR]';messageClass='text-danger';}}
else if(data.type==='start'){prefix='[START]';messageClass='text-info';}
else if(data.type==='success'){prefix='[SUCCESS]';messageClass='text-success';}
else if(data.type==='warning'){prefix='[WARNING]';messageClass='text-warning';}
else if(data.type==='info'){prefix='[INFO]';messageClass='text-info';}
else{prefix='[PROGRESS]';messageClass='text-light';}
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
}
if (data.type === 'complete' && responseDiv) {
const summaryData = data.data || {}; let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
const fileFilterUsed = summaryData.skip_files_filter ? summaryData.skip_files_filter.join(', ') : 'None';
if (summaryData.pushed_packages_summary?.length > 0) { pushedPkgs = summaryData.pushed_packages_summary.map(p => `${sanitizeText(p.id)} (${sanitizeText(p.resource_count)} resources)`).join('<br>'); }
if (summaryData.status === 'success') { alertClass = 'alert-success'; statusText = 'Success';} else if (summaryData.status === 'partial') { alertClass = 'alert-warning'; statusText = 'Partial Success'; } else { alertClass = 'alert-danger'; statusText = 'Error'; }
if (summaryData.failed_details?.length > 0) { failHtml = '<hr><strong>Failures:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.failed_details.forEach(f => {failHtml += `<li><strong>${sanitizeText(f.resource)}:</strong> ${sanitizeText(f.error)}</li>`;}); failHtml += '</ul>';}
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
if (reportActions) { reportActions.style.display = 'block'; }
}
} catch (parseError) {
console.error('Stream parse error:', parseError);
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Stream parse error: ${sanitizeText(parseError.message)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
}
}
}
// Process Final Buffer
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
if (data.type === 'complete' && responseDiv) {
// Same summary rendering as above
const summaryData = data.data || {};
let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
const fileFilterUsed = summaryData.skip_files_filter ? summaryData.skip_files_filter.join(', ') : 'None';
if (summaryData.pushed_packages_summary?.length > 0) { pushedPkgs = summaryData.pushed_packages_summary.map(p => `${sanitizeText(p.id)} (${sanitizeText(p.resource_count)} resources)`).join('<br>'); }
if (summaryData.status === 'success') { alertClass = 'alert-success'; statusText = 'Success';} else if (summaryData.status === 'partial') { alertClass = 'alert-warning'; statusText = 'Partial Success'; } else { alertClass = 'alert-danger'; statusText = 'Error'; }
if (summaryData.failed_details?.length > 0) { failHtml = '<hr><strong>Failures:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.failed_details.forEach(f => {failHtml += `<li><strong>${sanitizeText(f.resource)}:</strong> ${sanitizeText(f.error)}</li>`;}); failHtml += '</ul>';}
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
if (reportActions) { reportActions.style.display = 'block'; }
}
} catch (parseError) {
console.error('Final buffer parse error:', parseError);
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Final buffer parse error: ${sanitizeText(parseError.message)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
}
}
} catch (error) {
console.error("Push operation failed:", error);
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] ${sanitizeText(error.message || error)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
if (responseDiv) {
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message || error)}</div>`;
}
if (reportActions) { reportActions.style.display = 'none'; }
} finally {
if (pushButton) {
pushButton.disabled = false;
pushButton.innerHTML = '<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server';
}
}
});
} else { console.error("Push IG Form element not found."); }
});
</script>
{% endblock %}