mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-09-17 18:35:02 +00:00
Compare commits
14 Commits
5725f8a22f
...
47b746c4b1
Author | SHA1 | Date | |
---|---|---|---|
47b746c4b1 | |||
92fe759b0f | |||
356914f306 | |||
9f3748cc49 | |||
f7063484d9 | |||
b8014de7c3 | |||
f4c457043a | |||
dffe43beee | |||
3f1ac10290 | |||
85f980be0a | |||
d37af01b3a | |||
6ae0e56118 | |||
f1478561c1 | |||
236390e57b |
@ -2,57 +2,130 @@
|
|||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
REM --- Configuration ---
|
REM --- Configuration ---
|
||||||
set REPO_URL=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
|
set REPO_URL_HAPI=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
|
||||||
set CLONE_DIR=hapi-fhir-jpaserver
|
set REPO_URL_CANDLE=https://github.com/FHIR/fhir-candle.git
|
||||||
|
set CLONE_DIR_HAPI=hapi-fhir-jpaserver
|
||||||
|
set CLONE_DIR_CANDLE=fhir-candle
|
||||||
set SOURCE_CONFIG_DIR=hapi-fhir-setup
|
set SOURCE_CONFIG_DIR=hapi-fhir-setup
|
||||||
set CONFIG_FILE=application.yaml
|
set CONFIG_FILE=application.yaml
|
||||||
|
|
||||||
REM --- Define Paths ---
|
REM --- Define Paths ---
|
||||||
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
||||||
set DEST_CONFIG_PATH=%CLONE_DIR%\target\classes\%CONFIG_FILE%
|
set DEST_CONFIG_PATH=%CLONE_DIR_HAPI%\target\classes\%CONFIG_FILE%
|
||||||
|
|
||||||
REM === CORRECTED: Prompt for Version ===
|
REM --- NEW: Define a variable for the custom FHIR URL and server type ---
|
||||||
|
set "CUSTOM_FHIR_URL_VAL="
|
||||||
|
set "SERVER_TYPE="
|
||||||
|
set "CANDLE_FHIR_VERSION="
|
||||||
|
|
||||||
|
REM === MODIFIED: Prompt for Installation Mode ===
|
||||||
:GetModeChoice
|
:GetModeChoice
|
||||||
SET "APP_MODE=" REM Clear the variable first
|
SET "APP_MODE=" REM Clear the variable first
|
||||||
|
echo.
|
||||||
echo Select Installation Mode:
|
echo Select Installation Mode:
|
||||||
echo 1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)
|
echo 1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)
|
||||||
echo 2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)
|
echo 2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)
|
||||||
CHOICE /C 12 /N /M "Enter your choice (1 or 2):"
|
echo 3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)
|
||||||
|
echo 4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)
|
||||||
|
CHOICE /C 1234 /N /M "Enter your choice (1, 2, 3, or 4):"
|
||||||
|
|
||||||
|
IF ERRORLEVEL 4 (
|
||||||
|
SET APP_MODE=standalone
|
||||||
|
SET SERVER_TYPE=candle
|
||||||
|
goto :GetCandleFhirVersion
|
||||||
|
)
|
||||||
|
IF ERRORLEVEL 3 (
|
||||||
|
SET APP_MODE=standalone
|
||||||
|
SET SERVER_TYPE=hapi
|
||||||
|
goto :ModeSet
|
||||||
|
)
|
||||||
IF ERRORLEVEL 2 (
|
IF ERRORLEVEL 2 (
|
||||||
|
SET APP_MODE=standalone
|
||||||
|
goto :GetCustomUrl
|
||||||
|
)
|
||||||
|
IF ERRORLEVEL 1 (
|
||||||
SET APP_MODE=lite
|
SET APP_MODE=lite
|
||||||
goto :ModeSet
|
goto :ModeSet
|
||||||
)
|
)
|
||||||
IF ERRORLEVEL 1 (
|
|
||||||
SET APP_MODE=standalone
|
|
||||||
goto :ModeSet
|
|
||||||
)
|
|
||||||
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
|
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
|
||||||
echo Invalid input. Please try again.
|
echo Invalid input. Please try again.
|
||||||
goto :GetModeChoice
|
goto :GetModeChoice
|
||||||
|
|
||||||
|
:GetCustomUrl
|
||||||
|
set "CONFIRMED_URL="
|
||||||
|
:PromptUrlLoop
|
||||||
|
echo.
|
||||||
|
set /p "CUSTOM_URL_INPUT=Please enter the custom FHIR server URL: "
|
||||||
|
echo.
|
||||||
|
echo You entered: !CUSTOM_URL_INPUT!
|
||||||
|
set /p "CONFIRM_URL=Is this URL correct? (Y/N): "
|
||||||
|
if /i "!CONFIRM_URL!" EQU "Y" (
|
||||||
|
set "CONFIRMED_URL=!CUSTOM_URL_INPUT!"
|
||||||
|
goto :ConfirmUrlLoop
|
||||||
|
) else (
|
||||||
|
goto :PromptUrlLoop
|
||||||
|
)
|
||||||
|
:ConfirmUrlLoop
|
||||||
|
echo.
|
||||||
|
echo Please re-enter the URL to confirm it is correct:
|
||||||
|
set /p "CUSTOM_URL_INPUT=Re-enter URL: "
|
||||||
|
if /i "!CUSTOM_URL_INPUT!" EQU "!CONFIRMED_URL!" (
|
||||||
|
set "CUSTOM_FHIR_URL_VAL=!CUSTOM_URL_INPUT!"
|
||||||
|
echo.
|
||||||
|
echo Custom URL confirmed: !CUSTOM_FHIR_URL_VAL!
|
||||||
|
goto :ModeSet
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo URLs do not match. Please try again.
|
||||||
|
goto :PromptUrlLoop
|
||||||
|
)
|
||||||
|
|
||||||
|
:GetCandleFhirVersion
|
||||||
|
echo.
|
||||||
|
echo Select the FHIR version for the Candle server:
|
||||||
|
echo 1. R4 (4.0)
|
||||||
|
echo 2. R4B (4.3)
|
||||||
|
echo 3. R5 (5.0)
|
||||||
|
CHOICE /C 123 /N /M "Enter your choice (1, 2, or 3):"
|
||||||
|
IF ERRORLEVEL 3 (
|
||||||
|
SET CANDLE_FHIR_VERSION=r5
|
||||||
|
goto :ModeSet
|
||||||
|
)
|
||||||
|
IF ERRORLEVEL 2 (
|
||||||
|
SET CANDLE_FHIR_VERSION=r4b
|
||||||
|
goto :ModeSet
|
||||||
|
)
|
||||||
|
IF ERRORLEVEL 1 (
|
||||||
|
SET CANDLE_FHIR_VERSION=r4
|
||||||
|
goto :ModeSet
|
||||||
|
)
|
||||||
|
echo Invalid input. Please try again.
|
||||||
|
goto :GetCandleFhirVersion
|
||||||
|
|
||||||
:ModeSet
|
:ModeSet
|
||||||
IF "%APP_MODE%"=="" (
|
IF "%APP_MODE%"=="" (
|
||||||
echo Invalid choice detected after checks. Exiting.
|
echo Invalid choice detected after checks. Exiting.
|
||||||
goto :eof
|
goto :eof
|
||||||
)
|
)
|
||||||
echo Selected Mode: %APP_MODE%
|
echo Selected Mode: %APP_MODE%
|
||||||
|
echo Server Type: %SERVER_TYPE%
|
||||||
echo.
|
echo.
|
||||||
REM === END CORRECTION ===
|
REM === END MODIFICATION ===
|
||||||
|
|
||||||
|
|
||||||
REM === Conditionally Execute HAPI Setup ===
|
REM === Conditionally Execute Server Setup ===
|
||||||
IF "%APP_MODE%"=="standalone" (
|
IF "%SERVER_TYPE%"=="hapi" (
|
||||||
echo Running Standalone setup including HAPI FHIR...
|
echo Running Hapi server setup...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 0: Clean up previous clone (optional) ---
|
REM --- Step 0: Clean up previous clone (optional) ---
|
||||||
echo Checking for existing directory: %CLONE_DIR%
|
echo Checking for existing directory: %CLONE_DIR_HAPI%
|
||||||
if exist "%CLONE_DIR%" (
|
if exist "%CLONE_DIR_HAPI%" (
|
||||||
echo Found existing directory, removing it...
|
echo Found existing directory, removing it...
|
||||||
rmdir /s /q "%CLONE_DIR%"
|
rmdir /s /q "%CLONE_DIR_HAPI%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to remove existing directory: %CLONE_DIR%
|
echo ERROR: Failed to remove existing directory: %CLONE_DIR_HAPI%
|
||||||
goto :error
|
goto :error
|
||||||
)
|
)
|
||||||
echo Existing directory removed.
|
echo Existing directory removed.
|
||||||
@ -62,8 +135,8 @@ IF "%APP_MODE%"=="standalone" (
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 1: Clone the HAPI FHIR server repository ---
|
REM --- Step 1: Clone the HAPI FHIR server repository ---
|
||||||
echo Cloning repository: %REPO_URL% into %CLONE_DIR%...
|
echo Cloning repository: %REPO_URL_HAPI% into %CLONE_DIR_HAPI%...
|
||||||
git clone "%REPO_URL%" "%CLONE_DIR%"
|
git clone "%REPO_URL_HAPI%" "%CLONE_DIR_HAPI%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to clone repository. Check Git installation and network connection.
|
echo ERROR: Failed to clone repository. Check Git installation and network connection.
|
||||||
goto :error
|
goto :error
|
||||||
@ -72,10 +145,10 @@ IF "%APP_MODE%"=="standalone" (
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 2: Navigate into the cloned directory ---
|
REM --- Step 2: Navigate into the cloned directory ---
|
||||||
echo Changing directory to %CLONE_DIR%...
|
echo Changing directory to %CLONE_DIR_HAPI%...
|
||||||
cd "%CLONE_DIR%"
|
cd "%CLONE_DIR_HAPI%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to change directory to %CLONE_DIR%.
|
echo ERROR: Failed to change directory to %CLONE_DIR_HAPI%.
|
||||||
goto :error
|
goto :error
|
||||||
)
|
)
|
||||||
echo Current directory: %CD%
|
echo Current directory: %CD%
|
||||||
@ -118,47 +191,141 @@ IF "%APP_MODE%"=="standalone" (
|
|||||||
echo Current directory: %CD%
|
echo Current directory: %CD%
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
) ELSE (
|
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
||||||
echo Running Lite setup, skipping HAPI FHIR build...
|
echo Running FHIR Candle server setup...
|
||||||
REM Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen after a standalone attempt
|
echo.
|
||||||
if exist "%CLONE_DIR%" (
|
|
||||||
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
|
REM --- Step 0: Clean up previous clone (optional) ---
|
||||||
rmdir /s /q "%CLONE_DIR%"
|
echo Checking for existing directory: %CLONE_DIR_CANDLE%
|
||||||
|
if exist "%CLONE_DIR_CANDLE%" (
|
||||||
|
echo Found existing directory, removing it...
|
||||||
|
rmdir /s /q "%CLONE_DIR_CANDLE%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to remove existing directory: %CLONE_DIR_CANDLE%
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
echo Existing directory removed.
|
||||||
|
) else (
|
||||||
|
echo Directory does not exist, proceeding with clone.
|
||||||
)
|
)
|
||||||
REM Create empty target directories expected by Dockerfile COPY, even if not used
|
echo.
|
||||||
mkdir "%CLONE_DIR%\target\classes" 2> nul
|
|
||||||
mkdir "%CLONE_DIR%\custom" 2> nul
|
REM --- Step 1: Clone the FHIR Candle server repository ---
|
||||||
REM Create a placeholder empty WAR file to satisfy Dockerfile COPY
|
echo Cloning repository: %REPO_URL_CANDLE% into %CLONE_DIR_CANDLE%...
|
||||||
echo. > "%CLONE_DIR%\target\ROOT.war"
|
git clone "%REPO_URL_CANDLE%" "%CLONE_DIR_CANDLE%"
|
||||||
echo. > "%CLONE_DIR%\target\classes\application.yaml"
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to clone repository. Check Git and Dotnet installation and network connection.
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
echo Repository cloned successfully.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM --- Step 2: Navigate into the cloned directory ---
|
||||||
|
echo Changing directory to %CLONE_DIR_CANDLE%...
|
||||||
|
cd "%CLONE_DIR_CANDLE%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to change directory to %CLONE_DIR_CANDLE%.
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
echo Current directory: %CD%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM --- Step 3: Build the FHIR Candle server using Dotnet ---
|
||||||
|
echo ===> "Starting Dotnet build (Step 3)...""
|
||||||
|
dotnet publish -c Release -f net9.0 -o publish
|
||||||
|
echo ===> Dotnet command finished. Checking error level...
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Dotnet build failed. Check Dotnet SDK installation.
|
||||||
|
cd ..
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
echo Dotnet build completed successfully. ErrorLevel: %errorlevel%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM --- Step 4: Navigate back to the parent directory ---
|
||||||
|
echo ===> "Changing directory back (Step 4)..."
|
||||||
|
cd ..
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to change back to the parent directory. ErrorLevel: %errorlevel%
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
echo Current directory: %CD%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
) ELSE (
|
||||||
|
echo Running Lite setup, skipping server build...
|
||||||
|
REM Ensure the server directories don't exist in Lite mode
|
||||||
|
if exist "%CLONE_DIR_HAPI%" (
|
||||||
|
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
|
||||||
|
rmdir /s /q "%CLONE_DIR_HAPI%"
|
||||||
|
)
|
||||||
|
if exist "%CLONE_DIR_CANDLE%" (
|
||||||
|
echo Found existing Candle directory in Lite mode. Removing it to avoid build issues...
|
||||||
|
rmdir /s /q "%CLONE_DIR_CANDLE%"
|
||||||
|
)
|
||||||
|
REM Create empty placeholder files to satisfy Dockerfile COPY commands in Lite mode.
|
||||||
|
mkdir "%CLONE_DIR_HAPI%\target\classes" 2> nul
|
||||||
|
mkdir "%CLONE_DIR_HAPI%\custom" 2> nul
|
||||||
|
echo. > "%CLONE_DIR_HAPI%\target\ROOT.war"
|
||||||
|
echo. > "%CLONE_DIR_HAPI%\target\classes\application.yaml"
|
||||||
|
mkdir "%CLONE_DIR_CANDLE%\publish" 2> nul
|
||||||
|
echo. > "%CLONE_DIR_CANDLE%\publish\fhir-candle.dll"
|
||||||
echo Placeholder files created for Lite mode build.
|
echo Placeholder files created for Lite mode build.
|
||||||
echo.
|
echo.
|
||||||
)
|
)
|
||||||
|
|
||||||
REM === Modify docker-compose.yml to set APP_MODE ===
|
REM === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL ===
|
||||||
echo Updating docker-compose.yml with APP_MODE=%APP_MODE%...
|
echo Updating docker-compose.yml with APP_MODE=%APP_MODE% and HAPI_FHIR_URL...
|
||||||
(
|
(
|
||||||
echo version: '3.8'
|
echo version: '3.8'
|
||||||
echo services:
|
echo services:
|
||||||
echo fhirflare:
|
echo fhirflare:
|
||||||
echo build:
|
echo build:
|
||||||
echo context: .
|
echo context: .
|
||||||
echo dockerfile: Dockerfile
|
IF "%SERVER_TYPE%"=="hapi" (
|
||||||
|
echo dockerfile: Dockerfile.hapi
|
||||||
|
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
||||||
|
echo dockerfile: Dockerfile.candle
|
||||||
|
) ELSE (
|
||||||
|
echo dockerfile: Dockerfile.lite
|
||||||
|
)
|
||||||
echo ports:
|
echo ports:
|
||||||
echo - "5000:5000"
|
IF "%SERVER_TYPE%"=="candle" (
|
||||||
echo - "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
echo - "5000:5000"
|
||||||
|
echo - "5001:5826"
|
||||||
|
) ELSE (
|
||||||
|
echo - "5000:5000"
|
||||||
|
echo - "8080:8080"
|
||||||
|
)
|
||||||
echo volumes:
|
echo volumes:
|
||||||
echo - ./instance:/app/instance
|
echo - ./instance:/app/instance
|
||||||
echo - ./static/uploads:/app/static/uploads
|
echo - ./static/uploads:/app/static/uploads
|
||||||
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
IF "%SERVER_TYPE%"=="hapi" (
|
||||||
|
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||||
|
)
|
||||||
echo - ./logs:/app/logs
|
echo - ./logs:/app/logs
|
||||||
|
IF "%SERVER_TYPE%"=="hapi" (
|
||||||
|
echo - ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
|
||||||
|
echo - ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
|
||||||
|
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
||||||
|
echo - ./fhir-candle/publish/:/app/fhir-candle-publish/
|
||||||
|
)
|
||||||
echo environment:
|
echo environment:
|
||||||
echo - FLASK_APP=app.py
|
echo - FLASK_APP=app.py
|
||||||
echo - FLASK_ENV=development
|
echo - FLASK_ENV=development
|
||||||
echo - NODE_PATH=/usr/lib/node_modules
|
echo - NODE_PATH=/usr/lib/node_modules
|
||||||
echo - APP_MODE=%APP_MODE%
|
echo - APP_MODE=%APP_MODE%
|
||||||
echo - APP_BASE_URL=http://localhost:5000
|
echo - APP_BASE_URL=http://localhost:5000
|
||||||
echo - HAPI_FHIR_URL=http://localhost:8080/fhir
|
IF DEFINED CUSTOM_FHIR_URL_VAL (
|
||||||
|
echo - HAPI_FHIR_URL=!CUSTOM_FHIR_URL_VAL!
|
||||||
|
) ELSE (
|
||||||
|
IF "%SERVER_TYPE%"=="candle" (
|
||||||
|
echo - HAPI_FHIR_URL=http://localhost:5826/fhir/%CANDLE_FHIR_VERSION%
|
||||||
|
echo - ASPNETCORE_URLS=http://0.0.0.0:5826
|
||||||
|
) ELSE (
|
||||||
|
echo - HAPI_FHIR_URL=http://localhost:8080/fhir
|
||||||
|
)
|
||||||
|
)
|
||||||
echo command: supervisord -c /etc/supervisord.conf
|
echo command: supervisord -c /etc/supervisord.conf
|
||||||
) > docker-compose.yml.tmp
|
) > docker-compose.yml.tmp
|
||||||
|
|
||||||
|
24
Dockerfile
24
Dockerfile
@ -3,17 +3,30 @@ FROM tomcat:10.1-jdk17
|
|||||||
|
|
||||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip python3-venv curl coreutils \
|
python3 python3-pip python3-venv curl coreutils git \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ADDED: Install the Dotnet SDK for the FHIR Candle server
|
||||||
|
# This makes the image a universal base for either server type
|
||||||
|
RUN apt-get update && apt-get install -y dotnet-sdk-6.0
|
||||||
|
|
||||||
# Install specific versions of GoFSH and SUSHI
|
# Install specific versions of GoFSH and SUSHI
|
||||||
# REMOVED pip install fhirpath from this line
|
|
||||||
RUN npm install -g gofsh fsh-sushi
|
RUN npm install -g gofsh fsh-sushi
|
||||||
|
|
||||||
# Set up Python environment
|
# ADDED: Download the latest HL7 FHIR Validator CLI
|
||||||
|
RUN mkdir -p /app/validator_cli
|
||||||
|
WORKDIR /app/validator_cli
|
||||||
|
# Download the validator JAR and a separate checksum file for verification
|
||||||
|
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
||||||
|
|
||||||
|
# Set permissions for the downloaded file
|
||||||
|
RUN chmod 755 validator_cli.jar
|
||||||
|
|
||||||
|
# Change back to the main app directory for the next steps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
# Set up Python environment
|
||||||
RUN python3 -m venv /app/venv
|
RUN python3 -m venv /app/venv
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
ENV PATH="/app/venv/bin:$PATH"
|
||||||
|
|
||||||
@ -44,6 +57,9 @@ COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application
|
|||||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
||||||
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
||||||
|
|
||||||
|
# ADDED: Copy pre-built Candle DLL files
|
||||||
|
COPY fhir-candle/publish/ /app/fhir-candle-publish/
|
||||||
|
|
||||||
# Install supervisord
|
# Install supervisord
|
||||||
RUN pip install supervisor
|
RUN pip install supervisor
|
||||||
|
|
||||||
@ -51,7 +67,7 @@ RUN pip install supervisor
|
|||||||
COPY supervisord.conf /etc/supervisord.conf
|
COPY supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
# Expose ports
|
# Expose ports
|
||||||
EXPOSE 5000 8080
|
EXPOSE 5000 8080 5001
|
||||||
|
|
||||||
# Start supervisord
|
# Start supervisord
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
64
Dockerfile.candle
Normal file
64
Dockerfile.candle
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Base image with Python and Dotnet
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0
|
||||||
|
|
||||||
|
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-pip python3-venv \
|
||||||
|
default-jre-headless \
|
||||||
|
curl coreutils git \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install specific versions of GoFSH and SUSHI
|
||||||
|
RUN npm install -g gofsh fsh-sushi
|
||||||
|
|
||||||
|
# ADDED: Download the latest HL7 FHIR Validator CLI
|
||||||
|
RUN mkdir -p /app/validator_cli
|
||||||
|
WORKDIR /app/validator_cli
|
||||||
|
# Download the validator JAR and a separate checksum file for verification
|
||||||
|
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
||||||
|
|
||||||
|
# Set permissions for the downloaded file
|
||||||
|
RUN chmod 755 validator_cli.jar
|
||||||
|
|
||||||
|
# Change back to the main app directory for the next steps
|
||||||
|
WORKDIR /app
|
||||||
|
# Set up Python environment
|
||||||
|
RUN python3 -m venv /app/venv
|
||||||
|
ENV PATH="/app/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
||||||
|
RUN pip uninstall -y fhirpath || true
|
||||||
|
# ADDED: Install the new fhirpathpy library
|
||||||
|
RUN pip install --no-cache-dir fhirpathpy
|
||||||
|
|
||||||
|
# Copy Flask files
|
||||||
|
COPY requirements.txt .
|
||||||
|
# Install requirements (including Pydantic - check version compatibility if needed)
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app.py .
|
||||||
|
COPY services.py .
|
||||||
|
COPY forms.py .
|
||||||
|
COPY package.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
COPY static/ static/
|
||||||
|
COPY tests/ tests/
|
||||||
|
|
||||||
|
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
||||||
|
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
||||||
|
|
||||||
|
# Copy pre-built Candle DLL files
|
||||||
|
COPY fhir-candle/publish/ /app/fhir-candle-publish/
|
||||||
|
|
||||||
|
# Install supervisord
|
||||||
|
RUN pip install supervisor
|
||||||
|
|
||||||
|
# Configure supervisord
|
||||||
|
COPY supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 5000 5001
|
||||||
|
|
||||||
|
# Start supervisord
|
||||||
|
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
66
Dockerfile.hapi
Normal file
66
Dockerfile.hapi
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Base image with Python, Java, and Maven
|
||||||
|
FROM tomcat:10.1-jdk17
|
||||||
|
|
||||||
|
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-pip python3-venv curl coreutils git maven \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install specific versions of GoFSH and SUSHI
|
||||||
|
RUN npm install -g gofsh fsh-sushi
|
||||||
|
|
||||||
|
# ADDED: Download the latest HL7 FHIR Validator CLI
|
||||||
|
RUN mkdir -p /app/validator_cli
|
||||||
|
WORKDIR /app/validator_cli
|
||||||
|
# Download the validator JAR and a separate checksum file for verification
|
||||||
|
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
||||||
|
|
||||||
|
# Set permissions for the downloaded file
|
||||||
|
RUN chmod 755 validator_cli.jar
|
||||||
|
|
||||||
|
# Change back to the main app directory for the next steps
|
||||||
|
WORKDIR /app
|
||||||
|
# Set up Python environment
|
||||||
|
RUN python3 -m venv /app/venv
|
||||||
|
ENV PATH="/app/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
||||||
|
RUN pip uninstall -y fhirpath || true
|
||||||
|
# ADDED: Install the new fhirpathpy library
|
||||||
|
RUN pip install --no-cache-dir fhirpathpy
|
||||||
|
|
||||||
|
# Copy Flask files
|
||||||
|
COPY requirements.txt .
|
||||||
|
# Install requirements (including Pydantic - check version compatibility if needed)
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app.py .
|
||||||
|
COPY services.py .
|
||||||
|
COPY forms.py .
|
||||||
|
COPY package.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
COPY static/ static/
|
||||||
|
COPY tests/ tests/
|
||||||
|
|
||||||
|
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
||||||
|
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
||||||
|
|
||||||
|
# Copy pre-built HAPI WAR and configuration
|
||||||
|
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/
|
||||||
|
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/
|
||||||
|
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml
|
||||||
|
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
||||||
|
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
||||||
|
|
||||||
|
# Install supervisord
|
||||||
|
RUN pip install supervisor
|
||||||
|
|
||||||
|
# Configure supervisord
|
||||||
|
COPY supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 5000 8080
|
||||||
|
|
||||||
|
# Start supervisord
|
||||||
|
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
60
Dockerfile.lite
Normal file
60
Dockerfile.lite
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Base image with Python and Node.js
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Install JRE and other dependencies for the validator
|
||||||
|
# We need to install `default-jre-headless` to get a minimal Java Runtime Environment
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
default-jre-headless \
|
||||||
|
curl coreutils git \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install specific versions of GoFSH and SUSHI
|
||||||
|
RUN npm install -g gofsh fsh-sushi
|
||||||
|
|
||||||
|
# ADDED: Download the latest HL7 FHIR Validator CLI
|
||||||
|
RUN mkdir -p /app/validator_cli
|
||||||
|
WORKDIR /app/validator_cli
|
||||||
|
# Download the validator JAR and a separate checksum file for verification
|
||||||
|
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
||||||
|
|
||||||
|
# Set permissions for the downloaded file
|
||||||
|
RUN chmod 755 validator_cli.jar
|
||||||
|
|
||||||
|
# Change back to the main app directory for the next steps
|
||||||
|
WORKDIR /app
|
||||||
|
# Set up Python environment
|
||||||
|
RUN python3 -m venv /app/venv
|
||||||
|
ENV PATH="/app/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
||||||
|
RUN pip uninstall -y fhirpath || true
|
||||||
|
# ADDED: Install the new fhirpathpy library
|
||||||
|
RUN pip install --no-cache-dir fhirpathpy
|
||||||
|
|
||||||
|
# Copy Flask files
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app.py .
|
||||||
|
COPY services.py .
|
||||||
|
COPY forms.py .
|
||||||
|
COPY package.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
COPY static/ static/
|
||||||
|
COPY tests/ tests/
|
||||||
|
|
||||||
|
# Ensure necessary directories are writable
|
||||||
|
RUN mkdir -p /app/static/uploads /app/logs && chmod 777 /app/static/uploads /app/logs
|
||||||
|
|
||||||
|
# Install supervisord
|
||||||
|
RUN pip install supervisor
|
||||||
|
|
||||||
|
# Configure supervisord
|
||||||
|
COPY supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Start supervisord
|
||||||
|
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
51
README.md
51
README.md
@ -5,22 +5,65 @@
|
|||||||
|
|
||||||
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), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. The toolkit includes live consoles for real-time feedback, 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), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. 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:
|
The application can run in four modes:
|
||||||
|
|
||||||
* **Standalone:** Includes a Dockerized Flask frontend, SQLite database, and an embedded HAPI FHIR server for local validation and interaction.
|
|
||||||
* **Lite:** Includes only the Dockerized Flask frontend and SQLite database, excluding the local HAPI FHIR server. Requires connection to external FHIR servers for certain features.
|
* Installation Modes (Lite, Custom URL, Hapi, and Candle)
|
||||||
|
This toolkit now offers four primary installation modes, configured via a simple command-line prompt during setup:
|
||||||
|
|
||||||
|
Lite Mode
|
||||||
|
|
||||||
|
Includes the Flask frontend, SQLite database, and core tooling (GoFSH, SUSHI) without an embedded FHIR server.
|
||||||
|
|
||||||
|
Requires you to provide a URL for an external FHIR server when using features like the FHIR API Explorer and validation.
|
||||||
|
|
||||||
|
Does not require Git, Maven, or .NET for setup.
|
||||||
|
|
||||||
|
Ideal for users who will always connect to an existing external FHIR server.
|
||||||
|
|
||||||
|
Custom URL Mode
|
||||||
|
|
||||||
|
Similar to Lite Mode, but prompts you for a specific external FHIR server URL to use as the default for the toolkit.
|
||||||
|
|
||||||
|
The application will be pre-configured to use your custom URL for all FHIR-related operations.
|
||||||
|
|
||||||
|
Does not require Git, Maven, or .NET for setup.
|
||||||
|
|
||||||
|
Hapi Mode
|
||||||
|
|
||||||
|
Includes the full FHIRFLARE toolkit and an embedded HAPI FHIR server.
|
||||||
|
|
||||||
|
The docker-compose configuration will proxy requests to the internal HAPI server, which is accessible via http://localhost:8080/fhir.
|
||||||
|
|
||||||
|
Requires Git and Maven during the initial build process to compile the HAPI FHIR server.
|
||||||
|
|
||||||
|
Ideal for users who want a self-contained, offline development and testing environment.
|
||||||
|
|
||||||
|
Candle Mode
|
||||||
|
|
||||||
|
Includes the full FHIRFLARE toolkit and an embedded FHIR Candle server.
|
||||||
|
|
||||||
|
The docker-compose configuration will proxy requests to the internal Candle server, which is accessible via http://localhost:5001/fhir/<version>.
|
||||||
|
|
||||||
|
Requires Git and the .NET SDK during the initial build process.
|
||||||
|
|
||||||
|
Ideal for users who want to use the .NET-based FHIR server for development and testing.
|
||||||
|
|
||||||
## Installation Modes (Lite vs. Standalone)
|
## Installation Modes (Lite vs. Standalone)
|
||||||
|
|
||||||
This toolkit offers two primary installation modes to suit different needs:
|
This toolkit offers two primary installation modes to suit different needs:
|
||||||
|
|
||||||
* **Standalone Version:**
|
* **Standalone Version - Hapi / Candle:**
|
||||||
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
||||||
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
||||||
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
||||||
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
||||||
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
||||||
|
|
||||||
|
* **Standalone Version - Custom Mode:**
|
||||||
|
* Includes the full FHIRFLARE Toolkit application **and** point the Environmet variable at your chosen custom fhir url endpoint allowing for Custom setups
|
||||||
|
|
||||||
|
|
||||||
* **Lite Version:**
|
* **Lite Version:**
|
||||||
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
||||||
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
||||||
|
256
app.py
256
app.py
@ -29,6 +29,7 @@ import re
|
|||||||
import yaml
|
import yaml
|
||||||
import threading
|
import threading
|
||||||
import time # Add time import
|
import time # Add time import
|
||||||
|
import io
|
||||||
import services
|
import services
|
||||||
from services import (
|
from services import (
|
||||||
services_bp,
|
services_bp,
|
||||||
@ -44,9 +45,14 @@ from services import (
|
|||||||
pkg_version,
|
pkg_version,
|
||||||
get_package_description,
|
get_package_description,
|
||||||
safe_parse_version,
|
safe_parse_version,
|
||||||
import_manual_package_and_dependencies
|
import_manual_package_and_dependencies,
|
||||||
|
parse_and_list_igs,
|
||||||
|
generate_ig_yaml,
|
||||||
|
apply_and_validate,
|
||||||
|
run_validator_cli,
|
||||||
|
uuid
|
||||||
)
|
)
|
||||||
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm
|
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
|
||||||
from wtforms import SubmitField
|
from wtforms import SubmitField
|
||||||
from package import package_bp
|
from package import package_bp
|
||||||
from flasgger import Swagger, swag_from # Import Flasgger
|
from flasgger import Swagger, swag_from # Import Flasgger
|
||||||
@ -66,11 +72,16 @@ app.config['FHIR_PACKAGES_DIR'] = os.path.join(instance_path, 'fhir_packages')
|
|||||||
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
|
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
|
||||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||||
|
app.config['LATEST_BUILD_URL'] = "http://build.fhir.org/ig/qas.json"
|
||||||
|
app.config['VALIDATION_LOG_DIR'] = os.path.join(app.instance_path, 'validation_logs')
|
||||||
|
os.makedirs(app.config['VALIDATION_LOG_DIR'], exist_ok=True)
|
||||||
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
|
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
|
||||||
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
|
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
|
||||||
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
|
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
|
||||||
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
|
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Basic Swagger configuration
|
# Basic Swagger configuration
|
||||||
app.config['SWAGGER'] = {
|
app.config['SWAGGER'] = {
|
||||||
'title': 'FHIRFLARE IG Toolkit API',
|
'title': 'FHIRFLARE IG Toolkit API',
|
||||||
@ -532,6 +543,29 @@ def restart_tomcat():
|
|||||||
def config_hapi():
|
def config_hapi():
|
||||||
return render_template('config_hapi.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
return render_template('config_hapi.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
||||||
|
|
||||||
|
@app.route('/api/get-local-server-url', methods=['GET'])
|
||||||
|
@swag_from({
|
||||||
|
'tags': ['FHIR Server Configuration'],
|
||||||
|
'summary': 'Get the local FHIR server URL.',
|
||||||
|
'description': 'Retrieves the base URL of the configured local FHIR server (HAPI). This is used by frontend components to make direct requests, bypassing the proxy.',
|
||||||
|
'responses': {
|
||||||
|
'200': {
|
||||||
|
'description': 'The URL of the local FHIR server.',
|
||||||
|
'schema': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'url': {'type': 'string', 'example': 'http://localhost:8080/fhir'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def get_local_server_url():
|
||||||
|
"""
|
||||||
|
Expose the local HAPI FHIR server URL to the frontend.
|
||||||
|
"""
|
||||||
|
return jsonify({'url': app.config.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')})
|
||||||
|
|
||||||
@app.route('/manual-import-ig', methods=['GET', 'POST'])
|
@app.route('/manual-import-ig', methods=['GET', 'POST'])
|
||||||
def manual_import_ig():
|
def manual_import_ig():
|
||||||
"""
|
"""
|
||||||
@ -1738,6 +1772,157 @@ csrf.exempt(api_push_ig) # Add this line
|
|||||||
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
|
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
|
||||||
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
|
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
|
||||||
|
|
||||||
|
@app.route('/validate-sample', methods=['GET'])
|
||||||
|
def validate_resource_page():
|
||||||
|
"""Renders the validation form page."""
|
||||||
|
form = ValidationForm()
|
||||||
|
# Fetch list of packages to populate the dropdown
|
||||||
|
packages = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
|
||||||
|
return render_template('validate_sample.html', form=form, packages=packages)
|
||||||
|
|
||||||
|
@app.route('/api/validate', methods=['POST'])
|
||||||
|
@swag_from({
|
||||||
|
'tags': ['Validation'],
|
||||||
|
'summary': 'Starts a validation process for a FHIR resource or bundle.',
|
||||||
|
'description': 'Starts a validation task and immediately returns a task ID for polling. The actual validation is run in a separate thread to prevent timeouts.',
|
||||||
|
'security': [{'ApiKeyAuth': []}],
|
||||||
|
'consumes': ['application/json'],
|
||||||
|
'parameters': [
|
||||||
|
{
|
||||||
|
'name': 'validation_payload',
|
||||||
|
'in': 'body',
|
||||||
|
'required': True,
|
||||||
|
'schema': {
|
||||||
|
'type': 'object',
|
||||||
|
'required': ['package_name', 'version', 'fhir_resource'],
|
||||||
|
'properties': {
|
||||||
|
'package_name': {'type': 'string', 'example': 'hl7.fhir.us.core'},
|
||||||
|
'version': {'type': 'string', 'example': '6.1.0'},
|
||||||
|
'fhir_resource': {'type': 'string', 'description': 'A JSON string of the FHIR resource or Bundle to validate.'},
|
||||||
|
'fhir_version': {'type': 'string', 'example': '4.0.1'},
|
||||||
|
'do_native': {'type': 'boolean', 'default': False},
|
||||||
|
'hint_about_must_support': {'type': 'boolean', 'default': False},
|
||||||
|
'assume_valid_rest_references': {'type': 'boolean', 'default': False},
|
||||||
|
'no_extensible_binding_warnings': {'type': 'boolean', 'default': False},
|
||||||
|
'show_times': {'type': 'boolean', 'default': False},
|
||||||
|
'allow_example_urls': {'type': 'boolean', 'default': False},
|
||||||
|
'check_ips_codes': {'type': 'boolean', 'default': False},
|
||||||
|
'allow_any_extensions': {'type': 'boolean', 'default': False},
|
||||||
|
'tx_routing': {'type': 'boolean', 'default': False},
|
||||||
|
'snomed_ct_version': {'type': 'string', 'default': ''},
|
||||||
|
'profiles': {'type': 'string', 'description': 'Comma-separated list of profile URLs.'},
|
||||||
|
'extensions': {'type': 'string', 'description': 'Comma-separated list of extension URLs.'},
|
||||||
|
'terminology_server': {'type': 'string', 'description': 'URL of an alternate terminology server.'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'responses': {
|
||||||
|
'202': {'description': 'Validation process started successfully, returns a task_id.'},
|
||||||
|
'400': {'description': 'Invalid request (e.g., missing fields, invalid JSON).'}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def validate_resource():
|
||||||
|
"""API endpoint to run validation on a FHIR resource."""
|
||||||
|
try:
|
||||||
|
# Get data from the JSON payload
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid JSON: No data received.'}), 400
|
||||||
|
|
||||||
|
fhir_resource_text = data.get('fhir_resource')
|
||||||
|
fhir_version = data.get('fhir_version')
|
||||||
|
package_name = data.get('package_name')
|
||||||
|
version = data.get('version')
|
||||||
|
profiles = data.get('profiles')
|
||||||
|
extensions = data.get('extensions')
|
||||||
|
terminology_server = data.get('terminology_server')
|
||||||
|
snomed_ct_version = data.get('snomed_ct_version')
|
||||||
|
|
||||||
|
# Build the list of IGs from the package name and version
|
||||||
|
igs = [f"{package_name}#{version}"] if package_name and version else []
|
||||||
|
|
||||||
|
flags = {
|
||||||
|
'doNative': data.get('do_native'),
|
||||||
|
'hintAboutMustSupport': data.get('hint_about_must_support'),
|
||||||
|
'assumeValidRestReferences': data.get('assume_valid_rest_references'),
|
||||||
|
'noExtensibleBindingWarnings': data.get('no_extensible_binding_warnings'),
|
||||||
|
'showTimes': data.get('show_times'),
|
||||||
|
'allowExampleUrls': data.get('allow_example_urls'),
|
||||||
|
'checkIpsCodes': data.get('check_ips_codes'),
|
||||||
|
'allowAnyExtensions': data.get('allow_any_extensions'),
|
||||||
|
'txRouting': data.get('tx_routing'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for empty resource
|
||||||
|
if not fhir_resource_text:
|
||||||
|
return jsonify({'status': 'error', 'message': 'No FHIR resource provided.'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Generate a unique task ID
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Store a pending status in the global cache
|
||||||
|
with services.cache_lock:
|
||||||
|
services.validation_results_cache[task_id] = {"status": "pending"}
|
||||||
|
|
||||||
|
# Start the validation process in a new thread
|
||||||
|
app_context = app.app_context()
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=services.validate_in_thread,
|
||||||
|
args=(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
logger.info(f"Validation task {task_id} started in background thread.")
|
||||||
|
|
||||||
|
return jsonify({"status": "accepted", "message": "Validation started", "task_id": task_id}), 202
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting validation thread: {e}", exc_info=True)
|
||||||
|
return jsonify({'status': 'error', 'message': f'An internal server error occurred while starting the validation: {e}'}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Validation API error: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': f'An internal server error occurred: {e}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/check-validation-status/<task_id>', methods=['GET'])
|
||||||
|
@swag_from({
|
||||||
|
'tags': ['Validation'],
|
||||||
|
'summary': 'Check the status of a validation task.',
|
||||||
|
'description': 'Polls for the result of a validation task by its ID. Returns the full validation report when the task is complete.',
|
||||||
|
'security': [{'ApiKeyAuth': []}],
|
||||||
|
'parameters': [
|
||||||
|
{'name': 'task_id', 'in': 'path', 'type': 'string', 'required': True, 'description': 'The unique ID of the validation task.'}
|
||||||
|
],
|
||||||
|
'responses': {
|
||||||
|
'200': {'description': 'Returns the full validation report if the task is complete.'},
|
||||||
|
'202': {'description': 'The validation task is still in progress.'},
|
||||||
|
'404': {'description': 'The specified task ID was not found.'}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def check_validation_status(task_id):
|
||||||
|
"""
|
||||||
|
This GET endpoint is polled by the frontend to check the status of a validation task.
|
||||||
|
"""
|
||||||
|
with services.cache_lock:
|
||||||
|
task_result = services.validation_results_cache.get(task_id)
|
||||||
|
|
||||||
|
if task_result is None:
|
||||||
|
return jsonify({"status": "error", "message": "Task not found."}), 404
|
||||||
|
|
||||||
|
if task_result["status"] == "pending":
|
||||||
|
return jsonify({"status": "pending", "message": "Validation is still in progress."}), 202
|
||||||
|
|
||||||
|
# Remove the result from the cache after it has been retrieved to prevent a memory leak
|
||||||
|
with services.cache_lock:
|
||||||
|
del services.validation_results_cache[task_id]
|
||||||
|
|
||||||
|
return jsonify({"status": "complete", "result": task_result["result"]}), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_db():
|
def create_db():
|
||||||
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
try:
|
try:
|
||||||
@ -3133,7 +3318,74 @@ def package_details_view(name):
|
|||||||
package_name=actual_package_name,
|
package_name=actual_package_name,
|
||||||
latest_official_version=latest_official_version_str)
|
latest_official_version=latest_official_version_str)
|
||||||
|
|
||||||
|
@app.route('/ig-configurator', methods=['GET', 'POST'])
|
||||||
|
def ig_configurator():
|
||||||
|
form = IgYamlForm()
|
||||||
|
packages_dir = current_app.config['FHIR_PACKAGES_DIR']
|
||||||
|
loaded_igs = services.parse_and_list_igs(packages_dir)
|
||||||
|
|
||||||
|
form.igs.choices = [(name, f"{name} ({info['version']})") for name, info in loaded_igs.items()]
|
||||||
|
|
||||||
|
yaml_output = None
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
selected_ig_names = form.igs.data
|
||||||
|
|
||||||
|
try:
|
||||||
|
yaml_output = generate_ig_yaml(selected_ig_names, loaded_igs)
|
||||||
|
flash("YAML configuration generated successfully!", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error generating YAML: {str(e)}", "error")
|
||||||
|
logger.error(f"Error generating YAML: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
return render_template('ig_configurator.html', form=form, yaml_output=yaml_output)
|
||||||
|
|
||||||
|
@app.route('/download/<path:filename>')
|
||||||
|
def download_file(filename):
|
||||||
|
"""
|
||||||
|
Serves a file from the temporary directory created during validation.
|
||||||
|
The filename includes the unique temporary directory name to prevent
|
||||||
|
unauthorized file access and to correctly locate the file.
|
||||||
|
"""
|
||||||
|
# Use the stored temporary directory path from app.config
|
||||||
|
temp_dir = current_app.config.get('VALIDATION_TEMP_DIR')
|
||||||
|
|
||||||
|
# If no temporary directory is set, or it's a security risk, return a 404.
|
||||||
|
if not temp_dir or not os.path.exists(temp_dir):
|
||||||
|
logger.error(f"Download request failed: Temporary directory not found or expired. Path: {temp_dir}")
|
||||||
|
return jsonify({"error": "File not found or validation session expired."}), 404
|
||||||
|
|
||||||
|
# Construct the full file path.
|
||||||
|
file_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
logger.error(f"File not found for download: {file_path}")
|
||||||
|
return jsonify({"error": "File not found."}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use send_file with as_attachment=True to trigger a download
|
||||||
|
return send_file(file_path, as_attachment=True, download_name=filename)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error serving file {file_path}: {str(e)}")
|
||||||
|
return jsonify({"error": "Error serving file.", "details": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/clear-validation-results', methods=['POST'])
|
||||||
|
@csrf.exempt
|
||||||
|
def clear_validation_results():
|
||||||
|
"""
|
||||||
|
Clears the temporary directory containing the last validation run's results.
|
||||||
|
"""
|
||||||
|
temp_dir = current_app.config.get('VALIDATION_TEMP_DIR')
|
||||||
|
if temp_dir and os.path.exists(temp_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
current_app.config['VALIDATION_TEMP_DIR'] = None
|
||||||
|
return jsonify({"status": "success", "message": f"Results cleared successfully."})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to clear validation results at {temp_dir}: {e}")
|
||||||
|
return jsonify({"status": "error", "message": f"Failed to clear results: {e}"}), 500
|
||||||
|
else:
|
||||||
|
return jsonify({"status": "success", "message": "No results to clear."})
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon():
|
def favicon():
|
||||||
|
@ -3,14 +3,13 @@ services:
|
|||||||
fhirflare:
|
fhirflare:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.lite
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./instance:/app/instance
|
||||||
- ./static/uploads:/app/static/uploads
|
- ./static/uploads:/app/static/uploads
|
||||||
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app.py
|
- FLASK_APP=app.py
|
||||||
@ -18,5 +17,5 @@ services:
|
|||||||
- NODE_PATH=/usr/lib/node_modules
|
- NODE_PATH=/usr/lib/node_modules
|
||||||
- APP_MODE=standalone
|
- APP_MODE=standalone
|
||||||
- APP_BASE_URL=http://localhost:5000
|
- APP_BASE_URL=http://localhost:5000
|
||||||
- HAPI_FHIR_URL=http://localhost:8080/fhir
|
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
|
||||||
command: supervisord -c /etc/supervisord.conf
|
command: supervisord -c /etc/supervisord.conf
|
||||||
|
62
forms.py
62
forms.py
@ -1,6 +1,6 @@
|
|||||||
# forms.py
|
# forms.py
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField
|
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField, SelectMultipleField
|
||||||
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
|
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
|
||||||
from flask import request
|
from flask import request
|
||||||
import json
|
import json
|
||||||
@ -307,3 +307,63 @@ class FhirRequestForm(FlaskForm):
|
|||||||
self.basic_auth_password.errors.append('Password is required for Basic Authentication with a custom URL.')
|
self.basic_auth_password.errors.append('Password is required for Basic Authentication with a custom URL.')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
class IgYamlForm(FlaskForm):
|
||||||
|
"""Form to select IGs for YAML generation."""
|
||||||
|
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
|
||||||
|
generate_yaml = SubmitField('Generate YAML')
|
||||||
|
|
||||||
|
# --- New ValidationForm class for the new page ---
|
||||||
|
class ValidationForm(FlaskForm):
|
||||||
|
"""Form for validating a single FHIR resource with various options."""
|
||||||
|
# Added fields to match the HTML template
|
||||||
|
package_name = StringField('Package Name', validators=[Optional()])
|
||||||
|
version = StringField('Package Version', validators=[Optional()])
|
||||||
|
|
||||||
|
# Main content fields
|
||||||
|
fhir_resource = TextAreaField('FHIR Resource (JSON or XML)', validators=[InputRequired()],
|
||||||
|
render_kw={'placeholder': 'Paste your FHIR JSON here...'})
|
||||||
|
fhir_version = SelectField('FHIR Version', choices=[
|
||||||
|
('4.0.1', 'R4 (4.0.1)'),
|
||||||
|
('3.0.2', 'STU3 (3.0.2)'),
|
||||||
|
('5.0.0', 'R5 (5.0.0)')
|
||||||
|
], default='4.0.1', validators=[DataRequired()])
|
||||||
|
|
||||||
|
# Flags and options from validator settings.pdf
|
||||||
|
do_native = BooleanField('Native Validation (doNative)')
|
||||||
|
hint_about_must_support = BooleanField('Must Support (hintAboutMustSupport)')
|
||||||
|
assume_valid_rest_references = BooleanField('Assume Valid Rest References (assumeValidRestReferences)')
|
||||||
|
no_extensible_binding_warnings = BooleanField('Extensible Binding Warnings (noExtensibleBindingWarnings)')
|
||||||
|
show_times = BooleanField('Show Times (-show-times)')
|
||||||
|
allow_example_urls = BooleanField('Allow Example URLs (-allow-example-urls)')
|
||||||
|
check_ips_codes = BooleanField('Check IPS Codes (-check-ips-codes)')
|
||||||
|
allow_any_extensions = BooleanField('Allow Any Extensions (-allow-any-extensions)')
|
||||||
|
tx_routing = BooleanField('Show Terminology Routing (-tx-routing)')
|
||||||
|
|
||||||
|
# SNOMED CT options
|
||||||
|
snomed_ct_version = SelectField('Select SNOMED Version', choices=[
|
||||||
|
('', '-- No selection --'),
|
||||||
|
('intl', 'International edition (900000000000207008)'),
|
||||||
|
('us', 'US edition (731000124108)'),
|
||||||
|
('uk', 'United Kingdom Edition (999000041000000102)'),
|
||||||
|
('es', 'Spanish Language Edition (449081005)'),
|
||||||
|
('nl', 'Netherlands Edition (11000146104)'),
|
||||||
|
('ca', 'Canadian Edition (20611000087101)'),
|
||||||
|
('dk', 'Danish Edition (554471000005108)'),
|
||||||
|
('se', 'Swedish Edition (45991000052106)'),
|
||||||
|
('au', 'Australian Edition (32506021000036107)'),
|
||||||
|
('be', 'Belgium Edition (11000172109)')
|
||||||
|
], validators=[Optional()])
|
||||||
|
|
||||||
|
# Text input fields
|
||||||
|
profiles = StringField('Profiles',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'})
|
||||||
|
extensions = StringField('Extensions',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace'})
|
||||||
|
terminology_server = StringField('Terminology Server',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://tx.fhir.org'})
|
||||||
|
|
||||||
|
submit = SubmitField('Validate')
|
1081
services.py
1081
services.py
File diff suppressed because it is too large
Load Diff
414
setup_linux.sh
414
setup_linux.sh
@ -1,18 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
REPO_URL="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
|
REPO_URL_HAPI="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
|
||||||
CLONE_DIR="hapi-fhir-jpaserver"
|
REPO_URL_CANDLE="https://github.com/FHIR/fhir-candle.git"
|
||||||
SOURCE_CONFIG_DIR="hapi-fhir-Setup" # Assuming this is relative to the script's parent
|
CLONE_DIR_HAPI="hapi-fhir-jpaserver"
|
||||||
|
CLONE_DIR_CANDLE="fhir-candle"
|
||||||
|
SOURCE_CONFIG_DIR="hapi-fhir-Setup"
|
||||||
CONFIG_FILE="application.yaml"
|
CONFIG_FILE="application.yaml"
|
||||||
|
|
||||||
# --- Define Paths ---
|
# --- Define Paths ---
|
||||||
# Note: Adjust SOURCE_CONFIG_PATH if SOURCE_CONFIG_DIR is not a sibling directory
|
|
||||||
# This assumes the script is run from a directory, and hapi-fhir-setup is at the same level
|
|
||||||
SOURCE_CONFIG_PATH="../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
|
SOURCE_CONFIG_PATH="../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
|
||||||
DEST_CONFIG_PATH="${CLONE_DIR}/target/classes/${CONFIG_FILE}"
|
DEST_CONFIG_PATH="${CLONE_DIR_HAPI}/target/classes/${CONFIG_FILE}"
|
||||||
|
|
||||||
APP_MODE=""
|
APP_MODE=""
|
||||||
|
CUSTOM_FHIR_URL_VAL=""
|
||||||
|
SERVER_TYPE=""
|
||||||
|
CANDLE_FHIR_VERSION=""
|
||||||
|
|
||||||
# --- Error Handling Function ---
|
# --- Error Handling Function ---
|
||||||
handle_error() {
|
handle_error() {
|
||||||
@ -20,25 +23,39 @@ handle_error() {
|
|||||||
echo "An error occurred: $1"
|
echo "An error occurred: $1"
|
||||||
echo "Script aborted."
|
echo "Script aborted."
|
||||||
echo "------------------------------------"
|
echo "------------------------------------"
|
||||||
# Removed 'read -p "Press Enter to exit..."' as it's not typical for non-interactive CI/CD
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Prompt for Installation Mode ===
|
# === MODIFIED: Prompt for Installation Mode ===
|
||||||
get_mode_choice() {
|
get_mode_choice() {
|
||||||
|
echo ""
|
||||||
echo "Select Installation Mode:"
|
echo "Select Installation Mode:"
|
||||||
echo "1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)"
|
echo "1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)"
|
||||||
echo "2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)"
|
echo "2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)"
|
||||||
|
echo "3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)"
|
||||||
|
echo "4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p "Enter your choice (1 or 2): " choice
|
read -r -p "Enter your choice (1, 2, 3, or 4): " choice
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1)
|
1)
|
||||||
APP_MODE="standalone"
|
APP_MODE="lite"
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
APP_MODE="lite"
|
APP_MODE="standalone"
|
||||||
|
get_custom_url_prompt
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
APP_MODE="standalone"
|
||||||
|
SERVER_TYPE="hapi"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
APP_MODE="standalone"
|
||||||
|
SERVER_TYPE="candle"
|
||||||
|
get_candle_fhir_version
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@ -47,156 +64,301 @@ get_mode_choice() {
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
echo "Selected Mode: $APP_MODE"
|
echo "Selected Mode: $APP_MODE"
|
||||||
|
echo "Server Type: $SERVER_TYPE"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# === NEW: Prompt for Custom URL ===
|
||||||
|
get_custom_url_prompt() {
|
||||||
|
local confirmed_url=""
|
||||||
|
while true; do
|
||||||
|
echo
|
||||||
|
read -r -p "Please enter the custom FHIR server URL: " custom_url_input
|
||||||
|
echo
|
||||||
|
echo "You entered: $custom_url_input"
|
||||||
|
read -r -p "Is this URL correct? (Y/N): " confirm_url
|
||||||
|
if [[ "$confirm_url" =~ ^[Yy]$ ]]; then
|
||||||
|
confirmed_url="$custom_url_input"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "URL not confirmed. Please re-enter."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
echo
|
||||||
|
read -r -p "Please re-enter the URL to confirm it is correct: " custom_url_input
|
||||||
|
if [ "$custom_url_input" = "$confirmed_url" ]; then
|
||||||
|
CUSTOM_FHIR_URL_VAL="$custom_url_input"
|
||||||
|
echo
|
||||||
|
echo "Custom URL confirmed: $CUSTOM_FHIR_URL_VAL"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "URLs do not match. Please try again."
|
||||||
|
confirmed_url="$custom_url_input"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# === NEW: Prompt for Candle FHIR version ===
|
||||||
|
get_candle_fhir_version() {
|
||||||
|
echo ""
|
||||||
|
echo "Select the FHIR version for the Candle server:"
|
||||||
|
echo "1. R4 (4.0)"
|
||||||
|
echo "2. R4B (4.3)"
|
||||||
|
echo "3. R5 (5.0)"
|
||||||
|
while true; do
|
||||||
|
read -r -p "Enter your choice (1, 2, or 3): " choice
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
CANDLE_FHIR_VERSION=r4
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
CANDLE_FHIR_VERSION=r4b
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
CANDLE_FHIR_VERSION=r5
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid input. Please try again."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Call the function to get mode choice
|
# Call the function to get mode choice
|
||||||
get_mode_choice
|
get_mode_choice
|
||||||
|
|
||||||
# === Conditionally Execute HAPI Setup ===
|
# === Conditionally Execute Server Setup ===
|
||||||
if [ "$APP_MODE" = "standalone" ]; then
|
case "$SERVER_TYPE" in
|
||||||
echo "Running Standalone setup including HAPI FHIR..."
|
"hapi")
|
||||||
echo
|
echo "Running Hapi server setup..."
|
||||||
|
echo
|
||||||
|
|
||||||
# --- Step 0: Clean up previous clone (optional) ---
|
# --- Step 0: Clean up previous clone (optional) ---
|
||||||
echo "Checking for existing directory: $CLONE_DIR"
|
echo "Checking for existing directory: $CLONE_DIR_HAPI"
|
||||||
if [ -d "$CLONE_DIR" ]; then
|
if [ -d "$CLONE_DIR_HAPI" ]; then
|
||||||
echo "Found existing directory, removing it..."
|
echo "Found existing directory, removing it..."
|
||||||
rm -rf "$CLONE_DIR"
|
rm -rf "$CLONE_DIR_HAPI"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
handle_error "Failed to remove existing directory: $CLONE_DIR"
|
handle_error "Failed to remove existing directory: $CLONE_DIR_HAPI"
|
||||||
|
fi
|
||||||
|
echo "Existing directory removed."
|
||||||
|
else
|
||||||
|
echo "Directory does not exist, proceeding with clone."
|
||||||
fi
|
fi
|
||||||
echo "Existing directory removed."
|
echo
|
||||||
else
|
|
||||||
echo "Directory does not exist, proceeding with clone."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 1: Clone the HAPI FHIR server repository ---
|
# --- Step 1: Clone the HAPI FHIR server repository ---
|
||||||
echo "Cloning repository: $REPO_URL into $CLONE_DIR..."
|
echo "Cloning repository: $REPO_URL_HAPI into $CLONE_DIR_HAPI..."
|
||||||
git clone "$REPO_URL" "$CLONE_DIR"
|
git clone "$REPO_URL_HAPI" "$CLONE_DIR_HAPI"
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Failed to clone repository. Check Git installation and network connection."
|
|
||||||
fi
|
|
||||||
echo "Repository cloned successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 2: Navigate into the cloned directory ---
|
|
||||||
echo "Changing directory to $CLONE_DIR..."
|
|
||||||
cd "$CLONE_DIR" || handle_error "Failed to change directory to $CLONE_DIR."
|
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 3: Build the HAPI server using Maven ---
|
|
||||||
echo "===> Starting Maven build (Step 3)..."
|
|
||||||
mvn clean package -DskipTests=true -Pboot
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "ERROR: Maven build failed."
|
|
||||||
cd ..
|
|
||||||
handle_error "Maven build process resulted in an error."
|
|
||||||
fi
|
|
||||||
echo "Maven build completed successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 4: Copy the configuration file ---
|
|
||||||
echo "===> Starting file copy (Step 4)..."
|
|
||||||
echo "Copying configuration file..."
|
|
||||||
# Corrected SOURCE_CONFIG_PATH to be relative to the new current directory ($CLONE_DIR)
|
|
||||||
# This assumes the original script's SOURCE_CONFIG_PATH was relative to its execution location
|
|
||||||
# If SOURCE_CONFIG_DIR is ../hapi-fhir-setup relative to script's original location:
|
|
||||||
# Then from within CLONE_DIR, it becomes ../../hapi-fhir-setup
|
|
||||||
# We defined SOURCE_CONFIG_PATH earlier relative to the script start.
|
|
||||||
# So, when inside CLONE_DIR, the path from original script location should be used.
|
|
||||||
# The original script had: set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
|
||||||
# And then: xcopy "%SOURCE_CONFIG_PATH%" "target\classes\"
|
|
||||||
# This implies SOURCE_CONFIG_PATH is relative to the original script's location, not the $CLONE_DIR
|
|
||||||
# Therefore, we need to construct the correct relative path from *within* $CLONE_DIR back to the source.
|
|
||||||
# Assuming the script is in dir X, and SOURCE_CONFIG_DIR is ../hapi-fhir-setup from X.
|
|
||||||
# So, hapi-fhir-setup is a sibling of X's parent.
|
|
||||||
# If CLONE_DIR is also in X, then from within CLONE_DIR, the path is ../ + original SOURCE_CONFIG_PATH
|
|
||||||
# For simplicity and robustness, let's use an absolute path or a more clearly defined relative path from the start.
|
|
||||||
# The original `SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%` implies
|
|
||||||
# that `hapi-fhir-setup` is a sibling of the directory where the script *is being run from*.
|
|
||||||
|
|
||||||
# Let's assume the script is run from the root of FHIRFLARE-IG-Toolkit.
|
|
||||||
# And hapi-fhir-setup is also in the root, next to this script.
|
|
||||||
# Then SOURCE_CONFIG_PATH would be ./hapi-fhir-setup/target/classes/application.yaml
|
|
||||||
# And from within ./hapi-fhir-jpaserver/, the path would be ../hapi-fhir-setup/target/classes/application.yaml
|
|
||||||
|
|
||||||
# The original batch file sets SOURCE_CONFIG_PATH as "..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%"
|
|
||||||
# And COPIES it to "target\classes\" *while inside CLONE_DIR*.
|
|
||||||
# This means the source path is relative to where the *cd %CLONE_DIR%* happened from.
|
|
||||||
# Let's make it relative to the script's initial execution directory.
|
|
||||||
INITIAL_SCRIPT_DIR=$(pwd)
|
|
||||||
ABSOLUTE_SOURCE_CONFIG_PATH="${INITIAL_SCRIPT_DIR}/../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}" # This matches the ..\ logic
|
|
||||||
|
|
||||||
echo "Source: $ABSOLUTE_SOURCE_CONFIG_PATH"
|
|
||||||
echo "Destination: target/classes/$CONFIG_FILE"
|
|
||||||
|
|
||||||
if [ ! -f "$ABSOLUTE_SOURCE_CONFIG_PATH" ]; then
|
|
||||||
echo "WARNING: Source configuration file not found at $ABSOLUTE_SOURCE_CONFIG_PATH."
|
|
||||||
echo "The script will continue, but the server might use default configuration."
|
|
||||||
else
|
|
||||||
cp "$ABSOLUTE_SOURCE_CONFIG_PATH" "target/classes/"
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "WARNING: Failed to copy configuration file. Check if the source file exists and permissions."
|
handle_error "Failed to clone repository. Check Git installation and network connection."
|
||||||
|
fi
|
||||||
|
echo "Repository cloned successfully."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 2: Navigate into the cloned directory ---
|
||||||
|
echo "Changing directory to $CLONE_DIR_HAPI..."
|
||||||
|
cd "$CLONE_DIR_HAPI" || handle_error "Failed to change directory to $CLONE_DIR_HAPI."
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 3: Build the HAPI server using Maven ---
|
||||||
|
echo "===> Starting Maven build (Step 3)..."
|
||||||
|
mvn clean package -DskipTests=true -Pboot
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "ERROR: Maven build failed."
|
||||||
|
cd ..
|
||||||
|
handle_error "Maven build process resulted in an error."
|
||||||
|
fi
|
||||||
|
echo "Maven build completed successfully."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 4: Copy the configuration file ---
|
||||||
|
echo "===> Starting file copy (Step 4)..."
|
||||||
|
echo "Copying configuration file..."
|
||||||
|
INITIAL_SCRIPT_DIR=$(pwd)
|
||||||
|
ABSOLUTE_SOURCE_CONFIG_PATH="${INITIAL_SCRIPT_DIR}/../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
|
||||||
|
|
||||||
|
echo "Source: $ABSOLUTE_SOURCE_CONFIG_PATH"
|
||||||
|
echo "Destination: target/classes/$CONFIG_FILE"
|
||||||
|
|
||||||
|
if [ ! -f "$ABSOLUTE_SOURCE_CONFIG_PATH" ]; then
|
||||||
|
echo "WARNING: Source configuration file not found at $ABSOLUTE_SOURCE_CONFIG_PATH."
|
||||||
echo "The script will continue, but the server might use default configuration."
|
echo "The script will continue, but the server might use default configuration."
|
||||||
else
|
else
|
||||||
echo "Configuration file copied successfully."
|
cp "$ABSOLUTE_SOURCE_CONFIG_PATH" "target/classes/"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "WARNING: Failed to copy configuration file. Check if the source file exists and permissions."
|
||||||
|
echo "The script will continue, but the server might use default configuration."
|
||||||
|
else
|
||||||
|
echo "Configuration file copied successfully."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
echo
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 5: Navigate back to the parent directory ---
|
# --- Step 5: Navigate back to the parent directory ---
|
||||||
echo "===> Changing directory back (Step 5)..."
|
echo "===> Changing directory back (Step 5)..."
|
||||||
cd .. || handle_error "Failed to change back to the parent directory."
|
cd .. || handle_error "Failed to change back to the parent directory."
|
||||||
echo "Current directory: $(pwd)"
|
echo "Current directory: $(pwd)"
|
||||||
echo
|
echo
|
||||||
|
;;
|
||||||
|
|
||||||
else # APP_MODE is "lite"
|
"candle")
|
||||||
echo "Running Lite setup, skipping HAPI FHIR build..."
|
echo "Running FHIR Candle server setup..."
|
||||||
# Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen
|
echo
|
||||||
if [ -d "$CLONE_DIR" ]; then
|
|
||||||
echo "Found existing HAPI directory ($CLONE_DIR) in Lite mode. Removing it..."
|
|
||||||
rm -rf "$CLONE_DIR"
|
|
||||||
fi
|
|
||||||
# Create empty target directories expected by Dockerfile COPY, even if not used
|
|
||||||
mkdir -p "${CLONE_DIR}/target/classes"
|
|
||||||
mkdir -p "${CLONE_DIR}/custom" # This was in the original batch, ensure it's here
|
|
||||||
# Create a placeholder empty WAR file and application.yaml to satisfy Dockerfile COPY
|
|
||||||
touch "${CLONE_DIR}/target/ROOT.war"
|
|
||||||
touch "${CLONE_DIR}/target/classes/application.yaml"
|
|
||||||
echo "Placeholder files and directories created for Lite mode build in $CLONE_DIR."
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
# === Modify docker-compose.yml to set APP_MODE ===
|
# --- Step 0: Clean up previous clone (optional) ---
|
||||||
echo "Updating docker-compose.yml with APP_MODE=$APP_MODE..."
|
echo "Checking for existing directory: $CLONE_DIR_CANDLE"
|
||||||
|
if [ -d "$CLONE_DIR_CANDLE" ]; then
|
||||||
|
echo "Found existing directory, removing it..."
|
||||||
|
rm -rf "$CLONE_DIR_CANDLE"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
handle_error "Failed to remove existing directory: $CLONE_DIR_CANDLE"
|
||||||
|
fi
|
||||||
|
echo "Existing directory removed."
|
||||||
|
else
|
||||||
|
echo "Directory does not exist, proceeding with clone."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 1: Clone the FHIR Candle server repository ---
|
||||||
|
echo "Cloning repository: $REPO_URL_CANDLE into $CLONE_DIR_CANDLE..."
|
||||||
|
git clone "$REPO_URL_CANDLE" "$CLONE_DIR_CANDLE"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
handle_error "Failed to clone repository. Check Git and Dotnet SDK installation and network connection."
|
||||||
|
fi
|
||||||
|
echo "Repository cloned successfully."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 2: Navigate into the cloned directory ---
|
||||||
|
echo "Changing directory to $CLONE_DIR_CANDLE..."
|
||||||
|
cd "$CLONE_DIR_CANDLE" || handle_error "Failed to change directory to $CLONE_DIR_CANDLE."
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 3: Build the FHIR Candle server using Dotnet ---
|
||||||
|
echo "===> Starting Dotnet build (Step 3)..."
|
||||||
|
dotnet publish -c Release -f net9.0 -o publish
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
handle_error "Dotnet build failed. Check Dotnet SDK installation."
|
||||||
|
fi
|
||||||
|
echo "Dotnet build completed successfully."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Step 4: Navigate back to the parent directory ---
|
||||||
|
echo "===> Changing directory back (Step 4)..."
|
||||||
|
cd .. || handle_error "Failed to change back to the parent directory."
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) # APP_MODE is Lite, no SERVER_TYPE
|
||||||
|
echo "Running Lite setup, skipping server build..."
|
||||||
|
if [ -d "$CLONE_DIR_HAPI" ]; then
|
||||||
|
echo "Found existing HAPI directory in Lite mode. Removing it to avoid build issues..."
|
||||||
|
rm -rf "$CLONE_DIR_HAPI"
|
||||||
|
fi
|
||||||
|
if [ -d "$CLONE_DIR_CANDLE" ]; then
|
||||||
|
echo "Found existing Candle directory in Lite mode. Removing it to avoid build issues..."
|
||||||
|
rm -rf "$CLONE_DIR_CANDLE"
|
||||||
|
fi
|
||||||
|
mkdir -p "${CLONE_DIR_HAPI}/target/classes"
|
||||||
|
mkdir -p "${CLONE_DIR_HAPI}/custom"
|
||||||
|
touch "${CLONE_DIR_HAPI}/target/ROOT.war"
|
||||||
|
touch "${CLONE_DIR_HAPI}/target/classes/application.yaml"
|
||||||
|
mkdir -p "${CLONE_DIR_CANDLE}/publish"
|
||||||
|
touch "${CLONE_DIR_CANDLE}/publish/fhir-candle.dll"
|
||||||
|
echo "Placeholder files and directories created for Lite mode build."
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL and DOCKERFILE ===
|
||||||
|
echo "Updating docker-compose.yml with APP_MODE=$APP_MODE and HAPI_FHIR_URL..."
|
||||||
DOCKER_COMPOSE_TMP="docker-compose.yml.tmp"
|
DOCKER_COMPOSE_TMP="docker-compose.yml.tmp"
|
||||||
DOCKER_COMPOSE_ORIG="docker-compose.yml"
|
DOCKER_COMPOSE_ORIG="docker-compose.yml"
|
||||||
|
|
||||||
|
HAPI_URL_TO_USE="https://fhir.hl7.org.au/aucore/fhir/DEFAULT/"
|
||||||
|
if [ -n "$CUSTOM_FHIR_URL_VAL" ]; then
|
||||||
|
HAPI_URL_TO_USE="$CUSTOM_FHIR_URL_VAL"
|
||||||
|
elif [ "$SERVER_TYPE" = "candle" ]; then
|
||||||
|
HAPI_URL_TO_USE="http://localhost:5826/fhir/${CANDLE_FHIR_VERSION}"
|
||||||
|
else
|
||||||
|
HAPI_URL_TO_USE="http://localhost:8080/fhir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCKERFILE_TO_USE="Dockerfile.lite"
|
||||||
|
if [ "$SERVER_TYPE" = "hapi" ]; then
|
||||||
|
DOCKERFILE_TO_USE="Dockerfile.hapi"
|
||||||
|
elif [ "$SERVER_TYPE" = "candle" ]; then
|
||||||
|
DOCKERFILE_TO_USE="Dockerfile.candle"
|
||||||
|
fi
|
||||||
|
|
||||||
cat << EOF > "$DOCKER_COMPOSE_TMP"
|
cat << EOF > "$DOCKER_COMPOSE_TMP"
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
fhirflare:
|
fhirflare:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: ${DOCKERFILE_TO_USE}
|
||||||
ports:
|
ports:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$SERVER_TYPE" = "candle" ]; then
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
- "5001:5826"
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
|
- "5000:5000"
|
||||||
|
- "8080:8080"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./instance:/app/instance
|
||||||
- ./static/uploads:/app/static/uploads
|
- ./static/uploads:/app/static/uploads
|
||||||
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$SERVER_TYPE" = "hapi" ]; then
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
|
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||||
|
- ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
|
||||||
|
- ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
|
||||||
|
EOF
|
||||||
|
elif [ "$SERVER_TYPE" = "candle" ]; then
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
|
- ./fhir-candle/publish/:/app/fhir-candle-publish/
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app.py
|
- FLASK_APP=app.py
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- NODE_PATH=/usr/lib/node_modules
|
- NODE_PATH=/usr/lib/node_modules
|
||||||
- APP_MODE=${APP_MODE}
|
- APP_MODE=${APP_MODE}
|
||||||
- APP_BASE_URL=http://localhost:5000
|
- APP_BASE_URL=http://localhost:5000
|
||||||
- HAPI_FHIR_URL=http://localhost:8080/fhir
|
- HAPI_FHIR_URL=${HAPI_URL_TO_USE}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$SERVER_TYPE" = "candle" ]; then
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
|
- ASPNETCORE_URLS=http://0.0.0.0:5826
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
||||||
command: supervisord -c /etc/supervisord.conf
|
command: supervisord -c /etc/supervisord.conf
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
1
static/animations/Validation abstract.json
Normal file
1
static/animations/Validation abstract.json
Normal file
File diff suppressed because one or more lines are too long
@ -34,3 +34,17 @@ stdout_logfile_backups=5
|
|||||||
stderr_logfile=/app/logs/tomcat_err.log
|
stderr_logfile=/app/logs/tomcat_err.log
|
||||||
stderr_logfile_maxbytes=10MB
|
stderr_logfile_maxbytes=10MB
|
||||||
stderr_logfile_backups=5
|
stderr_logfile_backups=5
|
||||||
|
|
||||||
|
[program:candle]
|
||||||
|
command=dotnet /app/fhir-candle-publish/fhir-candle.dll
|
||||||
|
directory=/app/fhir-candle-publish
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startsecs=10
|
||||||
|
stopwaitsecs=10
|
||||||
|
stdout_logfile=/app/logs/candle.log
|
||||||
|
stdout_logfile_maxbytes=10MB
|
||||||
|
stdout_logfile_backups=5
|
||||||
|
stderr_logfile=/app/logs/candle_err.log
|
||||||
|
stderr_logfile_maxbytes=10MB
|
||||||
|
stderr_logfile_backups=5
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
|
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
|
||||||
{% if app_mode == 'lite' %}
|
{% if app_mode == 'lite' %}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in HAPI FHIR server and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
|
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in FHIR server depending and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-success" role="alert">
|
<div class="alert alert-success" role="alert">
|
||||||
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in HAPI FHIR server (accessible via the `/fhir` proxy) for local validation and exploration.
|
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in FHIR server based on your build choice (accessible via the `/fhir` proxy) for local validation and exploration, if you configured the Server ENV variable this could also be whatever customer server you have pointed the local ENV variable at allowing you to use any fhir server of your choice.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<li><strong>FHIR Server Interaction:</strong>
|
<li><strong>FHIR Server Interaction:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Push processed IGs (including dependencies) to a target FHIR server with real-time console feedback.</li>
|
<li>Push processed IGs (including dependencies) to a target FHIR server with real-time console feedback.</li>
|
||||||
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local HAPI server (in Standalone mode) and external servers.</li>
|
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local server (in Standalone mode) and external servers.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
|
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>Backend:</strong> Python with the Flask web framework and SQLAlchemy for database interaction (SQLite).</li>
|
<li><strong>Backend:</strong> Python with the Flask web framework and SQLAlchemy for database interaction (SQLite).</li>
|
||||||
<li><strong>Frontend:</strong> HTML, Bootstrap 5 for styling, and JavaScript for interactivity and dynamic content loading. Uses Lottie-Web for animations.</li>
|
<li><strong>Frontend:</strong> HTML, Bootstrap 5 for styling, and JavaScript for interactivity and dynamic content loading. Uses Lottie-Web for animations.</li>
|
||||||
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the HAPI FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
|
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the Local FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
|
||||||
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
|
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -796,11 +796,14 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
|
||||||
</li>
|
</li>
|
||||||
{% if app_mode != 'lite' %}
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link {{ 'active' if request.endpoint == 'ig_configurator' else '' }}" href="{{ url_for('ig_configurator') }}"><i class="fas fa-wrench me-1"></i> IG Valid-Conf Gen</a>
|
||||||
|
</li> -->
|
||||||
|
<!-- {% if app_mode != 'lite' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %} -->
|
||||||
</ul>
|
</ul>
|
||||||
<div class="navbar-controls d-flex align-items-center">
|
<div class="navbar-controls d-flex align-items-center">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- LEFT: Downloaded Packages -->
|
<!-- LEFT: Downloaded Packages -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-5">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
|
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -65,20 +65,20 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ pkg.version }}</td>
|
<td>{{ pkg.version }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group-vertical btn-group-sm w-80">
|
||||||
{% if is_processed %}
|
{% if is_processed %}
|
||||||
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
<button type="submit" class="btn btn-outline-primary w-80"><i class="bi bi-gear"></i> Process</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
<button type="submit" class="btn btn-outline-danger w-80" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -105,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: Processed Packages -->
|
<!-- RIGHT: Processed Packages -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-7">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -139,12 +139,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group-vertical btn-group-sm w-100">
|
<div class="btn-group-vertical btn-group-sm w-80">
|
||||||
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
|
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-80" title="View Details"><i class="bi bi-search"></i> View</a>
|
||||||
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
||||||
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
<button type="submit" class="btn btn-outline-warning w-80 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<label class="form-label">FHIR Server</label>
|
<label class="form-label">FHIR Server</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
||||||
<span id="toggleLabel">Use Local HAPI</span>
|
<span id="toggleLabel">Use Local Server</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
||||||
</div>
|
</div>
|
||||||
@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const authSection = document.getElementById('authSection');
|
const authSection = document.getElementById('authSection');
|
||||||
const authTypeSelect = document.getElementById('authType');
|
const authTypeSelect = document.getElementById('authType');
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
const authInputsGroup = document.getElementById('authInputsGroup');
|
||||||
const bearerTokenInput = document.getElementById('bearerToken');
|
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
||||||
const usernameInput = document.getElementById('username');
|
const usernameInput = document.getElementById('username');
|
||||||
const passwordInput = document.getElementById('password');
|
const passwordInput = document.getElementById('password');
|
||||||
@ -212,7 +212,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
toggleServerButton.style.pointerEvents = 'auto';
|
toggleServerButton.style.pointerEvents = 'auto';
|
||||||
toggleServerButton.removeAttribute('aria-disabled');
|
toggleServerButton.removeAttribute('aria-disabled');
|
||||||
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
||||||
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
|
toggleLabel.textContent = useLocalHapi ? 'Using Local Server' : 'Using Custom URL';
|
||||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||||
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
||||||
fhirServerUrlInput.required = !useLocalHapi;
|
fhirServerUrlInput.required = !useLocalHapi;
|
||||||
@ -244,7 +244,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
fhirServerUrlInput.value = '';
|
fhirServerUrlInput.value = '';
|
||||||
}
|
}
|
||||||
updateServerToggleUI();
|
updateServerToggleUI();
|
||||||
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
updateAuthInputsUI();
|
||||||
|
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local Server' : 'Custom URL'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRequestBodyVisibility() {
|
function updateRequestBodyVisibility() {
|
||||||
@ -295,7 +296,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const customUrl = fhirServerUrlInput.value.trim();
|
const customUrl = fhirServerUrlInput.value.trim();
|
||||||
let body = undefined;
|
let body = undefined;
|
||||||
const authType = authTypeSelect ? authTypeSelect.value : 'none';
|
const authType = authTypeSelect ? authTypeSelect.value : 'none';
|
||||||
const bearerToken = bearerTokenInput ? bearerTokenInput.value.trim() : '';
|
const bearerToken = document.getElementById('bearerToken').value.trim();
|
||||||
const username = usernameInput ? usernameInput.value.trim() : '';
|
const username = usernameInput ? usernameInput.value.trim() : '';
|
||||||
const password = passwordInput ? passwordInput.value : '';
|
const password = passwordInput ? passwordInput.value : '';
|
||||||
|
|
||||||
@ -334,7 +335,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (!useLocalHapi) {
|
if (!useLocalHapi) {
|
||||||
if (authType === 'bearer' && !bearerToken) {
|
if (authType === 'bearer' && !bearerToken) {
|
||||||
alert('Please enter a Bearer Token.');
|
alert('Please enter a Bearer Token.');
|
||||||
bearerTokenInput.classList.add('is-invalid');
|
document.getElementById('bearerToken').classList.add('is-invalid');
|
||||||
sendButton.disabled = false;
|
sendButton.disabled = false;
|
||||||
sendButton.textContent = 'Send Request';
|
sendButton.textContent = 'Send Request';
|
||||||
return;
|
return;
|
||||||
@ -453,7 +454,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error('Fetch error:', error);
|
||||||
responseCard.style.display = 'block';
|
responseCard.style.display = 'block';
|
||||||
responseStatus.textContent = `Network Error`;
|
responseStatus.textContent = 'Network Error';
|
||||||
responseStatus.className = 'badge bg-danger';
|
responseStatus.className = 'badge bg-danger';
|
||||||
responseHeaders.textContent = 'N/A';
|
responseHeaders.textContent = 'N/A';
|
||||||
let errorDetail = `Error: ${error.message}\n\n`;
|
let errorDetail = `Error: ${error.message}\n\n`;
|
||||||
|
File diff suppressed because it is too large
Load Diff
40
templates/ig_configurator.html
Normal file
40
templates/ig_configurator.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>IG Configuration Generator</h2>
|
||||||
|
<p>Select the Implementation Guides you want to load into Local FHIR for validation. This will generate the YAML configuration required for the <code>application.yaml</code> file.</p>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">Select IGs</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">IGs and their dependencies</label>
|
||||||
|
{% for choice in form.igs.choices %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="igs" value="{{ choice[0] }}" id="ig-{{ choice[0] }}">
|
||||||
|
<label class="form-check-label" for="ig-{{ choice[0] }}">{{ choice[1] }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{{ form.generate_yaml(class="btn btn-primary") }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if yaml_output %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">Generated <code>application.yaml</code> Excerpt</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Copy the following configuration block and paste it into the `hapi.fhir` section of your HAPI server's <code>application.yaml</code> file.</p>
|
||||||
|
<pre><code class="language-yaml">{{ yaml_output }}</code></pre>
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<strong>Important:</strong> After updating the <code>application.yaml</code> file, you must **restart your HAPI server** for the changes to take effect.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -10,7 +10,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -28,18 +27,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form id="retrieveForm" method="POST">
|
<form id="retrieveForm" method="POST">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.csrf_token(id='retrieve_csrf_token') }}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">FHIR Server</label>
|
<label class="form-label fw-bold">FHIR Server</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
||||||
<span id="toggleLabel">Use Local HAPI</span>
|
<span id="toggleLabel">Use Local Server</span>
|
||||||
</button>
|
</button>
|
||||||
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
|
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
|
||||||
</div>
|
</div>
|
||||||
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (/fhir proxy) or enter a custom FHIR server URL.</small>
|
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local Fhir Server(/fhir proxy) or enter a custom FHIR server URL.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Authentication Section (Shown for Custom URL) #}
|
{# Authentication Section (Shown for Custom URL) #}
|
||||||
<div class="mb-3" id="authSection" style="display: none;">
|
<div class="mb-3" id="authSection" style="display: none;">
|
||||||
<label class="form-label fw-bold">Authentication</label>
|
<label class="form-label fw-bold">Authentication</label>
|
||||||
@ -68,7 +66,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
|
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Checkbox Row #}
|
{# Checkbox Row #}
|
||||||
<div class="row g-3 mb-3 align-items-center">
|
<div class="row g-3 mb-3 align-items-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -78,7 +75,6 @@
|
|||||||
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
|
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
|
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
|
||||||
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
|
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
|
||||||
<h5>Resource Types</h5>
|
<h5>Resource Types</h5>
|
||||||
@ -102,14 +98,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
|
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="splitForm" method="POST" enctype="multipart/form-data">
|
<form id="splitForm" method="POST" enctype="multipart/form-data">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.csrf_token(id='split_csrf_token') }}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">Bundle Source</label>
|
<label class="form-label fw-bold">Bundle Source</label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@ -141,7 +136,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// --- DOM Element References ---
|
// --- DOM Element References ---
|
||||||
@ -168,11 +162,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const authSection = document.getElementById('authSection');
|
const authSection = document.getElementById('authSection');
|
||||||
const authTypeSelect = document.getElementById('authType');
|
const authTypeSelect = document.getElementById('authType');
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
const authInputsGroup = document.getElementById('authInputsGroup');
|
||||||
const bearerTokenInput = document.getElementById('bearerToken');
|
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
||||||
const usernameInput = document.getElementById('username');
|
const usernameInput = document.getElementById('username');
|
||||||
const passwordInput = document.getElementById('password');
|
const passwordInput = document.getElementById('password');
|
||||||
|
|
||||||
// --- State & Config Variables ---
|
// --- State & Config Variables ---
|
||||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||||
const apiKey = '{{ api_key }}';
|
const apiKey = '{{ api_key }}';
|
||||||
@ -181,10 +174,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let retrieveZipPath = null;
|
let retrieveZipPath = null;
|
||||||
let splitZipPath = null;
|
let splitZipPath = null;
|
||||||
let fetchedMetadataCache = null;
|
let fetchedMetadataCache = null;
|
||||||
|
let localHapiBaseUrl = ''; // To store the fetched URL
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
||||||
|
|
||||||
// --- UI Update Functions ---
|
// --- UI Update Functions ---
|
||||||
function updateBundleSourceUI() {
|
function updateBundleSourceUI() {
|
||||||
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
||||||
@ -195,10 +187,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
splitBundleZipInput.required = selectedSource === 'upload';
|
splitBundleZipInput.required = selectedSource === 'upload';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function fetchLocalUrlAndSetUI() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/get-local-server-url');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch local server URL.');
|
||||||
|
const data = await response.json();
|
||||||
|
localHapiBaseUrl = data.url.replace(/\/+$/, ''); // Normalize by removing trailing slashes
|
||||||
|
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
localHapiBaseUrl = '/fhir'; // Fallback to proxy
|
||||||
|
alert('Could not fetch local HAPI server URL. Falling back to proxy.');
|
||||||
|
} finally {
|
||||||
|
updateServerToggleUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
function updateServerToggleUI() {
|
function updateServerToggleUI() {
|
||||||
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
|
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
|
||||||
|
|
||||||
if (appMode === 'lite') {
|
if (appMode === 'lite') {
|
||||||
useLocalHapi = false;
|
useLocalHapi = false;
|
||||||
toggleServerButton.disabled = true;
|
toggleServerButton.disabled = true;
|
||||||
@ -216,7 +221,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
toggleServerButton.classList.remove('disabled');
|
toggleServerButton.classList.remove('disabled');
|
||||||
toggleServerButton.style.pointerEvents = 'auto';
|
toggleServerButton.style.pointerEvents = 'auto';
|
||||||
toggleServerButton.removeAttribute('aria-disabled');
|
toggleServerButton.removeAttribute('aria-disabled');
|
||||||
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
|
toggleLabel.textContent = useLocalHapi ? 'Use Local Server' : 'Use Custom URL';
|
||||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||||
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
||||||
fhirServerUrlInput.required = !useLocalHapi;
|
fhirServerUrlInput.required = !useLocalHapi;
|
||||||
@ -226,18 +231,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateAuthInputsUI();
|
updateAuthInputsUI();
|
||||||
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
|
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAuthInputsUI() {
|
function updateAuthInputsUI() {
|
||||||
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
|
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
|
||||||
const authType = authTypeSelect.value;
|
const authType = authTypeSelect.value;
|
||||||
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
|
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
|
||||||
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
|
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
|
||||||
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
|
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
|
||||||
if (authType !== 'bearer' && bearerTokenInput) bearerTokenInput.value = '';
|
if (authType !== 'bearer') {
|
||||||
if (authType !== 'basic' && usernameInput) usernameInput.value = '';
|
// Correctly reference the input element by its ID
|
||||||
if (authType !== 'basic' && passwordInput) passwordInput.value = '';
|
const tokenInput = document.getElementById('bearerToken');
|
||||||
|
if (tokenInput) tokenInput.value = '';
|
||||||
|
}
|
||||||
|
if (authType !== 'basic') {
|
||||||
|
if (usernameInput) usernameInput.value = '';
|
||||||
|
if (passwordInput) passwordInput.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFetchReferenceBundles() {
|
function toggleFetchReferenceBundles() {
|
||||||
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
|
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
|
||||||
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
|
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
|
||||||
@ -248,44 +257,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.warn("Could not find checkbox elements needed for toggling.");
|
console.warn("Could not find checkbox elements needed for toggling.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Event Listeners ---
|
// --- Event Listeners ---
|
||||||
bundleSourceRadios.forEach(radio => {
|
bundleSourceRadios.forEach(radio => {
|
||||||
radio.addEventListener('change', updateBundleSourceUI);
|
radio.addEventListener('change', updateBundleSourceUI);
|
||||||
});
|
});
|
||||||
|
|
||||||
toggleServerButton.addEventListener('click', () => {
|
toggleServerButton.addEventListener('click', () => {
|
||||||
if (appMode === 'lite') return;
|
if (appMode === 'lite') return;
|
||||||
useLocalHapi = !useLocalHapi;
|
useLocalHapi = !useLocalHapi;
|
||||||
if (useLocalHapi) fhirServerUrlInput.value = '';
|
if (useLocalHapi) fhirServerUrlInput.value = '';
|
||||||
updateServerToggleUI();
|
updateServerToggleUI();
|
||||||
|
updateAuthInputsUI();
|
||||||
resourceTypesDiv.style.display = 'none';
|
resourceTypesDiv.style.display = 'none';
|
||||||
fetchedMetadataCache = null;
|
fetchedMetadataCache = null;
|
||||||
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
|
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (authTypeSelect) {
|
if (authTypeSelect) {
|
||||||
authTypeSelect.addEventListener('change', updateAuthInputsUI);
|
authTypeSelect.addEventListener('change', updateAuthInputsUI);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateReferencesCheckbox) {
|
if (validateReferencesCheckbox) {
|
||||||
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
|
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
|
||||||
} else {
|
} else {
|
||||||
console.warn("Validate references checkbox not found.");
|
console.warn("Validate references checkbox not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMetadataButton.addEventListener('click', async () => {
|
fetchMetadataButton.addEventListener('click', async () => {
|
||||||
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
|
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
|
||||||
resourceTypesDiv.style.display = 'block';
|
resourceTypesDiv.style.display = 'block';
|
||||||
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
|
let customUrl = null;
|
||||||
|
|
||||||
if (!useLocalHapi && !customUrl) {
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
|
||||||
alert('Please enter a valid FHIR server URL.');
|
|
||||||
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!useLocalHapi) {
|
if (!useLocalHapi) {
|
||||||
|
customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, ''); // Normalize custom URL
|
||||||
|
if (!customUrl) {
|
||||||
|
fhirServerUrlInput.classList.add('is-invalid');
|
||||||
|
alert('Please enter a valid FHIR server URL.');
|
||||||
|
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
try { new URL(customUrl); } catch (_) {
|
try { new URL(customUrl); } catch (_) {
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
fhirServerUrlInput.classList.add('is-invalid');
|
||||||
alert('Invalid custom URL format.');
|
alert('Invalid custom URL format.');
|
||||||
@ -294,32 +299,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
fhirServerUrlInput.classList.remove('is-invalid');
|
||||||
|
|
||||||
fetchMetadataButton.disabled = true;
|
fetchMetadataButton.disabled = true;
|
||||||
fetchMetadataButton.textContent = 'Fetching...';
|
fetchMetadataButton.textContent = 'Fetching...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fetchUrl = useLocalHapi ? '/fhir/metadata' : `${customUrl}/metadata`;
|
// Build URL and headers based on the toggle state
|
||||||
|
let fetchUrl;
|
||||||
const headers = { 'Accept': 'application/fhir+json' };
|
const headers = { 'Accept': 'application/fhir+json' };
|
||||||
if (!useLocalHapi && customUrl) {
|
if (useLocalHapi) {
|
||||||
if (authTypeSelect && authTypeSelect.value !== 'none') {
|
if (!localHapiBaseUrl) {
|
||||||
const authType = authTypeSelect.value;
|
throw new Error("Local HAPI URL not available. Please refresh.");
|
||||||
if (authType === 'bearer' && bearerTokenInput && bearerTokenInput.value) {
|
|
||||||
headers['Authorization'] = `Bearer ${bearerTokenInput.value}`;
|
|
||||||
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
|
|
||||||
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
|
|
||||||
headers['Authorization'] = `Basic ${credentials}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log(`Fetching metadata directly from: ${customUrl}`);
|
fetchUrl = `${localHapiBaseUrl}/metadata`; // localHapiBaseUrl is already normalized
|
||||||
|
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("Fetching metadata from local HAPI server via proxy");
|
fetchUrl = `${customUrl}/metadata`;
|
||||||
|
// Add authentication headers for the direct request to the custom server
|
||||||
|
const authType = authTypeSelect.value;
|
||||||
|
if (authType === 'bearer' && document.getElementById('bearerToken') && document.getElementById('bearerToken').value) {
|
||||||
|
headers['Authorization'] = `Bearer ${document.getElementById('bearerToken').value}`;
|
||||||
|
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
|
||||||
|
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
|
||||||
|
headers['Authorization'] = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`);
|
||||||
}
|
}
|
||||||
console.log(`Fetch URL: ${fetchUrl}`);
|
|
||||||
console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
|
console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
|
||||||
|
|
||||||
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
|
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
|
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
|
||||||
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
|
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
|
||||||
@ -329,7 +334,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
|
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
|
||||||
fetchedMetadataCache = data;
|
fetchedMetadataCache = data;
|
||||||
|
|
||||||
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
|
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
|
||||||
resourceButtonsContainer.innerHTML = '';
|
resourceButtonsContainer.innerHTML = '';
|
||||||
if (!resourceTypes.length) {
|
if (!resourceTypes.length) {
|
||||||
@ -360,7 +364,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchMetadataButton.textContent = 'Fetch Metadata';
|
fetchMetadataButton.textContent = 'Fetch Metadata';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
retrieveForm.addEventListener('submit', async (e) => {
|
retrieveForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const spinner = retrieveButton.querySelector('.spinner-border');
|
const spinner = retrieveButton.querySelector('.spinner-border');
|
||||||
@ -371,7 +374,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
downloadRetrieveButton.style.display = 'none';
|
downloadRetrieveButton.style.display = 'none';
|
||||||
retrieveZipPath = null;
|
retrieveZipPath = null;
|
||||||
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
|
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
|
||||||
|
|
||||||
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
|
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
|
||||||
if (!selectedResources.length) {
|
if (!selectedResources.length) {
|
||||||
alert('Please select at least one resource type.');
|
alert('Please select at least one resource type.');
|
||||||
@ -380,31 +382,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
|
||||||
const currentFhirServerUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim();
|
if (!currentFhirServerUrl) {
|
||||||
if (!useLocalHapi && !currentFhirServerUrl) {
|
alert('FHIR Server URL is required.');
|
||||||
alert('Custom FHIR Server URL is required.');
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
fhirServerUrlInput.classList.add('is-invalid');
|
||||||
retrieveButton.disabled = false;
|
retrieveButton.disabled = false;
|
||||||
if (spinner) spinner.style.display = 'none';
|
if (spinner) spinner.style.display = 'none';
|
||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
|
||||||
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
||||||
selectedResources.forEach(res => formData.append('resources', res));
|
selectedResources.forEach(res => formData.append('resources', res));
|
||||||
|
formData.append('fhir_server_url', currentFhirServerUrl);
|
||||||
// --- FIX APPLIED HERE ---
|
|
||||||
if (!useLocalHapi && currentFhirServerUrl) {
|
|
||||||
formData.append('fhir_server_url', currentFhirServerUrl);
|
|
||||||
} else {
|
|
||||||
// Explicitly use the proxy URL if local HAPI is selected
|
|
||||||
formData.append('fhir_server_url', '/fhir');
|
|
||||||
}
|
|
||||||
// --- END FIX ---
|
|
||||||
|
|
||||||
if (validateReferencesCheckbox) {
|
if (validateReferencesCheckbox) {
|
||||||
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
|
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
@ -417,19 +408,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} else {
|
} else {
|
||||||
formData.append('fetch_reference_bundles', 'false');
|
formData.append('fetch_reference_bundles', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useLocalHapi && authTypeSelect) {
|
if (!useLocalHapi && authTypeSelect) {
|
||||||
const authType = authTypeSelect.value;
|
const authType = authTypeSelect.value;
|
||||||
formData.append('auth_type', authType);
|
formData.append('auth_type', authType);
|
||||||
if (authType === 'bearer' && bearerTokenInput) {
|
if (authType === 'bearer') {
|
||||||
if (!bearerTokenInput.value) {
|
const bearerToken = document.getElementById('bearerToken').value.trim();
|
||||||
|
if (!bearerToken) {
|
||||||
alert('Please enter a Bearer Token.');
|
alert('Please enter a Bearer Token.');
|
||||||
retrieveButton.disabled = false;
|
retrieveButton.disabled = false;
|
||||||
if (spinner) spinner.style.display = 'none';
|
if (spinner) spinner.style.display = 'none';
|
||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formData.append('bearer_token', bearerTokenInput.value);
|
formData.append('bearer_token', bearerToken);
|
||||||
} else if (authType === 'basic' && usernameInput && passwordInput) {
|
} else if (authType === 'basic' && usernameInput && passwordInput) {
|
||||||
if (!usernameInput.value || !passwordInput.value) {
|
if (!usernameInput.value || !passwordInput.value) {
|
||||||
alert('Please enter both Username and Password for Basic Authentication.');
|
alert('Please enter both Username and Password for Basic Authentication.');
|
||||||
@ -442,24 +433,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
formData.append('password', passwordInput.value);
|
formData.append('password', passwordInput.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/x-ndjson',
|
'Accept': 'application/x-ndjson',
|
||||||
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
||||||
'X-API-Key': apiKey
|
'X-API-Key': apiKey
|
||||||
};
|
};
|
||||||
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}, AuthType: ${formData.get('auth_type')}`);
|
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}, AuthType: ${formData.get('auth_type')}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
|
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
|
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
|
||||||
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
|
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
|
||||||
}
|
}
|
||||||
retrieveZipPath = response.headers.get('X-Zip-Path');
|
retrieveZipPath = response.headers.get('X-Zip-Path');
|
||||||
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
|
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@ -481,13 +468,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
||||||
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
||||||
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div');
|
||||||
messageDiv.className = messageClass;
|
messageDiv.className = messageClass;
|
||||||
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
|
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
|
||||||
retrieveConsole.appendChild(messageDiv);
|
retrieveConsole.appendChild(messageDiv);
|
||||||
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
|
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
|
||||||
|
|
||||||
if (data.type === 'complete' && retrieveZipPath) {
|
if (data.type === 'complete' && retrieveZipPath) {
|
||||||
const completeData = data.data || {};
|
const completeData = data.data || {};
|
||||||
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
|
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
|
||||||
@ -505,7 +490,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.trim()) { /* Handle final buffer */ }
|
if (buffer.trim()) { /* Handle final buffer */ }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Retrieval error:', e);
|
console.error('Retrieval error:', e);
|
||||||
const ts = new Date().toLocaleTimeString();
|
const ts = new Date().toLocaleTimeString();
|
||||||
@ -517,20 +501,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadRetrieveButton.addEventListener('click', () => {
|
downloadRetrieveButton.addEventListener('click', () => {
|
||||||
if (retrieveZipPath) {
|
if (retrieveZipPath) {
|
||||||
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
|
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
|
||||||
const downloadUrl = `/tmp/${filename}`;
|
const downloadUrl = `/tmp/${filename}`;
|
||||||
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
|
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.download = 'retrieved_bundles.zip';
|
link.download = 'retrieved_bundles.zip';
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
|
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
|
||||||
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
||||||
@ -548,7 +529,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
alert("Download error: No file path available.");
|
alert("Download error: No file path available.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
splitForm.addEventListener('submit', async (e) => {
|
splitForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const spinner = splitButton.querySelector('.spinner-border');
|
const spinner = splitButton.querySelector('.spinner-border');
|
||||||
@ -559,13 +539,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
downloadSplitButton.style.display = 'none';
|
downloadSplitButton.style.display = 'none';
|
||||||
splitZipPath = null;
|
splitZipPath = null;
|
||||||
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
|
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
|
||||||
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
||||||
|
|
||||||
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
||||||
|
|
||||||
if (bundleSource === 'upload') {
|
if (bundleSource === 'upload') {
|
||||||
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
|
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
|
||||||
alert('Please select a ZIP file to upload for splitting.');
|
alert('Please select a ZIP file to upload for splitting.');
|
||||||
@ -597,16 +574,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/x-ndjson',
|
'Accept': 'application/x-ndjson',
|
||||||
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
||||||
'X-API-Key': apiKey
|
'X-API-Key': apiKey
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
|
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorMsg = await response.text();
|
const errorMsg = await response.text();
|
||||||
let detail = errorMsg;
|
let detail = errorMsg;
|
||||||
@ -615,7 +589,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
splitZipPath = response.headers.get('X-Zip-Path');
|
splitZipPath = response.headers.get('X-Zip-Path');
|
||||||
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
|
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@ -648,7 +621,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.trim()) { /* Handle final buffer */ }
|
if (buffer.trim()) { /* Handle final buffer */ }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Splitting error:', e);
|
console.error('Splitting error:', e);
|
||||||
const ts = new Date().toLocaleTimeString();
|
const ts = new Date().toLocaleTimeString();
|
||||||
@ -660,7 +632,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadSplitButton.addEventListener('click', () => {
|
downloadSplitButton.addEventListener('click', () => {
|
||||||
if (splitZipPath) {
|
if (splitZipPath) {
|
||||||
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
|
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
|
||||||
@ -685,10 +656,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
alert("Download error: No file path available.");
|
alert("Download error: No file path available.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initial Setup Calls ---
|
// --- Initial Setup Calls ---
|
||||||
updateBundleSourceUI();
|
updateBundleSourceUI();
|
||||||
updateServerToggleUI();
|
fetchLocalUrlAndSetUI();
|
||||||
toggleFetchReferenceBundles();
|
toggleFetchReferenceBundles();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<!-- templates/validate_sample.html -->
|
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -7,14 +6,15 @@
|
|||||||
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
||||||
<div class="col-lg-6 mx-auto">
|
<div class="col-lg-6 mx-auto">
|
||||||
<p class="lead mb-4">
|
<p class="lead mb-4">
|
||||||
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
|
Validate a FHIR resource or bundle against a selected Implementation Guide.
|
||||||
</p>
|
</p>
|
||||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
</div>
|
||||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
</div>
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
|
||||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
<div id="spinner-overlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-flex align-items-center justify-content-center" style="z-index: 1050;">
|
||||||
</div>
|
<div class="text-center text-white">
|
||||||
----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
<div id="spinner-animation" style="width: 200px; height: 200px;"></div>
|
||||||
|
<p class="mt-3" id="spinner-message">Waiting, don't leave this page...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -22,8 +22,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Validation Form</div>
|
<div class="card-header">Validation Form</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="validationForm">
|
<form id="validationForm" method="POST" action="/api/validate">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||||
@ -46,6 +47,7 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||||
{{ form.version(class="form-control", id=form.version.id) }}
|
{{ form.version(class="form-control", id=form.version.id) }}
|
||||||
@ -53,145 +55,404 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- New Fields from ValidationForm -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
|
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
|
||||||
<div class="form-check">
|
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
||||||
{{ form.include_dependencies(class="form-check-input") }}
|
</div>
|
||||||
{{ form.include_dependencies.label(class="form-check-label") }}
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="fhir_version" class="form-label">FHIR Version</label>
|
||||||
|
{{ form.fhir_version(class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
|
<hr class="my-4">
|
||||||
{{ form.mode(class="form-select") }}
|
|
||||||
|
<h5>Validation Flags</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.do_native(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.hint_about_must_support(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.assume_valid_rest_references(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.show_times(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.allow_example_urls(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.check_ips_codes(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.allow_any_extensions(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.tx_routing(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
|
<hr class="my-4">
|
||||||
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
|
||||||
{% for error in form.sample_input.errors %}
|
<h5>Other Settings</h5>
|
||||||
<div class="text-danger">{{ error }}</div>
|
<div class="row g-3">
|
||||||
{% endfor %}
|
<div class="col-md-6">
|
||||||
|
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
|
||||||
|
{{ form.profiles(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
|
||||||
|
{{ form.extensions(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
|
||||||
|
{{ form.terminology_server(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
|
||||||
|
{{ form.snomed_ct_version(class="form-select") }}
|
||||||
|
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
|
||||||
|
Validate Resource
|
||||||
|
</button>
|
||||||
|
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
|
||||||
|
Clear Results
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-4" id="validationResult" style="display: none;">
|
<div id="validationResult" class="card mt-4" style="display: none;">
|
||||||
<div class="card-header">Validation Report</div>
|
<div class="card-header">Validation Results</div>
|
||||||
<div class="card-body" id="validationContent">
|
<div class="card-body" id="validationContent">
|
||||||
<!-- Results will be populated dynamically -->
|
<!-- Results will be displayed here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/lottie.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// --- Global Variables and Helper Functions ---
|
||||||
const packageSelect = document.getElementById('packageSelect');
|
let currentLottieAnimation = null;
|
||||||
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
let pollInterval = null;
|
||||||
const versionInput = document.getElementById('{{ form.version.id }}');
|
const spinnerOverlay = document.getElementById('spinner-overlay');
|
||||||
const validateButton = document.getElementById('validateButton');
|
const spinnerMessage = document.getElementById('spinner-message');
|
||||||
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
const spinnerContainer = document.getElementById('spinner-animation');
|
||||||
const validationResult = document.getElementById('validationResult');
|
const perPage = 10;
|
||||||
const validationContent = document.getElementById('validationContent');
|
|
||||||
|
|
||||||
// Debug DOM elements
|
// Function to load the Lottie animation based on theme
|
||||||
console.log('Package Select:', packageSelect ? 'Found' : 'Missing');
|
function loadLottieAnimation(theme) {
|
||||||
console.log('Package Name Input:', packageNameInput ? 'Found' : 'Missing');
|
if (currentLottieAnimation) {
|
||||||
console.log('Version Input:', versionInput ? 'Found' : 'Missing');
|
currentLottieAnimation.destroy();
|
||||||
|
currentLottieAnimation = null;
|
||||||
// Update package_name and version fields from dropdown
|
|
||||||
function updatePackageFields(select) {
|
|
||||||
const value = select.value;
|
|
||||||
console.log('Selected package:', value);
|
|
||||||
if (!packageNameInput || !versionInput) {
|
|
||||||
console.error('Input fields not found');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (value) {
|
const animationPath = (theme === 'dark')
|
||||||
const [name, version] = value.split('#');
|
? '{{ url_for('static', filename='animations/Validation abstract.json') }}'
|
||||||
if (name && version) {
|
: '{{ url_for('static', filename='animations/Validation abstract.json') }}';
|
||||||
packageNameInput.value = name;
|
|
||||||
versionInput.value = version;
|
try {
|
||||||
console.log('Updated fields:', { packageName: name, version });
|
currentLottieAnimation = lottie.loadAnimation({
|
||||||
|
container: spinnerContainer,
|
||||||
|
renderer: 'svg',
|
||||||
|
loop: true,
|
||||||
|
autoplay: true,
|
||||||
|
path: animationPath
|
||||||
|
});
|
||||||
|
} catch(lottieError) {
|
||||||
|
console.error("Error loading Lottie animation:", lottieError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show/hide the spinner with a custom message
|
||||||
|
function toggleSpinner(show, message = "Running validator...") {
|
||||||
|
if (show) {
|
||||||
|
spinnerMessage.textContent = message;
|
||||||
|
spinnerOverlay.classList.remove('d-none');
|
||||||
|
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||||
|
loadLottieAnimation(currentTheme);
|
||||||
|
} else {
|
||||||
|
spinnerOverlay.classList.add('d-none');
|
||||||
|
if (currentLottieAnimation) {
|
||||||
|
currentLottieAnimation.destroy();
|
||||||
|
currentLottieAnimation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Frontend logic starts here
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.getElementById('validationForm');
|
||||||
|
const validateButton = document.getElementById('validateButton');
|
||||||
|
const clearResultsButton = document.getElementById('clearResultsButton');
|
||||||
|
const validationResult = document.getElementById('validationResult');
|
||||||
|
const validationContent = document.getElementById('validationContent');
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
const packageSelect = document.getElementById('packageSelect');
|
||||||
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
||||||
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
||||||
|
const fhirResourceInput = document.getElementById('fhir_resource');
|
||||||
|
|
||||||
|
// Function to update package fields when a package is selected from the dropdown
|
||||||
|
function updatePackageFields(select) {
|
||||||
|
const value = select.value;
|
||||||
|
if (!packageNameInput || !versionInput) return;
|
||||||
|
if (value) {
|
||||||
|
const [name, version] = value.split('#');
|
||||||
|
if (name && version) {
|
||||||
|
packageNameInput.value = name;
|
||||||
|
versionInput.value = version;
|
||||||
|
} else {
|
||||||
|
packageNameInput.value = '';
|
||||||
|
versionInput.value = '';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Invalid package format:', value);
|
|
||||||
packageNameInput.value = '';
|
packageNameInput.value = '';
|
||||||
versionInput.value = '';
|
versionInput.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
packageNameInput.value = '';
|
|
||||||
versionInput.value = '';
|
|
||||||
console.log('Cleared fields: no package selected');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize dropdown based on form values
|
// Sanitize text to prevent XSS
|
||||||
if (packageNameInput && versionInput && packageNameInput.value && versionInput.value) {
|
function sanitizeText(text) {
|
||||||
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
const div = document.createElement('div');
|
||||||
if (packageSelect.querySelector(`option[value="${currentValue}"]`)) {
|
div.textContent = text;
|
||||||
packageSelect.value = currentValue;
|
return div.innerHTML;
|
||||||
console.log('Initialized dropdown with:', currentValue);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Debug dropdown options
|
// Tidy up the display by paginating results
|
||||||
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => opt.value));
|
function renderResultList(container, items, title, isWarnings = false) {
|
||||||
|
container.innerHTML = '';
|
||||||
// Attach event listener for package selection
|
if (items.length === 0) {
|
||||||
if (packageSelect) {
|
return;
|
||||||
packageSelect.addEventListener('change', function() {
|
|
||||||
updatePackageFields(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client-side JSON validation preview
|
|
||||||
if (sampleInput) {
|
|
||||||
sampleInput.addEventListener('input', function() {
|
|
||||||
try {
|
|
||||||
if (this.value.trim()) {
|
|
||||||
JSON.parse(this.value);
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
this.classList.add('is-valid');
|
|
||||||
} else {
|
|
||||||
this.classList.remove('is-valid', 'is-invalid');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.classList.remove('is-valid');
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check HAPI server status
|
const wrapper = document.createElement('div');
|
||||||
function checkHapiStatus() {
|
const listId = `list-${title.toLowerCase()}`;
|
||||||
return fetch('/fhir/metadata', { method: 'GET' })
|
|
||||||
.then(response => {
|
let initialItems = items.slice(0, perPage);
|
||||||
if (!response.ok) {
|
let remainingItems = items.slice(perPage);
|
||||||
throw new Error('HAPI server unavailable');
|
|
||||||
}
|
const titleElement = document.createElement('h5');
|
||||||
return true;
|
titleElement.textContent = title;
|
||||||
})
|
wrapper.appendChild(titleElement);
|
||||||
.catch(error => {
|
|
||||||
console.warn('HAPI status check failed:', error.message);
|
const ul = document.createElement('ul');
|
||||||
validationContent.innerHTML = '<div class="alert alert-warning">HAPI server unavailable; using local validation.</div>';
|
ul.className = 'list-unstyled';
|
||||||
return false;
|
initialItems.forEach(item => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
||||||
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
||||||
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
ul.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
wrapper.appendChild(ul);
|
||||||
|
|
||||||
// Sanitize text to prevent XSS
|
if (remainingItems.length > 0) {
|
||||||
function sanitizeText(text) {
|
const toggleButton = document.createElement('a');
|
||||||
const div = document.createElement('div');
|
toggleButton.href = '#';
|
||||||
div.textContent = text;
|
toggleButton.className = 'btn btn-sm btn-link text-decoration-none';
|
||||||
return div.innerHTML;
|
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
||||||
}
|
|
||||||
|
|
||||||
// Validate button handler
|
const remainingUl = document.createElement('ul');
|
||||||
if (validateButton) {
|
remainingUl.className = 'list-unstyled d-none';
|
||||||
validateButton.addEventListener('click', function() {
|
remainingItems.forEach(item => {
|
||||||
const packageName = packageNameInput.value;
|
const li = document.createElement('li');
|
||||||
const version = versionInput.value;
|
li.innerHTML = `
|
||||||
const sampleData = sampleInput.value.trim();
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
||||||
const includeDependencies = document.getElementById('{{ form.include_dependencies.id }}').checked;
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
||||||
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
remainingUl.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (remainingUl.classList.contains('d-none')) {
|
||||||
|
remainingUl.classList.remove('d-none');
|
||||||
|
toggleButton.textContent = 'Show Less';
|
||||||
|
} else {
|
||||||
|
remainingUl.classList.add('d-none');
|
||||||
|
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrapper.appendChild(toggleButton);
|
||||||
|
wrapper.appendChild(remainingUl);
|
||||||
|
}
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayFinalReport(data) {
|
||||||
|
validationContent.innerHTML = '';
|
||||||
|
let hasContent = false;
|
||||||
|
|
||||||
|
clearResultsButton.classList.remove('d-none');
|
||||||
|
|
||||||
|
if (data.file_paths) {
|
||||||
|
// Use the paths returned from the server to create download links
|
||||||
|
const jsonFileName = data.file_paths.json.split('/').pop();
|
||||||
|
const htmlFileName = data.file_paths.html.split('/').pop();
|
||||||
|
const downloadDiv = document.createElement('div');
|
||||||
|
downloadDiv.className = 'alert alert-info d-flex justify-content-between align-items-center';
|
||||||
|
downloadDiv.innerHTML = `
|
||||||
|
<span>Download Raw Reports:</span>
|
||||||
|
<div>
|
||||||
|
<a href="/download/${jsonFileName}" class="btn btn-sm btn-secondary me-2" download>Download JSON</a>
|
||||||
|
<a href="/download/${htmlFileName}" class="btn btn-sm btn-secondary" download>Download HTML</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
validationContent.appendChild(downloadDiv);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
hasContent = true;
|
||||||
|
const errorContainer = document.createElement('div');
|
||||||
|
renderResultList(errorContainer, data.errors, 'Errors');
|
||||||
|
validationContent.appendChild(errorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.warnings && data.warnings.length > 0) {
|
||||||
|
hasContent = true;
|
||||||
|
const warningContainer = document.createElement('div');
|
||||||
|
renderResultList(warningContainer, data.warnings, 'Warnings', true);
|
||||||
|
validationContent.appendChild(warningContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.results) {
|
||||||
|
hasContent = true;
|
||||||
|
const resultsDiv = document.createElement('div');
|
||||||
|
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
|
||||||
|
let index = 0;
|
||||||
|
for (const [resourceId, result] of Object.entries(data.results)) {
|
||||||
|
const collapseId = `result-collapse-${index++}`;
|
||||||
|
resultsDiv.innerHTML += `
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
||||||
|
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
|
||||||
|
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
<div id="${collapseId}" class="collapse">
|
||||||
|
<div class="card-body">
|
||||||
|
${result.details && result.details.length > 0 ? `
|
||||||
|
<ul>
|
||||||
|
${result.details.map(d => `
|
||||||
|
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
|
||||||
|
${sanitizeText(d.issue)}
|
||||||
|
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ul>
|
||||||
|
` : '<p>No additional details.</p>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
validationContent.appendChild(resultsDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.summary) {
|
||||||
|
const summaryDiv = document.createElement('div');
|
||||||
|
summaryDiv.className = 'alert alert-info';
|
||||||
|
summaryDiv.innerHTML = `
|
||||||
|
<h5>Summary</h5>
|
||||||
|
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
|
||||||
|
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
|
||||||
|
`;
|
||||||
|
validationContent.appendChild(summaryDiv);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContent && data.valid) {
|
||||||
|
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
||||||
|
target.classList.toggle('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||||
|
new bootstrap.Tooltip(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidationStatus(taskId) {
|
||||||
|
fetch(`/api/check-validation-status/${taskId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 202) {
|
||||||
|
console.log('Validation still in progress...');
|
||||||
|
return null;
|
||||||
|
} else if (response.status === 200) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
toggleSpinner(false);
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
toggleSpinner(false);
|
||||||
|
return response.json().then(errorData => {
|
||||||
|
throw new Error(errorData.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.status === 'complete') {
|
||||||
|
validationResult.style.display = 'block';
|
||||||
|
displayFinalReport(data.result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
toggleSpinner(false);
|
||||||
|
console.error('Validation failed during polling:', error);
|
||||||
|
validationResult.style.display = 'block';
|
||||||
|
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBlockingValidation(e) {
|
||||||
|
e.preventDefault(); // This is the crucial line to prevent default form submission
|
||||||
|
const packageName = document.getElementById('{{ form.package_name.id }}').value;
|
||||||
|
const version = document.getElementById('{{ form.version.id }}').value;
|
||||||
|
const sampleData = document.getElementById('fhir_resource').value.trim();
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!packageName || !version) {
|
if (!packageName || !version) {
|
||||||
validationResult.style.display = 'block';
|
validationResult.style.display = 'block';
|
||||||
@ -201,176 +462,126 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (!sampleData) {
|
if (!sampleData) {
|
||||||
validationResult.style.display = 'block';
|
validationResult.style.display = 'block';
|
||||||
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
|
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON or XML.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
|
||||||
JSON.parse(sampleData);
|
|
||||||
} catch (e) {
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
validationResult.style.display = 'block';
|
// Collect all form data including the new fields
|
||||||
validationContent.innerHTML = '<div class="text-center py-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Validating...</span></div></div>';
|
const data = {
|
||||||
|
package_name: packageName,
|
||||||
|
version: version,
|
||||||
|
fhir_resource: sampleData,
|
||||||
|
fhir_version: document.getElementById('fhir_version').value,
|
||||||
|
do_native: document.getElementById('do_native').checked,
|
||||||
|
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
|
||||||
|
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
|
||||||
|
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
|
||||||
|
show_times: document.getElementById('show_times').checked,
|
||||||
|
allow_example_urls: document.getElementById('allow_example_urls').checked,
|
||||||
|
check_ips_codes: document.getElementById('check_ips_codes').checked,
|
||||||
|
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
|
||||||
|
tx_routing: document.getElementById('tx_routing').checked,
|
||||||
|
snomed_ct_version: document.getElementById('snomed_ct_version').value,
|
||||||
|
profiles: document.getElementById('profiles').value,
|
||||||
|
extensions: document.getElementById('extensions').value,
|
||||||
|
terminology_server: document.getElementById('terminology_server').value
|
||||||
|
};
|
||||||
|
|
||||||
// Check HAPI status before validation
|
fetch('/api/validate', {
|
||||||
checkHapiStatus().then(() => {
|
method: 'POST',
|
||||||
console.log('Sending request to /api/validate-sample with:', {
|
headers: {
|
||||||
package_name: packageName,
|
'Content-Type': 'application/json',
|
||||||
version: version,
|
'X-CSRFToken': csrfToken
|
||||||
include_dependencies: includeDependencies,
|
},
|
||||||
sample_data_length: sampleData.length
|
body: JSON.stringify(data)
|
||||||
});
|
})
|
||||||
|
.then(response => {
|
||||||
fetch('/api/validate-sample', {
|
if (response.status === 202) {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': '{{ form.csrf_token._value() }}'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
package_name: packageName,
|
|
||||||
version: version,
|
|
||||||
include_dependencies: includeDependencies,
|
|
||||||
sample_data: sampleData
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
console.log('Server response status:', response.status, response.statusText);
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.text().then(text => {
|
|
||||||
console.error('Server response text:', text.substring(0, 200) + (text.length > 200 ? '...' : ''));
|
|
||||||
throw new Error(`HTTP error ${response.status}: ${text.substring(0, 100)}...`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
} else {
|
||||||
.then(data => {
|
toggleSpinner(false);
|
||||||
console.log('Validation response:', data);
|
return response.json().then(errorData => {
|
||||||
validationContent.innerHTML = '';
|
throw new Error(errorData.message);
|
||||||
let hasContent = false;
|
|
||||||
|
|
||||||
// Display profile information
|
|
||||||
if (data.results) {
|
|
||||||
const profileDiv = document.createElement('div');
|
|
||||||
profileDiv.className = 'mb-3';
|
|
||||||
const profiles = new Set();
|
|
||||||
Object.values(data.results).forEach(result => {
|
|
||||||
if (result.profile) profiles.add(result.profile);
|
|
||||||
});
|
|
||||||
if (profiles.size > 0) {
|
|
||||||
profileDiv.innerHTML = `
|
|
||||||
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
|
|
||||||
<ul>
|
|
||||||
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
validationContent.appendChild(profileDiv);
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display errors
|
|
||||||
if (data.errors && data.errors.length > 0) {
|
|
||||||
hasContent = true;
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.className = 'alert alert-danger';
|
|
||||||
errorDiv.innerHTML = `
|
|
||||||
<h5>Errors</h5>
|
|
||||||
<ul>
|
|
||||||
${data.errors.map(e => `<li>${sanitizeText(typeof e === 'string' ? e : e.message)}</li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
validationContent.appendChild(errorDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display warnings
|
|
||||||
if (data.warnings && data.warnings.length > 0) {
|
|
||||||
hasContent = true;
|
|
||||||
const warningDiv = document.createElement('div');
|
|
||||||
warningDiv.className = 'alert alert-warning';
|
|
||||||
warningDiv.innerHTML = `
|
|
||||||
<h5>Warnings</h5>
|
|
||||||
<ul>
|
|
||||||
${data.warnings.map(w => {
|
|
||||||
const isMustSupport = w.includes('Must Support');
|
|
||||||
return `<li>${sanitizeText(typeof w === 'string' ? w : w.message)}${isMustSupport ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}</li>`;
|
|
||||||
}).join('')}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
validationContent.appendChild(warningDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detailed results with collapsible sections
|
|
||||||
if (data.results) {
|
|
||||||
hasContent = true;
|
|
||||||
const resultsDiv = document.createElement('div');
|
|
||||||
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
|
|
||||||
let index = 0;
|
|
||||||
for (const [resourceId, result] of Object.entries(data.results)) {
|
|
||||||
const collapseId = `result-collapse-${index++}`;
|
|
||||||
resultsDiv.innerHTML += `
|
|
||||||
<div class="card mb-2">
|
|
||||||
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
|
||||||
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
|
|
||||||
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
|
||||||
</div>
|
|
||||||
<div id="${collapseId}" class="collapse">
|
|
||||||
<div class="card-body">
|
|
||||||
${result.details && result.details.length > 0 ? `
|
|
||||||
<ul>
|
|
||||||
${result.details.map(d => `
|
|
||||||
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
|
|
||||||
${sanitizeText(d.issue)}
|
|
||||||
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
|
|
||||||
</li>
|
|
||||||
`).join('')}
|
|
||||||
</ul>
|
|
||||||
` : '<p>No additional details.</p>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
validationContent.appendChild(resultsDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
const summaryDiv = document.createElement('div');
|
|
||||||
summaryDiv.className = 'alert alert-info';
|
|
||||||
summaryDiv.innerHTML = `
|
|
||||||
<h5>Summary</h5>
|
|
||||||
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
|
|
||||||
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
|
|
||||||
`;
|
|
||||||
validationContent.appendChild(summaryDiv);
|
|
||||||
hasContent = true;
|
|
||||||
|
|
||||||
if (!hasContent && data.valid) {
|
|
||||||
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Bootstrap collapse and tooltips
|
|
||||||
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
|
||||||
target.classList.toggle('show');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
}
|
||||||
new bootstrap.Tooltip(el);
|
})
|
||||||
});
|
.then(data => {
|
||||||
})
|
if (data.status === 'accepted' && data.task_id) {
|
||||||
.catch(error => {
|
pollInterval = setInterval(() => checkValidationStatus(data.task_id), 3000);
|
||||||
console.error('Validation error:', error);
|
} else {
|
||||||
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
|
toggleSpinner(false);
|
||||||
});
|
validationResult.style.display = 'block';
|
||||||
|
validationContent.innerHTML = `<div class="alert alert-danger">Server response format unexpected.</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
toggleSpinner(false);
|
||||||
|
console.error('Validation failed:', error);
|
||||||
|
validationResult.style.display = 'block';
|
||||||
|
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
clearResultsButton.addEventListener('click', function() {
|
||||||
|
fetch('/api/clear-validation-results', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
validationResult.style.display = 'none';
|
||||||
|
clearResultsButton.classList.add('d-none');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to clear results:', data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Network error while clearing results:', error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
if (packageSelect) {
|
||||||
|
packageSelect.addEventListener('change', function() {
|
||||||
|
updatePackageFields(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateButton) {
|
||||||
|
validateButton.addEventListener('click', function(e) {
|
||||||
|
runBlockingValidation(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageNameInput.value && versionInput.value) {
|
||||||
|
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
||||||
|
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
|
||||||
|
if (optionExists) {
|
||||||
|
packageSelect.value = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fhirResourceInput) {
|
||||||
|
fhirResourceInput.addEventListener('input', function() {
|
||||||
|
try {
|
||||||
|
if (this.value.trim()) {
|
||||||
|
JSON.parse(this.value);
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
this.classList.add('is-valid');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('is-valid', 'is-invalid');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.classList.remove('is-valid');
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user