Remove Hapi validaotr
replace with Hl7 Validator
restructure validation report
provide Json and Html validation reports
remove hardcoded local references and couple to ENV allowing custom env Fhir Server URl to be set for local ""instance:
This commit is contained in:
Joshua Hare 2025-08-26 21:15:24 +10:00
parent 5725f8a22f
commit 236390e57b
10 changed files with 986 additions and 671 deletions

View File

@ -12,6 +12,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# REMOVED pip install fhirpath from this line
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
WORKDIR /app
RUN python3 -m venv /app/venv

66
app.py
View File

@ -29,6 +29,7 @@ import re
import yaml
import threading
import time # Add time import
import io
import services
from services import (
services_bp,
@ -44,9 +45,12 @@ from services import (
pkg_version,
get_package_description,
safe_parse_version,
import_manual_package_and_dependencies
import_manual_package_and_dependencies,
parse_and_list_igs,
generate_ig_yaml,
apply_and_validate
)
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
from wtforms import SubmitField
from package import package_bp
from flasgger import Swagger, swag_from # Import Flasgger
@ -3133,7 +3137,65 @@ def package_details_view(name):
package_name=actual_package_name,
latest_official_version=latest_official_version_str)
@app.route('/ig-configurator', methods=['GET', 'POST'])
def ig_configurator():
form = IgYamlForm()
packages_dir = current_app.config['FHIR_PACKAGES_DIR']
loaded_igs = services.parse_and_list_igs(packages_dir)
form.igs.choices = [(name, f"{name} ({info['version']})") for name, info in loaded_igs.items()]
yaml_output = None
if form.validate_on_submit():
selected_ig_names = form.igs.data
try:
yaml_output = generate_ig_yaml(selected_ig_names, loaded_igs)
flash("YAML configuration generated successfully!", "success")
except Exception as e:
flash(f"Error generating YAML: {str(e)}", "error")
logger.error(f"Error generating YAML: {str(e)}", exc_info=True)
return render_template('ig_configurator.html', form=form, yaml_output=yaml_output)
@app.route('/download/<path:filename>')
def download_file(filename):
"""
Serves a file from the temporary directory.
This route is necessary to allow the user to download the validator reports.
"""
# The temporary directory path is hardcoded for security
temp_dir = tempfile.gettempdir()
file_path = os.path.join(temp_dir, filename)
if not os.path.exists(file_path):
return jsonify({"error": "File not found."}), 404
try:
# Use send_file with as_attachment=True to trigger a download
return send_file(file_path, as_attachment=True, download_name=filename)
except Exception as e:
logger.error(f"Error serving file {file_path}: {str(e)}")
return jsonify({"error": "Error serving file.", "details": str(e)}), 500
@app.route('/api/clear-validation-results', methods=['POST'])
@csrf.exempt
def clear_validation_results():
"""
Clears the temporary directory containing the last validation run's results.
"""
temp_dir = current_app.config.get('VALIDATION_TEMP_DIR')
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
current_app.config['VALIDATION_TEMP_DIR'] = None
return jsonify({"status": "success", "message": f"Results cleared successfully."})
except Exception as e:
logger.error(f"Failed to clear validation results at {temp_dir}: {e}")
return jsonify({"status": "error", "message": f"Failed to clear results: {e}"}), 500
else:
return jsonify({"status": "success", "message": "No results to clear."})
@app.route('/favicon.ico')
def favicon():

View File

@ -18,5 +18,5 @@ services:
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=standalone
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=http://localhost:8080/fhir
- HAPI_FHIR_URL=https://fhir.hl7.org.au/aucore/fhir/DEFAULT/
command: supervisord -c /etc/supervisord.conf

View File

@ -1,6 +1,6 @@
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField, SelectMultipleField
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
from flask import request
import json
@ -307,3 +307,8 @@ class FhirRequestForm(FlaskForm):
self.basic_auth_password.errors.append('Password is required for Basic Authentication with a custom URL.')
return False
return True
class IgYamlForm(FlaskForm):
"""Form to select IGs for YAML generation."""
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
generate_yaml = SubmitField('Generate YAML')

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -796,6 +796,9 @@
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'ig_configurator' else '' }}" href="{{ url_for('ig_configurator') }}"><i class="fas fa-wrench me-1"></i> IG Valid-Conf Gen</a>
</li>
{% if app_mode != 'lite' %}
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>IG Configuration Generator</h2>
<p>Select the Implementation Guides you want to load into HAPI for validation. This will generate the YAML configuration required for the <code>application.yaml</code> file.</p>
<div class="card mb-4">
<div class="card-header">Select IGs</div>
<div class="card-body">
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label">IGs and their dependencies</label>
{% for choice in form.igs.choices %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="igs" value="{{ choice[0] }}" id="ig-{{ choice[0] }}">
<label class="form-check-label" for="ig-{{ choice[0] }}">{{ choice[1] }}</label>
</div>
{% endfor %}
</div>
{{ form.generate_yaml(class="btn btn-primary") }}
</form>
</div>
</div>
{% if yaml_output %}
<div class="card mt-4">
<div class="card-header">Generated <code>application.yaml</code> Excerpt</div>
<div class="card-body">
<p>Copy the following configuration block and paste it into the `hapi.fhir` section of your HAPI server's <code>application.yaml</code> file.</p>
<pre><code class="language-yaml">{{ yaml_output }}</code></pre>
<div class="alert alert-warning mt-3">
<strong>Important:</strong> After updating the <code>application.yaml</code> file, you must **restart your HAPI server** for the changes to take effect.
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,4 +1,3 @@
<!-- templates/validate_sample.html -->
{% extends "base.html" %}
{% block content %}
@ -7,14 +6,15 @@
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
Validate a FHIR resource or bundle against a selected Implementation Guide.
</p>
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
</div>
----------------------------------------------------------------remove the buttons----------------------------------------------------->
</div>
<div id="spinner-overlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-flex align-items-center justify-content-center" style="z-index: 1050;">
<div class="text-center text-white">
<div id="spinner-animation" style="width: 200px; height: 200px;"></div>
<p class="mt-3" id="spinner-message">Waiting, don't leave this page...</p>
</div>
</div>
@ -71,7 +71,10 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
<button type="button" class="btn btn-secondary d-none" id="clearResultsButton">Clear Results</button>
</div>
</form>
</div>
</div>
@ -79,72 +82,103 @@
<div class="card mt-4" id="validationResult" style="display: none;">
<div class="card-header">Validation Report</div>
<div class="card-body" id="validationContent">
<!-- Results will be populated dynamically -->
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/lottie.min.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- Global Variables and Helper Functions ---
let currentLottieAnimation = null;
let pollInterval = null;
const spinnerOverlay = document.getElementById('spinner-overlay');
const spinnerMessage = document.getElementById('spinner-message');
const spinnerContainer = document.getElementById('spinner-animation');
// Function to load the Lottie animation based on theme
function loadLottieAnimation(theme) {
if (currentLottieAnimation) {
currentLottieAnimation.destroy();
currentLottieAnimation = null;
}
const animationPath = (theme === 'dark')
? '{{ url_for('static', filename='animations/Validation abstract.json') }}'
: '{{ url_for('static', filename='animations/Validation abstract.json') }}';
try {
currentLottieAnimation = lottie.loadAnimation({
container: spinnerContainer,
renderer: 'svg',
loop: true,
autoplay: true,
path: animationPath
});
} catch(lottieError) {
console.error("Error loading Lottie animation:", lottieError);
}
}
// Function to show/hide the spinner with a custom message
function toggleSpinner(show, message = "Running validator...") {
if (show) {
spinnerMessage.textContent = message;
spinnerOverlay.classList.remove('d-none');
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
loadLottieAnimation(currentTheme);
} else {
spinnerOverlay.classList.add('d-none');
if (currentLottieAnimation) {
currentLottieAnimation.destroy();
currentLottieAnimation = null;
}
}
}
// --- Frontend logic starts here
document.addEventListener('DOMContentLoaded', function() {
const packageSelect = document.getElementById('packageSelect');
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
const versionInput = document.getElementById('{{ form.version.id }}');
const validateButton = document.getElementById('validateButton');
const clearResultsButton = document.getElementById('clearResultsButton');
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
const validationResult = document.getElementById('validationResult');
const validationContent = document.getElementById('validationContent');
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
const perPage = 10;
// Debug DOM elements
console.log('Package Select:', packageSelect ? 'Found' : 'Missing');
console.log('Package Name Input:', packageNameInput ? 'Found' : 'Missing');
console.log('Version Input:', versionInput ? 'Found' : 'Missing');
// Update package_name and version fields from dropdown
function updatePackageFields(select) {
const value = select.value;
console.log('Selected package:', value);
if (!packageNameInput || !versionInput) {
console.error('Input fields not found');
return;
}
if (!packageNameInput || !versionInput) return;
if (value) {
const [name, version] = value.split('#');
if (name && version) {
packageNameInput.value = name;
versionInput.value = version;
console.log('Updated fields:', { packageName: name, version });
} else {
console.warn('Invalid package format:', value);
packageNameInput.value = '';
versionInput.value = '';
}
} else {
packageNameInput.value = '';
versionInput.value = '';
console.log('Cleared fields: no package selected');
}
}
// Initialize dropdown based on form values
if (packageNameInput && versionInput && packageNameInput.value && versionInput.value) {
if (packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
if (packageSelect.querySelector(`option[value="${currentValue}"]`)) {
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
packageSelect.value = currentValue;
console.log('Initialized dropdown with:', currentValue);
}
}
// Debug dropdown options
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => opt.value));
// Attach event listener for package selection
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
// Client-side JSON validation preview
if (sampleInput) {
sampleInput.addEventListener('input', function() {
try {
@ -162,149 +196,114 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Check HAPI server status
function checkHapiStatus() {
return fetch('/fhir/metadata', { method: 'GET' })
.then(response => {
if (!response.ok) {
throw new Error('HAPI server unavailable');
}
return true;
})
.catch(error => {
console.warn('HAPI status check failed:', error.message);
validationContent.innerHTML = '<div class="alert alert-warning">HAPI server unavailable; using local validation.</div>';
return false;
});
}
// Sanitize text to prevent XSS
function sanitizeText(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Validate button handler
if (validateButton) {
validateButton.addEventListener('click', function() {
const packageName = packageNameInput.value;
const version = versionInput.value;
const sampleData = sampleInput.value.trim();
const includeDependencies = document.getElementById('{{ form.include_dependencies.id }}').checked;
if (!packageName || !version) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please select a package and version.</div>';
// Tidy up the display by paginating results
function renderResultList(container, items, title, isWarnings = false) {
container.innerHTML = '';
if (items.length === 0) {
return;
}
if (!sampleData) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
return;
}
const wrapper = document.createElement('div');
const listId = `list-${title.toLowerCase()}`;
try {
JSON.parse(sampleData);
} catch (e) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
return;
}
let initialItems = items.slice(0, perPage);
let remainingItems = items.slice(perPage);
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="text-center py-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Validating...</span></div></div>';
const titleElement = document.createElement('h5');
titleElement.textContent = title;
wrapper.appendChild(titleElement);
// Check HAPI status before validation
checkHapiStatus().then(() => {
console.log('Sending request to /api/validate-sample with:', {
package_name: packageName,
version: version,
include_dependencies: includeDependencies,
sample_data_length: sampleData.length
const ul = document.createElement('ul');
ul.className = 'list-unstyled';
initialItems.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
ul.appendChild(li);
});
wrapper.appendChild(ul);
if (remainingItems.length > 0) {
const toggleButton = document.createElement('a');
toggleButton.href = '#';
toggleButton.className = 'btn btn-sm btn-link text-decoration-none';
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
const remainingUl = document.createElement('ul');
remainingUl.className = 'list-unstyled d-none';
remainingItems.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
remainingUl.appendChild(li);
});
fetch('/api/validate-sample', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ form.csrf_token._value() }}'
},
body: JSON.stringify({
package_name: packageName,
version: version,
include_dependencies: includeDependencies,
sample_data: sampleData
})
})
.then(response => {
console.log('Server response status:', response.status, response.statusText);
if (!response.ok) {
return response.text().then(text => {
console.error('Server response text:', text.substring(0, 200) + (text.length > 200 ? '...' : ''));
throw new Error(`HTTP error ${response.status}: ${text.substring(0, 100)}...`);
});
toggleButton.addEventListener('click', (e) => {
e.preventDefault();
if (remainingUl.classList.contains('d-none')) {
remainingUl.classList.remove('d-none');
toggleButton.textContent = 'Show Less';
} else {
remainingUl.classList.add('d-none');
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
}
return response.json();
})
.then(data => {
console.log('Validation response:', data);
});
wrapper.appendChild(toggleButton);
wrapper.appendChild(remainingUl);
}
container.appendChild(wrapper);
}
function displayFinalReport(data) {
validationContent.innerHTML = '';
let hasContent = false;
// Display profile information
if (data.results) {
const profileDiv = document.createElement('div');
profileDiv.className = 'mb-3';
const profiles = new Set();
Object.values(data.results).forEach(result => {
if (result.profile) profiles.add(result.profile);
});
if (profiles.size > 0) {
profileDiv.innerHTML = `
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
<ul>
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
</ul>
clearResultsButton.classList.remove('d-none');
if (data.file_paths) {
const jsonFileName = data.file_paths.json.split('/').pop();
const htmlFileName = data.file_paths.html.split('/').pop();
const downloadDiv = document.createElement('div');
downloadDiv.className = 'alert alert-info d-flex justify-content-between align-items-center';
downloadDiv.innerHTML = `
<span>Download Raw Reports:</span>
<div>
<a href="/download/${jsonFileName}" class="btn btn-sm btn-secondary me-2" download>Download JSON</a>
<a href="/download/${htmlFileName}" class="btn btn-sm btn-secondary" download>Download HTML</a>
</div>
`;
validationContent.appendChild(profileDiv);
validationContent.appendChild(downloadDiv);
hasContent = true;
}
}
// Display errors
if (data.errors && data.errors.length > 0) {
hasContent = true;
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger';
errorDiv.innerHTML = `
<h5>Errors</h5>
<ul>
${data.errors.map(e => `<li>${sanitizeText(typeof e === 'string' ? e : e.message)}</li>`).join('')}
</ul>
`;
validationContent.appendChild(errorDiv);
const errorContainer = document.createElement('div');
renderResultList(errorContainer, data.errors, 'Errors');
validationContent.appendChild(errorContainer);
}
// Display warnings
if (data.warnings && data.warnings.length > 0) {
hasContent = true;
const warningDiv = document.createElement('div');
warningDiv.className = 'alert alert-warning';
warningDiv.innerHTML = `
<h5>Warnings</h5>
<ul>
${data.warnings.map(w => {
const isMustSupport = w.includes('Must Support');
return `<li>${sanitizeText(typeof w === 'string' ? w : w.message)}${isMustSupport ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}</li>`;
}).join('')}
</ul>
`;
validationContent.appendChild(warningDiv);
const warningContainer = document.createElement('div');
renderResultList(warningContainer, data.warnings, 'Warnings', true);
validationContent.appendChild(warningContainer);
}
// Detailed results with collapsible sections
if (data.results) {
hasContent = true;
const resultsDiv = document.createElement('div');
@ -338,7 +337,7 @@ document.addEventListener('DOMContentLoaded', function() {
validationContent.appendChild(resultsDiv);
}
// Summary
if (data.summary) {
const summaryDiv = document.createElement('div');
summaryDiv.className = 'alert alert-info';
summaryDiv.innerHTML = `
@ -348,12 +347,12 @@ document.addEventListener('DOMContentLoaded', function() {
`;
validationContent.appendChild(summaryDiv);
hasContent = true;
}
if (!hasContent && data.valid) {
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
}
// Initialize Bootstrap collapse and tooltips
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
el.addEventListener('click', () => {
const target = document.querySelector(el.getAttribute('data-bs-target'));
@ -363,14 +362,135 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
new bootstrap.Tooltip(el);
});
})
.catch(error => {
console.error('Validation error:', error);
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
});
});
}
function checkValidationStatus(taskId) {
fetch(`/api/check-validation-status/${taskId}`)
.then(response => {
if (response.status === 202) {
console.log('Validation still in progress...');
return null;
} else if (response.status === 200) {
clearInterval(pollInterval);
toggleSpinner(false);
return response.json();
} else {
clearInterval(pollInterval);
toggleSpinner(false);
return response.json().then(errorData => {
throw new Error(errorData.message);
});
}
});
})
.then(data => {
if (data && data.status === 'complete') {
validationResult.style.display = 'block';
displayFinalReport(data.result);
}
})
.catch(error => {
clearInterval(pollInterval);
toggleSpinner(false);
console.error('Validation failed during polling:', error);
validationResult.style.display = 'block';
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
});
}
function runBlockingValidation() {
const packageName = packageNameInput.value;
const version = versionInput.value;
const sampleData = sampleInput.value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!packageName || !version) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please select a package and version.</div>';
return;
}
if (!sampleData) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
return;
}
try {
JSON.parse(sampleData);
} catch (e) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
return;
}
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
fetch('/api/validate-sample', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
package_name: packageName,
version: version,
sample_data: sampleData
})
})
.then(response => {
if (response.status === 202) {
return response.json();
} else {
toggleSpinner(false);
return response.json().then(errorData => {
throw new Error(errorData.message);
});
}
})
.then(data => {
if (data.status === 'accepted' && data.task_id) {
pollInterval = setInterval(() => checkValidationStatus(data.task_id), 3000);
} else {
toggleSpinner(false);
validationResult.style.display = 'block';
validationContent.innerHTML = `<div class="alert alert-danger">Server response format unexpected.</div>`;
}
})
.catch(error => {
clearInterval(pollInterval);
toggleSpinner(false);
console.error('Validation failed:', error);
validationResult.style.display = 'block';
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred: ${sanitizeText(error.message)}</div>`;
});
}
clearResultsButton.addEventListener('click', function() {
fetch('/api/clear-validation-results', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
validationResult.style.display = 'none';
clearResultsButton.classList.add('d-none');
} else {
console.error('Failed to clear results:', data.message);
}
})
.catch(error => {
console.error('Network error while clearing results:', error);
});
});
if (validateButton) {
validateButton.addEventListener('click', function() {
runBlockingValidation();
});
}
});
</script>
{% endblock %}