mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
Incremental
Added large functionality to push Ig uploader and created a test data upload feature
This commit is contained in:
parent
3365255a5b
commit
8f2c90087d
572
README.md
572
README.md
@ -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 server’s 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 server’s 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
425
app.py
@ -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')
|
||||
|
@ -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
132
forms.py
@ -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
|
||||
|
@ -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
@ -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
|
@ -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:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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:[33mPress CTRL+C to quit[0m
|
||||
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:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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:[33mPress CTRL+C to quit[0m
|
||||
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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "[36mGET /static/js/lottie.min.js HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "[36mGET /static/animations/loading-light.json HTTP/1.1[0m" 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] "[36mGET /static/animations/loading-light.json HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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:[33mPress CTRL+C to quit[0m
|
||||
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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 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:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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:[33mPress CTRL+C to quit[0m
|
||||
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] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
@ -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)
|
@ -1 +0,0 @@
|
||||
1
|
@ -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"]
|
1092
services.py
1092
services.py
File diff suppressed because it is too large
Load Diff
@ -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">
|
||||
|
@ -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, "<").replace(/>/g, ">") : "";
|
||||
|
||||
// --- 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, "<").replace(/>/g, ">") : "N/A";
|
||||
const escapedError = fail.error ? fail.error.replace(/</g, "<").replace(/>/g, ">") : "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, "<").replace(/>/g, ">") : "N/A";
|
||||
const escapedError = fail.error ? fail.error.replace(/</g, "<").replace(/>/g, ">") : "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 %}
|
||||
|
@ -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.");
|
||||
}
|
||||
|
1788
templates/fhir_ui_operations.html.bak
Normal file
1788
templates/fhir_ui_operations.html.bak
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
307
templates/upload_test_data.html
Normal file
307
templates/upload_test_data.html
Normal 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, "<").replace(/>/g, ">") : "";
|
||||
|
||||
// --- 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 %}
|
Loading…
x
Reference in New Issue
Block a user