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
461 lines
30 KiB
HTML
461 lines
30 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_form_helpers.html" import render_field %}
|
|
|
|
{% block extra_head %} {# Assuming base.html has an 'extra_head' block for additional head elements #}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fire-animation.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{# Main page content for searching and importing packages #}
|
|
|
|
<div class="container">
|
|
{# Flash messages area - Ensure _flash_messages.html exists and is included in base.html or rendered here #}
|
|
{% include "_flash_messages.html" %}
|
|
|
|
{# Display warning if package fetching failed on initial load #}
|
|
{% if fetch_failed %}
|
|
<div class="alert alert-warning">
|
|
Unable to fetch packages from registries. Showing a fallback list. Please try again later or contact support.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
{# Left Column: Search Packages Area #}
|
|
<div class="col-md-9 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
{# Header with Refresh Button #}
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2><i class="bi bi-search me-2"></i>Search Packages</h2>
|
|
<button class="btn btn-sm btn-outline-warning" id="clear-cache-btn"
|
|
title="Clear package cache and fetch fresh list"
|
|
hx-post="{{ url_for('refresh_cache_task') }}"
|
|
hx-indicator="#cache-spinner"
|
|
hx-swap="none"
|
|
hx-target="this">
|
|
<i class="bi bi-arrow-clockwise"></i> Clear & Refresh Cache
|
|
<span id="cache-spinner" class="spinner-border spinner-border-sm ms-1" role="status" aria-hidden="true" style="display: none;"></span>
|
|
</button>
|
|
</div>
|
|
|
|
{# Cache Status Timestamp #}
|
|
<div class="mb-3 text-muted" style="font-size: 0.85em;">
|
|
{% if last_cached_timestamp %}
|
|
Package list last fetched: {{ last_cached_timestamp.strftime('%Y-%m-%d %H:%M:%S %Z') if last_cached_timestamp else 'Never' }}
|
|
{% if fetch_failed %} (Fetch Failed){% endif %}
|
|
{% elif is_fetching %} {# Show text spinner specifically during initial fetch state triggered by backend #}
|
|
Fetching package list... <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
{% else %}
|
|
Never fetched or cache cleared.
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# Search Input with HTMX #}
|
|
<div class="input-group mb-3">
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
<input type="search" class="form-control" placeholder="Search packages..." name="search" autofocus
|
|
hx-get="{{ url_for('api_search_packages') }}" hx-indicator=".htmx-indicator" hx-target="#search-results" hx-trigger="input changed delay:500ms, search">
|
|
<span class="input-group-text htmx-indicator"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></span>
|
|
</div>
|
|
<div id="search-error" class="alert alert-danger d-none"></div>
|
|
|
|
{# Search Results Area (populated by HTMX) #}
|
|
<div id="search-results">
|
|
{% include '_search_results_table.html' %} {# Includes the initial table state or updated results #}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Right Column: Import Form, Log Window, Animation, Warning #}
|
|
<div class="col-md-3 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
{# Import Form #}
|
|
<h2><i class="bi bi-download me-2"></i>Import a New IG</h2>
|
|
<form id="import-ig-form"> {# Form ID used by JS #}
|
|
{{ form.hidden_tag() }} {# Include CSRF token if using Flask-WTF #}
|
|
{{ render_field(form.package_name, class="form-control") }}
|
|
{{ render_field(form.package_version, class="form-control") }}
|
|
{{ render_field(form.dependency_mode, class="form-select") }}
|
|
<div class="d-grid gap-2 d-sm-flex mt-3">
|
|
{# Import Button triggers HTMX POST #}
|
|
<button class="btn btn-success" id="submit-btn"
|
|
hx-post="{{ url_for('import_ig') }}"
|
|
hx-indicator="#import-spinner"
|
|
hx-include="closest form"
|
|
hx-swap="none">
|
|
Import
|
|
</button>
|
|
<span id="import-spinner" class="spinner-border spinner-border-sm align-middle ms-2" role="status" aria-hidden="true" style="display: none;"></span>
|
|
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a> {# Simple link back #}
|
|
</div>
|
|
</form>
|
|
|
|
{# Live Log Output Window #}
|
|
<div class="log-window mt-4">
|
|
<h5><i class="bi bi-terminal me-2"></i>Live Log Output</h5>
|
|
<div id="log-output" class="log-output">
|
|
<p class="text-muted small">Logs from caching or import actions will appear here.</p>
|
|
</div>
|
|
{# Indicator shown while connecting to SSE #}
|
|
<div id="log-loading-indicator" class="mt-2 text-muted small" style="display: none;">
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Connecting to log stream...
|
|
</div>
|
|
</div>
|
|
|
|
{# Animation Window (Hidden by default) #}
|
|
<div id="log-animation-window" class="minifire-container" style="display: none;">
|
|
{# Status Text Overlay #}
|
|
<div id="animation-status-text" class="minifire-status-text"></div>
|
|
{# Campfire Animation HTML Structure #}
|
|
<div class="minifire-campfire">
|
|
<div class="minifire-sparks">
|
|
<div class="minifire-spark"></div> <div class="minifire-spark"></div> <div class="minifire-spark"></div> <div class="minifire-spark"></div>
|
|
<div class="minifire-spark"></div> <div class="minifire-spark"></div> <div class="minifire-spark"></div> <div class="minifire-spark"></div>
|
|
</div>
|
|
<div class="minifire-logs">
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
<div class="minifire-log"><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div><div class="minifire-streak"></div></div>
|
|
</div>
|
|
<div class="minifire-sticks">
|
|
<div class="minifire-stick"></div> <div class="minifire-stick"></div> <div class="minifire-stick"></div> <div class="minifire-stick"></div>
|
|
</div>
|
|
<div class="minifire-fire">
|
|
<div class="minifire-fire__red"> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> </div>
|
|
<div class="minifire-fire__orange"> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> </div>
|
|
<div class="minifire-fire__yellow"> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> </div>
|
|
<div class="minifire-fire__white"> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> <div class="minifire-flame"></div> </div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Warning Text (Hidden by default) #}
|
|
<div id="process-warning-text" class="import-warning-text" style="display: none; margin-top: 1rem;">
|
|
DO NOT LEAVE PAGE<br>
|
|
UNTIL COMPLETED<br>
|
|
!ANIMATION DISAPEARS!
|
|
</div>
|
|
|
|
</div> {# End card-body #}
|
|
</div> {# End card #}
|
|
</div> {# End col-md-3 #}
|
|
</div> {# End row #}
|
|
</div> {# End container #}
|
|
|
|
<style>
|
|
/* Internal CSS Styles */
|
|
.log-window { margin-top: 1.5rem; }
|
|
.log-output {
|
|
background-color: #1e2228; border: 1px solid #444; border-radius: 4px; padding: 10px;
|
|
height: 250px; overflow-y: auto; font-family: 'Courier New', Courier, monospace;
|
|
font-size: 0.85rem; color: #e0e0e0; white-space: pre-wrap; word-wrap: break-word;
|
|
}
|
|
.log-output p { margin: 0; padding: 1px 0; border-bottom: 1px solid #333; line-height: 1.3; }
|
|
.log-output p:last-child { border-bottom: none; }
|
|
.log-output .log-timestamp { color: #777; margin-right: 8px; }
|
|
.log-output .text-muted.small { color: #888 !important; font-style: italic; border-bottom: none; }
|
|
|
|
/* Animation Status Text (from fire-animation.css or here) */
|
|
.minifire-status-text {
|
|
position: absolute; top: 8px; left: 0; width: 100%; text-align: center;
|
|
color: #f0f0f0; font-size: 0.85em; font-weight: bold; z-index: 60;
|
|
pointer-events: none; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); line-height: 1.2;
|
|
}
|
|
|
|
/* Warning Text Style */
|
|
.import-warning-text {
|
|
color: #dc3545; /* Bootstrap danger color */
|
|
font-style: italic;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
font-size: 0.9em;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
/* Other general styles */
|
|
.text-muted { color: #ccc !important; font-size: 1.1rem; margin-top: 1rem; }
|
|
.table, .card { background-color: #2a2e34; color: #fff; }
|
|
.table th, .table td { border-color: #444; }
|
|
.htmx-indicator { display: none; }
|
|
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline-block; opacity: 1; }
|
|
#cache-spinner, #import-spinner { vertical-align: text-bottom; }
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Check if HTMX is loaded (essential for button actions)
|
|
if (typeof htmx === 'undefined') {
|
|
console.error('HTMX is not loaded. Button functionality will be disabled.');
|
|
const searchErrorDiv = document.getElementById('search-error');
|
|
if(searchErrorDiv) {
|
|
searchErrorDiv.textContent = 'Error: Core page functionality failed to load. Refresh or contact support.';
|
|
searchErrorDiv.classList.remove('d-none');
|
|
}
|
|
// Disable interactive elements that rely on HTMX
|
|
document.getElementById('clear-cache-btn')?.setAttribute('disabled', 'true');
|
|
document.getElementById('submit-btn')?.setAttribute('disabled', 'true');
|
|
document.querySelector('input[name="search"]')?.setAttribute('disabled', 'true');
|
|
return; // Stop script execution
|
|
}
|
|
|
|
// Get references to frequently used DOM elements
|
|
const logOutput = document.getElementById('log-output');
|
|
const logLoadingIndicator = document.getElementById('log-loading-indicator');
|
|
const logAnimationWindow = document.getElementById('log-animation-window');
|
|
const animationStatusText = document.getElementById('animation-status-text');
|
|
const processWarningText = document.getElementById('process-warning-text'); // Get warning text element
|
|
let currentEventSource = null; // Variable to hold the active Server-Sent Events connection
|
|
|
|
// --- Helper Functions ---
|
|
|
|
// Appends a formatted log message to the log output window
|
|
function appendLogMessage(message) {
|
|
if (!logOutput) return;
|
|
const initialMsg = logOutput.querySelector('.text-muted.small'); // Find placeholder
|
|
if (initialMsg) initialMsg.remove(); // Remove placeholder on first message
|
|
|
|
const timestamp = new Date().toLocaleTimeString(); // Get current time for timestamp
|
|
const logEntry = document.createElement('p'); // Create paragraph for the log line
|
|
const messageText = String(message).replace(/^INFO:services:/, '').replace(/^INFO:app:/, '').trim(); // Clean message
|
|
|
|
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span>`; // Add timestamp
|
|
logEntry.appendChild(document.createTextNode(messageText)); // Add log message text safely
|
|
|
|
// Style error/warning messages
|
|
if (messageText.startsWith("ERROR:") || messageText.startsWith("CRITICAL ERROR:")) {
|
|
logEntry.style.color = "#f8d7da"; logEntry.style.backgroundColor = "#49272a";
|
|
} else if (messageText.startsWith("WARNING:")) {
|
|
logEntry.style.color = "#fff3cd";
|
|
}
|
|
|
|
logOutput.appendChild(logEntry); // Add the log line to the window
|
|
logOutput.scrollTop = logOutput.scrollHeight; // Scroll to the bottom
|
|
}
|
|
|
|
// Clears the log window and resets the placeholder text
|
|
function clearLogWindow() {
|
|
if (logOutput) logOutput.innerHTML = '<p class="text-muted small">Waiting for logs...</p>';
|
|
}
|
|
|
|
// Shows the animation window and sets the status text
|
|
function showAnimation(statusText = '') {
|
|
if (logAnimationWindow) logAnimationWindow.style.display = 'flex'; // Use flex to utilize justify/align center for campfire
|
|
if (animationStatusText) animationStatusText.textContent = statusText; // Update status text
|
|
if (processWarningText) processWarningText.style.display = 'block'; // Show the warning text
|
|
}
|
|
|
|
// Hides the animation window and clears status/warning text
|
|
function hideAnimation() {
|
|
if (logAnimationWindow) logAnimationWindow.style.display = 'none';
|
|
if (animationStatusText) animationStatusText.textContent = '';
|
|
if (processWarningText) processWarningText.style.display = 'none'; // Hide the warning text
|
|
}
|
|
|
|
// --- Server-Sent Events (SSE) Setup Function ---
|
|
// Connects to the log stream and handles incoming messages
|
|
function setupLogStream(onDoneCallback) {
|
|
if (currentEventSource) { currentEventSource.close(); currentEventSource = null; } // Close existing connection
|
|
clearLogWindow(); // Clear previous logs
|
|
if (logLoadingIndicator) logLoadingIndicator.style.display = 'block'; // Show "Connecting..."
|
|
// Note: showAnimation() is now called by the function initiating the action
|
|
|
|
try {
|
|
currentEventSource = new EventSource("{{ url_for('stream_import_logs') }}"); // Establish connection
|
|
|
|
// On successful connection opening
|
|
currentEventSource.onopen = function() {
|
|
console.log("SSE connection opened.");
|
|
if (logLoadingIndicator) logLoadingIndicator.style.display = 'none'; // Hide "Connecting..."
|
|
// Animation should already be visible here if started correctly
|
|
};
|
|
|
|
// On receiving a message from the server
|
|
currentEventSource.onmessage = function(event) {
|
|
if (logLoadingIndicator) logLoadingIndicator.style.display = 'none'; // Hide indicator if still visible
|
|
// Keep animation visible
|
|
|
|
// Check for the special [DONE] signal
|
|
if (event.data === '[DONE]') {
|
|
console.log("SSE stream finished ([DONE] received).");
|
|
hideAnimation(); // Hide animation and warning text
|
|
const lastLog = logOutput.lastElementChild?.textContent || ""; // Check last log for errors
|
|
const hadError = lastLog.includes("ERROR:") || lastLog.includes("CRITICAL ERROR:");
|
|
if (!hadError) { appendLogMessage("Operation complete."); } // Log completion if no errors seen
|
|
|
|
if (currentEventSource) { currentEventSource.close(); currentEventSource = null; } // Close SSE
|
|
if (onDoneCallback) onDoneCallback(true, hadError); // Trigger callback (success=true)
|
|
} else {
|
|
appendLogMessage(event.data); // Log the received message
|
|
}
|
|
};
|
|
|
|
// On SSE connection error
|
|
currentEventSource.onerror = function(err) {
|
|
console.error('SSE connection error:', err);
|
|
if (logLoadingIndicator) logLoadingIndicator.style.display = 'none';
|
|
hideAnimation(); // Hide animation and warning text on error
|
|
appendLogMessage("ERROR: Log streaming connection error or closed prematurely.");
|
|
if (currentEventSource) { currentEventSource.close(); currentEventSource = null; }
|
|
if (onDoneCallback) onDoneCallback(false, true); // Trigger callback (success=false, error=true)
|
|
};
|
|
|
|
} catch (e) { // On error initializing EventSource
|
|
console.error('Failed to initialize SSE:', e);
|
|
if (logLoadingIndicator) logLoadingIndicator.style.display = 'none';
|
|
hideAnimation(); // Hide animation and warning text on failure
|
|
appendLogMessage("ERROR: Unable to establish log stream: " + (e.message || 'Unknown Error'));
|
|
if (onDoneCallback) onDoneCallback(false, true); // Trigger callback (success=false, error=true)
|
|
}
|
|
}
|
|
|
|
// --- Page Load / Initial State Logic ---
|
|
|
|
// Check if the backend flagged that an initial fetch is happening
|
|
const isFetchingInitial = {{ is_fetching | tojson }};
|
|
if (isFetchingInitial) {
|
|
console.log("Initial fetch detected, setting up log stream.");
|
|
showAnimation('Refreshing cache...'); // Show animation and "Refreshing" text immediately
|
|
setupLogStream(function(success, streamHadError) {
|
|
console.log(`Initial fetch log stream ended. Success: ${success}, Stream Error: ${streamHadError}`);
|
|
hideAnimation(); // Hide animation when stream ends
|
|
// Backend handles the page update after its fetch finishes
|
|
});
|
|
}
|
|
|
|
// --- Event Listeners for Buttons ---
|
|
|
|
// Listener for "Clear & Refresh Cache" Button (HTMX Triggered)
|
|
const clearCacheBtn = document.getElementById('clear-cache-btn');
|
|
const cacheSpinner = document.getElementById('cache-spinner');
|
|
if (clearCacheBtn) {
|
|
// Before HTMX sends the request to start the cache refresh task
|
|
clearCacheBtn.addEventListener('htmx:beforeRequest', function(evt) {
|
|
console.log("HTMX Clear Cache request starting.");
|
|
clearCacheBtn.disabled = true; if(cacheSpinner) cacheSpinner.style.display = 'inline-block';
|
|
showAnimation('Refreshing cache...'); // Show animation and set text
|
|
// Setup the log stream, providing the callback for when SSE finishes
|
|
setupLogStream(function(success, streamHadError) {
|
|
// This runs ONLY when the SSE stream sends [DONE] or errors out
|
|
console.log(`Clear cache log stream ended. Success: ${success}, Stream Error: ${streamHadError}`);
|
|
hideAnimation(); // Hide animation and warning text
|
|
clearCacheBtn.disabled = false; if(cacheSpinner) cacheSpinner.style.display = 'none'; // Re-enable button/hide spinner
|
|
|
|
// Determine message and reload page
|
|
if (success && !streamHadError) { appendLogMessage("Cache refresh complete. Reloading page..."); }
|
|
else if (success && streamHadError) { appendLogMessage("Cache refresh finished with errors. Reloading page..."); }
|
|
else { appendLogMessage("ERROR: Cache refresh failed or log stream error. Reloading page..."); }
|
|
setTimeout(() => window.location.reload(), 750); // Reload after slight delay
|
|
});
|
|
});
|
|
|
|
// After HTMX gets the *initial* response from the API (should be 202 Accepted)
|
|
clearCacheBtn.addEventListener('htmx:afterRequest', function(evt) {
|
|
console.log('HTMX Clear Cache API response received. Status:', evt.detail.xhr.status);
|
|
if (evt.detail.failed || evt.detail.xhr.status >= 400) {
|
|
// The API call itself failed to start the task
|
|
hideAnimation(); // Hide animation/warning immediately
|
|
console.error("Failed to trigger cache refresh API:", evt.detail);
|
|
appendLogMessage(`ERROR: Could not start cache refresh (Server status: ${evt.detail.xhr.status})`);
|
|
clearCacheBtn.disabled = false; if(cacheSpinner) cacheSpinner.style.display = 'none'; // Reset button
|
|
// Close SSE stream if it was prematurely opened
|
|
if (currentEventSource) { currentEventSource.close(); currentEventSource = null; appendLogMessage("Log stream closed."); }
|
|
} else if (evt.detail.xhr.status === 202) {
|
|
// Success: Task started in background
|
|
appendLogMessage("Cache refresh process started in background...");
|
|
// Keep animation and warning text visible, waiting for SSE logs
|
|
} else {
|
|
// Unexpected success status from API
|
|
hideAnimation(); // Hide animation/warning
|
|
appendLogMessage(`Warning: Unexpected server response ${evt.detail.xhr.status}. Refresh may not have started.`);
|
|
clearCacheBtn.disabled = false; if(cacheSpinner) cacheSpinner.style.display = 'none';
|
|
if (currentEventSource) { currentEventSource.close(); currentEventSource = null; } // Close stream
|
|
}
|
|
});
|
|
}
|
|
|
|
// Listener for Import Button (HTMX Triggered)
|
|
const importForm = document.getElementById('import-ig-form');
|
|
const importSubmitBtn = document.getElementById('submit-btn');
|
|
const importSpinner = document.getElementById('import-spinner');
|
|
if (importForm && importSubmitBtn) {
|
|
// Before HTMX sends the import request
|
|
importSubmitBtn.addEventListener('htmx:beforeRequest', function(evt) {
|
|
console.log('HTMX import request starting.');
|
|
importSubmitBtn.disabled = true; if(importSpinner) importSpinner.style.display = 'inline-block';
|
|
showAnimation('Importing...'); // Show animation and set "Importing..." text
|
|
// Setup log stream, callback runs when SSE part finishes
|
|
setupLogStream(function(success, streamHadError) {
|
|
console.log(`Import log stream ended. Success: ${success}, Stream Error: ${streamHadError}`);
|
|
hideAnimation(); // Hide animation/warning when SSE finishes
|
|
// Note: Redirect or final status is handled in 'afterRequest' below
|
|
if (!success || streamHadError) { appendLogMessage("Warning: Log stream for import finished with errors or failed."); }
|
|
});
|
|
});
|
|
|
|
// After HTMX gets the response from the import API call
|
|
importSubmitBtn.addEventListener('htmx:afterRequest', function(evt) {
|
|
console.log('HTMX import request finished. Status:', evt.detail.xhr.status);
|
|
importSubmitBtn.disabled = false; if(importSpinner) importSpinner.style.display = 'none'; // Reset button/spinner
|
|
|
|
// Ensure animation/warning are hidden if the main request fails quickly
|
|
if (evt.detail.failed) hideAnimation();
|
|
|
|
// Check SSE state (though it should manage its own closure)
|
|
if (currentEventSource) console.warn("SSE connection might still be open after import HTMX request completed.");
|
|
|
|
// Handle failed import API request
|
|
if (evt.detail.failed) {
|
|
appendLogMessage(`ERROR: Import request failed (Status: ${evt.detail.xhr.status})`);
|
|
try { const errorData = JSON.parse(evt.detail.xhr.responseText); if (errorData?.message) appendLogMessage(` -> ${errorData.message}`); } catch(e) {}
|
|
} else {
|
|
// Handle successful import API request (parse JSON response for status/redirect)
|
|
try {
|
|
const data = JSON.parse(evt.detail.xhr.responseText);
|
|
console.log("Import response data:", data);
|
|
|
|
// --- REDIRECT/STATUS LOGIC ---
|
|
if (data?.status === 'success' && data.redirect) {
|
|
appendLogMessage("Import successful. Redirecting...");
|
|
setTimeout(() => { window.location.href = data.redirect; }, 1000); // Redirect after 1s
|
|
} else if (data?.status === 'warning') {
|
|
appendLogMessage(`Warning: ${data.message || 'Import partially successful.'}`); htmx.process(document.body); // Process flash messages if swapped
|
|
} else if (data?.status === 'error') {
|
|
appendLogMessage(`ERROR: ${data.message || 'Import failed.'}`); htmx.process(document.body); // Process flash messages if swapped
|
|
} else {
|
|
appendLogMessage("Import request completed (unexpected response format)."); htmx.process(document.body); // Process flash messages if swapped
|
|
}
|
|
// --- END REDIRECT/STATUS LOGIC ---
|
|
|
|
} catch (e) { // Handle JSON parsing error
|
|
console.error("Error parsing import response JSON:", e);
|
|
appendLogMessage("ERROR: Could not process server response after import.");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- HTMX Search Event Listener (No changes needed) ---
|
|
htmx.on('htmx:afterRequest', function(evt) {
|
|
if (evt.detail.requestConfig?.path?.includes('api/search-packages')) { /* ... existing search error handling ... */ }
|
|
});
|
|
htmx.on('htmx:afterSwap', function(evt) { console.log("HTMX content swapped for target:", evt.detail.target.id); });
|
|
|
|
// --- Page Unload Cleanup ---
|
|
// Close SSE connection if user navigates away or closes tab
|
|
window.addEventListener('beforeunload', () => {
|
|
if (currentEventSource) {
|
|
console.log("Page unloading, closing SSE connection.");
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
}
|
|
});
|
|
|
|
}); // End DOMContentLoaded
|
|
</script>
|
|
{% endblock %} |