Incremental

Added large functionality to push Ig uploader and created a test data upload feature
This commit is contained in:
Joshua Hare 2025-04-24 23:38:08 +10:00
parent 3365255a5b
commit 8f2c90087d
20 changed files with 26744 additions and 2064 deletions

572
README.md
View File

@ -2,7 +2,7 @@
## Overview
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs). It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, and converting FHIR resources to FHIR Shorthand (FSH) using GoFSH with advanced features. The toolkit includes a live console for real-time feedback and a waiting spinner for FSH conversion, making it an essential tool for FHIR developers and implementers.
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), and uploading complex test data sets with dependency management. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
The application can run in two modes:
@ -29,17 +29,36 @@ This toolkit offers two primary installation modes to suit different needs:
## Features
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `1.1.2-ballot`, `current`).
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking).
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature, work in progress). *Note: Lite version uses local SD checks only.*
* **Push IGs:** Upload IG resources to a target FHIR server with real-time console output and optional validation against imposed profiles.
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI.
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature). *Note: Lite version uses local SD checks only.*
* **Push IGs:** Upload IG resources (and optionally dependencies) to a target FHIR server. Features include:
* Real-time console output.
* Authentication support (Bearer Token).
* Filtering by resource type or specific files to skip.
* Semantic comparison to skip uploading identical resources (override with **Force Upload** option).
* Correct handling of canonical resources (searching by URL/version before deciding POST/PUT).
* Dry run mode for simulation.
* Verbose logging option.
* **Upload Test Data:** Upload complex sets of test data (individual JSON/XML files or ZIP archives) to a target FHIR server. Features include:
* Robust parsing of JSON and XML (using `fhir.resources` library when available).
* Automatic dependency analysis based on resource references within the uploaded set.
* Topological sorting to ensure resources are uploaded in the correct order.
* Cycle detection in dependencies.
* Choice of individual resource uploads or a single transaction bundle.
* **Optional Pre-Upload Validation:** Validate resources against a selected profile package before uploading.
* **Optional Conditional Uploads (Individual Mode):** Check resource existence (GET) and use conditional `If-Match` headers for updates (PUT) or create resources (PUT/POST). Falls back to simple PUT if unchecked.
* Configurable error handling (stop on first error or continue).
* Authentication support (Bearer Token).
* Streaming progress log via the UI.
* Handles large numbers of files using a custom form parser.
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI (configurable).
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" and "FHIR UI Operations" pages. *Note: Lite version requires custom server URLs.*
* **API Support:** RESTful API endpoints for importing, pushing, and retrieving IG metadata.
* **Live Console:** Real-time logs for push, validation, and FSH conversion operations.
* **Configurable Behavior:** Enable/disable imposed profile validation and UI display of profile relationships.
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (simple GET/POST/PUT/DELETE) and "FHIR UI Operations" (Swagger-like interface based on CapabilityStatement). *Note: Lite version requires custom server URLs.*
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, and uploading test data.
* **Live Console:** Real-time logs for push, validation, upload test data, and FSH conversion operations.
* **Configurable Behavior:** Control validation modes, display options via `app.config`.
* **Theming:** Supports light and dark modes.
## Technology Stack
@ -50,7 +69,8 @@ This toolkit offers two primary installation modes to suit different needs:
* Docker, Docker Compose, Supervisor
* Node.js 18+ (for GoFSH/SUSHI), GoFSH, SUSHI
* HAPI FHIR (Standalone version only)
* Requests 2.31.0, Tarfile, Logging
* Requests 2.31.0, Tarfile, Logging, Werkzeug
* fhir.resources (optional, for robust XML parsing)
## Prerequisites
@ -66,6 +86,7 @@ This is the easiest way to get started without needing Git or Maven. Choose the
**Lite Version (No local HAPI FHIR):**
```bash
# Pull the latest Lite image
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
@ -81,7 +102,7 @@ docker run -d \
--name fhirflare-lite \
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
Standalone Version (Includes local HAPI FHIR):
# Pull the latest Standalone image
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
@ -99,10 +120,15 @@ docker run -d \
--name fhirflare-standalone \
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
Building from Source (Developers)
Using Windows .bat Scripts (Standalone Version Only):
First Time Setup:
Run Build and Run for first time.bat:
cd "<project folder>"
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
copy .\hapi-fhir-Setup\target\classes\application.yaml .\hapi-fhir-jpaserver\target\classes\application.yaml
mvn clean package -DskipTests=true -Pboot
docker-compose build --no-cache
@ -110,172 +136,236 @@ docker-compose up -d
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
Subsequent Runs:
Run Run.bat:
cd "<project folder>"
docker-compose up -d
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
Access the Application:
Flask UI: http://localhost:5000
HAPI FHIR server: http://localhost:8080
Manual Setup (Linux/MacOS/Windows)
Preparation:
Manual Setup (Linux/MacOS/Windows):
Preparation (Standalone Version Only):
cd <project folder>
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
Build:
# Build HAPI FHIR (Standalone Version Only)
mvn clean package -DskipTests=true -Pboot
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version)
docker-compose build --no-cache
Run:
docker-compose up -d
Access the Application:
Flask UI: http://localhost:5000
HAPI FHIR server: http://localhost:8080
Local Development (Without Docker)
HAPI FHIR server (Standalone only): http://localhost:8080
Local Development (Without Docker):
Clone the Repository:
git clone https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git)
cd FHIRFLARE-IG-Toolkit
Install Dependencies:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -
# Example for Debian/Ubuntu
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
sudo apt-get install -y nodejs
# Install globally
npm install -g gofsh fsh-sushi
Set Environment Variables:
export FLASK_SECRET_KEY='your-secure-secret-key'
export API_KEY='your-api-key'
# Optional: Set APP_MODE to 'lite' if desired
# export APP_MODE='lite'
Initialize Directories:
mkdir -p instance static/uploads logs
chmod -R 777 instance static/uploads logs
# Ensure write permissions if needed
# chmod -R 777 instance static/uploads logs
Run the Application:
export FLASK_APP=app.py
flask run
Access at http://localhost:5000.
Usage
Import an IG
Navigate to Import IG (/import-ig).
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
Choose a dependency mode:
Recursive: Import all dependencies.
Patch Canonical: Import only canonical FHIR packages.
Tree Shaking: Import only used dependencies.
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
Choose a dependency mode:
Current Recursive: Import all dependencies listed in package.json recursively.
Patch Canonical Versions: Import only canonical FHIR packages (e.g., hl7.fhir.r4.core).
Tree Shaking: Import only dependencies containing resources actually used by the main package.
Click Import to download the package and dependencies.
Manage IGs
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
Actions:
Process: Extract metadata (resource types, profiles, must-support elements, examples).
Unload: Remove processed IG data from the database.
Delete: Remove package files from the filesystem.
Actions:
Process: Extract metadata (resource types, profiles, must-support elements, examples).
Unload: Remove processed IG data from the database.
Delete: Remove package files from the filesystem.
Duplicates are highlighted for resolution.
View Processed IGs
After processing, view IG details (/view-ig/<id>), including:
Resource types and profiles.
Must-support elements and examples.
Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
Interactive StructureDefinition viewer (Differential, Snapshot, Must Support, Key Elements, Constraints, Terminology, Search Params).
Validate FHIR Resources/Bundles
Navigate to Validate FHIR Sample (/validate-sample).
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
Choose Single Resource or Bundle mode.
Paste or upload FHIR JSON/XML (e.g., a Patient resource).
Submit to view validation errors/warnings.
Note: Alpha feature; report issues to GitHub (remove PHI).
Push IGs to a FHIR Server
Go to Push IGs (/push-igs).
Select a package, enter a FHIR server URL (e.g., http://localhost:8080/fhir), and choose whether to include dependencies.
Click Push to FHIR Server to upload resources, with validation against imposed profiles (if enabled via VALIDATE_IMPOSED_PROFILES).
Select a downloaded package.
Enter the Target FHIR Server URL.
Configure Authentication (None, Bearer Token).
Choose options: Include Dependencies, Force Upload (skips comparison check), Dry Run, Verbose Log.
Optionally filter by Resource Types (comma-separated) or Skip Specific Files (paths within package, comma/newline separated).
Click Push to FHIR Server to upload resources. Canonical resources are checked before upload. Identical resources are skipped unless Force Upload is checked.
Monitor progress in the live console.
Convert FHIR to FSH
Upload Test Data
Navigate to Upload Test Data (/upload-test-data).
Enter the Target FHIR Server URL.
Configure Authentication (None, Bearer Token).
Select one or more .json, .xml files, or a single .zip file containing test resources.
Optionally check Validate Resources Before Upload? and select a Validation Profile Package.
Choose Upload Mode:
Individual Resources: Uploads each resource one by one in dependency order.
Transaction Bundle: Uploads all resources in a single transaction.
Optionally check Use Conditional Upload (Individual Mode Only)? to use If-Match headers for updates.
Choose Error Handling:
Stop on First Error: Halts the process if any validation or upload fails.
Continue on Error: Reports errors but attempts to process/upload remaining resources.
Click Upload and Process. The tool parses files, optionally validates, analyzes dependencies, topologically sorts resources, and uploads them according to selected options.
Monitor progress in the streaming log output.
Convert FHIR to FSH
Navigate to FSH Converter (/fsh-converter).
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
Choose input mode:
Upload File: Upload a FHIR JSON/XML file.
Paste Text: Paste FHIR JSON/XML content.
Configure options:
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
Log Level: error, warn, info, debug.
FHIR Version: R4, R4B, R5, or auto-detect.
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
Indent Rules: Enable context path indentation for readable FSH.
Meta Profile: Choose only-one, first, or none for meta.profile handling.
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
No Alias: Disable automatic alias generation.
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
Download the result as a .fsh file.
Example Input:
{
"resourceType": "Patient",
"id": "banks-mia-leanne",
"meta": {
"profile": ["http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"]
},
"name": [
{
"family": "Banks",
"given": ["Mia", "Leanne"]
}
]
}
Example Output:
Profile: AUCorePatient
Parent: Patient
* name 1..*
* name.family 1..1
* name.given 1..*
Explore FHIR Operations
Navigate to FHIR UI Operations (/fhir-ui-operations).
Toggle between local HAPI (/fhir) or a custom FHIR server.
Click Fetch Metadata to load the servers CapabilityStatement.
Select a resource type (e.g., Patient, Observation) or System to view operations:
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
Resource operations: GET Patient/:id, POST Observation/_search, etc.
Toggle between local HAPI (/fhir) or a custom FHIR server.
Click Fetch Metadata to load the servers CapabilityStatement.
Select a resource type (e.g., Patient, Observation) or System to view operations:
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
Resource operations: GET Patient/:id, POST Observation/_search, etc.
Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
@ -283,216 +373,228 @@ API Usage
Import IG
curl -X POST http://localhost:5000/api/import-ig \
-H "Content-Type: application/json" \
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "api_key": "your-api-key"}'
-H "X-API-Key: your-api-key" \
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "dependency_mode": "recursive"}'
Returns complies_with_profiles, imposed_profiles, and duplicate_packages_present info.
Returns complies_with_profiles, imposed_profiles, and duplicate info.
Push IG
curl -X POST http://localhost:5000/api/push-ig \
-H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "fhir_server_url": "http://localhost:8080/fhir", "include_dependencies": true, "api_key": "your-api-key"}'
-H "X-API-Key: your-api-key" \
-d '{
"package_name": "hl7.fhir.au.core",
"version": "1.1.0-preview",
"fhir_server_url": "http://localhost:8080/fhir",
"include_dependencies": true,
"force_upload": false,
"dry_run": false,
"verbose": false,
"auth_type": "none"
}'
Returns a streaming NDJSON response with progress and final summary.
Upload Test Data
curl -X POST http://localhost:5000/api/upload-test-data \
-H "X-API-Key: your-api-key" \
-H "Accept: application/x-ndjson" \
-F "fhir_server_url=http://your-fhir-server/fhir" \
-F "auth_type=bearerToken" \
-F "auth_token=YOUR_TOKEN" \
-F "upload_mode=individual" \
-F "error_handling=continue" \
-F "validate_before_upload=true" \
-F "validation_package_id=hl7.fhir.r4.core#4.0.1" \
-F "use_conditional_uploads=true" \
-F "test_data_files=@/path/to/your/patient.json" \
-F "test_data_files=@/path/to/your/observations.zip"
Returns a streaming NDJSON response with progress and final summary.
Uses multipart/form-data for file uploads.
Validates resources against imposed profiles (if enabled).
Validate Resource/Bundle
Not yet exposed via API; use the UI at /validate-sample.
Configuration Options
Located in app.py:
VALIDATE_IMPOSED_PROFILES:
VALIDATE_IMPOSED_PROFILES: (Default: True) Validates resources against imposed profiles during push.
Default: True
DISPLAY_PROFILE_RELATIONSHIPS: (Default: True) Shows compliesWithProfile and imposeProfile in the UI.
Validates resources against imposed profiles during push.
FHIR_PACKAGES_DIR: (Default: /app/instance/fhir_packages) Stores .tgz packages and metadata.
Set to False to skip:
app.config['VALIDATE_IMPOSED_PROFILES'] = False
DISPLAY_PROFILE_RELATIONSHIPS:
Default: True
Shows compliesWithProfile and imposeProfile in the UI.
Set to False to hide:
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = False
FHIR_PACKAGES_DIR:
Default: /app/instance/fhir_packages
Stores .tgz packages and metadata.
UPLOAD_FOLDER:
Default: /app/static/uploads
Stores GoFSH output files and FSH comparison reports.
SECRET_KEY:
Required for CSRF protection and sessions:
app.config['SECRET_KEY'] = 'your-secure-secret-key'
API_KEY:
Required for API authentication:
app.config['API_KEY'] = 'your-api-key'
UPLOAD_FOLDER: (Default: /app/static/uploads) Stores GoFSH output files and FSH comparison reports.
SECRET_KEY: Required for CSRF protection and sessions. Set via environment variable or directly.
API_KEY: Required for API authentication. Set via environment variable or directly.
MAX_CONTENT_LENGTH: (Default: Flask default) Max size for HTTP request body (e.g., 16 * 1024 * 1024 for 16MB). Important for large uploads.
MAX_FORM_PARTS: (Default: Werkzeug default, often 1000) Default max number of form parts. Overridden for /api/upload-test-data by CustomFormDataParser.
Testing
The project includes a test suite covering UI, API, database, file operations, and security.
Test Prerequisites
Test Prerequisites:
pytest: For running tests.
pytest-mock: For mocking dependencies.
Install:
pip install pytest pytest-mock
Install: pip install pytest pytest-mock
Running Tests:
Running Tests
cd <project folder>
pytest tests/test_app.py -v
Test Coverage
Test Coverage:
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, Upload Test Data.
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data.
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter.
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example.
Database: IG processing, unloading, viewing.
File Operations: Package processing, deletion, FSH output.
Security: CSRF protection, flash messages, secret key.
FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
Example Test Output
================================================================ test session starts =================================================================
platform linux -- Python 3.12, pytest-8.3.5, pluggy-1.5.0
rootdir: /app/tests
collected 27 items
test_app.py::TestFHIRFlareIGToolkit::test_homepage PASSED [ 3%]
test_app.py::TestFHIRFlareIGToolkit::test_import_ig_page PASSED [ 7%]
test_app.py::TestFHIRFlareIGToolkit::test_fsh_converter_page PASSED [ 11%]
...
test_app.py::TestFHIRFlareIGToolkit::test_validate_sample_success PASSED [ 88%]
============================================================= 27 passed in 1.23s ==============================================================
Troubleshooting Tests
ModuleNotFoundError: Ensure app.py, services.py, forms.py are in /app/.
TemplateNotFound: Verify templates are in /app/templates/.
Database Errors: Ensure instance/fhir_ig.db is writable (chmod 777 instance).
Mock Failures: Check tests/test_app.py for correct mocking.
Upload Test Data: Parsing, dependency graph, sorting, upload modes, validation, conditional uploads.
Development Notes
Background
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, and flexible versioning, making it a versatile platform for FHIR developers.
Technical Decisions
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, flexible versioning, improved IG pushing, and dependency-aware test data uploading, making it a versatile platform for FHIR developers.
Technical Decisions
Flask: Lightweight and flexible for web development.
SQLite: Simple for development; consider PostgreSQL for production.
Bootstrap 5.3.3: Responsive UI with custom styling for duplicates, FSH output, and waiting spinner.
Bootstrap 5.3.3: Responsive UI with custom styling.
Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
Docker: Ensures consistent deployment with Flask and HAPI FHIR.
Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
Live Console: Real-time feedback for complex operations.
Live Console/Streaming: Real-time feedback for complex operations (Push, Upload Test Data, FSH).
Validation: Alpha feature with ongoing FHIRPath improvements.
Dependency Management: Uses topological sort for Upload Test Data feature.
Form Parsing: Uses custom Werkzeug parser for Upload Test Data to handle large numbers of files.
Recent Updates
Upload Test Data Enhancements (April 2025):
Added optional Pre-Upload Validation against selected IG profiles.
Added optional Conditional Uploads (GET + POST/PUT w/ If-Match) for individual mode.
Implemented robust XML parsing using fhir.resources library (when available).
Fixed 413 Request Entity Too Large errors for large file counts using a custom Werkzeug FormDataParser.
Path: templates/upload_test_data.html, app.py, services.py, forms.py.
Push IG Enhancements (April 2025):
Added semantic comparison to skip uploading identical resources.
Added "Force Upload" option to bypass comparison.
Improved handling of canonical resources (search before PUT/POST).
Added filtering by specific files to skip during push.
More detailed summary report in stream response.
Path: templates/cp_push_igs.html, app.py, services.py.
Waiting Spinner for FSH Converter (April 2025):
Added a themed (light/dark) Lottie animation spinner during FSH execution to indicate processing.
Path: templates/fsh_converter.html, static/animations/loading-dark.json, static/animations/loading-light.json, static/js/lottie-web.min.js.
Added a themed (light/dark) Lottie animation spinner during FSH execution.
Path: templates/fsh_converter.html, static/animations/, static/js/lottie-web.min.js.
Advanced FSH Converter (April 2025):
Added support for GoFSH advanced options: --fshing-trip (round-trip validation with SUSHI), --dependency (additional packages), --indent (indented rules), --meta-profile (only-one, first, none), --alias-file (custom aliases), --no-alias (disable alias generation).
Displays Fishing Trip comparison reports via a badge button.
Added support for GoFSH advanced options: --fshing-trip, --dependency, --indent, --meta-profile, --alias-file, --no-alias.
Displays Fishing Trip comparison reports.
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
FSH Converter (April 2025):
Added /fsh-converter page for FHIR to FSH conversion using GoFSH.
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
Favicon Fix (April 2025):
Resolved 404 for /favicon.ico on /fsh-converter by ensuring static/favicon.ico is served.
Added fallback /favicon.ico route in app.py.
Menu Item (April 2025):
Added “FSH Converter” to the navbar in base.html.
UPLOAD_FOLDER Fix (April 2025):
Fixed 500 error on /fsh-converter by setting app.config['UPLOAD_FOLDER'] = '/app/static/uploads'.
Validation (April 2025):
Alpha support for validating resources/bundles in /validate-sample.
Path: templates/validate_sample.html, app.py, services.py.
CSRF Protection: Fixed missing CSRF tokens in cp_downloaded_igs.html, cp_push_igs.html.
Version Support: Added flexible version formats (e.g., 1.1.0-preview) in forms.py.
(Keep older updates listed here if desired)
Known Issues and Workarounds
Favicon 404: Clear browser cache or verify /app/static/favicon.ico:
docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
Favicon 404: Clear browser cache or verify /app/static/favicon.ico.
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
Import Fails: Check package name/version and connectivity.
Validation Accuracy: Alpha feature; FHIRPath may miss complex constraints. Report issues to GitHub (remove PHI).
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
Permissions: Ensure instance/ and static/uploads/ are writable:
chmod -R 777 instance static/uploads logs
Permissions: Ensure instance/ and static/uploads/ are writable.
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation.
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation:
docker exec -it <container_name> sushi --version
Upload Test Data XML Parsing: Relies on fhir.resources library for full validation; basic parsing used as fallback. Complex XML structures might not be fully analyzed for dependencies with basic parsing. Prefer JSON for reliable dependency analysis.
413 Request Entity Too Large: Primarily handled by CustomFormDataParser for /api/upload-test-data. Check the parser's max_form_parts limit if still occurring. MAX_CONTENT_LENGTH in app.py controls overall size. Reverse proxy limits (client_max_body_size in Nginx) might also apply.
Future Improvements
Upload Test Data: Improve XML parsing further (direct XML->fhir.resource object if possible), add visual progress bar, add upload order preview, implement transaction bundle size splitting, add 'Clear Target Server' option (with confirmation).
Validation: Enhance FHIRPath for complex constraints; add API endpoint.
Sorting: Sort IG versions in /view-igs (e.g., ascending).
Duplicate Resolution: Options to keep latest version or merge resources.
Production Database: Support PostgreSQL.
Error Reporting: Detailed validation error paths in the UI.
FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
Spinner Enhancements: Customize spinner animation speed or size.
Completed Items
Testing suite with basic coverage.
Testing suite with 27 cases.
API endpoints for POST /api/import-ig and POST /api/push-ig.
Flexible versioning (-preview, -ballot).
CSRF fixes for forms.
Resource validation UI (alpha).
FSH Converter with advanced GoFSH features and waiting spinner.
Far-Distant Improvements
Push IG enhancements (force upload, semantic comparison, canonical handling, skip files).
Upload Test Data feature with dependency sorting, multiple upload modes, pre-upload validation, conditional uploads, robust XML parsing, and fix for large file counts.
Far-Distant Improvements
Cache Service: Use Redis for IG metadata caching.
Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
Directory Structure
@ -506,7 +608,7 @@ FHIRFLARE-IG-Toolkit/
├── README.md # Project documentation
├── requirements.txt # Python dependencies
├── Run.bat # Windows script for running Docker
├── services.py # Logic for IG import, processing, validation, pushing, and FSH conversion
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload
├── supervisord.conf # Supervisor configuration
├── hapi-fhir-Setup/
│ ├── README.md # HAPI FHIR setup instructions
@ -517,24 +619,7 @@ FHIRFLARE-IG-Toolkit/
│ ├── fhir_ig.db # SQLite database
│ ├── fhir_ig.db.old # Database backup
│ └── fhir_packages/ # Stored IG packages and metadata
│ ├── hl7.fhir.au.base-5.1.0-preview.metadata.json
│ ├── hl7.fhir.au.base-5.1.0-preview.tgz
│ ├── hl7.fhir.au.core-1.1.0-preview.metadata.json
│ ├── hl7.fhir.au.core-1.1.0-preview.tgz
│ ├── hl7.fhir.r4.core-4.0.1.metadata.json
│ ├── hl7.fhir.r4.core-4.0.1.tgz
│ ├── hl7.fhir.uv.extensions.r4-5.2.0.metadata.json
│ ├── hl7.fhir.uv.extensions.r4-5.2.0.tgz
│ ├── hl7.fhir.uv.ipa-1.0.0.metadata.json
│ ├── hl7.fhir.uv.ipa-1.0.0.tgz
│ ├── hl7.fhir.uv.smart-app-launch-2.0.0.metadata.json
│ ├── hl7.fhir.uv.smart-app-launch-2.0.0.tgz
│ ├── hl7.fhir.uv.smart-app-launch-2.1.0.metadata.json
│ ├── hl7.fhir.uv.smart-app-launch-2.1.0.tgz
│ ├── hl7.terminology.r4-5.0.0.metadata.json
│ ├── hl7.terminology.r4-5.0.0.tgz
│ ├── hl7.terminology.r4-6.2.0.metadata.json
│ └── hl7.terminology.r4-6.2.0.tgz
│ ├── ... (example packages) ...
├── logs/
│ ├── flask.log # Flask application logs
│ ├── flask_err.log # Flask error logs
@ -551,15 +636,9 @@ FHIRFLARE-IG-Toolkit/
│ ├── js/
│ │ └── lottie-web.min.js # Lottie library for spinner
│ └── uploads/
│ ├── output.fsh # Generated FSH output
│ └── fsh_output/
│ ├── sushi-config.yaml # SUSHI configuration
│ └── input/
│ └── fsh/
│ ├── aliases.fsh # FSH aliases
│ ├── index.txt # FSH index
│ └── instances/
│ └── banks-mia-leanne.fsh # Example FSH instance
│ ├── output.fsh # Generated FSH output (temp location)
│ └── fsh_output/ # GoFSH output directory
│ ├── ... (example GoFSH output) ...
├── templates/
│ ├── base.html # Base template
│ ├── cp_downloaded_igs.html # UI for managing IGs
@ -570,43 +649,38 @@ FHIRFLARE-IG-Toolkit/
│ ├── fsh_converter.html # UI for FSH conversion
│ ├── import_ig.html # UI for importing IGs
│ ├── index.html # Homepage
│ ├── upload_test_data.html # UI for Uploading Test Data
│ ├── validate_sample.html # UI for validating resources/bundles
│ └── _form_helpers.html # Form helper macros
├── tests/
│ └── test_app.py # Test suite with 27 cases
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources
Contributing
Fork the repository.
Create a feature branch (git checkout -b feature/your-feature).
Commit changes (git commit -m "Add your feature").
Push to your branch (git push origin feature/your-feature).
Open a Pull Request.
Ensure code follows PEP 8 and includes tests in tests/test_app.py.
Troubleshooting
Favicon 404: Clear browser cache or verify /app/static/favicon.ico:
docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
│ └── test_app.py # Test suite
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources (if Standalone)
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
## Contributing
Import Fails: Check package name/version and connectivity.
* Fork the repository.
* Create a feature branch (`git checkout -b feature/your-feature`).
* Commit changes (`git commit -m "Add your feature"`).
* Push to your branch (`git push origin feature/your-feature`).
* Open a Pull Request.
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
Ensure code follows PEP 8 and includes tests in `tests/test_app.py`.
Package Parsing: Non-standard .tgz filenamesgrass may parse incorrectly. Fallback uses name-only parsing.
## Troubleshooting
Permissions: Ensure instance/ and static/uploads/ are writable:
chmod -R 777 instance static/uploads logs
* **Favicon 404:** Clear browser cache or verify `/app/static/favicon.ico`:
`docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico`
* **CSRF Errors:** Set `FLASK_SECRET_KEY` and ensure `{{ form.hidden_tag() }}` in forms.
* **Import Fails:** Check package name/version and connectivity.
* **Validation Accuracy:** Alpha feature; report issues to GitHub (remove PHI).
* **Package Parsing:** Non-standard `.tgz` filenames may parse incorrectly. Fallback uses name-only parsing.
* **Permissions:** Ensure `instance/` and `static/uploads/` are writable:
`chmod -R 777 instance static/uploads logs`
* **GoFSH/SUSHI Errors:** Check `./logs/flask_err.log` for `ERROR:services:GoFSH failed`. Ensure valid FHIR inputs and SUSHI installation:
`docker exec -it <container_name> sushi --version`
* **413 Request Entity Too Large:** Increase `MAX_CONTENT_LENGTH` and `MAX_FORM_PARTS` in `app.py`. If using a reverse proxy (e.g., Nginx), increase its `client_max_body_size` setting as well. Ensure the application/container is fully restarted/rebuilt.
## License
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation:
docker exec -it <container_name> sushi --version
License
Licensed under the Apache 2.0 License. See LICENSE.md for details.
Licensed under the Apache 2.0 License. See `LICENSE.md` for details.

425
app.py
View File

@ -7,15 +7,19 @@ from flask import Flask, render_template, request, redirect, url_for, flash, jso
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect
from werkzeug.utils import secure_filename
from werkzeug.formparser import FormDataParser
from werkzeug.exceptions import RequestEntityTooLarge
import tarfile
import json
import logging
import requests
import re
import services # Restore full module import
from services import services_bp # Keep Blueprint import
from forms import IgImportForm, ValidationForm, FSHConverterForm
from services import services_bp, construct_tgz_filename, parse_package_filename # Keep Blueprint import
from forms import IgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm
from wtforms import SubmitField
#from models import ProcessedIg
import tempfile
# Set up logging
@ -32,6 +36,22 @@ app.config['VALIDATE_IMPOSED_PROFILES'] = True
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
app.config['UPLOAD_FOLDER'] = '/app/static/uploads' # For GoFSH output
# Set max upload size (e.g., 12 MB, adjust as needed)
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
# Increase max number of form parts (default is often 1000)
#app.config['MAX_FORM_PARTS'] = 1000 # Allow up to 1000 parts this is a hard coded stop limit in MAX_FORM_PARTS of werkzeug
# --- NEW: Define Custom Form Parser ---
class CustomFormDataParser(FormDataParser):
"""Subclass to increase the maximum number of form parts."""
def __init__(self, *args, **kwargs):
# Set a higher limit for max_form_parts. Adjust value as needed.
# This overrides the default limit checked by Werkzeug's parser.
# Set to a sufficiently high number for your expected maximum file count.
super().__init__(*args, max_form_parts=2000, **kwargs) # Example: Allow 2000 parts
# --- END NEW ---
# <<< ADD THIS CONTEXT PROCESSOR >>>
@app.context_processor
def inject_app_mode():
@ -812,241 +832,63 @@ def api_import_ig():
@app.route('/api/push-ig', methods=['POST'])
def api_push_ig():
auth_error = check_api_key()
if auth_error:
return auth_error
if not request.is_json:
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
if auth_error: return auth_error
if not request.is_json: return jsonify({"status": "error", "message": "Request must be JSON"}), 400
data = request.get_json()
package_name = data.get('package_name')
version = data.get('version')
fhir_server_url = data.get('fhir_server_url')
include_dependencies = data.get('include_dependencies', True) # Default to True if not provided
include_dependencies = data.get('include_dependencies', True)
auth_type = data.get('auth_type', 'none')
auth_token = data.get('auth_token')
resource_types_filter_raw = data.get('resource_types_filter')
skip_files_raw = data.get('skip_files')
dry_run = data.get('dry_run', False)
verbose = data.get('verbose', False)
force_upload = data.get('force_upload', False) # <<< ADD: Extract force_upload
# --- Input Validation ---
if not all([package_name, version, fhir_server_url]):
return jsonify({"status": "error", "message": "Missing package_name, version, or fhir_server_url"}), 400
if not (isinstance(fhir_server_url, str) and fhir_server_url.startswith(('http://', 'https://'))):
return jsonify({"status": "error", "message": "Invalid fhir_server_url format."}), 400
if not (isinstance(package_name, str) and isinstance(version, str) and
re.match(r'^[a-zA-Z0-9\-\.]+$', package_name) and
re.match(r'^[a-zA-Z0-9\.\-\+]+$', version)):
return jsonify({"status": "error", "message": "Invalid characters in package name or version"}), 400
# --- Input Validation (Assume previous validation is sufficient) ---
if not all([package_name, version, fhir_server_url]): return jsonify({"status": "error", "message": "Missing required fields"}), 400
# ... (Keep other specific validations as needed) ...
valid_auth_types = ['apiKey', 'bearerToken', 'none'];
if auth_type not in valid_auth_types: return jsonify({"status": "error", "message": f"Invalid auth_type."}), 400
if auth_type == 'bearerToken' and not auth_token: return jsonify({"status": "error", "message": "auth_token required for bearerToken."}), 400
# --- File Path Setup ---
# Parse filters (same as before)
resource_types_filter = None
if resource_types_filter_raw:
if isinstance(resource_types_filter_raw, list): resource_types_filter = [s for s in resource_types_filter_raw if isinstance(s, str)]
elif isinstance(resource_types_filter_raw, str): resource_types_filter = [s.strip() for s in resource_types_filter_raw.split(',') if s.strip()]
else: return jsonify({"status": "error", "message": "Invalid resource_types_filter format."}), 400
skip_files = None
if skip_files_raw:
if isinstance(skip_files_raw, list): skip_files = [s.strip().replace('\\', '/') for s in skip_files_raw if isinstance(s, str) and s.strip()]
elif isinstance(skip_files_raw, str): skip_files = [s.strip().replace('\\', '/') for s in re.split(r'[,\n]', skip_files_raw) if s.strip()]
else: return jsonify({"status": "error", "message": "Invalid skip_files format."}), 400
# --- File Path Setup (Same as before) ---
packages_dir = current_app.config.get('FHIR_PACKAGES_DIR')
if not packages_dir:
logger.error("[API Push] FHIR_PACKAGES_DIR not configured.")
return jsonify({"status": "error", "message": "Server configuration error: Package directory not set."}), 500
if not packages_dir: return jsonify({"status": "error", "message": "Server config error: Package dir missing."}), 500
# ... (check if package tgz exists - same as before) ...
tgz_filename = services.construct_tgz_filename(package_name, version)
tgz_path = os.path.join(packages_dir, tgz_filename)
if not os.path.exists(tgz_path):
logger.error(f"[API Push] Main package not found: {tgz_path}")
return jsonify({"status": "error", "message": f"Package not found locally: {package_name}#{version}"}), 404
if not os.path.exists(tgz_path): return jsonify({"status": "error", "message": f"Package not found locally: {package_name}#{version}"}), 404
# --- Streaming Response ---
def generate_stream():
pushed_packages_info = []
success_count = 0
failure_count = 0
validation_failure_count = 0 # Note: This count is not currently incremented anywhere.
total_resources_attempted = 0
processed_resources = set()
failed_uploads_details = [] # <<<<< Initialize list for failure details
def generate_stream_wrapper():
yield from services.generate_push_stream(
package_name=package_name, version=version, fhir_server_url=fhir_server_url,
include_dependencies=include_dependencies, auth_type=auth_type,
auth_token=auth_token, resource_types_filter=resource_types_filter,
skip_files=skip_files, dry_run=dry_run, verbose=verbose,
force_upload=force_upload, # <<< ADD: Pass force_upload
packages_dir=packages_dir
)
return Response(generate_stream_wrapper(), mimetype='application/x-ndjson')
try:
yield json.dumps({"type": "start", "message": f"Starting push for {package_name}#{version} to {fhir_server_url}"}) + "\n"
packages_to_push = [(package_name, version, tgz_path)]
dependencies_to_include = []
# --- Dependency Handling ---
if include_dependencies:
yield json.dumps({"type": "progress", "message": "Checking dependencies..."}) + "\n"
metadata = services.get_package_metadata(package_name, version)
if metadata and metadata.get('imported_dependencies'):
dependencies_to_include = metadata['imported_dependencies']
yield json.dumps({"type": "info", "message": f"Found {len(dependencies_to_include)} dependencies in metadata."}) + "\n"
for dep in dependencies_to_include:
dep_name = dep.get('name')
dep_version = dep.get('version')
if not dep_name or not dep_version:
continue
dep_tgz_filename = services.construct_tgz_filename(dep_name, dep_version)
dep_tgz_path = os.path.join(packages_dir, dep_tgz_filename)
if os.path.exists(dep_tgz_path):
# Add dependency package to the list of packages to process
if (dep_name, dep_version, dep_tgz_path) not in packages_to_push: # Avoid adding duplicates if listed multiple times
packages_to_push.append((dep_name, dep_version, dep_tgz_path))
yield json.dumps({"type": "progress", "message": f"Queued dependency: {dep_name}#{dep_version}"}) + "\n"
else:
yield json.dumps({"type": "warning", "message": f"Dependency package file not found, skipping: {dep_name}#{dep_version} ({dep_tgz_filename})"}) + "\n"
else:
yield json.dumps({"type": "info", "message": "No dependency metadata found or no dependencies listed."}) + "\n"
# --- Resource Extraction ---
resources_to_upload = []
seen_resource_files = set() # Track processed filenames to avoid duplicates across packages
for pkg_name, pkg_version, pkg_path in packages_to_push:
yield json.dumps({"type": "progress", "message": f"Extracting resources from {pkg_name}#{pkg_version}..."}) + "\n"
try:
with tarfile.open(pkg_path, "r:gz") as tar:
for member in tar.getmembers():
if (member.isfile() and
member.name.startswith('package/') and
member.name.lower().endswith('.json') and
os.path.basename(member.name).lower() not in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']): # Skip common metadata files
if member.name in seen_resource_files: # Skip if already seen from another package (e.g., core resource in multiple IGs)
continue
seen_resource_files.add(member.name)
try:
with tar.extractfile(member) as f:
resource_data = json.load(f) # Read and parse JSON content
# Basic check for a valid FHIR resource structure
if isinstance(resource_data, dict) and 'resourceType' in resource_data and 'id' in resource_data:
resources_to_upload.append({
"data": resource_data,
"source_package": f"{pkg_name}#{pkg_version}",
"source_filename": member.name
})
else:
yield json.dumps({"type": "warning", "message": f"Skipping invalid/incomplete resource in {member.name} from {pkg_name}#{pkg_version}"}) + "\n"
except (json.JSONDecodeError, UnicodeDecodeError) as json_e:
yield json.dumps({"type": "warning", "message": f"Skipping non-JSON or corrupt file {member.name} from {pkg_name}#{pkg_version}: {json_e}"}) + "\n"
except Exception as extract_e: # Catch potential errors during file extraction within the tar
yield json.dumps({"type": "warning", "message": f"Error extracting file {member.name} from {pkg_name}#{pkg_version}: {extract_e}"}) + "\n"
except (tarfile.TarError, FileNotFoundError) as tar_e:
# Error reading the package itself
error_msg = f"Error reading package {pkg_name}#{pkg_version}: {tar_e}. Skipping its resources."
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1 # Increment general failure count
failed_uploads_details.append({'resource': f"Package: {pkg_name}#{pkg_version}", 'error': f"TarError/FileNotFound: {tar_e}"}) # <<<<< ADDED package level error
continue # Skip to the next package if one fails to open
total_resources_attempted = len(resources_to_upload)
yield json.dumps({"type": "info", "message": f"Found {total_resources_attempted} potential resources to upload across all packages."}) + "\n"
# --- Resource Upload Loop ---
session = requests.Session() # Use a session for potential connection reuse
headers = {'Content-Type': 'application/fhir+json', 'Accept': 'application/fhir+json'}
for i, resource_info in enumerate(resources_to_upload, 1):
resource = resource_info["data"]
source_pkg = resource_info["source_package"]
# source_file = resource_info["source_filename"] # Available if needed
resource_type = resource.get('resourceType')
resource_id = resource.get('id')
resource_log_id = f"{resource_type}/{resource_id}"
# Skip if already processed (e.g., if same resource ID appeared in multiple packages)
if resource_log_id in processed_resources:
yield json.dumps({"type": "info", "message": f"Skipping duplicate resource ID: {resource_log_id} (already attempted/uploaded)"}) + "\n"
continue
processed_resources.add(resource_log_id)
resource_url = f"{fhir_server_url.rstrip('/')}/{resource_type}/{resource_id}" # Construct PUT URL
yield json.dumps({"type": "progress", "message": f"Uploading {resource_log_id} ({i}/{total_resources_attempted}) from {source_pkg}..."}) + "\n"
try:
# Attempt to PUT the resource
response = session.put(resource_url, json=resource, headers=headers, timeout=30)
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
yield json.dumps({"type": "success", "message": f"Uploaded {resource_log_id} successfully (Status: {response.status_code})"}) + "\n"
success_count += 1
# Track which packages contributed successful uploads
if source_pkg not in [p["id"] for p in pushed_packages_info]:
pushed_packages_info.append({"id": source_pkg, "resource_count": 1})
else:
for p in pushed_packages_info:
if p["id"] == source_pkg:
p["resource_count"] += 1
break
# --- Error Handling for Upload ---
except requests.exceptions.Timeout:
error_msg = f"Timeout uploading {resource_log_id} to {resource_url}"
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1
failed_uploads_details.append({'resource': resource_log_id, 'error': error_msg}) # <<<<< ADDED
except requests.exceptions.ConnectionError as e:
error_msg = f"Connection error uploading {resource_log_id}: {e}"
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1
failed_uploads_details.append({'resource': resource_log_id, 'error': error_msg}) # <<<<< ADDED
except requests.exceptions.HTTPError as e:
outcome_text = ""
# Try to parse OperationOutcome from response
try:
outcome = e.response.json()
if outcome and outcome.get('resourceType') == 'OperationOutcome' and outcome.get('issue'):
outcome_text = "; ".join([f"{issue.get('severity', 'info')}: {issue.get('diagnostics') or issue.get('details', {}).get('text', 'No details')}" for issue in outcome['issue']])
except ValueError: # Handle cases where response is not JSON
outcome_text = e.response.text[:200] # Fallback to raw text
error_msg = f"Failed to upload {resource_log_id} (Status: {e.response.status_code}): {outcome_text or str(e)}"
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1
failed_uploads_details.append({'resource': resource_log_id, 'error': error_msg}) # <<<<< ADDED
except requests.exceptions.RequestException as e: # Catch other potential request errors
error_msg = f"Request error uploading {resource_log_id}: {str(e)}"
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1
failed_uploads_details.append({'resource': resource_log_id, 'error': error_msg}) # <<<<< ADDED
except Exception as e: # Catch unexpected errors during the PUT request
error_msg = f"Unexpected error uploading {resource_log_id}: {str(e)}"
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
failure_count += 1
failed_uploads_details.append({'resource': resource_log_id, 'error': error_msg}) # <<<<< ADDED
logger.error(f"[API Push] Unexpected upload error for {resource_log_id}: {e}", exc_info=True)
# --- Final Summary ---
# Determine overall status based on counts
final_status = "success" if failure_count == 0 and total_resources_attempted > 0 else \
"partial" if success_count > 0 else \
"failure"
summary_message = f"Push finished: {success_count} succeeded, {failure_count} failed out of {total_resources_attempted} resources attempted."
if validation_failure_count > 0: # This count isn't used currently, but keeping structure
summary_message += f" ({validation_failure_count} failed validation)."
# Create final summary payload
summary = {
"status": final_status,
"message": summary_message,
"target_server": fhir_server_url,
"package_name": package_name, # Initial requested package
"version": version, # Initial requested version
"included_dependencies": include_dependencies,
"resources_attempted": total_resources_attempted,
"success_count": success_count,
"failure_count": failure_count,
"validation_failure_count": validation_failure_count,
"failed_details": failed_uploads_details, # <<<<< ADDED Failure details list
"pushed_packages_summary": pushed_packages_info # Summary of packages contributing uploads
}
yield json.dumps({"type": "complete", "data": summary}) + "\n"
logger.info(f"[API Push] Completed for {package_name}#{version}. Status: {final_status}. {summary_message}")
except Exception as e: # Catch critical errors during the entire stream generation
logger.error(f"[API Push] Critical error during push stream generation for {package_name}#{version}: {str(e)}", exc_info=True)
# Attempt to yield a final error message to the stream
error_response = {
"status": "error",
"message": f"Server error during push operation: {str(e)}"
# Optionally add failed_details collected so far if needed
# "failed_details": failed_uploads_details
}
try:
# Send error info both as a stream message and in the complete data
yield json.dumps({"type": "error", "message": error_response["message"]}) + "\n"
yield json.dumps({"type": "complete", "data": error_response}) + "\n"
except GeneratorExit: # If the client disconnects
logger.warning("[API Push] Stream closed before final error could be sent.")
except Exception as yield_e: # Error during yielding the error itself
logger.error(f"[API Push] Error yielding final error message: {yield_e}")
# Return the streaming response
return Response(generate_stream(), mimetype='application/x-ndjson')
# Ensure csrf.exempt(api_push_ig) remains
@app.route('/validate-sample', methods=['GET'])
def validate_sample():
@ -1348,6 +1190,139 @@ def download_fsh():
return send_file(temp_file, as_attachment=True, download_name='output.fsh')
@app.route('/upload-test-data', methods=['GET'])
def upload_test_data():
"""Renders the page for uploading test data."""
form = TestDataUploadForm()
try:
processed_igs = ProcessedIg.query.order_by(ProcessedIg.package_name, ProcessedIg.version).all()
form.validation_package_id.choices = [('', '-- Select Package for Validation --')] + [
(f"{ig.package_name}#{ig.version}", f"{ig.package_name}#{ig.version}") for ig in processed_igs ]
except Exception as e:
logger.error(f"Error fetching processed IGs: {e}")
flash("Could not load processed packages for validation.", "warning")
form.validation_package_id.choices = [('', '-- Error Loading Packages --')]
api_key = current_app.config.get('API_KEY', '')
return render_template('upload_test_data.html', title="Upload Test Data", form=form, api_key=api_key)
@app.route('/api/upload-test-data', methods=['POST'])
@csrf.exempt
def api_upload_test_data():
"""API endpoint to handle test data upload and processing, using custom parser."""
auth_error = check_api_key();
if auth_error: return auth_error
temp_dir = None # Initialize temp_dir to ensure cleanup happens
try:
# --- Use Custom Form Parser ---
# Instantiate the custom parser with the desired limit
parser = CustomFormDataParser()
#parser = CustomFormDataParser(max_form_parts=2000) # Match the class definition or set higher if needed
# Parse the request using the custom parser
# We need the stream, mimetype, content_length, and options from the request
# Note: Accessing request.stream consumes it, do this first.
stream = request.stream
mimetype = request.mimetype
content_length = request.content_length
options = request.mimetype_params
# The parse method returns (stream, form_dict, files_dict)
# stream: A wrapper around the original stream
# form_dict: A MultiDict containing non-file form fields
# files_dict: A MultiDict containing FileStorage objects for uploaded files
_, form_data, files_data = parser.parse(stream, mimetype, content_length, options)
logger.debug(f"Form parsed using CustomFormDataParser. Form fields: {len(form_data)}, Files: {len(files_data)}")
# --- END Custom Form Parser Usage ---
# --- Extract Form Data (using parsed data) ---
fhir_server_url = form_data.get('fhir_server_url')
auth_type = form_data.get('auth_type', 'none')
auth_token = form_data.get('auth_token')
upload_mode = form_data.get('upload_mode', 'individual')
error_handling = form_data.get('error_handling', 'stop')
validate_before_upload_str = form_data.get('validate_before_upload', 'false')
validate_before_upload = validate_before_upload_str.lower() == 'true'
validation_package_id = form_data.get('validation_package_id') if validate_before_upload else None
use_conditional_uploads_str = form_data.get('use_conditional_uploads', 'false')
use_conditional_uploads = use_conditional_uploads_str.lower() == 'true'
logger.debug(f"API Upload Request Params: validate={validate_before_upload}, pkg_id={validation_package_id}, conditional={use_conditional_uploads}")
# --- Basic Validation (using parsed data) ---
if not fhir_server_url or not fhir_server_url.startswith(('http://', 'https://')): return jsonify({"status": "error", "message": "Invalid Target FHIR Server URL."}), 400
if auth_type not in ['none', 'bearerToken']: return jsonify({"status": "error", "message": "Invalid Authentication Type."}), 400
if auth_type == 'bearerToken' and not auth_token: return jsonify({"status": "error", "message": "Bearer Token required."}), 400
if upload_mode not in ['individual', 'transaction']: return jsonify({"status": "error", "message": "Invalid Upload Mode."}), 400
if error_handling not in ['stop', 'continue']: return jsonify({"status": "error", "message": "Invalid Error Handling mode."}), 400
if validate_before_upload and not validation_package_id: return jsonify({"status": "error", "message": "Validation Package ID required."}), 400
# --- Handle File Uploads (using parsed data) ---
# Use files_data obtained from the custom parser
uploaded_files = files_data.getlist('test_data_files')
if not uploaded_files or all(f.filename == '' for f in uploaded_files): return jsonify({"status": "error", "message": "No files selected."}), 400
temp_dir = tempfile.mkdtemp(prefix='fhirflare_upload_')
saved_file_paths = []
allowed_extensions = {'.json', '.xml', '.zip'}
try:
for file_storage in uploaded_files: # Iterate through FileStorage objects
if file_storage and file_storage.filename:
filename = secure_filename(file_storage.filename)
file_ext = os.path.splitext(filename)[1].lower()
if file_ext not in allowed_extensions: raise ValueError(f"Invalid file type: '{filename}'. Only JSON, XML, ZIP allowed.")
save_path = os.path.join(temp_dir, filename)
file_storage.save(save_path) # Use the save method of FileStorage
saved_file_paths.append(save_path)
if not saved_file_paths: raise ValueError("No valid files saved.")
logger.debug(f"Saved {len(saved_file_paths)} files to {temp_dir}")
except ValueError as ve:
if temp_dir and os.path.exists(temp_dir): shutil.rmtree(temp_dir)
logger.warning(f"Upload rejected: {ve}"); return jsonify({"status": "error", "message": str(ve)}), 400
except Exception as file_err:
if temp_dir and os.path.exists(temp_dir): shutil.rmtree(temp_dir)
logger.error(f"Error saving uploaded files: {file_err}", exc_info=True); return jsonify({"status": "error", "message": "Error saving uploaded files."}), 500
# --- Prepare Server Info and Options ---
server_info = {'url': fhir_server_url, 'auth_type': auth_type, 'auth_token': auth_token}
options = {
'upload_mode': upload_mode,
'error_handling': error_handling,
'validate_before_upload': validate_before_upload,
'validation_package_id': validation_package_id,
'use_conditional_uploads': use_conditional_uploads
}
# --- Call Service Function (Streaming Response) ---
def generate_stream_wrapper():
try:
with app.app_context():
yield from services.process_and_upload_test_data(server_info, options, temp_dir)
finally:
try: logger.debug(f"Cleaning up temp dir: {temp_dir}"); shutil.rmtree(temp_dir)
except Exception as cleanup_e: logger.error(f"Error cleaning up temp dir {temp_dir}: {cleanup_e}")
return Response(generate_stream_wrapper(), mimetype='application/x-ndjson')
except RequestEntityTooLarge as e:
# Catch the specific exception if the custom parser still fails (e.g., limit too low)
logger.error(f"RequestEntityTooLarge error in /api/upload-test-data despite custom parser: {e}", exc_info=True)
if temp_dir and os.path.exists(temp_dir):
try: shutil.rmtree(temp_dir)
except Exception as cleanup_e: logger.error(f"Error cleaning up temp dir during exception: {cleanup_e}")
return jsonify({"status": "error", "message": f"Upload failed: Request entity too large. Try increasing parser limit or reducing files/size. ({str(e)})"}), 413
except Exception as e:
# Catch other potential errors during parsing or setup
logger.error(f"Error in /api/upload-test-data: {e}", exc_info=True)
if temp_dir and os.path.exists(temp_dir):
try: shutil.rmtree(temp_dir)
except Exception as cleanup_e: logger.error(f"Error cleaning up temp dir during exception: {cleanup_e}")
return jsonify({"status": "error", "message": f"Unexpected server error: {str(e)}"}), 500
@app.route('/favicon.ico')
def favicon():
return send_file(os.path.join(app.static_folder, 'favicon.ico'), mimetype='image/x-icon')

View File

@ -16,5 +16,5 @@ services:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=lite
- APP_MODE=standalone
command: supervisord -c /etc/supervisord.conf

132
forms.py
View File

@ -1,13 +1,18 @@
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField
from wtforms.validators import DataRequired, Regexp, ValidationError, Optional
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
from flask import request # Import request for file validation in FSHConverterForm
import json
import xml.etree.ElementTree as ET
import re
import logging # Import logging
logger = logging.getLogger(__name__) # Setup logger if needed elsewhere
# Existing forms (IgImportForm, ValidationForm) remain unchanged
class IgImportForm(FlaskForm):
"""Form for importing Implementation Guides."""
package_name = StringField('Package Name', validators=[
DataRequired(),
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
@ -24,6 +29,7 @@ class IgImportForm(FlaskForm):
submit = SubmitField('Import')
class ValidationForm(FlaskForm):
"""Form for validating FHIR samples."""
package_name = StringField('Package Name', validators=[DataRequired()])
version = StringField('Package Version', validators=[DataRequired()])
include_dependencies = BooleanField('Include Dependencies', default=True)
@ -33,24 +39,12 @@ class ValidationForm(FlaskForm):
], default='single')
sample_input = TextAreaField('Sample Input', validators=[
DataRequired(),
lambda form, field: validate_json(field.data, form.mode.data)
# Removed lambda validator for simplicity, can be added back if needed
])
submit = SubmitField('Validate')
def validate_json(data, mode):
"""Custom validator to ensure input is valid JSON and matches the selected mode."""
try:
parsed = json.loads(data)
if mode == 'single' and not isinstance(parsed, dict):
raise ValueError("Single resource mode requires a JSON object.")
if mode == 'bundle' and (not isinstance(parsed, dict) or parsed.get('resourceType') != 'Bundle'):
raise ValueError("Bundle mode requires a JSON object with resourceType 'Bundle'.")
except json.JSONDecodeError:
raise ValidationError("Invalid JSON format.")
except ValueError as e:
raise ValidationError(str(e))
class FSHConverterForm(FlaskForm):
"""Form for converting FHIR resources to FSH."""
package = SelectField('FHIR Package (Optional)', choices=[('', 'None')], validators=[Optional()])
input_mode = SelectField('Input Mode', choices=[
('file', 'Upload File'),
@ -70,7 +64,7 @@ class FSHConverterForm(FlaskForm):
('info', 'Info'),
('debug', 'Debug')
], validators=[DataRequired()])
fhir_version = SelectField('FXML Version', choices=[
fhir_version = SelectField('FHIR Version', choices=[ # Corrected label
('', 'Auto-detect'),
('4.0.1', 'R4'),
('4.3.0', 'R4B'),
@ -89,36 +83,116 @@ class FSHConverterForm(FlaskForm):
submit = SubmitField('Convert to FSH')
def validate(self, extra_validators=None):
"""Custom validation for FSH Converter Form."""
# Run default validators first
if not super().validate(extra_validators):
return False
if self.input_mode.data == 'file' and not self.fhir_file.data:
self.fhir_file.errors.append('File is required when input mode is Upload File.')
return False
# Check file/text input based on mode
# Need to check request.files for file uploads as self.fhir_file.data might be None during initial POST validation
has_file_in_request = request and request.files and self.fhir_file.name in request.files and request.files[self.fhir_file.name].filename != ''
if self.input_mode.data == 'file' and not has_file_in_request:
# If it's not in request.files, check if data is already populated (e.g., on re-render after error)
if not self.fhir_file.data:
self.fhir_file.errors.append('File is required when input mode is Upload File.')
return False
if self.input_mode.data == 'text' and not self.fhir_text.data:
self.fhir_text.errors.append('Text input is required when input mode is Paste Text.')
return False
# Validate text input format
if self.input_mode.data == 'text' and self.fhir_text.data:
try:
content = self.fhir_text.data.strip()
if content.startswith('{'):
if not content: # Empty text is technically valid but maybe not useful
pass # Allow empty text for now
elif content.startswith('{'):
json.loads(content)
elif content.startswith('<'):
ET.fromstring(content)
ET.fromstring(content) # Basic XML check
else:
# If content exists but isn't JSON or XML, it's an error
self.fhir_text.errors.append('Text input must be valid JSON or XML.')
return False
except (json.JSONDecodeError, ET.ParseError):
self.fhir_text.errors.append('Invalid JSON or XML format.')
return False
# Validate dependency format
if self.dependencies.data:
for dep in self.dependencies.data.split('\n'):
for dep in self.dependencies.data.splitlines():
dep = dep.strip()
# Allow versions like 'current', 'dev', etc. but require package@version format
if dep and not re.match(r'^[a-zA-Z0-9\-\.]+@[a-zA-Z0-9\.\-]+$', dep):
self.dependencies.errors.append(f'Invalid dependency format: {dep}. Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
self.dependencies.errors.append(f'Invalid dependency format: "{dep}". Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
return False
if self.alias_file.data:
content = self.alias_file.data.read().decode('utf-8')
if not content.strip().endswith('.fsh'):
self.alias_file.errors.append('Alias file must be a valid FSH file (.fsh).')
return False
return True
# Validate alias file extension (optional, basic check)
# Check request.files for alias file as well
has_alias_file_in_request = request and request.files and self.alias_file.name in request.files and request.files[self.alias_file.name].filename != ''
alias_file_data = self.alias_file.data or (request.files.get(self.alias_file.name) if request else None)
if alias_file_data and alias_file_data.filename:
if not alias_file_data.filename.lower().endswith('.fsh'):
self.alias_file.errors.append('Alias file should have a .fsh extension.')
# return False # Might be too strict, maybe just warn?
return True
class TestDataUploadForm(FlaskForm):
"""Form for uploading FHIR test data."""
fhir_server_url = StringField('Target FHIR Server URL', validators=[DataRequired(), URL()],
render_kw={'placeholder': 'e.g., http://localhost:8080/fhir'})
auth_type = SelectField('Authentication Type', choices=[
('none', 'None'),
('bearerToken', 'Bearer Token')
], default='none')
auth_token = StringField('Bearer Token', validators=[Optional()],
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
test_data_file = FileField('Select Test Data File(s)', validators=[InputRequired("Please select at least one file.")],
render_kw={'multiple': True, 'accept': '.json,.xml,.zip'})
validate_before_upload = BooleanField('Validate Resources Before Upload?', default=False,
description="Validate resources against selected package profile before uploading.")
validation_package_id = SelectField('Validation Profile Package (Optional)',
choices=[('', '-- Select Package for Validation --')],
validators=[Optional()],
description="Select the processed IG package to use for validation.")
upload_mode = SelectField('Upload Mode', choices=[
('individual', 'Individual Resources'), # Simplified label
('transaction', 'Transaction Bundle') # Simplified label
], default='individual')
# --- NEW FIELD for Conditional Upload ---
use_conditional_uploads = BooleanField('Use Conditional Upload (Individual Mode Only)?', default=True,
description="If checked, checks resource existence (GET) and uses If-Match (PUT) or creates (PUT). If unchecked, uses simple PUT for all.")
# --- END NEW FIELD ---
error_handling = SelectField('Error Handling', choices=[
('stop', 'Stop on First Error'),
('continue', 'Continue on Error')
], default='stop')
submit = SubmitField('Upload and Process')
def validate(self, extra_validators=None):
"""Custom validation for Test Data Upload Form."""
if not super().validate(extra_validators): return False
if self.validate_before_upload.data and not self.validation_package_id.data:
self.validation_package_id.errors.append('Please select a package to validate against when pre-upload validation is enabled.')
return False
# Add check: Conditional uploads only make sense for individual mode
if self.use_conditional_uploads.data and self.upload_mode.data == 'transaction':
self.use_conditional_uploads.errors.append('Conditional Uploads only apply to the "Individual Resources" mode.')
# We might allow this combination but warn the user it has no effect,
# or enforce it here. Let's enforce for clarity.
# return False # Optional: Make this a hard validation failure
# Or just let it pass and ignore the flag in the backend for transaction mode.
pass # Let it pass for now, backend will ignore if mode is transaction
return True

View File

@ -1,6 +1,6 @@
#FileLock
#Mon Apr 21 13:28:52 UTC 2025
server=172.18.0.2\:37033
hostName=d3691f0dbfcf
#Thu Apr 24 13:26:14 UTC 2025
server=172.19.0.2\:33797
hostName=3e6e009eccb3
method=file
id=196588987143154de87600de82a07a5280026b49718
id=19667fa3b1585036c013c3404aaa964d49518f08071

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
* Serving Flask app 'app'
* Debug mode: off
* Serving Flask app 'app'
* Debug mode: off
* Serving Flask app 'app'
* Debug mode: off
* Serving Flask app 'app'
* Debug mode: off

View File

@ -1,311 +0,0 @@
INFO:__main__:Application running in mode: lite
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.18.0.2:5000
INFO:werkzeug:Press CTRL+C to quit
INFO:__main__:Application running in mode: lite
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.18.0.2:5000
INFO:werkzeug:Press CTRL+C to quit
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:15] "GET / HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:15] "GET /static/FHIRFLARE.png HTTP/1.1" 200 -
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
DEBUG:services:Parsed 'hl7.fhir.au.base-5.1.0-preview.tgz' -> name='hl7.fhir.au.base-5.1.0', version='preview'
DEBUG:services:Parsed 'hl7.fhir.au.core-1.1.0-preview.tgz' -> name='hl7.fhir.au.core-1.1.0', version='preview'
DEBUG:services:Parsed 'hl7.fhir.r4.core-4.0.1.tgz' -> name='hl7.fhir.r4.core', version='4.0.1'
DEBUG:services:Parsed 'hl7.fhir.uv.extensions.r4-5.2.0.tgz' -> name='hl7.fhir.uv.extensions.r4', version='5.2.0'
DEBUG:services:Parsed 'hl7.fhir.uv.ipa-1.0.0.tgz' -> name='hl7.fhir.uv.ipa', version='1.0.0'
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.0.0'
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.1.0'
DEBUG:services:Parsed 'hl7.terminology.r4-5.0.0.tgz' -> name='hl7.terminology.r4', version='5.0.0'
DEBUG:services:Parsed 'hl7.terminology.r4-6.2.0.tgz' -> name='hl7.terminology.r4', version='6.2.0'
DEBUG:__main__:Found packages: [{'name': 'hl7.fhir.au.base', 'version': '5.1.0-preview', 'filename': 'hl7.fhir.au.base-5.1.0-preview.tgz'}, {'name': 'hl7.fhir.au.core', 'version': '1.1.0-preview', 'filename': 'hl7.fhir.au.core-1.1.0-preview.tgz'}, {'name': 'hl7.fhir.r4.core', 'version': '4.0.1', 'filename': 'hl7.fhir.r4.core-4.0.1.tgz'}, {'name': 'hl7.fhir.uv.extensions.r4', 'version': '5.2.0', 'filename': 'hl7.fhir.uv.extensions.r4-5.2.0.tgz'}, {'name': 'hl7.fhir.uv.ipa', 'version': '1.0.0', 'filename': 'hl7.fhir.uv.ipa-1.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.0.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.1.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '5.0.0', 'filename': 'hl7.terminology.r4-5.0.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '6.2.0', 'filename': 'hl7.terminology.r4-6.2.0.tgz'}]
DEBUG:__main__:Errors during package listing: []
DEBUG:__main__:Duplicate groups: {'hl7.fhir.uv.smart-app-launch': ['2.0.0', '2.1.0'], 'hl7.terminology.r4': ['5.0.0', '6.2.0']}
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:33] "GET /view-igs HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:33] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
DEBUG:__main__:Viewing IG hl7.fhir.au.core-1.1.0#preview: 25 profiles, 17 base resources, 1 optional elements
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:35] "GET /view-ig/1 HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:35] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
DEBUG:__main__:Attempting to find SD for 'au-core-patient' in hl7.fhir.au.core-1.1.0-preview.tgz
DEBUG:services:Searching for SD matching 'au-core-patient' with profile 'None' in hl7.fhir.au.core-1.1.0-preview.tgz
DEBUG:services:Match found based on exact sd_id: au-core-patient
DEBUG:services:Removing narrative text from resource: StructureDefinition
INFO:services:Found high-confidence match for 'au-core-patient' (package/StructureDefinition-au-core-patient.json), stopping search.
DEBUG:__main__:Found 20 unique IDs in differential.
DEBUG:__main__:Processing 78 snapshot elements to add isInDifferential flag.
DEBUG:__main__:Retrieved 19 Must Support paths for 'au-core-patient' from processed IG DB record.
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:42] "GET /get-structure?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&resource_type=au-core-patient&view=snapshot HTTP/1.1" 200 -
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
DEBUG:__main__:Found 9 .tgz files: ['hl7.fhir.au.base-5.1.0-preview.tgz', 'hl7.fhir.au.core-1.1.0-preview.tgz', 'hl7.fhir.r4.core-4.0.1.tgz', 'hl7.fhir.uv.extensions.r4-5.2.0.tgz', 'hl7.fhir.uv.ipa-1.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz', 'hl7.terminology.r4-5.0.0.tgz', 'hl7.terminology.r4-6.2.0.tgz']
DEBUG:__main__:Added package: hl7.fhir.au.base#5.1.0-preview
DEBUG:__main__:Added package: hl7.fhir.au.core#1.1.0-preview
DEBUG:__main__:Added package: hl7.fhir.r4.core#4.0.1
DEBUG:__main__:Added package: hl7.fhir.uv.extensions.r4#5.2.0
DEBUG:__main__:Added package: hl7.fhir.uv.ipa#1.0.0
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.0.0
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.1.0
DEBUG:__main__:Added package: hl7.terminology.r4#5.0.0
DEBUG:__main__:Added package: hl7.terminology.r4#6.2.0
DEBUG:__main__:Set package choices: [('', 'None'), ('hl7.fhir.au.base#5.1.0-preview', 'hl7.fhir.au.base#5.1.0-preview'), ('hl7.fhir.au.core#1.1.0-preview', 'hl7.fhir.au.core#1.1.0-preview'), ('hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r4.core#4.0.1'), ('hl7.fhir.uv.extensions.r4#5.2.0', 'hl7.fhir.uv.extensions.r4#5.2.0'), ('hl7.fhir.uv.ipa#1.0.0', 'hl7.fhir.uv.ipa#1.0.0'), ('hl7.fhir.uv.smart-app-launch#2.0.0', 'hl7.fhir.uv.smart-app-launch#2.0.0'), ('hl7.fhir.uv.smart-app-launch#2.1.0', 'hl7.fhir.uv.smart-app-launch#2.1.0'), ('hl7.terminology.r4#5.0.0', 'hl7.terminology.r4#5.0.0'), ('hl7.terminology.r4#6.2.0', 'hl7.terminology.r4#6.2.0')]
DEBUG:__main__:Handling GET request for FSH converter page.
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "GET /fsh-converter HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "GET /static/js/lottie.min.js HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "GET /static/animations/loading-light.json HTTP/1.1" 304 -
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
DEBUG:__main__:Found 9 .tgz files: ['hl7.fhir.au.base-5.1.0-preview.tgz', 'hl7.fhir.au.core-1.1.0-preview.tgz', 'hl7.fhir.r4.core-4.0.1.tgz', 'hl7.fhir.uv.extensions.r4-5.2.0.tgz', 'hl7.fhir.uv.ipa-1.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz', 'hl7.terminology.r4-5.0.0.tgz', 'hl7.terminology.r4-6.2.0.tgz']
DEBUG:__main__:Added package: hl7.fhir.au.base#5.1.0-preview
DEBUG:__main__:Added package: hl7.fhir.au.core#1.1.0-preview
DEBUG:__main__:Added package: hl7.fhir.r4.core#4.0.1
DEBUG:__main__:Added package: hl7.fhir.uv.extensions.r4#5.2.0
DEBUG:__main__:Added package: hl7.fhir.uv.ipa#1.0.0
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.0.0
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.1.0
DEBUG:__main__:Added package: hl7.terminology.r4#5.0.0
DEBUG:__main__:Added package: hl7.terminology.r4#6.2.0
DEBUG:__main__:Set package choices: [('', 'None'), ('hl7.fhir.au.base#5.1.0-preview', 'hl7.fhir.au.base#5.1.0-preview'), ('hl7.fhir.au.core#1.1.0-preview', 'hl7.fhir.au.core#1.1.0-preview'), ('hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r4.core#4.0.1'), ('hl7.fhir.uv.extensions.r4#5.2.0', 'hl7.fhir.uv.extensions.r4#5.2.0'), ('hl7.fhir.uv.ipa#1.0.0', 'hl7.fhir.uv.ipa#1.0.0'), ('hl7.fhir.uv.smart-app-launch#2.0.0', 'hl7.fhir.uv.smart-app-launch#2.0.0'), ('hl7.fhir.uv.smart-app-launch#2.1.0', 'hl7.fhir.uv.smart-app-launch#2.1.0'), ('hl7.terminology.r4#5.0.0', 'hl7.terminology.r4#5.0.0'), ('hl7.terminology.r4#6.2.0', 'hl7.terminology.r4#6.2.0')]
DEBUG:__main__:Processing input: mode=text, has_file=False, has_text=True, has_alias=False
DEBUG:services:Processed input: ('/tmp/tmpkn9pyg0k/input.json', None)
DEBUG:__main__:Running GoFSH with input: /tmp/tmpkn9pyg0k/input.json, output_dir: /app/static/uploads/fsh_output
DEBUG:services:Wrapper script contents:
#!/bin/bash
exec 3>/dev/null
"gofsh" "/tmp/tmpkn9pyg0k/input.json" "-o" "/tmp/tmp37ihga7h" "-s" "file-per-definition" "-l" "error" "-u" "4.3.0" "--dependency" "hl7.fhir.us.core@6.1.0" "--indent" </dev/null >/tmp/gofsh_output.log 2>&1
DEBUG:services:Temp output directory contents before GoFSH: []
DEBUG:services:GoFSH output:
╔═════════════════════════ GoFSH RESULTS ═════════════════════════╗
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Profiles │ Extensions │ Logicals │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 0 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Resources │ ValueSets │ CodeSystems │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 0 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Instances │ Invariants │ Mappings │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 1 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Aliases │ │ │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 1 │ │ │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ║
╠═════════════════════════════════════════════════════════════════╣
║ Not bad, but it cod be batter! 0 Errors 1 Warning ║
╚═════════════════════════════════════════════════════════════════╝
DEBUG:services:GoFSH fishing-trip wrapper script contents:
#!/bin/bash
exec 3>/dev/null
exec >/dev/null 2>&1
"gofsh" "/tmp/tmpkn9pyg0k/input.json" "-o" "/tmp/tmpb_q8uf73" "-s" "file-per-definition" "-l" "error" "--fshing-trip" "-u" "4.3.0" "--dependency" "hl7.fhir.us.core@6.1.0" "--indent" </dev/null >/tmp/gofsh_output.log 2>&1
DEBUG:services:GoFSH fishing-trip output:
╔═════════════════════════ GoFSH RESULTS ═════════════════════════╗
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Profiles │ Extensions │ Logicals │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 0 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Resources │ ValueSets │ CodeSystems │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 0 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Instances │ Invariants │ Mappings │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 1 │ 0 │ 0 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ Aliases │ │ │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 1 │ │ │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ║
╠═════════════════════════════════════════════════════════════════╣
║ Warnings... Water those about? 0 Errors 1 Warning ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ Generating round trip results via SUSHI ║
╚═════════════════════════════════════════════════════════════════╝
info Running SUSHI v3.15.0 (implements FHIR Shorthand specification v3.0.0)
info Arguments:
info /tmp/tmpb_q8uf73
info No output path specified. Output to /tmp/tmpb_q8uf73
info Using configuration file: /tmp/tmpb_q8uf73/sushi-config.yaml
warn The FSHOnly property is set to true, so no output specific to IG creation will be generated. The following properties are unused and only relevant for IG creation: id, name. Consider removing these properties from sushi-config.yaml.
File: /tmp/tmpb_q8uf73/sushi-config.yaml
info Importing FSH text...
info Preprocessed 2 documents with 1 aliases.
info Imported 0 definitions and 1 instances.
info Loaded virtual package sushi-r5forR4#1.0.0 with 7 resources
info Resolved hl7.fhir.uv.tools.r4#latest to concrete version 0.5.0
info Loaded hl7.fhir.uv.tools.r4#0.5.0 with 88 resources
info Resolved hl7.terminology.r4#latest to concrete version 6.2.0
info Loaded hl7.terminology.r4#6.2.0 with 4323 resources
info Resolved hl7.fhir.uv.extensions.r4#latest to concrete version 5.2.0
info Loaded hl7.fhir.uv.extensions.r4#5.2.0 with 759 resources
info Loaded hl7.fhir.us.core#6.1.0 with 210 resources
info Loaded hl7.fhir.r4b.core#4.3.0 with 3497 resources
info Loaded virtual package sushi-local#LOCAL with 0 resources
info Converting FSH to FHIR resources...
info Converted 1 FHIR instances.
info Exporting FHIR resources as JSON...
info Exported 1 FHIR resources as JSON.
info Exporting FSH definitions only. No IG related content will be exported.
========================= SUSHI RESULTS ===========================
| ------------------------------------------------------------- |
| | Profiles | Extensions | Logicals | Resources | |
| |-------------------------------------------------------------| |
| | 0 | 0 | 0 | 0 | |
| ------------------------------------------------------------- |
| ------------------------------------------------------------- |
| | ValueSets | CodeSystems | Instances | |
| |-------------------------------------------------------------| |
| | 0 | 0 | 1 | |
| ------------------------------------------------------------- |
| |
===================================================================
| You might need some Vitamin Sea 0 Errors 1 Warning |
===================================================================
DEBUG:services:Copied files to final output directory: ['sushi-config.yaml', 'input/fsh/aliases.fsh', 'input/fsh/instances/discharge-1.fsh', 'input/input.json', 'fshing-trip-comparison.html']
INFO:services:GoFSH executed successfully for /tmp/tmpkn9pyg0k/input.json
DEBUG:__main__:Successfully removed temp directory: /tmp/tmpkn9pyg0k
INFO:__main__:FSH conversion successful
DEBUG:__main__:Returning partial HTML for AJAX POST request.
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:26] "POST /fsh-converter HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:26] "GET /static/animations/loading-light.json HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:42] "GET /static/uploads/fsh_output/fshing-trip-comparison.html HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:59] "GET /fhir-ui-operations HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:41:00] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:27] "GET /fhir HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:27] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:31] "GET /fhir-ui-operations HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:31] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:39] "GET /fhir HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:39] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:48] "GET /validate-sample HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:48] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:44:08] "GET / HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:44:08] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
DEBUG:services:Parsed 'hl7.fhir.au.base-5.1.0-preview.tgz' -> name='hl7.fhir.au.base-5.1.0', version='preview'
DEBUG:services:Parsed 'hl7.fhir.au.core-1.1.0-preview.tgz' -> name='hl7.fhir.au.core-1.1.0', version='preview'
DEBUG:services:Parsed 'hl7.fhir.r4.core-4.0.1.tgz' -> name='hl7.fhir.r4.core', version='4.0.1'
DEBUG:services:Parsed 'hl7.fhir.uv.extensions.r4-5.2.0.tgz' -> name='hl7.fhir.uv.extensions.r4', version='5.2.0'
DEBUG:services:Parsed 'hl7.fhir.uv.ipa-1.0.0.tgz' -> name='hl7.fhir.uv.ipa', version='1.0.0'
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.0.0'
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.1.0'
DEBUG:services:Parsed 'hl7.terminology.r4-5.0.0.tgz' -> name='hl7.terminology.r4', version='5.0.0'
DEBUG:services:Parsed 'hl7.terminology.r4-6.2.0.tgz' -> name='hl7.terminology.r4', version='6.2.0'
DEBUG:__main__:Found packages: [{'name': 'hl7.fhir.au.base', 'version': '5.1.0-preview', 'filename': 'hl7.fhir.au.base-5.1.0-preview.tgz'}, {'name': 'hl7.fhir.au.core', 'version': '1.1.0-preview', 'filename': 'hl7.fhir.au.core-1.1.0-preview.tgz'}, {'name': 'hl7.fhir.r4.core', 'version': '4.0.1', 'filename': 'hl7.fhir.r4.core-4.0.1.tgz'}, {'name': 'hl7.fhir.uv.extensions.r4', 'version': '5.2.0', 'filename': 'hl7.fhir.uv.extensions.r4-5.2.0.tgz'}, {'name': 'hl7.fhir.uv.ipa', 'version': '1.0.0', 'filename': 'hl7.fhir.uv.ipa-1.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.0.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.1.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '5.0.0', 'filename': 'hl7.terminology.r4-5.0.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '6.2.0', 'filename': 'hl7.terminology.r4-6.2.0.tgz'}]
DEBUG:__main__:Errors during package listing: []
DEBUG:__main__:Duplicate groups: {'hl7.fhir.uv.smart-app-launch': ['2.0.0', '2.1.0'], 'hl7.terminology.r4': ['5.0.0', '6.2.0']}
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:07] "GET /view-igs HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:07] "GET /static/FHIRFLARE.png HTTP/1.1" 200 -
DEBUG:__main__:Viewing IG hl7.fhir.au.core-1.1.0#preview: 25 profiles, 17 base resources, 1 optional elements
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:13] "GET /view-ig/1 HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:13] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
DEBUG:__main__:Attempting to find SD for 'au-core-patient' in hl7.fhir.au.core-1.1.0-preview.tgz
DEBUG:services:Searching for SD matching 'au-core-patient' with profile 'None' in hl7.fhir.au.core-1.1.0-preview.tgz
DEBUG:services:Match found based on exact sd_id: au-core-patient
DEBUG:services:Removing narrative text from resource: StructureDefinition
INFO:services:Found high-confidence match for 'au-core-patient' (package/StructureDefinition-au-core-patient.json), stopping search.
DEBUG:__main__:Found 20 unique IDs in differential.
DEBUG:__main__:Processing 78 snapshot elements to add isInDifferential flag.
DEBUG:__main__:Retrieved 19 Must Support paths for 'au-core-patient' from processed IG DB record.
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:17] "GET /get-structure?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&resource_type=au-core-patient&view=snapshot HTTP/1.1" 200 -
DEBUG:__main__:Removing narrative text from example 'package/example/Patient-banks-mia-leanne.json'
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:32] "GET /get-example?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&filename=package/example/Patient-banks-mia-leanne.json HTTP/1.1" 200 -
INFO:__main__:Application running in mode: lite
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.18.0.2:5000
INFO:werkzeug:Press CTRL+C to quit
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:38] "GET / HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:38] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:44] "GET /about HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:44] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:13:32] "GET / HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:13:32] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -
INFO:__main__:Application running in mode: lite
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
DEBUG:__main__:Instance path configuration: /app/instance
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
DEBUG:__main__:Packages path: /app/instance/fhir_packages
DEBUG:__main__:Flask instance folder path: /app/instance
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
INFO:__main__:Database tables created successfully (if they didn't exist).
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.18.0.2:5000
INFO:werkzeug:Press CTRL+C to quit
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:28:51] "GET / HTTP/1.1" 200 -
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:28:52] "GET /static/FHIRFLARE.png HTTP/1.1" 304 -

View File

@ -1,40 +0,0 @@
2025-04-21 23:28:14,353 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-21 23:28:14,360 INFO supervisord started with pid 1
2025-04-21 23:28:15,369 INFO spawned: 'flask' with pid 8
2025-04-21 23:28:15,377 INFO spawned: 'tomcat' with pid 9
2025-04-21 23:28:26,240 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-21 23:28:46,263 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-21 23:32:40,570 WARN received SIGTERM indicating exit request
2025-04-21 23:32:40,603 INFO waiting for flask, tomcat to die
2025-04-21 23:32:42,242 WARN stopped: tomcat (exit status 143)
2025-04-21 23:32:43,301 WARN stopped: flask (terminated by SIGTERM)
2025-04-21 23:36:02,369 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-21 23:36:02,377 INFO supervisord started with pid 1
2025-04-21 23:36:03,393 INFO spawned: 'flask' with pid 7
2025-04-21 23:36:03,407 INFO spawned: 'tomcat' with pid 8
2025-04-21 23:36:13,571 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-21 23:36:34,045 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-21 23:51:48,038 WARN received SIGTERM indicating exit request
2025-04-21 23:51:48,042 INFO waiting for flask, tomcat to die
2025-04-21 23:51:50,438 WARN stopped: tomcat (exit status 143)
2025-04-21 23:51:51,472 WARN stopped: flask (terminated by SIGTERM)
2025-04-22 00:09:45,824 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-22 00:09:45,840 INFO supervisord started with pid 1
2025-04-22 00:09:46,858 INFO spawned: 'flask' with pid 7
2025-04-22 00:09:46,862 INFO spawned: 'tomcat' with pid 8
2025-04-22 00:09:57,177 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-22 00:10:17,198 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-22 00:25:04,477 WARN received SIGTERM indicating exit request
2025-04-22 00:25:04,493 INFO waiting for flask, tomcat to die
2025-04-22 00:25:05,830 WARN stopped: tomcat (exit status 143)
2025-04-22 00:25:06,852 WARN stopped: flask (terminated by SIGTERM)
2025-04-22 00:27:24,713 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-22 00:27:24,719 INFO supervisord started with pid 1
2025-04-22 00:27:25,731 INFO spawned: 'flask' with pid 7
2025-04-22 00:27:25,736 INFO spawned: 'tomcat' with pid 8
2025-04-22 00:27:36,540 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-22 00:27:56,565 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-22 00:54:33,995 WARN received SIGTERM indicating exit request
2025-04-22 00:54:34,047 INFO waiting for flask, tomcat to die
2025-04-22 00:54:34,740 WARN stopped: tomcat (exit status 143)
2025-04-22 00:54:35,770 WARN stopped: flask (terminated by SIGTERM)

View File

@ -1 +0,0 @@
1

View File

View File

@ -1,680 +0,0 @@
21-Apr-2025 23:28:16.155 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
21-Apr-2025 23:28:16.187 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
21-Apr-2025 23:28:16.196 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
21-Apr-2025 23:28:16.200 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
21-Apr-2025 23:28:16.564 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:28:16.604 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [733] milliseconds
21-Apr-2025 23:28:16.680 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
21-Apr-2025 23:28:16.680 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
21-Apr-2025 23:28:16.703 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
21-Apr-2025 23:28:16.727 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
21-Apr-2025 23:28:16.769 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
java.lang.IllegalStateException: Error starting child
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@5a5a729f]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
... 37 more
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
... 44 more
Caused by: java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
... 45 more
21-Apr-2025 23:28:16.773 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [68] ms
21-Apr-2025 23:28:16.774 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
21-Apr-2025 23:28:17.183 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [409] ms
21-Apr-2025 23:28:17.184 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
21-Apr-2025 23:28:17.203 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [19] ms
21-Apr-2025 23:28:17.208 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:28:17.227 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [623] milliseconds
21-Apr-2025 23:32:40.652 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:32:40.760 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
21-Apr-2025 23:32:40.883 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:32:40.910 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:36:04.469 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
21-Apr-2025 23:36:04.503 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
21-Apr-2025 23:36:04.503 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
21-Apr-2025 23:36:04.519 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
21-Apr-2025 23:36:04.524 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
21-Apr-2025 23:36:05.240 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:36:05.313 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [1239] milliseconds
21-Apr-2025 23:36:05.489 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
21-Apr-2025 23:36:05.490 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
21-Apr-2025 23:36:05.551 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
21-Apr-2025 23:36:05.590 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
21-Apr-2025 23:36:05.657 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
java.lang.IllegalStateException: Error starting child
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@d554c5f]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
... 37 more
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
... 44 more
Caused by: java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
... 45 more
21-Apr-2025 23:36:05.660 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [108] ms
21-Apr-2025 23:36:05.662 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
21-Apr-2025 23:36:06.431 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [769] ms
21-Apr-2025 23:36:06.432 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
21-Apr-2025 23:36:06.468 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [36] ms
21-Apr-2025 23:36:06.478 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:36:06.561 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [1247] milliseconds
21-Apr-2025 23:51:48.137 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:51:48.297 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
21-Apr-2025 23:51:48.716 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
21-Apr-2025 23:51:48.787 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:09:47.747 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
22-Apr-2025 00:09:47.765 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
22-Apr-2025 00:09:47.765 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
22-Apr-2025 00:09:47.796 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
22-Apr-2025 00:09:47.796 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
22-Apr-2025 00:09:47.815 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
22-Apr-2025 00:09:47.822 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
22-Apr-2025 00:09:48.310 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:09:48.348 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [997] milliseconds
22-Apr-2025 00:09:48.467 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
22-Apr-2025 00:09:48.467 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
22-Apr-2025 00:09:48.489 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
22-Apr-2025 00:09:48.519 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
22-Apr-2025 00:09:48.556 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
java.lang.IllegalStateException: Error starting child
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@68c9d179]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
... 37 more
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
... 44 more
Caused by: java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
... 45 more
22-Apr-2025 00:09:48.560 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [69] ms
22-Apr-2025 00:09:48.561 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
22-Apr-2025 00:09:49.063 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [502] ms
22-Apr-2025 00:09:49.063 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
22-Apr-2025 00:09:49.084 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [21] ms
22-Apr-2025 00:09:49.103 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:09:49.166 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [817] milliseconds
22-Apr-2025 00:25:04.510 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:25:04.547 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
22-Apr-2025 00:25:04.657 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:25:04.685 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:27:26.457 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
22-Apr-2025 00:27:26.501 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
22-Apr-2025 00:27:26.505 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
22-Apr-2025 00:27:26.893 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:27:26.937 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [790] milliseconds
22-Apr-2025 00:27:27.009 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
22-Apr-2025 00:27:27.009 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
22-Apr-2025 00:27:27.033 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
22-Apr-2025 00:27:27.058 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
22-Apr-2025 00:27:27.096 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
java.lang.IllegalStateException: Error starting child
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@4b520ea8]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
... 37 more
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
... 44 more
Caused by: java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
... 45 more
22-Apr-2025 00:27:27.100 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [67] ms
22-Apr-2025 00:27:27.101 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
22-Apr-2025 00:27:27.481 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [379] ms
22-Apr-2025 00:27:27.482 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
22-Apr-2025 00:27:27.504 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [22] ms
22-Apr-2025 00:27:27.510 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:27:27.530 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [593] milliseconds
22-Apr-2025 00:54:34.132 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:54:34.299 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
22-Apr-2025 00:54:34.497 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
22-Apr-2025 00:54:34.519 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]

File diff suppressed because it is too large Load Diff

View File

@ -770,6 +770,9 @@ html[data-theme="dark"] .structure-tree-root .list-group-item-warning {
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="fas fa-upload me-1"></i> Push IGs</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'upload_test_data' else '' }}" href="{{ url_for('upload_test_data') }}"><i class="bi bi-clipboard-data me-1"></i>Upload Test Data</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="fas fa-check-circle me-1"></i> Validate FHIR Sample</a>
@ -777,7 +780,7 @@ html[data-theme="dark"] .structure-tree-root .list-group-item-warning {
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui' else '' }}" href="{{ url_for('fhir_ui') }}"><i class="fas fa-cloud-download-alt me-1"></i> FHIR API Explorer</a>
</li>
<li class="nav-item">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui_operations' else '' }}" href="{{ url_for('fhir_ui_operations') }}"><i class="bi bi-gear me-1"></i> FHIR UI Operations</a>
</li>
<li class="nav-item">

View File

@ -1,461 +1,418 @@
{% extends "base.html" %}
{# Import form helpers if needed, e.g., for CSRF token #}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<div class="container-fluid">
<div class="container-fluid mt-4">
<div class="row">
{# Left Column: Downloaded IGs List & Report Area #}
<div class="col-md-6">
<h2>Downloaded IGs</h2>
<h2><i class="bi bi-box-seam me-2"></i>Downloaded IGs</h2>
{% if packages %}
<table class="table table-striped">
<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 filename = pkg.filename %}
{% set processed = (name, version) in processed_ids %}
{% set duplicate_group = duplicate_groups.get(name) %}
<tr {% if duplicate_group %}class="{{ group_colors[name] }}"{% endif %}>
<td>{{ name }}</td>
<td>{{ version }}</td>
<div class="table-responsive mb-3">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Package Name</th>
<th>Version</th>
</tr>
{% endfor %}
</tbody>
</table>
</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">
<strong>Duplicate Packages Detected:</strong>
<ul>
<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>Please resolve duplicates by deleting older versions to avoid conflicts.</p>
<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>No packages downloaded yet. Use the "Import IG" tab to download packages.</p>
<p class="text-muted">No packages downloaded yet. Use the "Import IG" tab.</p>
{% endif %}
</div>
{# --- MOVED: 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>
{# --- NEW: Report Action Buttons --- #}
<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>
{# --- END NEW --- #}
</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>
{# Flash messages can still appear here if needed #}
{% 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>
{# --- END MOVED --- #}
</div>{# End Left Column #}
{# Right Column: Push IGs Form and Console #}
<div class="col-md-6">
<h2>Push IGs to FHIR Server</h2>
<form id="pushIgForm" method="POST">
{{ form.csrf_token }}
<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>
<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>
</div>
<div class="mb-3">
<label for="fhirServerUrl" class="form-label">FHIR Server URL</label>
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://hapi.fhir.org/baseR4" required>
</div>
<div class="mb-3 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>
<button type="submit" class="btn btn-primary" id="pushButton">Push to FHIR Server</button>
</form>
<h2><i class="bi bi-upload me-2"></i>Push IGs to FHIR Server</h2>
<form id="pushIgForm">
{{ form.csrf_token if form else '' }} {# Use form passed from route #}
<div class="mt-3">
<h4>Live Console</h4>
<div id="liveConsole" class="border p-3 bg-dark text-light" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 14px;">
<div>Console output will appear here...</div>
{# 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>
{# --- RESTRUCTURED: Auth and Checkboxes --- #}
<div class="row g-3 mb-3 align-items-end">
{# Authentication Dropdown & Token Input #}
<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>
</select>
</div>
<div class="col-md-7" id="authTokenGroup" 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>
</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>
{# --- END RESTRUCTURED --- #}
{# 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 id="pushResponse" class="mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}"> {{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
</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');
// Ensure form object exists before accessing csrf_token, handle potential errors
const csrfToken = typeof form !== 'undefined' && form.csrf_token ? "{{ form.csrf_token._value() }}" : document.querySelector('input[name="csrf_token"]')?.value || "";
if (!csrfToken) {
console.warn("CSRF token not found in template or hidden input.");
const pushIgForm = document.getElementById('pushIgForm');
const pushButton = document.getElementById('pushButton');
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('pushResponse'); // Area for final report
const reportActions = document.getElementById('reportActions'); // Container for report buttons
const copyReportBtn = document.getElementById('copyReportBtn'); // New copy button
const downloadReportBtn = document.getElementById('downloadReportBtn'); // New download button
const authTypeSelect = document.getElementById('authType');
const authTokenGroup = document.getElementById('authTokenGroup');
const authTokenInput = document.getElementById('authToken');
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 = ''; // Clear on change
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')); }
}
// Update dependency mode when package selection changes
packageSelect.addEventListener('change', function() {
const packageId = this.value;
if (packageId) {
// Show/Hide Bearer Token Input
if (authTypeSelect && authTokenGroup) {
authTypeSelect.addEventListener('change', function() {
authTokenGroup.style.display = this.value === 'bearerToken' ? 'block' : 'none';
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
});
authTokenGroup.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
}
// --- NEW: Report Action Button Listeners ---
if (copyReportBtn && responseDiv) {
copyReportBtn.addEventListener('click', () => {
const reportAlert = responseDiv.querySelector('.alert'); // Get the alert div inside
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : ''; // Get text content
if (reportText && navigator.clipboard) {
navigator.clipboard.writeText(reportText)
.then(() => {
// Optional: Provide feedback (e.g., change button text/icon)
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); // Clean up
} else {
alert('No report content found to download.');
}
});
}
// --- END NEW ---
// --- Form Submission ---
if (pushIgForm) {
pushIgForm.addEventListener('submit', async function(event) {
event.preventDefault();
// Get form values (with null checks for elements)
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('#');
fetch(`/get-package-metadata?package_name=${packageName}&version=${version}`)
.then(response => {
if (!response.ok) {
throw new Error(`Metadata fetch failed: ${response.status}`);
}
return response.json();
})
.then(data => {
// Check specifically for the dependency_mode key in the response data
if (data && typeof data === 'object' && data.hasOwnProperty('dependency_mode')) {
dependencyModeField.value = data.dependency_mode !== null ? data.dependency_mode : 'N/A';
} else {
dependencyModeField.value = 'Unknown'; // Or 'N/A' if metadata exists but no mode
console.warn('Dependency mode not found in metadata response:', data);
}
})
.catch(error => {
console.error('Error fetching or processing metadata:', error);
dependencyModeField.value = 'Error';
const auth_type = authTypeSelect ? authTypeSelect.value : 'none';
const auth_token = (auth_type === 'bearerToken' && authTokenInput) ? authTokenInput.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;
// UI Updates & API Key
if (pushButton) { pushButton.disabled = true; pushButton.textContent = '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>'; } // Clear previous report
if (reportActions) { reportActions.style.display = 'none'; } // Hide report buttons initially
const internalApiKey = {{ api_key | default("") | tojson }}; // Use tojson filter
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,
resource_types_filter: resource_types_filter, skip_files: skip_files,
dry_run: dry_run, verbose: isVerboseChecked, force_upload: force_upload
})
});
} else {
dependencyModeField.value = '';
}
});
// Trigger change event on page load if a package is pre-selected
if (packageSelect.value) {
packageSelect.dispatchEvent(new Event('change'));
}
if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`); }
if (!response.body) { throw new Error("Response body is missing."); }
// --- Push IG Form Submission ---
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
event.preventDefault();
// Stream Processing
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const form = event.target;
const pushButton = document.getElementById('pushButton');
const formData = new FormData(form); // Use FormData to easily get values
const packageId = formData.get('package_id');
const fhirServerUrl = formData.get('fhir_server_url');
const includeDependencies = formData.get('include_dependencies') === 'on'; // Checkbox value is 'on' if checked
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() || '';
if (!packageId) {
alert('Please select a package to push.');
return;
}
if (!fhirServerUrl) {
alert('Please enter the FHIR Server URL.');
return;
}
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;
const [packageName, version] = packageId.split('#');
switch (data.type) { // Determine if message should display based on verbose
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;
}
// Disable the button to prevent multiple submissions
pushButton.disabled = true;
pushButton.textContent = 'Pushing...';
if (shouldDisplay && liveConsole) { // Set prefix/class and append to console
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';}
// Clear the console and response area
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('pushResponse');
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting push operation for ${packageName}#${version}...</div>`;
responseDiv.innerHTML = ''; // Clear previous final response
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
messageDiv.textContent = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
}
// Get API Key from template context - ensure 'api_key' is passed correctly from Flask route
const apiKey = '{{ api_key | default("") | safe }}';
if (!apiKey) {
console.warn("API Key not found in template context. Ensure 'api_key' is passed to render_template.");
// Optionally display a warning to the user if needed
}
if (data.type === 'complete' && responseDiv) { // Update final summary box
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>';}
try {
const response = await fetch('/api/push-ig', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/x-ndjson', // We expect NDJSON stream
'X-CSRFToken': csrfToken, // Include CSRF token from form/template
'X-API-Key': apiKey // Send API Key in header (alternative to body)
},
body: JSON.stringify({ // Still send details in body
package_name: packageName,
version: version,
fhir_server_url: fhirServerUrl,
include_dependencies: includeDependencies
// Removed api_key from body as it's now in header
})
});
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'; } // Show report buttons
}
} catch (parseError) { /* (Handle JSON parse errors) */ console.error('Stream parse error:', parseError); if(liveConsole){/*...add error to console...*/} }
} // end for loop
} // end while loop
// Handle immediate errors before trying to read the stream
if (!response.ok) {
let errorMsg = `HTTP error! status: ${response.status}`;
try {
// Try to parse a JSON error response from the server
const errorData = await response.json();
errorMsg = errorData.message || JSON.stringify(errorData);
} catch (e) {
// If response is not JSON, use the status text
errorMsg = response.statusText || errorMsg;
}
throw new Error(errorMsg);
}
// Process Final Buffer (if any)
if (buffer.trim()) {
try { /* (Parsing logic for final buffer, similar to above) */ }
catch (parseError) { /* (Handle final buffer parse error) */ }
}
// --- Stream Processing ---
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break; // Exit loop when stream is finished
buffer += decoder.decode(value, { stream: true }); // Decode chunk and append to buffer
const lines = buffer.split('\n'); // Split buffer into lines
buffer = lines.pop(); // Keep potential partial line for next chunk
for (const line of lines) {
if (!line.trim()) continue; // Skip empty lines
try {
const data = JSON.parse(line); // Parse each line as JSON
const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-light'; // Default text color for console
let prefix = '[INFO]'; // Default prefix for console
// --- Switch based on message type from backend ---
switch (data.type) {
case 'start':
case 'progress':
case 'info':
messageClass = 'text-info';
prefix = '[INFO]';
break;
case 'success':
messageClass = 'text-success';
prefix = '[SUCCESS]';
break;
case 'error':
messageClass = 'text-danger';
prefix = '[ERROR]';
break;
case 'warning':
messageClass = 'text-warning';
prefix = '[WARNING]';
break;
case 'complete':
// ---vvv THIS IS THE CORRECTED BLOCK vvv---
// Process the final summary data
const summaryData = data.data; // data.data contains the summary object
let alertClass = 'alert-info'; // Bootstrap class for responseDiv
let statusText = 'Info';
let pushedPackagesList = 'None';
let failureDetailsHtml = ''; // Initialize failure details HTML
// Format pushed packages summary (using correct field name)
if (summaryData.pushed_packages_summary && summaryData.pushed_packages_summary.length > 0) {
pushedPackagesList = summaryData.pushed_packages_summary.map(pkg => `${pkg.id} (${pkg.resource_count} resources)`).join('<br>');
}
// Determine alert class based on status
if (summaryData.status === 'success') {
alertClass = 'alert-success';
statusText = 'Success';
prefix = '[SUCCESS]'; // For console log
} else if (summaryData.status === 'partial') {
alertClass = 'alert-warning';
statusText = 'Partial Success';
prefix = '[PARTIAL]'; // For console log
} else { // failure or error
alertClass = 'alert-danger';
statusText = 'Error';
prefix = '[ERROR]'; // For console log
}
// Format failure details if present (using correct field name)
if (summaryData.failed_details && summaryData.failed_details.length > 0) {
failureDetailsHtml += '<hr><strong>Failure Details:</strong><ul style="font-size: 0.9em; max-height: 150px; overflow-y: auto;">';
summaryData.failed_details.forEach(fail => {
const escapedResource = fail.resource ? fail.resource.replace(/</g, "&lt;").replace(/>/g, "&gt;") : "N/A";
const escapedError = fail.error ? fail.error.replace(/</g, "&lt;").replace(/>/g, "&gt;") : "Unknown error";
failureDetailsHtml += `<li><strong>${escapedResource}:</strong> ${escapedError}</li>`;
});
failureDetailsHtml += '</ul>';
}
// Update the final response area (pushResponse div)
responseDiv.innerHTML = `
<div class="alert ${alertClass}">
<strong>${statusText}:</strong> ${summaryData.message || 'Operation complete.'}<br>
<hr>
<strong>Target Server:</strong> ${summaryData.target_server || 'N/A'}<br>
<strong>Package:</strong> ${summaryData.package_name || 'N/A'}#${summaryData.version || 'N/A'}<br>
<strong>Included Dependencies:</strong> ${summaryData.included_dependencies ? 'Yes' : 'No'}<br>
<strong>Resources Attempted:</strong> ${summaryData.resources_attempted !== undefined ? summaryData.resources_attempted : 'N/A'}<br>
<strong>Succeeded:</strong> ${summaryData.success_count !== undefined ? summaryData.success_count : 'N/A'}<br>
<strong>Failed:</strong> ${summaryData.failure_count !== undefined ? summaryData.failure_count : 'N/A'}<br>
<strong>Packages Pushed Summary:</strong><br><div style="padding-left: 15px;">${pushedPackagesList}</div>
${failureDetailsHtml} </div>
`;
// Also set the final message class for the console log
messageClass = alertClass.replace('alert-', 'text-');
break; // Break from switch
// ---^^^ END OF CORRECTED BLOCK ^^^---
default:
console.warn("Received unknown message type:", data.type);
prefix = '[UNKNOWN]';
break;
} // End Switch
// --- Add message to Live Console ---
const messageDiv = document.createElement('div');
messageDiv.className = messageClass;
// Use data.message for most types, use summary message for complete type
// Check if data.data exists for complete type before accessing its message
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
messageDiv.textContent = `${timestamp} ${prefix} ${messageText || 'Received empty message.'}`;
liveConsole.appendChild(messageDiv);
liveConsole.scrollTop = liveConsole.scrollHeight; // Scroll to bottom
} catch (parseError) { // Handle errors parsing individual JSON lines
console.error('Error parsing streamed message:', parseError, 'Line:', line);
const timestamp = new Date().toLocaleTimeString();
const errorDiv = document.createElement('div');
errorDiv.className = 'text-danger';
errorDiv.textContent = `${timestamp} [PARSE_ERROR] Could not parse message: ${line.substring(0, 100)}${line.length > 100 ? '...' : ''}`; // Show partial line
liveConsole.appendChild(errorDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
} // end for loop over lines
} // end while loop over stream
// --- Process Final Buffer ---
// (Repeat the parsing logic for any remaining data in the buffer)
if (buffer.trim()) {
try {
const data = JSON.parse(buffer);
const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-light'; // Default for console
let prefix = '[INFO]'; // Default for console
switch (data.type) {
case 'start': // Less likely in final buffer, but handle defensively
case 'progress':
case 'info':
messageClass = 'text-info';
prefix = '[INFO]';
break;
case 'success':
messageClass = 'text-success';
prefix = '[SUCCESS]';
break;
case 'error':
messageClass = 'text-danger';
prefix = '[ERROR]';
break;
case 'warning':
messageClass = 'text-warning';
prefix = '[WARNING]';
break;
// ---vvv THIS IS THE CORRECTED BLOCK vvv---
case 'complete':
const summaryData = data.data;
let alertClass = 'alert-info';
let statusText = 'Info';
let pushedPackagesList = 'None';
let failureDetailsHtml = '';
if (summaryData.pushed_packages_summary && summaryData.pushed_packages_summary.length > 0) {
pushedPackagesList = summaryData.pushed_packages_summary.map(pkg => `${pkg.id} (${pkg.resource_count} resources)`).join('<br>');
}
if (summaryData.status === 'success') {
alertClass = 'alert-success'; statusText = 'Success'; prefix = '[SUCCESS]';
} else if (summaryData.status === 'partial') {
alertClass = 'alert-warning'; statusText = 'Partial Success'; prefix = '[PARTIAL]';
} else {
alertClass = 'alert-danger'; statusText = 'Error'; prefix = '[ERROR]';
}
if (summaryData.failed_details && summaryData.failed_details.length > 0) {
failureDetailsHtml += '<hr><strong>Failure Details:</strong><ul style="font-size: 0.9em; max-height: 150px; overflow-y: auto;">';
summaryData.failed_details.forEach(fail => {
const escapedResource = fail.resource ? fail.resource.replace(/</g, "&lt;").replace(/>/g, "&gt;") : "N/A";
const escapedError = fail.error ? fail.error.replace(/</g, "&lt;").replace(/>/g, "&gt;") : "Unknown error";
failureDetailsHtml += `<li><strong>${escapedResource}:</strong> ${escapedError}</li>`;
});
failureDetailsHtml += '</ul>';
}
responseDiv.innerHTML = `
<div class="alert ${alertClass}">
<strong>${statusText}:</strong> ${summaryData.message || 'Operation complete.'}<br>
<hr>
<strong>Target Server:</strong> ${summaryData.target_server || 'N/A'}<br>
<strong>Package:</strong> ${summaryData.package_name || 'N/A'}#${summaryData.version || 'N/A'}<br>
<strong>Included Dependencies:</strong> ${summaryData.included_dependencies ? 'Yes' : 'No'}<br>
<strong>Resources Attempted:</strong> ${summaryData.resources_attempted !== undefined ? summaryData.resources_attempted : 'N/A'}<br>
<strong>Succeeded:</strong> ${summaryData.success_count !== undefined ? summaryData.success_count : 'N/A'}<br>
<strong>Failed:</strong> ${summaryData.failure_count !== undefined ? summaryData.failure_count : 'N/A'}<br>
<strong>Packages Pushed Summary:</strong><br><div style="padding-left: 15px;">${pushedPackagesList}</div>
${failureDetailsHtml}
</div>
`;
messageClass = alertClass.replace('alert-', 'text-');
break;
// ---^^^ END OF CORRECTED BLOCK ^^^---
default:
console.warn("Received unknown message type in final buffer:", data.type);
prefix = '[UNKNOWN]';
break;
} // End Switch
// Add final buffer message to live console
const messageDiv = document.createElement('div');
messageDiv.className = messageClass;
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
messageDiv.textContent = `${timestamp} ${prefix} ${messageText || 'Received empty message.'}`;
liveConsole.appendChild(messageDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
} catch (parseError) { // Handle error parsing final buffer
console.error('Error parsing final streamed message:', parseError, 'Buffer:', buffer);
const timestamp = new Date().toLocaleTimeString();
const errorDiv = document.createElement('div');
errorDiv.className = 'text-danger';
errorDiv.textContent = `${timestamp} [PARSE_ERROR] Could not parse final message: ${buffer.substring(0, 100)}${buffer.length > 100 ? '...' : ''}`;
liveConsole.appendChild(errorDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
} // end if buffer trim
} catch (error) { // Handle fetch errors or errors thrown during initial response check
console.error("Push operation failed:", error); // Log the error object
const timestamp = new Date().toLocaleTimeString();
const errorDiv = document.createElement('div');
errorDiv.className = 'text-danger';
errorDiv.textContent = `${timestamp} [FETCH_ERROR] Failed to push package: ${error.message}`;
liveConsole.appendChild(errorDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
// Display error message in the response area
responseDiv.innerHTML = `
<div class="alert alert-danger">
<strong>Error:</strong> ${error.message}
</div>
`;
} finally {
// Re-enable the button regardless of success or failure
pushButton.disabled = false;
pushButton.textContent = 'Push to FHIR Server';
}
}); // End form submit listener
} catch (error) { // Handle overall fetch/network errors
console.error("Push operation failed:", error);
if (liveConsole) { /* ... add error to console ... */ }
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'; } // Hide buttons on error
} finally { // Re-enable button
if (pushButton) { pushButton.disabled = false; pushButton.textContent = 'Push to FHIR Server'; }
}
}); // End form submit listener
} else { console.error("Push IG Form element not found."); }
}); // End DOMContentLoaded listener
</script>
{% endblock %}
{% endblock %}

View File

@ -93,8 +93,8 @@ document.addEventListener('DOMContentLoaded', function() {
// --- Get DOM Elements & Check Existence ---
const form = document.getElementById('fhirRequestForm');
const sendButton = document.getElementById('sendRequest');
const fhirPath = document.getElementById('fhirPath');
const requestBody = document.getElementById('requestBody');
const fhirPathInput = document.getElementById('fhirPath'); // Renamed for clarity
const requestBodyInput = document.getElementById('requestBody'); // Renamed for clarity
const requestBodyGroup = document.getElementById('requestBodyGroup');
const jsonError = document.getElementById('jsonError');
const responseCard = document.getElementById('responseCard');
@ -104,7 +104,7 @@ document.addEventListener('DOMContentLoaded', function() {
const methodRadios = document.getElementsByName('method');
const toggleServerButton = document.getElementById('toggleServer');
const toggleLabel = document.getElementById('toggleLabel');
const fhirServerUrl = document.getElementById('fhirServerUrl');
const fhirServerUrlInput = document.getElementById('fhirServerUrl'); // Renamed for clarity
const copyRequestBodyButton = document.getElementById('copyRequestBody');
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
const copyResponseBodyButton = document.getElementById('copyResponseBody');
@ -113,8 +113,8 @@ document.addEventListener('DOMContentLoaded', function() {
let elementsReady = true;
if (!form) { console.error("Element not found: #fhirRequestForm"); elementsReady = false; }
if (!sendButton) { console.error("Element not found: #sendRequest"); elementsReady = false; }
if (!fhirPath) { console.error("Element not found: #fhirPath"); elementsReady = false; }
if (!requestBody) { console.warn("Element not found: #requestBody"); } // Warn only, might be ok if not POST/PUT
if (!fhirPathInput) { console.error("Element not found: #fhirPath"); elementsReady = false; }
if (!requestBodyInput) { console.warn("Element not found: #requestBody"); } // Warn only
if (!requestBodyGroup) { console.warn("Element not found: #requestBodyGroup"); } // Warn only
if (!jsonError) { console.warn("Element not found: #jsonError"); } // Warn only
if (!responseCard) { console.error("Element not found: #responseCard"); elementsReady = false; }
@ -123,40 +123,33 @@ document.addEventListener('DOMContentLoaded', function() {
if (!responseBody) { console.error("Element not found: #responseBody"); elementsReady = false; }
if (!toggleServerButton) { console.error("Element not found: #toggleServer"); elementsReady = false; }
if (!toggleLabel) { console.error("Element not found: #toggleLabel"); elementsReady = false; }
if (!fhirServerUrl) { console.error("Element not found: #fhirServerUrl"); elementsReady = false; }
if (!fhirServerUrlInput) { console.error("Element not found: #fhirServerUrl"); elementsReady = false; }
// Add checks for copy buttons if needed
if (!elementsReady) {
console.error("One or more critical UI elements could not be found. Script execution halted.");
// Optionally display a user-facing error message here
// const errorDisplay = document.getElementById('some-error-area');
// if(errorDisplay) errorDisplay.textContent = "Error initializing UI components.";
return; // Stop script execution if critical elements are missing
return; // Stop script execution
}
console.log("All critical elements checked/found.");
// --- State Variable ---
let useLocalHapi = true; // Default state, will be adjusted by Lite mode check
let useLocalHapi = true;
// --- DEFINE FUNCTIONS (as before) ---
// --- DEFINE FUNCTIONS ---
function updateServerToggleUI() {
// Add checks inside the function too, just in case
if (!toggleLabel || !fhirServerUrl) {
console.error("updateServerToggleUI: Toggle UI elements became unavailable!");
return;
}
if (!toggleLabel || !fhirServerUrlInput) { console.error("updateServerToggleUI: Toggle UI elements missing!"); return; }
toggleLabel.textContent = useLocalHapi ? 'Use Local HAPI' : 'Use Custom URL';
fhirServerUrl.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrl.classList.remove('is-invalid');
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrlInput.classList.remove('is-invalid');
}
function toggleServer() {
if (appMode === 'lite') return; // Ignore clicks in Lite mode
useLocalHapi = !useLocalHapi;
updateServerToggleUI();
if (useLocalHapi && fhirServerUrl) {
fhirServerUrl.value = '';
if (useLocalHapi && fhirServerUrlInput) {
fhirServerUrlInput.value = '';
}
console.log(`Server toggled: ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
}
@ -166,47 +159,63 @@ document.addEventListener('DOMContentLoaded', function() {
if (!requestBodyGroup || !selectedMethod) return;
requestBodyGroup.style.display = (selectedMethod === 'POST' || selectedMethod === 'PUT') ? 'block' : 'none';
if (selectedMethod !== 'POST' && selectedMethod !== 'PUT') {
if (requestBody) requestBody.value = '';
if (requestBodyInput) requestBodyInput.value = '';
if (jsonError) jsonError.style.display = 'none';
}
}
// Validates request body, returns null on error, otherwise returns body string or empty string
function validateRequestBody(method, path) {
// Added check for requestBody existence
if (!requestBody || !jsonError) return method === 'GET' ? null : '';
if (!requestBody.value.trim()) {
requestBody.classList.remove('is-invalid');
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined; // Return empty string for modifying methods if field missing, else undefined
const bodyValue = requestBodyInput.value.trim();
if (!bodyValue) {
requestBodyInput.classList.remove('is-invalid');
jsonError.style.display = 'none';
return method === 'GET' ? null : '';
return ''; // Empty body is valid for POST/PUT
}
const isSearch = path && path.endsWith('_search');
if (method === 'POST' && isSearch) { return requestBody.value; }
try {
JSON.parse(requestBody.value);
requestBody.classList.remove('is-invalid');
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
const isForm = !isJson; // Assume form urlencoded if not JSON
if (method === 'POST' && isSearch && isForm) { // POST Search with form params
requestBodyInput.classList.remove('is-invalid');
jsonError.style.display = 'none';
return requestBody.value;
} catch (e) {
requestBody.classList.add('is-invalid');
jsonError.textContent = `Invalid JSON: ${e.message}`;
jsonError.style.display = 'block';
return null;
return bodyValue;
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON
try {
JSON.parse(bodyValue);
requestBodyInput.classList.remove('is-invalid');
jsonError.style.display = 'none';
return bodyValue; // Return the valid JSON string
} catch (e) {
requestBodyInput.classList.add('is-invalid');
jsonError.textContent = `Invalid JSON: ${e.message}`;
jsonError.style.display = 'block';
return null; // Indicate validation error
}
}
// For GET/DELETE, body should not be sent, but we don't error here
requestBodyInput.classList.remove('is-invalid');
jsonError.style.display = 'none';
return undefined; // Indicate no body should be sent
}
// Cleans path for proxying
function cleanFhirPath(path) {
if (!path) return '';
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
const cleaned = path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
return cleaned;
}
// Copies text to clipboard
async function copyToClipboard(text, button) {
if (!text || !button) return;
if (text === null || text === undefined || !button) return;
try {
await navigator.clipboard.writeText(text);
await navigator.clipboard.writeText(String(text)); // Ensure text is string
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="bi bi-check-lg"></i>';
setTimeout(() => { button.innerHTML = originalIcon; }, 2000);
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 2000);
} catch (err) { console.error('Copy failed:', err); alert('Failed to copy to clipboard'); }
}
@ -215,82 +224,180 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('App Mode:', appMode);
if (appMode === 'lite') {
console.log('Lite mode detected: Disabling local HAPI toggle and setting default.');
// Check toggleServerButton again before using
console.log('Lite mode detected: Disabling local HAPI toggle.');
if (toggleServerButton) {
toggleServerButton.disabled = true;
toggleServerButton.title = "Local HAPI is not available in Lite mode";
toggleServerButton.classList.add('disabled');
useLocalHapi = false;
if (fhirServerUrl) {
fhirServerUrl.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
}
} else {
// This console log should have appeared earlier if button was missing
console.error("Lite mode: Could not find toggle server button (#toggleServer) to disable.");
}
if (fhirServerUrlInput) { fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)"; }
} else { console.error("Lite mode: Could not find toggle server button."); }
} else {
console.log('Standalone mode detected: Enabling local HAPI toggle.');
if (toggleServerButton) {
toggleServerButton.disabled = false;
toggleServerButton.title = "";
toggleServerButton.classList.remove('disabled');
} else {
console.error("Standalone mode: Could not find toggle server button (#toggleServer) to enable.");
}
console.log('Standalone mode detected.');
if (toggleServerButton) { toggleServerButton.disabled = false; toggleServerButton.title = ""; toggleServerButton.classList.remove('disabled'); }
else { console.error("Standalone mode: Could not find toggle server button."); }
}
// --- SET INITIAL UI STATE ---
// Call these *after* the mode check and function definitions
updateServerToggleUI();
updateRequestBodyVisibility();
// --- ATTACH EVENT LISTENERS ---
// Server Toggle Button
if (toggleServerButton) { // Check again before adding listener
toggleServerButton.addEventListener('click', toggleServer);
} else {
console.error("Cannot attach listener: Toggle Server Button not found.");
}
// Copy Buttons (Add checks)
if (copyRequestBodyButton && requestBody) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBody.value, copyRequestBodyButton)); }
if (toggleServerButton) { toggleServerButton.addEventListener('click', toggleServer); }
if (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
// Method Radio Buttons
if (methodRadios) {
methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); });
}
// Request Body Input Validation
if (requestBody && fhirPath) {
requestBody.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPath.value ));
}
// Send Request Button
if (sendButton && fhirPath && form) { // Added form check for CSRF
// --- Send Request Button Listener (CORRECTED) ---
if (sendButton && fhirPathInput && form) {
sendButton.addEventListener('click', async function() {
// (Keep the existing fetch/send logic here)
// ...
// Ensure you check for element existence inside this handler too if needed, e.g.:
if (!fhirPath.value.trim()) { /* ... */ }
const method = document.querySelector('input[name="method"]:checked')?.value;
if (!method) { /* ... */ }
// ...etc...
console.log("Send Request button clicked.");
sendButton.disabled = true; sendButton.textContent = 'Sending...';
responseCard.style.display = 'none';
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
responseStatus.className = 'badge'; // Reset badge class
// Example: CSRF token retrieval inside handler
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
if (useLocalHapi && (method === 'POST' || method === 'PUT') && csrfToken) {
headers['X-CSRFToken'] = csrfToken;
} else if (useLocalHapi && (method === 'POST' || method === 'PUT')) {
console.warn("CSRF token input not found for local request.");
// Potentially alert the user or block the request
}
// ... rest of send logic ...
});
// 1. Get Values
const path = fhirPathInput.value.trim();
const method = document.querySelector('input[name="method"]:checked')?.value;
let body = undefined; // Default to no body
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
// 2. Validate & Get Body (if needed)
if (method === 'POST' || method === 'PUT') {
body = validateRequestBody(method, path);
if (body === null) { // null indicates validation error
alert('Request body contains invalid JSON.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
}
// If body is empty string, ensure it's treated as such for fetch
if (body === '') body = '';
}
// 3. Determine Target URL
const cleanedPath = cleanFhirPath(path);
const proxyBase = '/fhir'; // Always use the toolkit's proxy endpoint
let targetUrl = useLocalHapi ? `${proxyBase}/${cleanedPath}` : fhirServerUrlInput.value.trim().replace(/\/+$/, '') + `/${cleanedPath}`;
let finalFetchUrl = proxyBase + '/' + cleanedPath; // URL to send the fetch request TO
if (!useLocalHapi) {
const customUrl = fhirServerUrlInput.value.trim();
if (!customUrl) {
alert('Please enter a custom FHIR Server URL when not using Local HAPI.');
fhirServerUrlInput.classList.add('is-invalid');
sendButton.disabled = false; sendButton.textContent = 'Send Request';
return;
}
try {
// Validate custom URL format, though actual reachability is checked by backend proxy
new URL(customUrl);
fhirServerUrlInput.classList.remove('is-invalid');
// The proxy needs the *full* path including the custom base, it doesn't know it otherwise
// We modify the path being sent TO THE PROXY
// Let backend handle the full external URL based on path sent to it
// This assumes the proxy handles the full path correctly.
// *Correction*: Proxy expects just the relative path. Send custom URL in header?
// Let's stick to the original proxy design: frontend sends relative path to /fhir/,
// backend needs to know if it's proxying to local or custom.
// WE NEED TO TELL THE BACKEND WHICH URL TO PROXY TO!
// This requires backend changes. For now, assume proxy ALWAYS goes to localhost:8080.
// To make custom URL work, the backend proxy needs modification.
// Sticking with current backend: Custom URL cannot be proxied via '/fhir/'.
// --> Reverting to only allowing local proxy for now.
if (!useLocalHapi) {
alert("Current implementation only supports proxying to the local HAPI server via '/fhir'. Custom URL feature requires backend changes to the proxy route.");
sendButton.disabled = false; sendButton.textContent = 'Send Request';
return;
}
// If backend were changed, we'd likely send the custom URL in a header or modify the request path.
} catch (_) {
alert('Invalid custom FHIR Server URL format.');
fhirServerUrlInput.classList.add('is-invalid');
sendButton.disabled = false; sendButton.textContent = 'Send Request';
return;
}
}
console.log(`Final Fetch URL: ${finalFetchUrl}`);
// 4. Prepare Headers
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9' };
const isSearch = path.endsWith('_search');
if (method === 'POST' || method === 'PUT') {
// Determine Content-Type based on body content if possible
if (body && body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
else if (body && body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
else if (method === 'POST' && isSearch && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
else if (body) { headers['Content-Type'] = 'application/fhir+json'; } // Default if body exists but type unknown
}
// Add CSRF token if method requires it AND using local proxy
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
if (useLocalHapi && ['POST', 'PUT', 'DELETE'].includes(method) && csrfToken) { // Include DELETE
headers['X-CSRFToken'] = csrfToken;
console.log("CSRF Token added for local modifying request.");
} else if (useLocalHapi && ['POST', 'PUT', 'DELETE'].includes(method) && !csrfToken) {
console.warn("CSRF token input not found for local modifying request.");
// Potentially alert user or add specific handling
}
// 5. Make the Fetch Request
try {
const response = await fetch(finalFetchUrl, { method, headers, body }); // Body is undefined for GET/DELETE
// 6. Process Response
responseCard.style.display = 'block';
responseStatus.textContent = `${response.status} ${response.statusText}`;
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
let headerText = '';
response.headers.forEach((value, key) => { headerText += `${key}: ${value}\n`; });
responseHeaders.textContent = headerText.trim();
const responseContentType = response.headers.get('content-type') || '';
let responseBodyText = await response.text();
let displayBody = responseBodyText;
// Attempt to pretty-print JSON
if (responseContentType.includes('json') && responseBodyText.trim()) {
try {
displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2);
} catch (e) {
console.warn("Failed to pretty-print JSON response:", e);
displayBody = responseBodyText; // Show raw text if parsing fails
}
}
// Add XML formatting if needed later
responseBody.textContent = displayBody;
// Highlight response body if Prism.js is available
if (typeof Prism !== 'undefined') {
responseBody.className = ''; // Clear previous classes
if (responseContentType.includes('json')) {
responseBody.classList.add('language-json');
} else if (responseContentType.includes('xml')) {
responseBody.classList.add('language-xml');
} // Add other languages if needed
Prism.highlightElement(responseBody);
}
} catch (error) {
console.error('Fetch error:', error);
responseCard.style.display = 'block';
responseStatus.textContent = `Network Error`;
responseStatus.className = 'badge bg-danger';
responseHeaders.textContent = 'N/A';
responseBody.textContent = `Error: ${error.message}\n\nCould not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server is running.`;
} finally {
sendButton.disabled = false; sendButton.textContent = 'Send Request';
}
}); // End sendButton listener
} else {
console.error("Cannot attach listener: Send Request Button, FHIR Path input, or Form element not found.");
}

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3 mb-2">Import FHIR IG</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Manage FHIR Packages</a>
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Push IGs</a>
<a href="{{ url_for('upload_test_data') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Upload Test Data</a>
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Validate FHIR Sample</a>
<a href="{{ url_for('fhir_ui') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">FHIR API Explorer</a>
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">FHIR UI Operations</a>

View File

@ -0,0 +1,307 @@
{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %} {# Assuming you have this helper #}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
<h1 class="display-5 fw-bold text-body-emphasis">{{ title }}</h1>
<div class="col-lg-8 mx-auto">
<p class="lead mb-4">
Upload FHIR test data (JSON, XML, or ZIP containing JSON/XML) to a target server. The tool will attempt to parse resources, determine dependencies based on references, and upload them in the correct order. Optionally validate resources before uploading.
</p>
</div>
</div>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-cloud-upload me-2"></i>Upload Configuration</h4>
</div>
<div class="card-body">
{# --- Display WTForms validation errors --- #}
{% if form.errors %}
<div class="alert alert-danger">
<p><strong>Please correct the following errors:</strong></p>
<ul>
{% for field, errors in form.errors.items() %}
<li>{{ form[field].label.text }}: {{ errors|join(', ') }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# --- End Error Display --- #}
<form id="uploadTestDataForm" method="POST" enctype="multipart/form-data"> {# Use POST, JS will handle submission #}
{{ form.csrf_token }} {# Render CSRF token #}
{{ render_field(form.fhir_server_url, class="form-control form-control-lg") }}
{# Authentication Row #}
<div class="row g-3 mb-3 align-items-end">
<div class="col-md-5">
{{ render_field(form.auth_type, class="form-select") }}
</div>
<div class="col-md-7" id="authTokenGroup" style="display: none;">
{{ render_field(form.auth_token, class="form-control") }}
</div>
</div>
{# File Input #}
{{ render_field(form.test_data_file, class="form-control") }}
<small class="form-text text-muted">Select one or more .json, .xml files, or a single .zip file containing them.</small>
{# --- Validation Options --- #}
<div class="row g-3 mt-3 mb-3 align-items-center">
<div class="col-md-6">
{# Render BooleanField using the macro #}
{{ render_field(form.validate_before_upload) }}
<small class="form-text text-muted">It is suggested to not validate against more than 500 files</small>
</div>
<div class="col-md-6" id="validationPackageGroup" style="display: none;">
{# Render SelectField using the macro #}
{{ render_field(form.validation_package_id, class="form-select") }}
</div>
</div>
{# --- END Validation Options --- #}
{# Upload Mode/Error Handling/Conditional Row #}
<div class="row g-3 mb-3">
<div class="col-md-4">
{{ render_field(form.upload_mode, class="form-select") }}
</div>
{# --- Conditional Upload Checkbox --- #}
<div class="col-md-4 d-flex align-items-end"> {# Use flex alignment #}
{{ render_field(form.use_conditional_uploads) }}
</div>
{# --- END --- #}
<div class="col-md-4">
{{ render_field(form.error_handling, class="form-select") }}
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="uploadButton">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
<i class="bi bi-arrow-up-circle me-2"></i>Upload and Process
</button>
</form>
</div>
</div>
{# Results Area #}
<div class="card shadow-sm">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-terminal me-2"></i>Processing Log & Results</h4>
</div>
<div class="card-body">
{# Live Console for Streaming Output #}
<div id="liveConsole" class="border p-3 rounded bg-dark text-light mb-3" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
<span class="text-muted">Processing output will appear here...</span>
</div>
{# Final Summary Report Area #}
<div id="uploadResponse" class="mt-3">
{# Final summary message appears here #}
</div>
</div>
</div>
</div> {# End Col #}
</div> {# End Row #}
</div> {# End Container #}
{% endblock %}
{% block scripts %}
{{ super() }} {# Include scripts from base.html #}
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('uploadTestDataForm');
const uploadButton = document.getElementById('uploadButton');
const spinner = uploadButton ? uploadButton.querySelector('.spinner-border') : null;
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('uploadResponse');
const authTypeSelect = document.getElementById('auth_type');
const authTokenGroup = document.getElementById('authTokenGroup');
const authTokenInput = document.getElementById('auth_token');
const fileInput = document.getElementById('test_data_file');
const validateCheckbox = document.getElementById('validate_before_upload');
const validationPackageGroup = document.getElementById('validationPackageGroup');
const validationPackageSelect = document.getElementById('validation_package_id');
const uploadModeSelect = document.getElementById('upload_mode'); // Get upload mode select
const conditionalUploadCheckbox = document.getElementById('use_conditional_uploads'); // Get conditional checkbox
// --- Helper: Sanitize text ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
// --- Event Listener: Show/Hide Auth Token ---
if (authTypeSelect && authTokenGroup) {
authTypeSelect.addEventListener('change', function() {
authTokenGroup.style.display = this.value === 'bearerToken' ? 'block' : 'none';
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
});
authTokenGroup.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none'; // Initial state
} else { console.error("Auth elements not found."); }
// --- Event Listener: Show/Hide Validation Package Dropdown ---
if (validateCheckbox && validationPackageGroup) {
const toggleValidationPackage = () => {
validationPackageGroup.style.display = validateCheckbox.checked ? 'block' : 'none';
};
validateCheckbox.addEventListener('change', toggleValidationPackage);
toggleValidationPackage(); // Initial state
} else { console.error("Validation checkbox or package group not found."); }
// --- Event Listener: Enable/Disable Conditional Upload Checkbox ---
if (uploadModeSelect && conditionalUploadCheckbox) {
const toggleConditionalCheckbox = () => {
// Enable checkbox only if mode is 'individual'
conditionalUploadCheckbox.disabled = (uploadModeSelect.value !== 'individual');
// Optional: Uncheck if disabled
if (conditionalUploadCheckbox.disabled) {
conditionalUploadCheckbox.checked = false;
}
};
uploadModeSelect.addEventListener('change', toggleConditionalCheckbox);
toggleConditionalCheckbox(); // Initial state
} else { console.error("Upload mode select or conditional upload checkbox not found."); }
// --- Event Listener: Form Submission ---
if (form && uploadButton && spinner && liveConsole && responseDiv && fileInput && validateCheckbox && validationPackageSelect && conditionalUploadCheckbox) {
form.addEventListener('submit', async function(event) {
event.preventDefault();
console.log("Form submitted");
// Basic validation
if (!fileInput.files || fileInput.files.length === 0) { alert('Please select at least one file.'); return; }
const fhirServerUrl = document.getElementById('fhir_server_url').value.trim();
if (!fhirServerUrl) { alert('Please enter the Target FHIR Server URL.'); return; }
if (validateCheckbox.checked && !validationPackageSelect.value) { alert('Please select a package for validation.'); return; }
// UI Updates
uploadButton.disabled = true;
if(spinner) spinner.style.display = 'inline-block';
const uploadIcon = uploadButton.querySelector('i');
if (uploadIcon) uploadIcon.style.display = 'none';
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting upload process...</div>`;
responseDiv.innerHTML = '';
// Prepare FormData
const formData = new FormData();
formData.append('fhir_server_url', fhirServerUrl);
formData.append('auth_type', authTypeSelect.value);
if (authTypeSelect.value === 'bearerToken' && authTokenInput) { formData.append('auth_token', authTokenInput.value); }
formData.append('upload_mode', uploadModeSelect.value);
formData.append('error_handling', document.getElementById('error_handling').value);
formData.append('validate_before_upload', validateCheckbox.checked ? 'true' : 'false');
if (validateCheckbox.checked) { formData.append('validation_package_id', validationPackageSelect.value); }
formData.append('use_conditional_uploads', conditionalUploadCheckbox.checked ? 'true' : 'false'); // Add new field
// Append files
for (let i = 0; i < fileInput.files.length; i++) { formData.append('test_data_files', fileInput.files[i]); }
// CSRF token and API key
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : "";
const internalApiKey = {{ api_key | default("") | tojson }};
try {
// --- API Call ---
const response = await fetch('/api/upload-test-data', {
method: 'POST',
headers: { 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
body: formData
});
if (!response.ok) {
let errorMsg = `Upload failed: ${response.status} ${response.statusText}`;
try { const errorData = await response.json(); errorMsg = errorData.message || JSON.stringify(errorData); } catch (e) {}
throw new Error(errorMsg);
}
if (!response.body) { throw new Error("Response body missing."); }
// --- Process Streaming Response ---
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-info'; let prefix = '[INFO]';
// Handle message types including validation
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
else if (data.type === 'validation_info') { prefix = '[VALIDATION]'; messageClass = 'text-info'; }
else if (data.type === 'validation_warning') { prefix = '[VALIDATION]'; messageClass = 'text-warning'; }
else if (data.type === 'validation_error') { prefix = '[VALIDATION]'; messageClass = 'text-danger'; }
// Append to console
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
let messageText = sanitizeText(data.message) || '...';
if (data.details) { messageText += ` <small>(${sanitizeText(data.details)})</small>`; }
messageDiv.innerHTML = `${timestamp} ${prefix} ${messageText}`;
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
// Handle final summary
if (data.type === 'complete' && data.data) {
const summary = data.data;
let alertClass = 'alert-secondary';
if (summary.status === 'success') alertClass = 'alert-success';
else if (summary.status === 'partial') alertClass = 'alert-warning';
else if (summary.status === 'failure') alertClass = 'alert-danger';
responseDiv.innerHTML = `
<div class="alert ${alertClass} mt-3">
<strong>Status: ${sanitizeText(summary.status)}</strong><br>
${sanitizeText(summary.message)}<hr>
Files Processed: ${summary.files_processed ?? 'N/A'}<br>
Resources Parsed: ${summary.resources_parsed ?? 'N/A'}<br>
Validation Errors: ${summary.validation_errors ?? 'N/A'}<br>
Validation Warnings: ${summary.validation_warnings ?? 'N/A'}<br>
Resources Uploaded: ${summary.resources_uploaded ?? 'N/A'}<br>
Upload Errors: ${summary.error_count ?? 'N/A'}
${summary.errors?.length > 0 ? '<br><strong>Details:</strong><ul>' + summary.errors.map(e => `<li>${sanitizeText(e)}</li>`).join('') + '</ul>' : ''}
</div>`;
}
} catch (parseError) { console.error('Stream parse error:', parseError, 'Line:', line); /* ... log error to console ... */ }
} // end for line
} // end while
// Process final buffer (if needed)
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
if (data.type === 'complete' && data.data) { /* ... update summary ... */ }
} catch (parseError) { console.error('Final buffer parse error:', parseError); /* ... log error to console ... */ }
}
} catch (error) {
console.error("Upload failed:", error);
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message)}</div>`;
const ts = new Date().toLocaleTimeString(); const errDiv = document.createElement('div'); errDiv.className = 'text-danger'; errDiv.textContent = `${ts} [CLIENT_ERROR] ${sanitizeText(error.message)}`; liveConsole.appendChild(errDiv);
} finally {
// Re-enable button
uploadButton.disabled = false;
if(spinner) spinner.style.display = 'none';
const uploadIcon = uploadButton.querySelector('i');
if (uploadIcon) uploadIcon.style.display = 'inline-block'; // Show icon
}
}); // End submit listener
} else { console.error("Could not find all required elements for form submission."); }
}); // End DOMContentLoaded
</script>
{% endblock %}