Compare commits

..

No commits in common. "main" and "V1.1-Pre-release" have entirely different histories.

153 changed files with 2209 additions and 123453 deletions

View File

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,23 +0,0 @@
name: str()
home: str()
version: str()
apiVersion: str()
appVersion: any(str(), num(), required=False)
type: str()
dependencies: any(required=False)
description: str()
keywords: list(str(), required=False)
sources: list(str(), required=False)
maintainers: list(include('maintainer'), required=False)
icon: str(required=False)
engine: str(required=False)
condition: str(required=False)
tags: str(required=False)
deprecated: bool(required=False)
kubeVersion: str(required=False)
annotations: map(str(), str(), required=False)
---
maintainer:
name: str()
email: str(required=False)
url: str(required=False)

View File

@ -1,15 +0,0 @@
debug: true
remote: origin
chart-yaml-schema: .github/ct/chart-schema.yaml
validate-maintainers: false
validate-chart-schema: true
validate-yaml: true
check-version-increment: true
chart-dirs:
- charts
helm-extra-args: --timeout 300s
upgrade: true
skip-missing-values: true
release-label: release
release-name-template: "helm-v{{ .Version }}"
target-branch: master

View File

@ -1,84 +0,0 @@
name: Build Container Images
on:
push:
tags:
- "image/v*"
paths-ignore:
- "charts/**"
pull_request:
branches: [master]
paths-ignore:
- "charts/**"
env:
IMAGES: docker.io/hapiproject/hapi
PLATFORMS: linux/amd64,linux/arm64/v8
jobs:
build:
name: Build
runs-on: ubuntu-22.04
steps:
- name: Container meta for default (distroless) image
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGES }}
tags: |
type=match,pattern=image/(.*),group=1,enable=${{github.event_name != 'pull_request'}}
- name: Container meta for tomcat image
id: docker_tomcat_meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGES }}
tags: |
type=match,pattern=image/(.*),group=1,enable=${{github.event_name != 'pull_request'}}
flavor: |
suffix=-tomcat,onlatest=true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build and push default (distroless) image
id: docker_build
uses: docker/build-push-action@v5
with:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
platforms: ${{ env.PLATFORMS }}
target: default
- name: Build and push tomcat image
id: docker_build_tomcat
uses: docker/build-push-action@v5
with:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_tomcat_meta.outputs.tags }}
labels: ${{ steps.docker_tomcat_meta.outputs.labels }}
platforms: ${{ env.PLATFORMS }}
target: tomcat

View File

@ -1,41 +0,0 @@
name: Release Charts
on:
push:
branches:
- main
paths:
- "charts/**"
jobs:
release:
runs-on: ubuntu-22.04
steps:
- name: Add workspace as safe directory
run: |
git config --global --add safe.directory /__w/FHIRFLARE-IG-Toolkit/FHIRFLARE-IG-Toolkit
- name: Checkout
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Update dependencies
run: find charts/ ! -path charts/ -maxdepth 1 -type d -exec helm dependency update {} \;
- name: Add Helm Repositories
run: |
helm repo add hapifhir https://hapifhir.github.io/hapi-fhir-jpaserver-starter/
helm repo update
- name: Run chart-releaser
uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0
with:
config: .github/ct/config.yaml
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -1,73 +0,0 @@
name: Lint and Test Charts
on:
pull_request:
branches:
- master
paths:
- "charts/**"
jobs:
lint:
runs-on: ubuntu-22.04
container: quay.io/helmpack/chart-testing:v3.11.0@sha256:f2fd21d30b64411105c7eafb1862783236a219d29f2292219a09fe94ca78ad2a
steps:
- name: Install helm-docs
working-directory: /tmp
env:
HELM_DOCS_URL: https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_x86_64.tar.gz
run: |
curl -LSs $HELM_DOCS_URL | tar xz && \
mv ./helm-docs /usr/local/bin/helm-docs && \
chmod +x /usr/local/bin/helm-docs && \
helm-docs --version
- name: Add workspace as safe directory
run: |
git config --global --add safe.directory /__w/hapi-fhir-jpaserver-starter/hapi-fhir-jpaserver-starter
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- name: Check if documentation is up-to-date
run: helm-docs && git diff --exit-code HEAD
- name: Run chart-testing (lint)
run: ct lint --config .github/ct/config.yaml
test:
runs-on: ubuntu-22.04
strategy:
matrix:
k8s-version: [1.30.8, 1.31.4, 1.32.0]
needs:
- lint
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- name: Set up chart-testing
uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1
- name: Run chart-testing (list-changed)
id: list-changed
run: |
changed=$(ct list-changed --config .github/ct/config.yaml)
if [[ -n "$changed" ]]; then
echo "::set-output name=changed::true"
fi
- name: Create k8s Kind Cluster
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
if: ${{ steps.list-changed.outputs.changed == 'true' }}
with:
cluster_name: kind-cluster-k8s-${{ matrix.k8s-version }}
node_image: kindest/node:v${{ matrix.k8s-version }}
- name: Run chart-testing (install)
run: ct install --config .github/ct/config.yaml
if: ${{ steps.list-changed.outputs.changed == 'true' }}

View File

@ -1,58 +0,0 @@
# This workflow builds and pushes a multi-architecture Docker image to GitHub Container Registry (ghcr.io).
#
# The Docker meta step is required because GitHub repository names can contain uppercase letters, but Docker image tags must be lowercase.
# The docker/metadata-action@v5 normalizes the repository name to lowercase, ensuring the build and push steps use a valid image tag.
#
# This workflow builds for both AMD64 and ARM64 architectures using Docker Buildx and QEMU emulation.
name: Build and Push Docker image
on:
push:
branches:
- main
- '*' # This will run the workflow on any branch
workflow_dispatch: # This enables manual triggering
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Set normalized image name
run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "IMAGE_NAME=$(echo ${{ steps.meta.outputs.tags }} | sed 's/:main/:latest/')" >> $GITHUB_ENV
else
echo "IMAGE_NAME=${{ steps.meta.outputs.tags }}" >> $GITHUB_ENV
fi
- name: Build and push multi-architecture Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE_NAME }}

View File

@ -1,40 +0,0 @@
name: Build and Deploy Jekyll site
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: website
steps:
- uses: actions/checkout@v3
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: website
- name: Install dependencies
run: |
bundle install
- name: Build site
run: bundle exec jekyll build
- name: Deploy to gh_pages branch
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./website/_site
publish_branch: gh-pages
keep_files: true

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
/logs/
/.pydevproject
/__pycache__/
/myenv/
/tmp/
/Gemfile.lock
/_site/

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>FHIRFLARE-IG-Toolkit</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
<classpathentry kind="src" path=""/>
<classpathentry kind="output" path=""/>
</classpath>

View File

@ -1 +0,0 @@
org.eclipse.wst.jsdt.launching.JRE_CONTAINER

View File

@ -1 +0,0 @@
Global

View File

@ -1,378 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM --- Configuration ---
set REPO_URL_HAPI=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
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 CONFIG_FILE=application.yaml
REM --- Define Paths ---
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
set DEST_CONFIG_PATH=%CLONE_DIR_HAPI%\target\classes\%CONFIG_FILE%
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
SET "APP_MODE=" REM Clear the variable first
echo.
echo Select Installation Mode:
echo 1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet 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)
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 (
SET APP_MODE=standalone
goto :GetCustomUrl
)
IF ERRORLEVEL 1 (
SET APP_MODE=lite
goto :ModeSet
)
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
echo Invalid input. Please try again.
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
IF "%APP_MODE%"=="" (
echo Invalid choice detected after checks. Exiting.
goto :eof
)
echo Selected Mode: %APP_MODE%
echo Server Type: %SERVER_TYPE%
echo.
REM === END MODIFICATION ===
REM === Conditionally Execute Server Setup ===
IF "%SERVER_TYPE%"=="hapi" (
echo Running Hapi server setup...
echo.
REM --- Step 0: Clean up previous clone (optional) ---
echo Checking for existing directory: %CLONE_DIR_HAPI%
if exist "%CLONE_DIR_HAPI%" (
echo Found existing directory, removing it...
rmdir /s /q "%CLONE_DIR_HAPI%"
if errorlevel 1 (
echo ERROR: Failed to remove existing directory: %CLONE_DIR_HAPI%
goto :error
)
echo Existing directory removed.
) else (
echo Directory does not exist, proceeding with clone.
)
echo.
REM --- Step 1: Clone the HAPI FHIR server repository ---
echo Cloning repository: %REPO_URL_HAPI% into %CLONE_DIR_HAPI%...
git clone "%REPO_URL_HAPI%" "%CLONE_DIR_HAPI%"
if errorlevel 1 (
echo ERROR: Failed to clone repository. Check Git 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_HAPI%...
cd "%CLONE_DIR_HAPI%"
if errorlevel 1 (
echo ERROR: Failed to change directory to %CLONE_DIR_HAPI%.
goto :error
)
echo Current directory: %CD%
echo.
REM --- Step 3: Build the HAPI server using Maven ---
echo ===> "Starting Maven build (Step 3)...""
cmd /c "mvn clean package -DskipTests=true -Pboot"
echo ===> Maven command finished. Checking error level...
if errorlevel 1 (
echo ERROR: Maven build failed or cmd /c failed
cd ..
goto :error
)
echo Maven build completed successfully. ErrorLevel: %errorlevel%
echo.
REM --- Step 4: Copy the configuration file ---
echo ===> "Starting file copy (Step 4)..."
echo Copying configuration file...
echo Source: %SOURCE_CONFIG_PATH%
echo Destination: target\classes\%CONFIG_FILE%
xcopy "%SOURCE_CONFIG_PATH%" "target\classes\" /Y /I
echo ===> xcopy command finished. Checking error level...
if errorlevel 1 (
echo WARNING: Failed to copy configuration file. Check if the source file exists.
echo The script will continue, but the server might use default configuration.
) else (
echo Configuration file copied successfully. ErrorLevel: %errorlevel%
)
echo.
REM --- Step 5: Navigate back to the parent directory ---
echo ===> "Changing directory back (Step 5)..."
cd ..
if errorlevel 1 (
echo ERROR: Failed to change back to the parent directory. ErrorLevel: %errorlevel%
goto :error
)
echo Current directory: %CD%
echo.
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo Running FHIR Candle server setup...
echo.
REM --- Step 0: Clean up previous clone (optional) ---
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.
)
echo.
REM --- 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 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.
)
REM === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL ===
echo Updating docker-compose.yml with APP_MODE=%APP_MODE% and HAPI_FHIR_URL...
(
echo version: '3.8'
echo services:
echo fhirflare:
echo build:
echo context: .
IF "%SERVER_TYPE%"=="hapi" (
echo dockerfile: Dockerfile.hapi
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo dockerfile: Dockerfile.candle
) ELSE (
echo dockerfile: Dockerfile.lite
)
echo ports:
IF "%SERVER_TYPE%"=="candle" (
echo - "5000:5000"
echo - "5001:5826"
) ELSE (
echo - "5000:5000"
echo - "8080:8080"
)
echo volumes:
echo - ./instance:/app/instance
echo - ./static/uploads:/app/static/uploads
IF "%SERVER_TYPE%"=="hapi" (
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
)
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 - FLASK_APP=app.py
echo - FLASK_ENV=development
echo - NODE_PATH=/usr/lib/node_modules
echo - APP_MODE=%APP_MODE%
echo - APP_BASE_URL=http://localhost:5000
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
) > docker-compose.yml.tmp
REM Check if docker-compose.yml.tmp was created successfully
if not exist docker-compose.yml.tmp (
echo ERROR: Failed to create temporary docker-compose file.
goto :error
)
REM Replace the original docker-compose.yml
del docker-compose.yml /Q > nul 2>&1
ren docker-compose.yml.tmp docker-compose.yml
echo docker-compose.yml updated successfully.
echo.
REM --- Step 6: Build Docker images ---
echo ===> Starting Docker build (Step 6)...
docker-compose build --no-cache
if errorlevel 1 (
echo ERROR: Docker Compose build failed. Check Docker installation and docker-compose.yml file. ErrorLevel: %errorlevel%
goto :error
)
echo Docker images built successfully. ErrorLevel: %errorlevel%
echo.
REM --- Step 7: Start Docker containers ---
echo ===> Starting Docker containers (Step 7)...
docker-compose up -d
if errorlevel 1 (
echo ERROR: Docker Compose up failed. Check Docker installation and container configurations. ErrorLevel: %errorlevel%
goto :error
)
echo Docker containers started successfully. ErrorLevel: %errorlevel%
echo.
echo ====================================
echo Script finished successfully! (Mode: %APP_MODE%)
echo ====================================
goto :eof
:error
echo ------------------------------------
echo An error occurred. Script aborted.
echo ------------------------------------
pause
exit /b 1
:eof
echo Script execution finished.
pause

View File

@ -1,26 +0,0 @@
Docker Commands.MD
<HAPI-server.>
to pull and clone:
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
to build:
mvn clean package -DskipTests=true -Pboot
to run:
java -jar target/ROOT.war
<rest-of-the-app:>
docker-compose build --no-cache
docker-compose up -d
<useful-stuff:>
cp <CONTAINERID>:/app/PATH/Filename.ext . - . copies to the root folder you ran it from
docker exec -it <CONTAINERID> bash - to get a bash - session in the container -

View File

@ -1,73 +1,23 @@
# Base image with Python and Java FROM python:3.9-slim
FROM tomcat:10.1-jdk17
WORKDIR /app
# 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 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/* && 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
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 . COPY requirements.txt .
# Install requirements (including Pydantic - check version compatibility if needed)
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY app.py . COPY app.py .
COPY services.py . COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/ COPY templates/ templates/
COPY static/ static/ COPY static/ static/
COPY tests/ tests/ COPY tests/ tests/
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable # Ensure /tmp is writable as a fallback
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs RUN mkdir -p /tmp && chmod 777 /tmp
# Copy pre-built HAPI WAR and configuration EXPOSE 5000
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/ ENV FLASK_APP=app.py
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/ ENV FLASK_ENV=development
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml CMD ["flask", "run", "--host=0.0.0.0"]
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/
# ADDED: 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 8080 5001
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -1,64 +0,0 @@
# 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"]

View File

@ -1,66 +0,0 @@
# 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"]

View File

@ -1,60 +0,0 @@
# 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"]

899
README.md
View File

@ -1,704 +1,235 @@
# FHIRFLARE IG Toolkit # FHIRFLARE IG Toolkit
![FHIRFLARE Logo](static/FHIRFLARE.png)
## Overview ## Overview
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) 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 simplify the management, processing, and deployment of FHIR Implementation Guides (IGs). It allows users to import IG packages, process them to extract resource types and profiles, and push them to a FHIR server. The application features a user-friendly interface with a live console for real-time feedback during operations like pushing IGs to a FHIR server.
The application can run in four modes:
* 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)
This toolkit offers two primary installation modes to suit different needs:
* **Standalone Version - Hapi / Candle:**
* 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.
* 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.
* 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:**
* 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.
* Resource validation relies solely on local checks against downloaded StructureDefinitions, which may be less comprehensive than HAPI FHIR's validation (e.g., for terminology bindings or complex invariants).
* **Does not require Git or Maven** for setup if using the `.bat` script or running the pre-built Docker image.
* Ideal for users who primarily want to use the IG management, processing, and FSH conversion features, or who will always connect to existing external FHIR servers.
## Features ## Features
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking). - **Import IGs**: Download FHIR IG packages and their dependencies from a package registry.
* **Enhanced Package Search and Import:** - **Manage IGs**: View, process, and delete downloaded IGs, with duplicate detection.
* Interactive page (`/search-and-import`) to search for FHIR IG packages from configured registries. - **Process IGs**: Extract resource types, profiles, must-support elements, and examples from IGs.
* Displays package details, version history, dependencies, and dependents. - **Push IGs**: Upload IG resources to a FHIR server with real-time console output.
* Utilizes a local database cache (`CachedPackage`) for faster subsequent searches. - **API Support**: Provides RESTful API endpoints for importing and pushing IGs.
* Background task to refresh the package cache from registries (`/api/refresh-cache-task`). - **Live Console**: Displays real-time logs during push operations.
* Direct import from search results.
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature). *Note: Lite version uses local SD checks only.*
* **Push IGs:** Upload IG resources (and optionally dependencies) to a target FHIR server. Features include:
* Real-time console output.
* Authentication support (Bearer Token).
* Filtering by resource type or specific files to skip.
* Semantic comparison to skip uploading identical resources (override with **Force Upload** option).
* Correct handling of canonical resources (searching by URL/version before deciding POST/PUT).
* Dry run mode for simulation.
* Verbose logging option.
* **Upload Test Data:** Upload complex sets of test data (individual JSON/XML files or ZIP archives) to a target FHIR server. Features include:
* Robust parsing of JSON and XML (using `fhir.resources` library when available).
* Automatic dependency analysis based on resource references within the uploaded set.
* Topological sorting to ensure resources are uploaded in the correct order.
* Cycle detection in dependencies.
* Choice of individual resource uploads or a single transaction bundle.
* **Optional Pre-Upload Validation:** Validate resources against a selected profile package before uploading.
* **Optional Conditional Uploads (Individual Mode):** Check resource existence (GET) and use conditional `If-Match` headers for updates (PUT) or create resources (PUT/POST). Falls back to simple PUT if unchecked.
* Configurable error handling (stop on first error or continue).
* Authentication support (Bearer Token).
* Streaming progress log via the UI.
* Handles large numbers of files using a custom form parser.
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI (configurable).
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
* **Retrieve and Split Bundles:**
* Retrieve specified resource types as bundles from a FHIR server.
* Optionally fetch referenced resources, either individually or as full bundles for each referenced type.
* Split uploaded ZIP files containing bundles into individual resource JSON files.
* Download retrieved/split resources as a ZIP archive.
* Streaming progress log via the UI for retrieval operations.
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (simple GET/POST/PUT/DELETE) and "FHIR UI Operations" (Swagger-like interface based on CapabilityStatement). *Note: Lite version requires custom server URLs.*
* **HAPI FHIR Configuration (Standalone Mode):**
* A dedicated page (`/config-hapi`) to view and edit the `application.yaml` configuration for the embedded HAPI FHIR server.
* Allows modification of HAPI FHIR properties directly from the UI.
* Option to restart the HAPI FHIR server (Tomcat) to apply changes.
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, uploading test data, and retrieving/splitting bundles.
* **Live Console:** Real-time logs for push, validation, upload test data, FSH conversion, and bundle retrieval operations.
* **Configurable Behavior:** Control validation modes, display options via `app.config`.
* **Theming:** Supports light and dark modes.
## Technology Stack ## Technology Stack
* Python 3.12+, Flask 2.3.3, Flask-SQLAlchemy 3.0.5, Flask-WTF 1.2.1 The FHIRFLARE IG Toolkit is built using the following technologies:
* Jinja2, Bootstrap 5.3.3, JavaScript (ES6), Lottie-Web 5.12.2
* SQLite - **Python 3.9+**: Core programming language for the backend.
* Docker, Docker Compose, Supervisor - **Flask 2.0+**: Lightweight web framework for building the application.
* Node.js 18+ (for GoFSH/SUSHI), GoFSH, SUSHI - **Flask-SQLAlchemy**: ORM for managing the SQLite database.
* HAPI FHIR (Standalone version only) - **Flask-WTF**: Handles form creation and CSRF protection.
* Requests 2.31.0, Tarfile, Logging, Werkzeug - **Jinja2**: Templating engine for rendering HTML pages.
* fhir.resources (optional, for robust XML parsing) - **Bootstrap 5**: Frontend framework for responsive UI design.
- **JavaScript (ES6)**: Client-side scripting for interactive features like the live console.
- **SQLite**: Lightweight database for storing processed IG metadata.
- **Docker**: Containerization for consistent deployment.
- **Requests**: Python library for making HTTP requests to FHIR servers.
- **Tarfile**: Python library for handling `.tgz` package files.
- **Logging**: Python's built-in logging for debugging and monitoring.
## Prerequisites ## Prerequisites
* **Docker:** Required for containerized deployment (both versions). - **Python 3.9+**: Ensure Python is installed on your system.
* **Git & Maven:** Required **only** for building the **Standalone** version from source using the `.bat` script or manual steps. Not required for the Lite version build or for running pre-built Docker Hub images. - **Docker**: Required for containerized deployment.
* **Windows:** Required if using the `.bat` scripts. - **pip**: Python package manager for installing dependencies.
## Setup Instructions ## Setup Instructions
### Running Pre-built Images (General Users) 1. **Clone the Repository**:
```bash
git clone <repository-url>
cd fhirflare-ig-toolkit
```
This is the easiest way to get started without needing Git or Maven. Choose the version you need: 2. **Install Dependencies**:
Create a virtual environment and install the required packages:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
```
**Lite Version (No local HAPI FHIR):** 3. **Set Environment Variables**:
Set the `FLASK_SECRET_KEY` and `API_KEY` environment variables for security:
```bash
export FLASK_SECRET_KEY='your-secure-secret-key'
export API_KEY='your-api-key'
```
4. **Run the Application Locally**:
Start the Flask development server:
```bash
flask run
```
The application will be available at `http://localhost:5000`.
5. **Run with Docker**:
Build and run the application using Docker:
```bash
docker build -t flare-fhir-ig-toolkit .
docker run -p 5000:5000 -e FLASK_SECRET_KEY='your-secure-secret-key' -e API_KEY='your-api-key' -v $(pwd)/instance:/app/instance flare-fhir-ig-toolkit
```
Access the application at `http://localhost:5000`.
## Usage
1. **Import an IG**:
- Navigate to the "Import IG" tab.
- Enter a package name (e.g., `hl7.fhir.us.core`) and version (e.g., `3.1.1`).
- Click "Fetch & Download IG" to download the package and its dependencies.
2. **Manage IGs**:
- Go to the "Manage FHIR Packages" tab to view downloaded IGs.
- Process, delete, or view details of IGs. Duplicates are highlighted for resolution.
3. **Push IGs to a FHIR Server**:
- Navigate to the "Push IGs" tab.
- Select a package, enter a FHIR server URL (e.g., `http://hapi.fhir.org/baseR4`), and choose whether to include dependencies.
- Click "Push to FHIR Server" to upload resources, with progress shown in the live console.
4. **API Usage**:
- **Import IG**: `POST /api/import-ig`
```bash
curl -X POST http://localhost:5000/api/import-ig \
-H "Content-Type: application/json" \
-d '{"package_name": "hl7.fhir.us.core", "version": "3.1.1", "api_key": "your-api-key"}'
```
- **Push IG**: `POST /api/push-ig`
```bash
curl -X POST http://localhost:5000/api/push-ig \
-H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-d '{"package_name": "hl7.fhir.us.core", "version": "3.1.1", "fhir_server_url": "http://hapi.fhir.org/baseR4", "include_dependencies": true, "api_key": "your-api-key"}'
```
## Testing
The project includes a comprehensive test suite to ensure the reliability of the application. Tests cover the UI, API endpoints, database operations, file handling, and security features like CSRF protection.
### Test Prerequisites
- **pytest**: For running the tests.
- **unittest-mock**: For mocking dependencies in tests.
Install the test dependencies:
```bash ```bash
# Pull the latest Lite image pip install pytest unittest-mock
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest ```
# Run the Lite version (maps port 5000 for the UI) ### Running Tests
# You'll need to create local directories for persistent data first:
# mkdir instance logs static static/uploads instance/hapi-h2-data 1. **Navigate to the Project Root**:
docker run -d \ ```bash
-p 5000:5000 \ cd /path/to/fhirflare-ig-toolkit
-v ./instance:/app/instance \ ```
-v ./static/uploads:/app/static/uploads \
-v ./instance/hapi-h2-data:/app/h2-data \ 2. **Run the Tests**:
-v ./logs:/app/logs \ Run the test suite using `pytest`:
--name fhirflare-lite \ ```bash
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest pytest tests/test_app.py -v
Standalone Version (Includes local HAPI FHIR): ```
- The `-v` flag provides verbose output, showing the status of each test.
Bash - Alternatively, run from the `tests/` directory with:
```bash
# Pull the latest Standalone image cd tests/
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest pytest test_app.py -v
```
# Run the Standalone version (maps ports 5000 and 8080)
# You'll need to create local directories for persistent data first: ### Test Coverage
# mkdir instance logs static static/uploads instance/hapi-h2-data
docker run -d \ The test suite includes 27 test cases covering the following areas:
-p 5000:5000 \
-p 8080:8080 \ - **UI Pages**:
-v ./instance:/app/instance \ - Homepage (`/`): Rendering and content.
-v ./static/uploads:/app/static/uploads \ - Import IG page (`/import-ig`): Form rendering and submission (success, failure, invalid input).
-v ./instance/hapi-h2-data:/app/h2-data \ - Manage IGs page (`/view-igs`): Rendering with and without packages.
-v ./logs:/app/logs \ - Push IGs page (`/push-igs`): Rendering and live console.
--name fhirflare-standalone \ - View Processed IG page (`/view-ig/<id>`): Rendering processed IG details.
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
Building from Source (Developers) - **API Endpoints**:
Using Windows .bat Scripts (Standalone Version Only): - `POST /api/import-ig`: Success, invalid API key, missing parameters.
- `POST /api/push-ig`: Success, invalid API key, package not found.
First Time Setup: - `GET /get-structure`: Fetching structure definitions (success, not found).
- `GET /get-example`: Fetching example content (success, invalid path).
Run Build and Run for first time.bat:
- **Database Operations**:
Code snippet - Processing IGs: Storing processed IG data in the database.
- Unloading IGs: Removing processed IG records.
cd "<project folder>" - Viewing processed IGs: Retrieving and displaying processed IG data.
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
copy .\\hapi-fhir-Setup\\target\\classes\\application.yaml .\\hapi-fhir-jpaserver\\target\\classes\\application.yaml - **File Operations**:
mvn clean package -DskipTests=true -Pboot - Processing IG packages: Extracting data from `.tgz` files.
docker-compose build --no-cache - Deleting IG packages: Removing `.tgz` files from the filesystem.
docker-compose up -d
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers. - **Security**:
- Secret Key: CSRF protection for form submissions.
Subsequent Runs: - Flash Messages: Session integrity for flash messages.
Run Run.bat: ### Example Test Output
Code snippet A successful test run will look like this:
```
cd "<project folder>" ================================================================ test session starts =================================================================
docker-compose up -d platform linux -- Python 3.9.22, pytest-8.3.5, pluggy-1.5.0 -- /usr/local/bin/python3.9
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080). cachedir: .pytest_cache
rootdir: /app/tests
Access the Application: collected 27 items
Flask UI: http://localhost:5000 test_app.py::TestFHIRFlareIGToolkit::test_homepage PASSED [ 3%]
HAPI FHIR server: http://localhost:8080 test_app.py::TestFHIRFlareIGToolkit::test_import_ig_page PASSED [ 7%]
Manual Setup (Linux/MacOS/Windows): test_app.py::TestFHIRFlareIGToolkit::test_import_ig_success PASSED [ 11%]
test_app.py::TestFHIRFlareIGToolkit::test_import_ig_failure PASSED [ 14%]
Preparation (Standalone Version Only): test_app.py::TestFHIRFlareIGToolkit::test_import_ig_invalid_input PASSED [ 18%]
test_app.py::TestFHIRFlareIGToolkit::test_view_igs_no_packages PASSED [ 22%]
Bash test_app.py::TestFHIRFlareIGToolkit::test_view_igs_with_packages PASSED [ 25%]
test_app.py::TestFHIRFlareIGToolkit::test_process_ig_success PASSED [ 29%]
cd <project folder> test_app.py::TestFHIRFlareIGToolkit::test_process_ig_invalid_file PASSED [ 33%]
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver test_app.py::TestFHIRFlareIGToolkit::test_delete_ig_success PASSED [ 37%]
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml test_app.py::TestFHIRFlareIGToolkit::test_delete_ig_file_not_found PASSED [ 40%]
Build: test_app.py::TestFHIRFlareIGToolkit::test_unload_ig_success PASSED [ 44%]
test_app.py::TestFHIRFlareIGToolkit::test_unload_ig_invalid_id PASSED [ 48%]
Bash test_app.py::TestFHIRFlareIGToolkit::test_view_processed_ig PASSED [ 51%]
test_app.py::TestFHIRFlareIGToolkit::test_push_igs_page PASSED [ 55%]
# Build HAPI FHIR (Standalone Version Only) test_app.py::TestFHIRFlareIGToolkit::test_api_import_ig_success PASSED [ 59%]
mvn clean package -DskipTests=true -Pboot test_app.py::TestFHIRFlareIGToolkit::test_api_import_ig_invalid_api_key PASSED [ 62%]
test_app.py::TestFHIRFlareIGToolkit::test_api_import_ig_missing_params PASSED [ 66%]
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version) test_app.py::TestFHIRFlareIGToolkit::test_api_push_ig_success PASSED [ 70%]
docker-compose build --no-cache test_app.py::TestFHIRFlareIGToolkit::test_api_push_ig_invalid_api_key PASSED [ 74%]
Run: test_app.py::TestFHIRFlareIGToolkit::test_api_push_ig_package_not_found PASSED [ 77%]
test_app.py::TestFHIRFlareIGToolkit::test_secret_key_csrf PASSED [ 81%]
Bash test_app.py::TestFHIRFlareIGToolkit::test_secret_key_flash_messages PASSED [ 85%]
test_app.py::TestFHIRFlareIGToolkit::test_get_structure_definition_success PASSED [ 88%]
docker-compose up -d test_app.py::TestFHIRFlareIGToolkit::test_get_structure_definition_not_found PASSED [ 92%]
Access the Application: test_app.py::TestFHIRFlareIGToolkit::test_get_example_content_success PASSED [ 96%]
test_app.py::TestFHIRFlareIGToolkit::test_get_example_content_invalid_path PASSED [100%]
Flask UI: http://localhost:5000
HAPI FHIR server (Standalone only): http://localhost:8080 ============================================================= 27 passed in 1.23s ==============================================================
Local Development (Without Docker): ```
Clone the Repository: ### Troubleshooting Tests
Bash - **ModuleNotFoundError**: If you encounter `ModuleNotFoundError: No module named 'app'`, ensure youre running the tests from the project root (`/app/`) or that the `sys.path` modification in `test_app.py` is correctly adding the parent directory.
- **Missing Templates**: If tests fail with `TemplateNotFound`, ensure all required templates (`index.html`, `import_ig.html`, etc.) are in the `/app/templates/` directory.
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git) - **Missing Dependencies**: If tests fail due to missing `services.py` or its functions, ensure `services.py` is present in `/app/` and contains the required functions (`import_package_and_dependencies`, `process_package_file`, etc.).
cd FHIRFLARE-IG-Toolkit
Install Dependencies: ## Directory Structure
Bash - `app.py`: Main Flask application file.
- `services.py`: Business logic for importing, processing, and pushing IGs.
python -m venv venv - `templates/`: HTML templates for the UI.
source venv/bin/activate # On Windows: venv\Scripts\activate - `instance/`: Directory for SQLite database and downloaded packages.
pip install -r requirements.txt - `tests/`: Directory for test files.
Install Node.js, GoFSH, and SUSHI (for FSH Converter): - `test_app.py`: Test suite for the application.
- `requirements.txt`: List of Python dependencies.
Bash - `Dockerfile`: Docker configuration for containerized deployment.
# Example for Debian/Ubuntu ## Contributing
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
sudo apt-get install -y nodejs Contributions are welcome! Please fork the repository, create a feature branch, and submit a pull request with your changes.
# Install globally
npm install -g gofsh fsh-sushi ## License
Set Environment Variables:
This project is licensed under the Apache 2.0 License. See the `LICENSE` file for details.
Bash
export FLASK_SECRET_KEY='your-secure-secret-key'
export API_KEY='your-api-key'
# Optional: Set APP_MODE to 'lite' if desired
# export APP_MODE='lite'
Initialize Directories:
Bash
mkdir -p instance static/uploads logs
# Ensure write permissions if needed
# chmod -R 777 instance static/uploads logs
Run the Application:
Bash
export FLASK_APP=app.py
flask run
Access at http://localhost:5000.
Usage
Import an IG
### Search, View Details, and Import Packages
Navigate to **Search and Import Packages** (`/search-and-import`).
1. The page will load a list of available FHIR Implementation Guide packages from a local cache or by fetching from configured registries.
* A loading animation and progress messages are shown if fetching from registries.
* The timestamp of the last cache update is displayed.
2. Use the search bar to filter packages by name or author.
3. Packages are paginated for easier Browse.
4. For each package, you can:
* View its latest official and absolute versions.
* Click on the package name to navigate to a **detailed view** (`/package-details/<name>`) showing:
* Comprehensive metadata (author, FHIR version, canonical URL, description).
* A full list of available versions with publication dates.
* Declared dependencies.
* Other packages that depend on it (dependents).
* Version history (logs).
* Directly import a specific version using the "Import" button on the search page or the details page.
5. **Cache Management:**
* A "Clear & Refresh Cache" button is available to trigger a background task (`/api/refresh-cache-task`) that clears the local database and in-memory cache and fetches the latest package information from all configured registries. Progress is shown via a live log.
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
Choose a dependency mode:
Current Recursive: Import all dependencies listed in package.json recursively.
Patch Canonical Versions: Import only canonical FHIR packages (e.g., hl7.fhir.r4.core).
Tree Shaking: Import only dependencies containing resources actually used by the main package.
Click Import to download the package and dependencies.
Manage IGs
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
Actions:
Process: Extract metadata (resource types, profiles, must-support elements, examples).
Unload: Remove processed IG data from the database.
Delete: Remove package files from the filesystem.
Duplicates are highlighted for resolution.
View Processed IGs
After processing, view IG details (/view-ig/<id>), including:
Resource types and profiles.
Must-support elements and examples.
Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
Interactive StructureDefinition viewer (Differential, Snapshot, Must Support, Key Elements, Constraints, Terminology, Search Params).
Validate FHIR Resources/Bundles
Navigate to Validate FHIR Sample (/validate-sample).
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
Choose Single Resource or Bundle mode.
Paste or upload FHIR JSON/XML (e.g., a Patient resource).
Submit to view validation errors/warnings. Note: Alpha feature; report issues to GitHub (remove PHI).
Push IGs to a FHIR Server
Go to Push IGs (/push-igs).
Select a downloaded package.
Enter the Target FHIR Server URL.
Configure Authentication (None, Bearer Token).
Choose options: Include Dependencies, Force Upload (skips comparison check), Dry Run, Verbose Log.
Optionally filter by Resource Types (comma-separated) or Skip Specific Files (paths within package, comma/newline separated).
Click Push to FHIR Server to upload resources. Canonical resources are checked before upload. Identical resources are skipped unless Force Upload is checked.
Monitor progress in the live console.
Upload Test Data
Navigate to Upload Test Data (/upload-test-data).
Enter the Target FHIR Server URL.
Configure Authentication (None, Bearer Token).
Select one or more .json, .xml files, or a single .zip file containing test resources.
Optionally check Validate Resources Before Upload? and select a Validation Profile Package.
Choose Upload Mode:
Individual Resources: Uploads each resource one by one in dependency order.
Transaction Bundle: Uploads all resources in a single transaction.
Optionally check Use Conditional Upload (Individual Mode Only)? to use If-Match headers for updates.
Choose Error Handling:
Stop on First Error: Halts the process if any validation or upload fails.
Continue on Error: Reports errors but attempts to process/upload remaining resources.
Click Upload and Process. The tool parses files, optionally validates, analyzes dependencies, topologically sorts resources, and uploads them according to selected options.
Monitor progress in the streaming log output.
Convert FHIR to FSH
Navigate to FSH Converter (/fsh-converter).
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
Choose input mode:
Upload File: Upload a FHIR JSON/XML file.
Paste Text: Paste FHIR JSON/XML content.
Configure options:
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
Log Level: error, warn, info, debug.
FHIR Version: R4, R4B, R5, or auto-detect.
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
Indent Rules: Enable context path indentation for readable FSH.
Meta Profile: Choose only-one, first, or none for meta.profile handling.
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
No Alias: Disable automatic alias generation.
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
Download the result as a .fsh file.
Retrieve and Split Bundles
Navigate to Retrieve/Split Data (/retrieve-split-data).
Retrieve Bundles from Server:
Enter the FHIR Server URL (defaults to the proxy if empty).
Select one or more Resource Types to retrieve (e.g., Patient, Observation).
Optionally check Fetch Referenced Resources.
If checked, further optionally check Fetch Full Reference Bundles to retrieve entire bundles for each referenced type (e.g., all Patients if a Patient is referenced) instead of individual resources by ID.
Click Retrieve Bundles.
Monitor progress in the streaming log. A ZIP file containing the retrieved bundles/resources will be prepared for download.
Split Uploaded Bundles:
Upload a ZIP file containing FHIR bundles (JSON format).
Click Split Bundles.
A ZIP file containing individual resources extracted from the bundles will be prepared for download.
Explore FHIR Operations
Navigate to FHIR UI Operations (/fhir-ui-operations).
Toggle between local HAPI (/fhir) or a custom FHIR server.
Click Fetch Metadata to load the servers CapabilityStatement.
Select a resource type (e.g., Patient, Observation) or System to view operations:
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
Resource operations: GET Patient/:id, POST Observation/_search, etc.
Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
### Configure Embedded HAPI FHIR Server (Standalone Mode)
For users running the **Standalone version**, which includes an embedded HAPI FHIR server.
1. Navigate to **Configure HAPI FHIR** (`/config-hapi`).
2. The page displays the content of the HAPI FHIR server's `application.yaml` file.
3. You can edit the configuration directly in the text area.
* *Caution: Incorrect modifications can break the HAPI FHIR server.*
4. Click **Save Configuration** to apply your changes to the `application.yaml` file.
5. Click **Restart Tomcat** to restart the HAPI FHIR server and load the new configuration. The restart process may take a few moments.
API Usage
Import IG
Bash
curl -X POST http://localhost:5000/api/import-ig \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "dependency_mode": "recursive"}'
Returns complies_with_profiles, imposed_profiles, and duplicate_packages_present info.
### Refresh Package Cache (Background Task)
```bash
curl -X POST http://localhost:5000/api/refresh-cache-task \
-H "X-API-Key: your-api-key"
Push IG
Bash
curl -X POST http://localhost:5000/api/push-ig \
-H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-H "X-API-Key: your-api-key" \
-d '{
"package_name": "hl7.fhir.au.core",
"version": "1.1.0-preview",
"fhir_server_url": "http://localhost:8080/fhir",
"include_dependencies": true,
"force_upload": false,
"dry_run": false,
"verbose": false,
"auth_type": "none"
}'
Returns a streaming NDJSON response with progress and final summary.
Upload Test Data
Bash
curl -X POST http://localhost:5000/api/upload-test-data \
-H "X-API-Key: your-api-key" \
-H "Accept: application/x-ndjson" \
-F "fhir_server_url=http://your-fhir-server/fhir" \
-F "auth_type=bearerToken" \
-F "auth_token=YOUR_TOKEN" \
-F "upload_mode=individual" \
-F "error_handling=continue" \
-F "validate_before_upload=true" \
-F "validation_package_id=hl7.fhir.r4.core#4.0.1" \
-F "use_conditional_uploads=true" \
-F "test_data_files=@/path/to/your/patient.json" \
-F "test_data_files=@/path/to/your/observations.zip"
Returns a streaming NDJSON response with progress and final summary. Uses multipart/form-data for file uploads.
Retrieve Bundles
Bash
curl -X POST http://localhost:5000/api/retrieve-bundles \
-H "X-API-Key: your-api-key" \
-H "Accept: application/x-ndjson" \
-F "fhir_server_url=http://your-fhir-server/fhir" \
-F "resources=Patient" \
-F "resources=Observation" \
-F "validate_references=true" \
-F "fetch_reference_bundles=false"
Returns a streaming NDJSON response with progress. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive (e.g., /tmp/retrieved_bundles_datetime.zip).
Split Bundles
Bash
curl -X POST http://localhost:5000/api/split-bundles \
-H "X-API-Key: your-api-key" \
-H "Accept: application/x-ndjson" \
-F "split_bundle_zip_path=@/path/to/your/bundles.zip"
Returns a streaming NDJSON response. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive of split resources.
Validate Resource/Bundle
Not yet exposed via API; use the UI at /validate-sample.
Configuration Options
Located in app.py:
VALIDATE_IMPOSED_PROFILES: (Default: True) Validates resources against imposed profiles during push.
DISPLAY_PROFILE_RELATIONSHIPS: (Default: True) Shows compliesWithProfile and imposeProfile in the UI.
FHIR_PACKAGES_DIR: (Default: /app/instance/fhir_packages) Stores .tgz packages and metadata.
UPLOAD_FOLDER: (Default: /app/static/uploads) Stores GoFSH output files and FSH comparison reports.
SECRET_KEY: Required for CSRF protection and sessions. Set via environment variable or directly.
API_KEY: Required for API authentication. Set via environment variable or directly.
MAX_CONTENT_LENGTH: (Default: Flask default) Max size for HTTP request body (e.g., 16 * 1024 * 1024 for 16MB). Important for large uploads.
MAX_FORM_PARTS: (Default: Werkzeug default, often 1000) Default max number of form parts. Overridden for /api/upload-test-data by CustomFormDataParser.
### Get HAPI FHIR Configuration (Standalone Mode)
```bash
curl -X GET http://localhost:5000/api/config \
-H "X-API-Key: your-api-key"
Save HAPI FHIR Configuration:
curl -X POST http://localhost:5000/api/config \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"your_yaml_key": "your_value", ...}' # Send the full YAML content as JSON
Restart HAPI FHIR Server:
curl -X POST http://localhost:5000/api/restart-tomcat \
-H "X-API-Key: your-api-key"
Testing
The project includes a test suite covering UI, API, database, file operations, and security.
Test Prerequisites:
pytest: For running tests.
pytest-mock: For mocking dependencies. Install: pip install pytest pytest-mock
Running Tests:
Bash
cd <project folder>
pytest tests/test_app.py -v
Test Coverage:
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, Upload Test Data, Retrieve/Split Data.
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data, POST /api/retrieve-bundles, POST /api/split-bundles.
Database: IG processing, unloading, viewing.
File Operations: Package processing, deletion, FSH output, ZIP handling.
Security: CSRF protection, flash messages, secret key.
FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
Upload Test Data: Parsing, dependency graph, sorting, upload modes, validation, conditional uploads.
Development Notes
Background
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, flexible versioning, improved IG pushing, dependency-aware test data uploading, and bundle retrieval/splitting, making it a versatile platform for FHIR developers.
Technical Decisions
Flask: Lightweight and flexible for web development.
SQLite: Simple for development; consider PostgreSQL for production.
Bootstrap 5.3.3: Responsive UI with custom styling.
Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
Docker: Ensures consistent deployment with Flask and HAPI FHIR.
Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
Live Console/Streaming: Real-time feedback for complex operations (Push, Upload Test Data, FSH, Retrieve Bundles).
Validation: Alpha feature with ongoing FHIRPath improvements.
Dependency Management: Uses topological sort for Upload Test Data feature.
Form Parsing: Uses custom Werkzeug parser for Upload Test Data to handle large numbers of files.
Recent Updates
* Enhanced package search page with caching, detailed views (dependencies, dependents, version history), and background cache refresh.
Upload Test Data Enhancements (April 2025):
Added optional Pre-Upload Validation against selected IG profiles.
Added optional Conditional Uploads (GET + POST/PUT w/ If-Match) for individual mode.
Implemented robust XML parsing using fhir.resources library (when available).
Fixed 413 Request Entity Too Large errors for large file counts using a custom Werkzeug FormDataParser.
Path: templates/upload_test_data.html, app.py, services.py, forms.py.
Push IG Enhancements (April 2025):
Added semantic comparison to skip uploading identical resources.
Added "Force Upload" option to bypass comparison.
Improved handling of canonical resources (search before PUT/POST).
Added filtering by specific files to skip during push.
More detailed summary report in stream response.
Path: templates/cp_push_igs.html, app.py, services.py.
Waiting Spinner for FSH Converter (April 2025):
Added a themed (light/dark) Lottie animation spinner during FSH execution.
Path: templates/fsh_converter.html, static/animations/, static/js/lottie-web.min.js.
Advanced FSH Converter (April 2025):
Added support for GoFSH advanced options: --fshing-trip, --dependency, --indent, --meta-profile, --alias-file, --no-alias.
Displays Fishing Trip comparison reports.
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
(New) Retrieve and Split Data (May 2025):
Added UI and API for retrieving bundles from a FHIR server by resource type.
Added options to fetch referenced resources (individually or as full type bundles).
Added functionality to split uploaded ZIP files of bundles into individual resources.
Streaming log for retrieval and ZIP download for results.
Paths: templates/retrieve_split_data.html, app.py, services.py, forms.py.
Known Issues and Workarounds
Favicon 404: Clear browser cache or verify /app/static/favicon.ico.
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
Import Fails: Check package name/version and connectivity.
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
Permissions: Ensure instance/ and static/uploads/ are writable.
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation.
Upload Test Data XML Parsing: Relies on fhir.resources library for full validation; basic parsing used as fallback. Complex XML structures might not be fully analyzed for dependencies with basic parsing. Prefer JSON for reliable dependency analysis.
413 Request Entity Too Large: Primarily handled by CustomFormDataParser for /api/upload-test-data. Check the parser's max_form_parts limit if still occurring. MAX_CONTENT_LENGTH in app.py controls overall size. Reverse proxy limits (client_max_body_size in Nginx) might also apply.
Future Improvements
Upload Test Data: Improve XML parsing further (direct XML->fhir.resource object if possible), add visual progress bar, add upload order preview, implement transaction bundle size splitting, add 'Clear Target Server' option (with confirmation).
Validation: Enhance FHIRPath for complex constraints; add API endpoint.
Sorting: Sort IG versions in /view-igs (e.g., ascending).
Duplicate Resolution: Options to keep latest version or merge resources.
Production Database: Support PostgreSQL.
Error Reporting: Detailed validation error paths in the UI.
FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
Retrieve/Split Data: Add option to filter resources during retrieval (e.g., by date, specific IDs).
Completed Items
Testing suite with basic coverage.
API endpoints for POST /api/import-ig and POST /api/push-ig.
Flexible versioning (-preview, -ballot).
CSRF fixes for forms.
Resource validation UI (alpha).
FSH Converter with advanced GoFSH features and waiting spinner.
Push IG enhancements (force upload, semantic comparison, canonical handling, skip files).
Upload Test Data feature with dependency sorting, multiple upload modes, pre-upload validation, conditional uploads, robust XML parsing, and fix for large file counts.
Retrieve and Split Data functionality with reference fetching and ZIP download.
Far-Distant Improvements
Cache Service: Use Redis for IG metadata caching.
Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
Directory Structure
FHIRFLARE-IG-Toolkit/
├── app.py # Main Flask application
├── Build and Run for first time.bat # Windows script for first-time Docker setup
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Docker configuration
├── forms.py # Form definitions
├── LICENSE.md # Apache 2.0 License
├── README.md # Project documentation
├── requirements.txt # Python dependencies
├── Run.bat # Windows script for running Docker
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload, retrieve/split
├── supervisord.conf # Supervisor configuration
├── hapi-fhir-Setup/
│ ├── README.md # HAPI FHIR setup instructions
│ └── target/
│ └── classes/
│ └── application.yaml # HAPI FHIR configuration
├── instance/
│ ├── fhir_ig.db # SQLite database
│ ├── fhir_ig.db.old # Database backup
│ └── fhir_packages/ # Stored IG packages and metadata
│ ├── ... (example packages) ...
├── logs/
│ ├── flask.log # Flask application logs
│ ├── flask_err.log # Flask error logs
│ ├── supervisord.log # Supervisor logs
│ ├── supervisord.pid # Supervisor PID file
│ ├── tomcat.log # Tomcat logs for HAPI FHIR
│ └── tomcat_err.log # Tomcat error logs
├── static/
│ ├── animations/
│ │ ├── loading-dark.json # Dark theme spinner animation
│ │ └── loading-light.json # Light theme spinner animation
│ ├── favicon.ico # Application favicon
│ ├── FHIRFLARE.png # Application logo
│ ├── js/
│ │ └── lottie-web.min.js # Lottie library for spinner
│ └── uploads/
│ ├── output.fsh # Generated FSH output (temp location)
│ └── fsh_output/ # GoFSH output directory
│ ├── ... (example GoFSH output) ...
├── templates/
│ ├── base.html # Base template
│ ├── cp_downloaded_igs.html # UI for managing IGs
│ ├── cp_push_igs.html # UI for pushing IGs
│ ├── cp_view_processed_ig.html # UI for viewing processed IGs
│ ├── fhir_ui.html # UI for FHIR API explorer
│ ├── fhir_ui_operations.html # UI for FHIR server operations
│ ├── fsh_converter.html # UI for FSH conversion
│ ├── import_ig.html # UI for importing IGs
│ ├── index.html # Homepage
│ ├── retrieve_split_data.html # UI for Retrieve and Split Data
│ ├── upload_test_data.html # UI for Uploading Test Data
│ ├── validate_sample.html # UI for validating resources/bundles
│ ├── config_hapi.html # UI for HAPI FHIR Configuration
│ └── _form_helpers.html # Form helper macros
├── tests/
│ └── test_app.py # Test suite
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources (if Standalone)
Contributing
Fork the repository.
Create a feature branch (git checkout -b feature/your-feature).
Commit changes (git commit -m "Add your feature").
Push to your branch (git push origin feature/your-feature).
Open a Pull Request.
Ensure code follows PEP 8 and includes tests in tests/test_app.py.
Troubleshooting
Favicon 404: Clear browser cache or verify /app/static/favicon.ico: docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
Import Fails: Check package name/version and connectivity.
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
Permissions: Ensure instance/ and static/uploads/ are writable: chmod -R 777 instance static/uploads logs
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation: docker exec -it <container_name> sushi --version
413 Request Entity Too Large: Increase MAX_CONTENT_LENGTH and MAX_FORM_PARTS in app.py. If using a reverse proxy (e.g., Nginx), increase its client_max_body_size setting as well. Ensure the application/container is fully restarted/rebuilt.
License
Licensed under the Apache 2.0 License. See LICENSE.md for details.

View File

@ -1,151 +0,0 @@
# Integrating FHIRVINE as a Module in FHIRFLARE
## Overview
FHIRFLARE is a Flask-based FHIR Implementation Guide (IG) toolkit for managing and validating FHIR packages. This guide explains how to integrate FHIRVINE—a SMART on FHIR proxy—as a module within FHIRFLARE, enabling OAuth2 authentication and FHIR request proxying directly in the application. This modular approach embeds FHIRVINEs functionality into FHIRFLARE, avoiding the need for a separate proxy service.
## Prerequisites
- FHIRFLARE repository cloned: `https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit`.
- FHIRVINE repository cloned: `<fhirvine-repository-url>`.
- Python 3.11 and dependencies installed (`requirements.txt` from both projects).
- A FHIR server (e.g., `http://hapi.fhir.org/baseR4`).
## Integration Steps
### 1. Prepare FHIRFLARE Structure
Ensure FHIRFLAREs file structure supports modular integration. It should look like:
```
FHIRFLARE-IG-Toolkit/
├── app.py
├── services.py
├── templates/
├── static/
└── requirements.txt
```
### 2. Copy FHIRVINE Files into FHIRFLARE
FHIRVINEs core functionality (OAuth2 proxy, app registration) will be integrated as a Flask Blueprint.
- **Copy Files**:
- Copy `smart_proxy.py`, `forms.py`, `models.py`, and `app.py` (relevant parts) from FHIRVINE into a new `fhirvine/` directory in FHIRFLARE:
```
FHIRFLARE-IG-Toolkit/
├── fhirvine/
│ ├── smart_proxy.py
│ ├── forms.py
│ ├── models.py
│ └── __init__.py
```
- Copy FHIRVINEs templates (e.g., `app_gallery/`, `configure/`, `test_client.html`) into `FHIRFLARE-IG-Toolkit/templates/` while maintaining their folder structure.
- **Add Dependencies**:
- Add FHIRVINEs dependencies to `requirements.txt` (e.g., `authlib`, `flasgger`, `flask-sqlalchemy`).
### 3. Modify FHIRVINE Code as a Module
- **Create Blueprint in** `fhirvine/__init__.py`:
```python
from flask import Blueprint
fhirvine_bp = Blueprint('fhirvine', __name__, template_folder='templates')
from .smart_proxy import *
```
This registers FHIRVINE as a Flask Blueprint.
- **Update** `smart_proxy.py`:
- Replace direct `app.route` decorators with `fhirvine_bp.route`. For example:
```python
@fhirvine_bp.route('/authorize', methods=['GET', 'POST'])
def authorize():
# Existing authorization logic
```
### 4. Integrate FHIRVINE Blueprint into FHIRFLARE
- **Update** `app.py` **in FHIRFLARE**:
- Import and register the FHIRVINE Blueprint:
```python
from fhirvine import fhirvine_bp
from fhirvine.models import database, RegisteredApp, OAuthToken, AuthorizationCode, Configuration
from fhirvine.smart_proxy import configure_oauth
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='your-secure-random-key',
SQLALCHEMY_DATABASE_URI='sqlite:////app/instance/fhirflare.db',
SQLALCHEMY_TRACK_MODIFICATIONS=False,
FHIR_SERVER_URL='http://hapi.fhir.org/baseR4',
PROXY_TIMEOUT=10,
TOKEN_DURATION=3600,
REFRESH_TOKEN_DURATION=86400,
ALLOWED_SCOPES='openid profile launch launch/patient patient/*.read offline_access'
)
database.init_app(app)
configure_oauth(app, db=database, registered_app_model=RegisteredApp, oauth_token_model=OAuthToken, auth_code_model=AuthorizationCode)
app.register_blueprint(fhirvine_bp, url_prefix='/fhirvine')
```
### 5. Update FHIRFLARE Templates
- **Add FHIRVINE Links to Navbar**:
- In `templates/base.html`, add links to FHIRVINE features:
```html
<li class="nav-item">
<a class="nav-link" href="{{ url_for('fhirvine.app_gallery') }}">App Gallery</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('fhirvine.test_client') }}">Test Client</a>
</li>
```
### 6. Run and Test
- **Install Dependencies**:
```bash
pip install -r requirements.txt
```
- **Run FHIRFLARE**:
```bash
flask db upgrade
flask run --host=0.0.0.0 --port=8080
```
- **Access FHIRVINE Features**:
- App Gallery: `http://localhost:8080/fhirvine/app-gallery`
- Test Client: `http://localhost:8080/fhirvine/test-client`
- Proxy Requests: Use `/fhirvine/oauth2/proxy/<path>` within FHIRFLARE.
## Using FHIRVINE in FHIRFLARE
- **Register Apps**: Use `/fhirvine/app-gallery` to register SMART apps within FHIRFLARE.
- **Authenticate**: Use `/fhirvine/oauth2/authorize` for OAuth2 flows.
- **Proxy FHIR Requests**: FHIRFLARE can now make FHIR requests via `/fhirvine/oauth2/proxy`, leveraging FHIRVINEs authentication.
## Troubleshooting
- **Route Conflicts**: Ensure no overlapping routes between FHIRFLARE and FHIRVINE.
- **Database Issues**: Verify `SQLALCHEMY_DATABASE_URI` points to the same database.
- **Logs**: Check `flask run` logs for errors.

25
Run.bat
View File

@ -1,25 +0,0 @@
REM --- Step 1: Start Docker containers ---
echo ===> Starting Docker containers (Step 7)...
docker-compose up -d
if errorlevel 1 (
echo ERROR: Docker Compose up failed. Check Docker installation and container configurations. ErrorLevel: %errorlevel%
goto :error
)
echo Docker containers started successfully. ErrorLevel: %errorlevel%
echo.
echo ====================================
echo Script finished successfully!
echo ====================================
goto :eof
:error
echo ------------------------------------
echo An error occurred. Script aborted.
echo ------------------------------------
pause
exit /b 1
:eof
echo Script execution finished.
pause

View File

@ -1 +0,0 @@
=== Docker containers (Step 7)...

28
__init__.py Normal file
View File

@ -0,0 +1,28 @@
# app/modules/fhir_ig_importer/__init__.py
from flask import Blueprint
# --- Module Metadata ---
metadata = {
'module_id': 'fhir_ig_importer', # Matches folder name
'display_name': 'FHIR IG Importer',
'description': 'Imports FHIR Implementation Guide packages from a registry.',
'version': '0.1.0',
# No main nav items, will be accessed via Control Panel
'nav_items': []
}
# --- End Module Metadata ---
# Define Blueprint
# We'll mount this under the control panel later
bp = Blueprint(
metadata['module_id'],
__name__,
template_folder='templates',
# Define a URL prefix if mounting standalone, but we'll likely register
# it under /control-panel via app/__init__.py later
# url_prefix='/fhir-importer'
)
# Import routes after creating blueprint
from . import routes, forms # Import forms too

3697
app.py

File diff suppressed because it is too large Load Diff

1
charts/.gitignore vendored
View File

@ -1 +0,0 @@
/hapi-fhir-jpaserver-0.20.0.tgz

View File

@ -1 +0,0 @@
/rendered/

View File

@ -1,16 +0,0 @@
apiVersion: v2
name: fhirflare-ig-toolkit
version: 0.5.0
description: Helm chart for deploying the fhirflare-ig-toolkit application
type: application
appVersion: "latest"
icon: https://github.com/jgsuess/FHIRFLARE-IG-Toolkit/raw/main/static/FHIRFLARE.png
keywords:
- fhir
- healthcare
- ig-toolkit
- implementation-guide
home: https://github.com/jgsuess/FHIRFLARE-IG-Toolkit
maintainers:
- name: Jörn Guy Süß
email: jgsuess@gmail.com

View File

@ -1,152 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "fhirflare-ig-toolkit.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "fhirflare-ig-toolkit.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "fhirflare-ig-toolkit.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "fhirflare-ig-toolkit.labels" -}}
helm.sh/chart: {{ include "fhirflare-ig-toolkit.chart" . }}
{{ include "fhirflare-ig-toolkit.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "fhirflare-ig-toolkit.selectorLabels" -}}
app.kubernetes.io/name: {{ include "fhirflare-ig-toolkit.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "hapi-fhir-jpaserver.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "hapi-fhir-jpaserver.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Create a default fully qualified postgresql name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "hapi-fhir-jpaserver.postgresql.fullname" -}}
{{- $name := default "postgresql" .Values.postgresql.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Get the Postgresql credentials secret name.
*/}}
{{- define "hapi-fhir-jpaserver.postgresql.secretName" -}}
{{- if .Values.postgresql.enabled -}}
{{- if .Values.postgresql.auth.existingSecret -}}
{{- printf "%s" .Values.postgresql.auth.existingSecret -}}
{{- else -}}
{{- printf "%s" (include "hapi-fhir-jpaserver.postgresql.fullname" .) -}}
{{- end -}}
{{- else }}
{{- if .Values.externalDatabase.existingSecret -}}
{{- printf "%s" .Values.externalDatabase.existingSecret -}}
{{- else -}}
{{ printf "%s-%s" (include "hapi-fhir-jpaserver.fullname" .) "external-db" }}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Get the Postgresql credentials secret key.
*/}}
{{- define "hapi-fhir-jpaserver.postgresql.secretKey" -}}
{{- if .Values.postgresql.enabled -}}
{{- if .Values.postgresql.auth.username -}}
{{- printf "%s" .Values.postgresql.auth.secretKeys.userPasswordKey -}}
{{- else -}}
{{- printf "%s" .Values.postgresql.auth.secretKeys.adminPasswordKey -}}
{{- end -}}
{{- else }}
{{- if .Values.externalDatabase.existingSecret -}}
{{- printf "%s" .Values.externalDatabase.existingSecretKey -}}
{{- else -}}
{{- printf "postgres-password" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Add environment variables to configure database values
*/}}
{{- define "hapi-fhir-jpaserver.database.host" -}}
{{- ternary (include "hapi-fhir-jpaserver.postgresql.fullname" .) .Values.externalDatabase.host .Values.postgresql.enabled -}}
{{- end -}}
{{/*
Add environment variables to configure database values
*/}}
{{- define "hapi-fhir-jpaserver.database.user" -}}
{{- if .Values.postgresql.enabled -}}
{{- printf "%s" .Values.postgresql.auth.username | default "postgres" -}}
{{- else -}}
{{- printf "%s" .Values.externalDatabase.user -}}
{{- end -}}
{{- end -}}
{{/*
Add environment variables to configure database values
*/}}
{{- define "hapi-fhir-jpaserver.database.name" -}}
{{- ternary .Values.postgresql.auth.database .Values.externalDatabase.database .Values.postgresql.enabled -}}
{{- end -}}
{{/*
Add environment variables to configure database values
*/}}
{{- define "hapi-fhir-jpaserver.database.port" -}}
{{- ternary "5432" .Values.externalDatabase.port .Values.postgresql.enabled -}}
{{- end -}}
{{/*
Create the JDBC URL from the host, port and database name.
*/}}
{{- define "hapi-fhir-jpaserver.database.jdbcUrl" -}}
{{- $host := (include "hapi-fhir-jpaserver.database.host" .) -}}
{{- $port := (include "hapi-fhir-jpaserver.database.port" .) -}}
{{- $name := (include "hapi-fhir-jpaserver.database.name" .) -}}
{{- $appName := .Release.Name -}}
{{ printf "jdbc:postgresql://%s:%d/%s?ApplicationName=%s" $host (int $port) $name $appName }}
{{- end -}}

View File

@ -1,91 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "fhirflare-ig-toolkit.fullname" . }}
labels:
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount | default 1 }}
selector:
matchLabels:
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 6 }}
strategy:
type: Recreate
template:
metadata:
labels:
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["supervisord", "-c", "/etc/supervisord.conf"]
env:
- name: APP_BASE_URL
value: {{ .Values.config.appBaseUrl | default "http://localhost:5000" | quote }}
- name: APP_MODE
value: {{ .Values.config.appMode | default "lite" | quote }}
- name: FLASK_APP
value: {{ .Values.config.flaskApp | default "app.py" | quote }}
- name: FLASK_ENV
value: {{ .Values.config.flaskEnv | default "development" | quote }}
- name: HAPI_FHIR_URL
value: {{ .Values.config.externalHapiServerUrl | default "http://external-hapi-fhir:8080/fhir" | quote }}
- name: NODE_PATH
value: {{ .Values.config.nodePath | default "/usr/lib/node_modules" | quote }}
- name: TMPDIR
value: "/tmp-dir"
ports:
- name: http
containerPort: {{ .Values.service.port | default 5000 }}
protocol: TCP
volumeMounts:
- name: logs
mountPath: /app/logs
- name: tmp-dir
mountPath: /tmp-dir
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: logs
emptyDir: {}
- name: tmp-dir
emptyDir: {}
# Always require Intel 64-bit architecture nodes
nodeSelector:
kubernetes.io/arch: amd64
{{- with .Values.nodeSelector }}
# Merge with user-defined nodeSelectors if any
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -1,36 +0,0 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "fhirflare-ig-toolkit.fullname" . -}}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }}
apiVersion: networking.k8s.io/v1beta1
{{- else }}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
rules:
- http:
paths:
- path: /
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
backend:
service:
name: {{ $fullName }}
port:
number: {{ .Values.service.port | default 5000 }}
{{- else }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ .Values.service.port | default 5000 }}
{{- end }}
{{- end }}

View File

@ -1,18 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "fhirflare-ig-toolkit.fullname" . }}
labels:
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type | default "ClusterIP" }}
ports:
- name: http
port: {{ .Values.service.port | default 5000 }}
targetPort: {{ .Values.service.port | default 5000 }}
protocol: TCP
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
selector:
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 4 }}

View File

@ -1,41 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-fhirflare-test-endpoint"
labels:
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/component: tests
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: test-fhirflare-endpoint
image: curlimages/curl:8.12.1
command: ["curl", "--fail-with-body", "--retry", "5", "--retry-delay", "10"]
args: ["http://fhirflare:5000"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
resources:
limits:
cpu: 150m
ephemeral-storage: 2Gi
memory: 192Mi
requests:
cpu: 100m
ephemeral-storage: 50Mi
memory: 128Mi

View File

@ -1,89 +0,0 @@
# Default values for fhirflare-ig-toolkit
replicaCount: 1
image:
repository: ghcr.io/jgsuess/fhirflare-ig-toolkit
pullPolicy: Always
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# FHIRflare specific configuration
config:
# Application mode: "lite" means using external HAPI server, "standalone" means running with embedded HAPI server
appMode: "lite"
# URL for the external HAPI FHIR server when in lite mode
externalHapiServerUrl: "http://external-hapi-fhir:8080/fhir"
appBaseUrl: "http://localhost:5000"
flaskApp: "app.py"
flaskEnv: "development"
nodePath: "/usr/lib/node_modules"
service:
type: ClusterIP
port: 5000
nodePort: null
podAnnotations: {}
# podSecurityContext:
# fsGroup: 65532
# fsGroupChangePolicy: OnRootMismatch
# runAsNonRoot: true
# runAsGroup: 65532
# runAsUser: 65532
# seccompProfile:
# type: RuntimeDefault
# securityContext:
# allowPrivilegeEscalation: false
# capabilities:
# drop:
# - ALL
# privileged: false
# readOnlyRootFilesystem: true
# runAsGroup: 65532
# runAsNonRoot: true
# runAsUser: 65532
# seccompProfile:
# type: RuntimeDefault
resources:
limits:
cpu: 500m
memory: 512Mi
ephemeral-storage: 1Gi
requests:
cpu: 100m
memory: 128Mi
ephemeral-storage: 100Mi
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
nodeSelector: {}
tolerations: []
affinity: {}
ingress:
# -- whether to create a primitive Ingress to expose the FHIR server HTTP endpoint
enabled: false

View File

@ -1,23 +0,0 @@
#!/bin/bash
#
# FHIRFLARE-IG-Toolkit Installation Script
#
# Description:
# This script installs the FHIRFLARE-IG-Toolkit Helm chart into a Kubernetes cluster.
# It adds the FHIRFLARE-IG-Toolkit Helm repository and then installs the chart
# in the 'flare' namespace, creating the namespace if it doesn't exist.
#
# Usage:
# ./install.sh
#
# Requirements:
# - Helm (v3+)
# - kubectl configured with access to your Kubernetes cluster
#
# Add the FHIRFLARE-IG-Toolkit Helm repository
helm repo add flare https://jgsuess.github.io/FHIRFLARE-IG-Toolkit/
# Install the FHIRFLARE-IG-Toolkit chart in the 'flare' namespace
helm install flare/fhirflare-ig-toolkit --namespace flare --create-namespace --generate-name --set hapi-fhir-jpaserver.postgresql.primary.persistence.storageClass=gp2 --atomic

View File

@ -1,21 +0,0 @@
version: '3.8'
services:
fhirflare:
build:
context: .
dockerfile: Dockerfile.lite
ports:
- "5000:5000"
- "8080:8080"
volumes:
- ./instance:/app/instance
- ./static/uploads:/app/static/uploads
- ./logs:/app/logs
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=standalone
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
command: supervisord -c /etc/supervisord.conf

View File

@ -1,22 +0,0 @@
# This docker-compose file uses ephemeral Docker named volumes for all data storage.
# These volumes persist only as long as the Docker volumes exist and are deleted if you run `docker-compose down -v`.
# No data is stored on the host filesystem. If you want persistent storage, replace these with host mounts.
services:
fhirflare-standalone:
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest}
container_name: fhirflare-standalone
ports:
- "5000:5000"
- "8080:8080"
volumes:
- fhirflare-instance:/app/instance
- fhirflare-uploads:/app/static/uploads
- fhirflare-h2-data:/app/h2-data
- fhirflare-logs:/app/logs
restart: unless-stopped
volumes:
fhirflare-instance:
fhirflare-uploads:
fhirflare-h2-data:
fhirflare-logs:

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Stop and remove all containers defined in the Docker Compose file,
# along with any anonymous volumes attached to them.
docker compose down --volumes

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Run Docker Compose
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps

View File

@ -1,18 +0,0 @@
hapi.fhir:
ig_runtime_upload_enabled: false
narrative_enabled: true
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
- http://snomed.info/*
- https://snomed.info/*
- http://unitsofmeasure.org/*
- https://unitsofmeasure.org/*
- http://loinc.org/*
- https://loinc.org/*
cors:
allow_Credentials: true
allowed_origin:
- '*'
tester.home.name: FHIRFLARE Tester
inline_resource_storage_below_size: 4000

View File

@ -1,50 +0,0 @@
services:
fhirflare:
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest}
ports:
- "5000:5000"
# Ephemeral Docker named volumes for all data storage. No data is stored on the host filesystem.
volumes:
- fhirflare-instance:/app/instance
- fhirflare-uploads:/app/static/uploads
- fhirflare-h2-data:/app/h2-data
- fhirflare-logs:/app/logs
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=lite
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=http://fhir:8080/fhir
command: supervisord -c /etc/supervisord.conf
fhir:
container_name: hapi
image: "hapiproject/hapi:v8.2.0-1"
ports:
- "8080:8080"
configs:
- source: hapi
target: /app/config/application.yaml
depends_on:
- db
db:
image: "postgres:17.2-bookworm"
restart: always
environment:
POSTGRES_PASSWORD: admin
POSTGRES_USER: admin
POSTGRES_DB: hapi
volumes:
- ./hapi.postgress.data:/var/lib/postgresql/data
configs:
hapi:
file: ./application.yaml
volumes:
fhirflare-instance:
fhirflare-uploads:
fhirflare-h2-data:
fhirflare-logs:

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Stop and remove all containers defined in the Docker Compose file,
# along with any anonymous volumes attached to them.
docker compose down --volumes

View File

@ -1,19 +0,0 @@
# FHIRFLARE IG Toolkit
This directory provides scripts and configuration to start and stop a FHIRFLARE instance with an attached HAPI FHIR server using Docker Compose.
## Usage
- To start the FHIRFLARE toolkit and HAPI server:
```sh
./docker-compose/up.sh
```
- To stop and remove the containers and volumes:
```sh
./docker-compose/down.sh
```
The web interface will be available at [http://localhost:5000](http://localhost:5000) and the HAPI FHIR server at [http://localhost:8080/fhir](http://localhost:8080/fhir).
For more details, see the configuration files in this directory.

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Run Docker Compose
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps

View File

@ -1,25 +0,0 @@
services:
fhirflare:
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest}
ports:
- "5000:5000"
# Ephemeral Docker named volumes for all data storage. No data is stored on the host filesystem.
volumes:
- fhirflare-instance:/app/instance
- fhirflare-uploads:/app/static/uploads
- fhirflare-h2-data:/app/h2-data
- fhirflare-logs:/app/logs
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=lite
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=https://cdr.fhirlab.net/fhir
command: supervisord -c /etc/supervisord.conf
volumes:
fhirflare-instance:
fhirflare-uploads:
fhirflare-h2-data:
fhirflare-logs:

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Stop and remove all containers defined in the Docker Compose file,
# along with any anonymous volumes attached to them.
docker compose down --volumes

View File

@ -1,19 +0,0 @@
# FHIRFLARE IG Toolkit
This directory provides scripts and configuration to start and stop a FHIRFLARE instance with an attached HAPI FHIR server using Docker Compose.
## Usage
- To start the FHIRFLARE toolkit and HAPI server:
```sh
./docker-compose/up.sh
```
- To stop and remove the containers and volumes:
```sh
./docker-compose/down.sh
```
The web interface will be available at [http://localhost:5000](http://localhost:5000) and the HAPI FHIR server at [http://localhost:8080/fhir](http://localhost:8080/fhir).
For more details, see the configuration files in this directory.

View File

@ -1,5 +0,0 @@
#!/bin/bash
# Run Docker Compose
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps

View File

@ -1,66 +0,0 @@
# ------------------------------------------------------------------------------
# Dockerfile for FHIRFLARE-IG-Toolkit (Optimized for Python/Flask)
#
# This Dockerfile builds a container for the FHIRFLARE-IG-Toolkit application.
#
# Key Features:
# - Uses python:3.11-slim as the base image for a minimal, secure Python runtime.
# - Installs Node.js and global NPM packages (gofsh, fsh-sushi) for FHIR IG tooling.
# - Sets up a Python virtual environment and installs all Python dependencies.
# - Installs and configures Supervisor to manage the Flask app and related processes.
# - Copies all necessary application code, templates, static files, and configuration.
# - Exposes ports 5000 (Flask) and 8080 (optional, for compatibility).
# - Entrypoint runs Supervisor for process management.
#
# Notes:
# - The Dockerfile is optimized for Python. Tomcat/Java is not included.
# - Node.js is only installed if needed for FHIR IG tooling.
# - The image is suitable for development and production with minimal changes.
# ------------------------------------------------------------------------------
# Optimized Dockerfile for Python (Flask)
FROM python:3.11-slim AS base
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
coreutils \
&& rm -rf /var/lib/apt/lists/*
# Optional: Install Node.js if needed for GoFSH/SUSHI
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g gofsh fsh-sushi \
&& rm -rf /var/lib/apt/lists/*
# Set workdir
WORKDIR /app
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN python -m venv /app/venv \
&& . /app/venv/bin/activate \
&& pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt \
&& pip uninstall -y fhirpath || true \
&& pip install --no-cache-dir fhirpathpy \
&& pip install supervisor
# Copy application files
COPY app.py .
COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 8080
# Set environment
ENV PATH="/app/venv/bin:$PATH"
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -1,7 +0,0 @@
#!/bin/bash
# Build FHIRFLARE-IG-Toolkit Docker image
# Build the image using the Dockerfile in the docker directory
docker build -f Dockerfile -t fhirflare-ig-toolkit:latest ..
echo "Docker image built successfully"

378
forms.py
View File

@ -1,369 +1,19 @@
# forms.py # app/modules/fhir_ig_importer/forms.py
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField, SelectMultipleField from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired from wtforms.validators import DataRequired, Regexp
from flask import request
import json
import xml.etree.ElementTree as ET
import re
import logging
import os
logger = logging.getLogger(__name__)
class RetrieveSplitDataForm(FlaskForm):
"""Form for retrieving FHIR bundles and splitting them into individual resources."""
fhir_server_url = StringField('FHIR Server URL', validators=[URL(), Optional()],
render_kw={'placeholder': 'e.g., https://hapi.fhir.org/baseR4'})
auth_type = SelectField('Authentication Type (for Custom URL)', choices=[
('none', 'None'),
('bearerToken', 'Bearer Token'),
('basicAuth', 'Basic Authentication')
], default='none', validators=[Optional()])
auth_token = StringField('Bearer Token', validators=[Optional()],
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
basic_auth_username = StringField('Username', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Username'})
basic_auth_password = PasswordField('Password', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Password'})
validate_references = BooleanField('Fetch Referenced Resources', default=False,
description="If checked, fetches resources referenced by the initial bundles.")
fetch_reference_bundles = BooleanField('Fetch Full Reference Bundles (instead of individual resources)', default=False,
description="Requires 'Fetch Referenced Resources'. Fetches e.g. /Patient instead of Patient/id for each reference.",
render_kw={'data-dependency': 'validate_references'})
split_bundle_zip = FileField('Upload Bundles to Split (ZIP)', validators=[Optional()],
render_kw={'accept': '.zip'})
submit_retrieve = SubmitField('Retrieve Bundles')
submit_split = SubmitField('Split Bundles')
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
if self.fetch_reference_bundles.data and not self.validate_references.data:
self.fetch_reference_bundles.errors.append('Cannot fetch full reference bundles unless "Fetch Referenced Resources" is also checked.')
return False
if self.auth_type.data == 'bearerToken' and self.submit_retrieve.data and not self.auth_token.data:
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected.')
return False
if self.auth_type.data == 'basicAuth' and self.submit_retrieve.data:
if not self.basic_auth_username.data:
self.basic_auth_username.errors.append('Username is required for Basic Authentication.')
return False
if not self.basic_auth_password.data:
self.basic_auth_password.errors.append('Password is required for Basic Authentication.')
return False
if self.split_bundle_zip.data:
if not self.split_bundle_zip.data.filename.lower().endswith('.zip'):
self.split_bundle_zip.errors.append('File must be a ZIP file.')
return False
return True
class IgImportForm(FlaskForm): class IgImportForm(FlaskForm):
"""Form for importing Implementation Guides.""" """Form for specifying an IG package to import."""
package_name = StringField('Package Name', validators=[ # Basic validation for FHIR package names (e.g., hl7.fhir.r4.core)
DataRequired(), package_name = StringField('Package Name (e.g., hl7.fhir.au.base)', validators=[
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
], render_kw={'placeholder': 'e.g., hl7.fhir.au.core'})
package_version = StringField('Package Version', validators=[
DataRequired(),
Regexp(r'^[a-zA-Z0-9\.\-]+$', message="Invalid version format. Use alphanumeric characters, dots, or hyphens (e.g., 1.2.3, 1.1.0-preview, current).")
], render_kw={'placeholder': 'e.g., 1.1.0-preview'})
dependency_mode = SelectField('Dependency Mode', choices=[
('recursive', 'Current Recursive'),
('patch-canonical', 'Patch Canonical Versions'),
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
], default='recursive')
submit = SubmitField('Import')
class ManualIgImportForm(FlaskForm):
"""Form for manual importing Implementation Guides via file or URL."""
import_mode = SelectField('Import Mode', choices=[
('file', 'Upload File'),
('url', 'From URL')
], default='file', validators=[DataRequired()])
tgz_file = FileField('IG Package File (.tgz)', validators=[Optional()],
render_kw={'accept': '.tgz'})
tgz_url = StringField('IG Package URL', validators=[Optional(), URL()],
render_kw={'placeholder': 'e.g., https://example.com/hl7.fhir.au.core-1.1.0-preview.tgz'})
dependency_mode = SelectField('Dependency Mode', choices=[
('recursive', 'Current Recursive'),
('patch-canonical', 'Patch Canonical Versions'),
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
], default='recursive')
resolve_dependencies = BooleanField('Resolve Dependencies', default=True,
render_kw={'class': 'form-check-input'})
submit = SubmitField('Import')
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
mode = self.import_mode.data
has_file = request and request.files and self.tgz_file.name in request.files and request.files[self.tgz_file.name].filename != ''
has_url = bool(self.tgz_url.data) # Convert to boolean: True if non-empty string
# Ensure exactly one input method is used
inputs_provided = sum([has_file, has_url])
if inputs_provided != 1:
if inputs_provided == 0:
self.import_mode.errors.append('Please provide input for one import method (File or URL).')
else:
self.import_mode.errors.append('Please use only one import method at a time.')
return False
# Validate based on import mode
if mode == 'file':
if not has_file:
self.tgz_file.errors.append('A .tgz file is required for File import.')
return False
if not self.tgz_file.data.filename.lower().endswith('.tgz'):
self.tgz_file.errors.append('File must be a .tgz file.')
return False
elif mode == 'url':
if not has_url:
self.tgz_url.errors.append('A valid URL is required for URL import.')
return False
if not self.tgz_url.data.lower().endswith('.tgz'):
self.tgz_url.errors.append('URL must point to a .tgz file.')
return False
else:
self.import_mode.errors.append('Invalid import mode selected.')
return False
return True
class ValidationForm(FlaskForm):
"""Form for validating FHIR samples."""
package_name = StringField('Package Name', validators=[DataRequired()])
version = StringField('Package Version', validators=[DataRequired()])
include_dependencies = BooleanField('Include Dependencies', default=True)
mode = SelectField('Validation Mode', choices=[
('single', 'Single Resource'),
('bundle', 'Bundle')
], default='single')
sample_input = TextAreaField('Sample Input', validators=[
DataRequired(), DataRequired(),
Regexp(r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
]) ])
submit = SubmitField('Validate') # Basic validation for version (e.g., 4.1.0, current)
package_version = StringField('Package Version (e.g., 4.1.0 or current)', validators=[
class FSHConverterForm(FlaskForm): DataRequired(),
"""Form for converting FHIR resources to FSH.""" Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
package = SelectField('FHIR Package (Optional)', choices=[('', 'None')], validators=[Optional()]) ])
input_mode = SelectField('Input Mode', choices=[ submit = SubmitField('Fetch & Download IG')
('file', 'Upload File'),
('text', 'Paste Text')
], validators=[DataRequired()])
fhir_file = FileField('FHIR Resource File (JSON/XML)', validators=[Optional()])
fhir_text = TextAreaField('FHIR Resource Text (JSON/XML)', validators=[Optional()])
output_style = SelectField('Output Style', choices=[
('file-per-definition', 'File per Definition'),
('group-by-fsh-type', 'Group by FSH Type'),
('group-by-profile', 'Group by Profile'),
('single-file', 'Single File')
], validators=[DataRequired()])
log_level = SelectField('Log Level', choices=[
('error', 'Error'),
('warn', 'Warn'),
('info', 'Info'),
('debug', 'Debug')
], validators=[DataRequired()])
fhir_version = SelectField('FHIR Version', choices=[
('', 'Auto-detect'),
('4.0.1', 'R4'),
('4.3.0', 'R4B'),
('5.0.0', 'R5')
], validators=[Optional()])
fishing_trip = BooleanField('Run Fishing Trip (Round-Trip Validation with SUSHI)', default=False)
dependencies = TextAreaField('Dependencies (e.g., hl7.fhir.us.core@6.1.0)', validators=[Optional()])
indent_rules = BooleanField('Indent Rules with Context Paths', default=False)
meta_profile = SelectField('Meta Profile Handling', choices=[
('only-one', 'Only One Profile (Default)'),
('first', 'First Profile'),
('none', 'Ignore Profiles')
], validators=[DataRequired()])
alias_file = FileField('Alias FSH File', validators=[Optional()])
no_alias = BooleanField('Disable Alias Generation', default=False)
submit = SubmitField('Convert to FSH')
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
has_file_in_request = request and request.files and self.fhir_file.name in request.files and request.files[self.fhir_file.name].filename != ''
if self.input_mode.data == 'file' and not has_file_in_request:
if not self.fhir_file.data:
self.fhir_file.errors.append('File is required when input mode is Upload File.')
return False
if self.input_mode.data == 'text' and not self.fhir_text.data:
self.fhir_text.errors.append('Text input is required when input mode is Paste Text.')
return False
if self.input_mode.data == 'text' and self.fhir_text.data:
try:
content = self.fhir_text.data.strip()
if not content: pass
elif content.startswith('{'): json.loads(content)
elif content.startswith('<'): ET.fromstring(content)
else:
self.fhir_text.errors.append('Text input must be valid JSON or XML.')
return False
except (json.JSONDecodeError, ET.ParseError):
self.fhir_text.errors.append('Invalid JSON or XML format.')
return False
if self.dependencies.data:
for dep in self.dependencies.data.splitlines():
dep = dep.strip()
if dep and not re.match(r'^[a-zA-Z0-9\-\.]+@[a-zA-Z0-9\.\-]+$', dep):
self.dependencies.errors.append(f'Invalid dependency format: "{dep}". Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
return False
has_alias_file_in_request = request and request.files and self.alias_file.name in request.files and request.files[self.alias_file.name].filename != ''
alias_file_data = self.alias_file.data or (request.files.get(self.alias_file.name) if request else None)
if alias_file_data and alias_file_data.filename:
if not alias_file_data.filename.lower().endswith('.fsh'):
self.alias_file.errors.append('Alias file should have a .fsh extension.')
return True
class TestDataUploadForm(FlaskForm):
"""Form for uploading FHIR test data."""
fhir_server_url = StringField('Target FHIR Server URL', validators=[DataRequired(), URL()],
render_kw={'placeholder': 'e.g., http://localhost:8080/fhir'})
auth_type = SelectField('Authentication Type', choices=[
('none', 'None'),
('bearerToken', 'Bearer Token'),
('basic', 'Basic Authentication')
], default='none')
auth_token = StringField('Bearer Token', validators=[Optional()],
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
username = StringField('Username', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Username'})
password = PasswordField('Password', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Password'})
test_data_file = FileField('Select Test Data File(s)', validators=[InputRequired("Please select at least one file.")],
render_kw={'multiple': True, 'accept': '.json,.xml,.zip'})
validate_before_upload = BooleanField('Validate Resources Before Upload?', default=False,
description="Validate resources against selected package profile before uploading.")
validation_package_id = SelectField('Validation Profile Package (Optional)',
choices=[('', '-- Select Package for Validation --')],
validators=[Optional()],
description="Select the processed IG package to use for validation.")
upload_mode = SelectField('Upload Mode', choices=[
('individual', 'Individual Resources'),
('transaction', 'Transaction Bundle')
], default='individual')
use_conditional_uploads = BooleanField('Use Conditional Upload (Individual Mode Only)?', default=True,
description="If checked, checks resource existence (GET) and uses If-Match (PUT) or creates (PUT). If unchecked, uses simple PUT for all.")
error_handling = SelectField('Error Handling', choices=[
('stop', 'Stop on First Error'),
('continue', 'Continue on Error')
], default='stop')
submit = SubmitField('Upload and Process')
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
if self.validate_before_upload.data and not self.validation_package_id.data:
self.validation_package_id.errors.append('Please select a package to validate against when pre-upload validation is enabled.')
return False
if self.use_conditional_uploads.data and self.upload_mode.data == 'transaction':
self.use_conditional_uploads.errors.append('Conditional Uploads only apply to the "Individual Resources" mode.')
return False
if self.auth_type.data == 'bearerToken' and not self.auth_token.data:
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected.')
return False
if self.auth_type.data == 'basic':
if not self.username.data:
self.username.errors.append('Username is required for Basic Authentication.')
return False
if not self.password.data:
self.password.errors.append('Password is required for Basic Authentication.')
return False
return True
class FhirRequestForm(FlaskForm):
fhir_server_url = StringField('FHIR Server URL', validators=[URL(), Optional()],
render_kw={'placeholder': 'e.g., https://hapi.fhir.org/baseR4'})
auth_type = SelectField('Authentication Type (for Custom URL)', choices=[
('none', 'None'),
('bearerToken', 'Bearer Token'),
('basicAuth', 'Basic Authentication')
], default='none', validators=[Optional()])
auth_token = StringField('Bearer Token', validators=[Optional()],
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
basic_auth_username = StringField('Username', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Username'})
basic_auth_password = PasswordField('Password', validators=[Optional()],
render_kw={'placeholder': 'Enter Basic Auth Password'})
submit = SubmitField('Send Request')
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
if self.fhir_server_url.data:
if self.auth_type.data == 'bearerToken' and not self.auth_token.data:
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected for a custom URL.')
return False
if self.auth_type.data == 'basicAuth':
if not self.basic_auth_username.data:
self.basic_auth_username.errors.append('Username is required for Basic Authentication with a custom URL.')
return False
if not self.basic_auth_password.data:
self.basic_auth_password.errors.append('Password is required for Basic Authentication with a custom URL.')
return False
return True
class IgYamlForm(FlaskForm):
"""Form to select IGs for YAML generation."""
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
generate_yaml = SubmitField('Generate YAML')
# --- 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')

View File

@ -1,111 +0,0 @@
# Application Build and Run Guide - MANUAL STEPS
This guide outlines the steps to set up, build, and run the application, including the HAPI FHIR server component and the rest of the application managed via Docker Compose.
## Prerequisites
Before you begin, ensure you have the following installed on your system:
* [Git](https://git-scm.com/)
* [Maven](https://maven.apache.org/)
* [Java Development Kit (JDK)](https://www.oracle.com/java/technologies/downloads/) (Ensure compatibility with the HAPI FHIR version)
* [Docker](https://www.docker.com/products/docker-desktop/)
* [Docker Compose](https://docs.docker.com/compose/install/) (Often included with Docker Desktop)
## Setup and Build
Follow these steps to clone the necessary repository and build the components.
### 1. Clone and Build the HAPI FHIR Server
First, clone the HAPI FHIR JPA Server Starter project and build the server application.
# Step 1: Clone the repository
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver hapi-fhir-jpaserver
# Navigate into the cloned directory
cd hapi-fhir-jpaserver
copy the folder from hapi-fhir-setup/target/classes/application.yaml to the hapi-fhir-jpaserver/target/classes/application.yaml folder created above
# Step 2: Build the HAPI server package (skipping tests, using 'boot' profile)
# This creates the runnable WAR file in the 'target/' directory
mvn clean package -DskipTests=true -Pboot
# Return to the parent directory (or your project root)
cd ..
2. Build the Rest of the Application (Docker)
Next, build the Docker images for the remaining parts of the application as defined in your docker-compose.yml file. Run this command from the root directory where your docker-compose.yml file is located.
# Step 3: Build Docker images without using cache
docker-compose build --no-cache
Running the Application
Option A: Running the Full Application (Recommended)
Use Docker Compose to start all services, including (presumably) the HAPI FHIR server if it's configured in your docker-compose.yml. Run this from the root directory containing your docker-compose.yml.
# Step 4: Start all services defined in docker-compose.yml in detached mode
docker-compose up -d
Option B: Running the HAPI FHIR Server Standalone (Debugging Only)
This method runs only the HAPI FHIR server directly using the built WAR file. Use this primarily for debugging the server in isolation.
# Navigate into the HAPI server directory where you built it
cd hapi-fhir-jpaserver
# Run the WAR file directly using Java
java -jar target/ROOT.war
# Note: You might need to configure ports or database connections
# separately when running this way, depending on the application's needs.
# Remember to navigate back when done
# cd ..
Useful Docker Commands
Here are some helpful commands for interacting with your running Docker containers:
Copying files from a container:
To copy a file from a running container to your local machine's current directory:
# Syntax: docker cp <CONTAINER_ID_OR_NAME>:<PATH_IN_CONTAINER> <LOCAL_DESTINATION_PATH>
docker cp <CONTAINER_ID>:/app/PATH/Filename.ext .
(Replace <CONTAINER_ID>, /app/PATH/Filename.ext with actual values. . refers to the current directory on your host machine.)
Accessing a container's shell:
To get an interactive bash shell inside a running container:
# Syntax: docker exec -it <CONTAINER_ID_OR_NAME> bash
docker exec -it <CONTAINER_ID> bash
(Replace <CONTAINER_ID> with the actual container ID or name. You can find this using docker ps.)
Viewing running containers:
docker ps
Viewing application logs:
# Follow logs for all services
docker-compose logs -f
# Follow logs for a specific service
docker-compose logs -f <SERVICE_NAME>
(Replace <SERVICE_NAME> with the name defined in your docker-compose.yml)
Stopping the application:
To stop the services started with docker-compose up -d:
docker-compose down

View File

@ -1,342 +0,0 @@
#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir
server:
# servlet:
# context-path: /example/path
port: 8080
#Adds the option to go to eg. http://localhost:8080/actuator/health for seeing the running configuration
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
management:
#The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus, /actuator/metrics. For security purposes, only /actuator/health is enabled by default.
endpoints:
enabled-by-default: false
web:
exposure:
include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all'
endpoint:
info:
enabled: true
metrics:
enabled: true
health:
enabled: true
probes:
enabled: true
group:
liveness:
include:
- livenessState
- readinessState
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
spring:
main:
allow-circular-references: true
flyway:
enabled: false
baselineOnMigrate: true
fail-on-missing-locations: false
datasource:
#url: 'jdbc:h2:file:./target/database/h2'
url: jdbc:h2:file:/app/h2-data/fhir;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE
#url: jdbc:h2:mem:test_mem
username: sa
password: null
driverClassName: org.h2.Driver
max-active: 15
# database connection pool size
hikari:
maximum-pool-size: 10
jpa:
properties:
hibernate.format_sql: false
hibernate.show_sql: false
#Hibernate dialect is automatically detected except Postgres and H2.
#If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
#If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
# hibernate.hbm2ddl.auto: update
# hibernate.jdbc.batch_size: 20
# hibernate.cache.use_query_cache: false
# hibernate.cache.use_second_level_cache: false
# hibernate.cache.use_structured_entries: false
# hibernate.cache.use_minimal_puts: false
### These settings will enable fulltext search with lucene or elastic
hibernate.search.enabled: false
### lucene parameters
# hibernate.search.backend.type: lucene
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
# hibernate.search.backend.directory.type: local-filesystem
# hibernate.search.backend.directory.root: target/lucenefiles
# hibernate.search.backend.lucene_version: lucene_current
### elastic parameters ===> see also elasticsearch section below <===
# hibernate.search.backend.type: elasticsearch
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
hapi:
fhir:
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### Flag is false by default, can be passed as command line argument to override.
cr:
enabled: false
caregaps:
reporter: "default"
section_author: "default"
cql:
use_embedded_libraries: true
compiler:
### These are low-level compiler options.
### They are not typically needed by most users.
# validate_units: true
# verify_only: false
# compatibility_level: "1.5"
error_level: Info
signature_level: All
# analyze_data_requirements: false
# collapse_data_requirements: false
# translator_format: JSON
# enable_date_range_optimization: true
enable_annotations: true
enable_locators: true
enable_results_type: true
enable_detailed_errors: true
# disable_list_traversal: false
# disable_list_demotion: false
# enable_interval_demotion: false
# enable_interval_promotion: false
# disable_method_invocation: false
# require_from_keyword: false
# disable_default_model_info_load: false
runtime:
debug_logging_enabled: false
# enable_validation: false
# enable_expression_caching: true
terminology:
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION
valueset_membership_mode: USE_EXPANSION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_EXPANSION
code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_CODESYSTEM_URL
data:
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO, USE_VALUE_SET_URL, USE_INLINE_CODES, FILTER_IN_MEMORY
profile_mode: DECLARED # ENFORCED, DECLARED, OPTIONAL, TRUST, OFF
cdshooks:
enabled: false
clientIdHeaderName: client_id
### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html)
openapi_enabled: true
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5
fhir_version: R4
### Flag is false by default. This flag enables runtime installation of IG's.
ig_runtime_upload_enabled: false
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers
### to determine the FHIR server address
# use_apache_address_strategy: false
### forces the use of the https:// protocol for the returned server address.
### alternatively, it may be set using the X-Forwarded-Proto header.
# use_apache_address_strategy_https: false
### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of eg. /content/custom **
### Folder with custom content MUST be named custom. If omitted then default content applies
custom_content_path: ./custom
### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content
### will be served under /web/app
#app_content_path: ./configs/app
### enable to set the Server URL
# server_address: http://hapi.fhir.org/baseR4
# defer_indexing_for_codesystems_of_size: 101
### Flag is true by default. This flag filters resources during package installation, allowing only those resources with a valid status (e.g. active) to be installed.
# validate_resource_status_for_package_upload: false
# install_transitive_ig_dependencies: true
#implementationguides:
### example from registry (packages.fhir.org)
# swiss:
# name: swiss.mednet.fhir
# version: 0.8.0
# reloadExisting: false
# installMode: STORE_AND_INSTALL
# example not from registry
# ips_1_0_0:
# packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz
# name: hl7.fhir.uv.ips
# version: 1.0.0
# supported_resource_types:
# - Patient
# - Observation
##################################################
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
##################################################
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# allow_cascading_deletes: true
# allow_contains_searches: true
# allow_external_references: true
# allow_multiple_delete: true
# allow_override_default_search_params: true
# auto_create_placeholder_reference_targets: false
# mass_ingestion_mode_enabled: false
### tells the server to automatically append the current version of the target resource to references at these paths
# auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject
# ips_enabled: false
# default_encoding: JSON
# default_pretty_print: true
# default_page_size: 20
# delete_expunge_enabled: true
# enable_repository_validating_interceptor: true
# enable_index_missing_fields: false
# enable_index_of_type: true
# enable_index_contained_resource: false
# upliftedRefchains_enabled: true
# resource_dbhistory_enabled: false
### !!Extended Lucene/Elasticsearch Indexing is still a experimental feature, expect some features (e.g. _total=accurate) to not work as expected!!
### more information here: https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
advanced_lucene_indexing: false
bulk_export_enabled: false
bulk_import_enabled: false
# language_search_parameter_enabled: true
# enforce_referential_integrity_on_delete: false
# This is an experimental feature, and does not fully support _total and other FHIR features.
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
narrative_enabled: true
mdm_enabled: false
mdm_rules_json_location: "mdm-rules.json"
## see: https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#jpa-server-retry-on-version-conflicts
# userRequestRetryVersionConflictsInterceptorEnabled : false
# local_base_urls:
# - https://hapi.fhir.org/baseR4
# pre_expand_value_sets: true
# enable_task_pre_expand_value_sets: true
# pre_expand_value_sets_default_count: 1000
# pre_expand_value_sets_max_count: 1000
# maximum_expansion_size: 1000
logical_urls:
- http://terminology.hl7.org/*
- https://terminology.hl7.org/*
- http://snomed.info/*
- https://snomed.info/*
- http://unitsofmeasure.org/*
- https://unitsofmeasure.org/*
- http://loinc.org/*
- https://loinc.org/*
# partitioning:
# allow_references_across_partitions: false
# partitioning_include_in_search_hashes: false
# conditional_create_duplicate_identifiers_enabled: false
cors:
allow_Credentials: true
# These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List-
allowed_origin:
- '*'
# Search coordinator thread pool sizes
search-coord-core-pool-size: 20
search-coord-max-pool-size: 100
search-coord-queue-capacity: 200
# Search Prefetch Thresholds.
# This setting sets the number of search results to prefetch. For example, if this list
# is set to [100, 1000, -1] then the server will initially load 100 results and not
# attempt to load more. If the user requests subsequent page(s) of results and goes
# past 100 results, the system will load the next 900 (up to the following threshold of 1000).
# The system will progressively work through these thresholds.
# A threshold of -1 means to load all results. Note that if the final threshold is a
# number other than -1, the system will never prefetch more than the given number.
search_prefetch_thresholds: 13,503,2003,-1
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
#custom-bean-packages:
# comma-separated list of fully qualified interceptor classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
#custom-interceptor-classes:
# comma-separated list of fully qualified provider classes.
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
#custom-provider-classes:
# Threadpool size for BATCH'ed GETs in a bundle.
# bundle_batch_pool_size: 10
# bundle_batch_pool_max_size: 50
# logger:
# error_format: 'ERROR - ${requestVerb} ${requestUrl}'
# format: >-
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
# Operation[${operationType} ${operationName} ${idOrResourceName}]
# UA[${requestHeader.user-agent}] Params[${requestParameters}]
# ResponseEncoding[${responseEncodingNoDefault}]
# log_exceptions: true
# name: fhirtest.access
# max_binary_size: 104857600
# max_page_size: 200
# retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000
tester:
home:
name: FHIRFLARE Tester
server_address: http://localhost:8080/fhir
refuse_to_fetch_third_party_urls: false
fhir_version: R4
global:
name: Global Tester
server_address: "http://hapi.fhir.org/baseR4"
refuse_to_fetch_third_party_urls: false
fhir_version: R4
# validation:
# requests_enabled: true
# responses_enabled: true
# binary_storage_enabled: true
inline_resource_storage_below_size: 4000
# bulk_export_enabled: true
# subscription:
# resthook_enabled: true
# websocket_enabled: false
# polling_interval_ms: 5000
# immediately_queued: false
# email:
# from: some@test.com
# host: google.com
# port:
# username:
# password:
# auth:
# startTlsEnable:
# startTlsRequired:
# quitWait:
# lastn_enabled: true
# store_resource_in_lucene_index_enabled: true
### This is configuration for normalized quantity search level default is 0
### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
# normalized_quantity_search_level: 2
#elasticsearch:
# debug:
# pretty_print_json_log: false
# refresh_after_write: false
# enabled: false
# password: SomePassword
# required_index_status: YELLOW
# rest_url: 'localhost:9200'
# protocol: 'http'
# schema_management_strategy: CREATE
# username: SomeUsername

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
Single-database configuration for Flask.

View File

@ -1,50 +0,0 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,113 +0,0 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -1,123 +0,0 @@
from flask import Blueprint, jsonify, current_app, render_template
import os
import tarfile
import json
from datetime import datetime
import time
from services import pkg_version, safe_parse_version
package_bp = Blueprint('package', __name__)
@package_bp.route('/logs/<name>')
def logs(name):
"""
Fetch logs for a package, listing each version with its publication date.
Args:
name (str): The name of the package.
Returns:
Rendered template with logs or an error message.
"""
try:
in_memory_cache = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
if not in_memory_cache:
current_app.logger.error(f"No in-memory cache found for package logs: {name}")
return "<p class='text-muted'>Package cache not found.</p>"
package_data = next((pkg for pkg in in_memory_cache if isinstance(pkg, dict) and pkg.get('name', '').lower() == name.lower()), None)
if not package_data:
current_app.logger.error(f"Package not found in cache: {name}")
return "<p class='text-muted'>Package not found.</p>"
# Get the versions list with pubDate
versions = package_data.get('all_versions', [])
if not versions:
current_app.logger.warning(f"No versions found for package: {name}. Package data: {package_data}")
return "<p class='text-muted'>No version history found for this package.</p>"
current_app.logger.debug(f"Found {len(versions)} versions for package {name}: {versions[:5]}...")
logs = []
now = time.time()
for version_info in versions:
if not isinstance(version_info, dict):
current_app.logger.warning(f"Invalid version info for {name}: {version_info}")
continue
version = version_info.get('version', '')
pub_date_str = version_info.get('pubDate', '')
if not version or not pub_date_str:
current_app.logger.warning(f"Skipping version info with missing version or pubDate: {version_info}")
continue
# Parse pubDate and calculate "when"
when = "Unknown"
try:
pub_date = datetime.strptime(pub_date_str, "%a, %d %b %Y %H:%M:%S %Z")
pub_time = pub_date.timestamp()
time_diff = now - pub_time
days_ago = int(time_diff / 86400)
if days_ago < 1:
hours_ago = int(time_diff / 3600)
if hours_ago < 1:
minutes_ago = int(time_diff / 60)
when = f"{minutes_ago} minute{'s' if minutes_ago != 1 else ''} ago"
else:
when = f"{hours_ago} hour{'s' if hours_ago != 1 else ''} ago"
else:
when = f"{days_ago} day{'s' if days_ago != 1 else ''} ago"
except ValueError as e:
current_app.logger.warning(f"Failed to parse pubDate '{pub_date_str}' for version {version}: {e}")
logs.append({
"version": version,
"pubDate": pub_date_str,
"when": when
})
if not logs:
current_app.logger.warning(f"No valid version entries with pubDate for package: {name}")
return "<p class='text-muted'>No version history found for this package.</p>"
# Sort logs by version number (newest first)
logs.sort(key=lambda x: safe_parse_version(x.get('version', '0.0.0a0')), reverse=True)
current_app.logger.debug(f"Rendering logs for {name} with {len(logs)} entries")
return render_template('package.logs.html', logs=logs)
except Exception as e:
current_app.logger.error(f"Error in logs endpoint for {name}: {str(e)}", exc_info=True)
return "<p class='text-danger'>Error loading version history.</p>", 500
@package_bp.route('/dependents/<name>')
def dependents(name):
"""
HTMX endpoint to fetch packages that depend on the current package.
Returns an HTML fragment with a table of dependent packages.
"""
in_memory_cache = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
package_data = next((pkg for pkg in in_memory_cache if isinstance(pkg, dict) and pkg.get('name', '').lower() == name.lower()), None)
if not package_data:
return "<p class='text-danger'>Package not found.</p>"
# Find dependents: packages whose dependencies include the current package
dependents = []
for pkg in in_memory_cache:
if not isinstance(pkg, dict):
continue
dependencies = pkg.get('dependencies', [])
for dep in dependencies:
dep_name = dep.get('name', '')
if dep_name.lower() == name.lower():
dependents.append({
"name": pkg.get('name', 'Unknown'),
"version": pkg.get('latest_absolute_version', 'N/A'),
"author": pkg.get('author', 'N/A'),
"fhir_version": pkg.get('fhir_version', 'N/A'),
"version_count": pkg.get('version_count', 0),
"canonical": pkg.get('canonical', 'N/A')
})
break
return render_template('package.dependents.html', dependents=dependents)

View File

@ -5,10 +5,3 @@ requests==2.31.0
Flask-WTF==1.2.1 Flask-WTF==1.2.1
WTForms==3.1.2 WTForms==3.1.2
Pytest Pytest
pyyaml==6.0.1
fhir.resources==8.0.0
Flask-Migrate==4.1.0
cachetools
beautifulsoup4
feedparser==6.0.11
flasgger

162
routes.py Normal file
View File

@ -0,0 +1,162 @@
# app/modules/fhir_ig_importer/routes.py
import requests
import os
import tarfile # Needed for find_and_extract_sd
import gzip
import json
import io
import re
from flask import (render_template, redirect, url_for, flash, request,
current_app, jsonify, send_file)
from flask_login import login_required
from app.decorators import admin_required
from werkzeug.utils import secure_filename
from . import bp
from .forms import IgImportForm
# Import the services module
from . import services
# Import ProcessedIg model for get_structure_definition
from app.models import ProcessedIg
from app import db
# --- Helper: Find/Extract SD ---
# Moved from services.py to be local to routes that use it, or keep in services and call services.find_and_extract_sd
def find_and_extract_sd(tgz_path, resource_identifier):
"""Helper to find and extract SD json from a given tgz path by ID, Name, or Type."""
sd_data = None; found_path = None; logger = current_app.logger # Use current_app logger
if not tgz_path or not os.path.exists(tgz_path): logger.error(f"File not found in find_and_extract_sd: {tgz_path}"); return None, None
try:
with tarfile.open(tgz_path, "r:gz") as tar:
logger.debug(f"Searching for SD matching '{resource_identifier}' in {os.path.basename(tgz_path)}")
for member in tar:
if member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json'):
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']: continue
fileobj = None
try:
fileobj = tar.extractfile(member)
if fileobj:
content_bytes = fileobj.read(); content_string = content_bytes.decode('utf-8-sig'); data = json.loads(content_string)
if isinstance(data, dict) and data.get('resourceType') == 'StructureDefinition':
sd_id = data.get('id'); sd_name = data.get('name'); sd_type = data.get('type')
if resource_identifier == sd_type or resource_identifier == sd_id or resource_identifier == sd_name:
sd_data = data; found_path = member.name; logger.info(f"Found matching SD for '{resource_identifier}' at path: {found_path}"); break
except Exception as e: logger.warning(f"Could not read/parse potential SD {member.name}: {e}")
finally:
if fileobj: fileobj.close()
if sd_data is None: logger.warning(f"SD matching '{resource_identifier}' not found within archive {os.path.basename(tgz_path)}")
except Exception as e: logger.error(f"Error reading archive {tgz_path} in find_and_extract_sd: {e}", exc_info=True); raise
return sd_data, found_path
# --- End Helper ---
# --- Route for the main import page ---
@bp.route('/import-ig', methods=['GET', 'POST'])
@login_required
@admin_required
def import_ig():
"""Handles FHIR IG recursive download using services."""
form = IgImportForm()
template_context = {"title": "Import FHIR IG", "form": form, "results": None }
if form.validate_on_submit():
package_name = form.package_name.data; package_version = form.package_version.data
template_context.update(package_name=package_name, package_version=package_version)
flash(f"Starting full import for {package_name}#{package_version}...", "info"); current_app.logger.info(f"Calling import service for: {package_name}#{package_version}")
try:
# Call the CORRECT orchestrator service function
import_results = services.import_package_and_dependencies(package_name, package_version)
template_context["results"] = import_results
# Flash summary messages
dl_count = len(import_results.get('downloaded', {})); proc_count = len(import_results.get('processed', set())); error_count = len(import_results.get('errors', []))
if dl_count > 0: flash(f"Downloaded/verified {dl_count} package file(s).", "success")
if proc_count < dl_count and dl_count > 0 : flash(f"Dependency data extraction failed for {dl_count - proc_count} package(s).", "warning")
if error_count > 0: flash(f"{error_count} total error(s) occurred.", "danger")
elif dl_count == 0 and error_count == 0: flash("No packages needed downloading or initial package failed.", "info")
elif error_count == 0: flash("Import process completed successfully.", "success")
except Exception as e:
fatal_error = f"Critical unexpected error during import: {e}"; template_context["fatal_error"] = fatal_error; current_app.logger.error(f"Critical import error: {e}", exc_info=True); flash(fatal_error, "danger")
return render_template('fhir_ig_importer/import_ig_page.html', **template_context)
return render_template('fhir_ig_importer/import_ig_page.html', **template_context)
# --- Route to get StructureDefinition elements ---
@bp.route('/get-structure')
@login_required
@admin_required
def get_structure_definition():
"""API endpoint to fetch SD elements and pre-calculated Must Support paths."""
package_name = request.args.get('package_name'); package_version = request.args.get('package_version'); resource_identifier = request.args.get('resource_type')
error_response_data = {"elements": [], "must_support_paths": []}
if not all([package_name, package_version, resource_identifier]): error_response_data["error"] = "Missing query parameters"; return jsonify(error_response_data), 400
current_app.logger.info(f"Request for structure: {package_name}#{package_version} / {resource_identifier}")
# Find the primary package file
package_dir_name = 'fhir_packages'; download_dir = os.path.join(current_app.instance_path, package_dir_name)
# Use service helper for consistency
filename = services._construct_tgz_filename(package_name, package_version)
tgz_path = os.path.join(download_dir, filename)
if not os.path.exists(tgz_path): error_response_data["error"] = f"Package file not found: {filename}"; return jsonify(error_response_data), 404
sd_data = None; found_path = None; error_msg = None
try:
# Call the local helper function correctly
sd_data, found_path = find_and_extract_sd(tgz_path, resource_identifier)
# Fallback check
if sd_data is None:
core_pkg_name = "hl7.fhir.r4.core"; core_pkg_version = "4.0.1" # TODO: Make dynamic
core_filename = services._construct_tgz_filename(core_pkg_name, core_pkg_version)
core_tgz_path = os.path.join(download_dir, core_filename)
if os.path.exists(core_tgz_path):
current_app.logger.info(f"Trying fallback search in {core_pkg_name}...")
sd_data, found_path = find_and_extract_sd(core_tgz_path, resource_identifier) # Call local helper
else: current_app.logger.warning(f"Core package {core_tgz_path} not found.")
except Exception as e:
error_msg = f"Error searching package(s): {e}"; current_app.logger.error(error_msg, exc_info=True); error_response_data["error"] = error_msg; return jsonify(error_response_data), 500
if sd_data is None: error_msg = f"SD for '{resource_identifier}' not found."; error_response_data["error"] = error_msg; return jsonify(error_response_data), 404
# Extract elements
elements = sd_data.get('snapshot', {}).get('element', [])
if not elements: elements = sd_data.get('differential', {}).get('element', [])
# Fetch pre-calculated Must Support paths from DB
must_support_paths = [];
try:
stmt = db.select(ProcessedIg).filter_by(package_name=package_name, package_version=package_version); processed_ig_record = db.session.scalar(stmt)
if processed_ig_record: all_ms_paths_dict = processed_ig_record.must_support_elements; must_support_paths = all_ms_paths_dict.get(resource_identifier, [])
else: current_app.logger.warning(f"No ProcessedIg record found for {package_name}#{package_version}")
except Exception as e: current_app.logger.error(f"Error fetching MS paths from DB: {e}", exc_info=True)
current_app.logger.info(f"Returning {len(elements)} elements for {resource_identifier} from {found_path or 'Unknown File'}")
return jsonify({"elements": elements, "must_support_paths": must_support_paths})
# --- Route to get raw example file content ---
@bp.route('/get-example')
@login_required
@admin_required
def get_example_content():
# ... (Function remains the same as response #147) ...
package_name = request.args.get('package_name'); package_version = request.args.get('package_version'); example_member_path = request.args.get('filename')
if not all([package_name, package_version, example_member_path]): return jsonify({"error": "Missing query parameters"}), 400
current_app.logger.info(f"Request for example: {package_name}#{package_version} / {example_member_path}")
package_dir_name = 'fhir_packages'; download_dir = os.path.join(current_app.instance_path, package_dir_name)
pkg_filename = services._construct_tgz_filename(package_name, package_version) # Use service helper
tgz_path = os.path.join(download_dir, pkg_filename)
if not os.path.exists(tgz_path): return jsonify({"error": f"Package file not found: {pkg_filename}"}), 404
# Basic security check on member path
safe_member_path = secure_filename(example_member_path.replace("package/","")) # Allow paths within package/
if not example_member_path.startswith('package/') or '..' in example_member_path: return jsonify({"error": "Invalid example file path."}), 400
try:
with tarfile.open(tgz_path, "r:gz") as tar:
try: example_member = tar.getmember(example_member_path) # Use original path here
except KeyError: return jsonify({"error": f"Example file '{example_member_path}' not found."}), 404
example_fileobj = tar.extractfile(example_member)
if not example_fileobj: return jsonify({"error": "Could not extract example file."}), 500
try: content_bytes = example_fileobj.read()
finally: example_fileobj.close()
return content_bytes # Return raw bytes
except tarfile.TarError as e: err_msg = f"Error reading {tgz_path}: {e}"; current_app.logger.error(err_msg); return jsonify({"error": err_msg}), 500
except Exception as e: err_msg = f"Unexpected error getting example {example_member_path}: {e}"; current_app.logger.error(err_msg, exc_info=True); return jsonify({"error": err_msg}), 500

File diff suppressed because it is too large Load Diff

View File

@ -1,395 +0,0 @@
#!/bin/bash
# --- Configuration ---
REPO_URL_HAPI="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
REPO_URL_CANDLE="https://github.com/FHIR/fhir-candle.git"
CLONE_DIR_HAPI="hapi-fhir-jpaserver"
CLONE_DIR_CANDLE="fhir-candle"
SOURCE_CONFIG_DIR="hapi-fhir-Setup"
CONFIG_FILE="application.yaml"
# --- Define Paths ---
SOURCE_CONFIG_PATH="../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
DEST_CONFIG_PATH="${CLONE_DIR_HAPI}/target/classes/${CONFIG_FILE}"
APP_MODE=""
CUSTOM_FHIR_URL_VAL=""
SERVER_TYPE=""
CANDLE_FHIR_VERSION=""
# --- Error Handling Function ---
handle_error() {
echo "------------------------------------"
echo "An error occurred: $1"
echo "Script aborted."
echo "------------------------------------"
exit 1
}
# === MODIFIED: Prompt for Installation Mode ===
get_mode_choice() {
echo ""
echo "Select Installation Mode:"
echo "1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet 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
read -r -p "Enter your choice (1, 2, 3, or 4): " choice
case "$choice" in
1)
APP_MODE="lite"
break
;;
2)
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
;;
*)
echo "Invalid input. Please try again."
;;
esac
done
echo "Selected Mode: $APP_MODE"
echo "Server Type: $SERVER_TYPE"
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
get_mode_choice
# === Conditionally Execute Server Setup ===
case "$SERVER_TYPE" in
"hapi")
echo "Running Hapi server setup..."
echo
# --- Step 0: Clean up previous clone (optional) ---
echo "Checking for existing directory: $CLONE_DIR_HAPI"
if [ -d "$CLONE_DIR_HAPI" ]; then
echo "Found existing directory, removing it..."
rm -rf "$CLONE_DIR_HAPI"
if [ $? -ne 0 ]; then
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
echo
# --- Step 1: Clone the HAPI FHIR server repository ---
echo "Cloning repository: $REPO_URL_HAPI into $CLONE_DIR_HAPI..."
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_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."
else
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
echo
# --- Step 5: Navigate back to the parent directory ---
echo "===> Changing directory back (Step 5)..."
cd .. || handle_error "Failed to change back to the parent directory."
echo "Current directory: $(pwd)"
echo
;;
"candle")
echo "Running FHIR Candle server setup..."
echo
# --- Step 0: Clean up previous clone (optional) ---
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_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"
version: '3.8'
services:
fhirflare:
build:
context: .
dockerfile: ${DOCKERFILE_TO_USE}
ports:
EOF
if [ "$SERVER_TYPE" = "candle" ]; then
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- "5000:5000"
- "5001:5826"
EOF
else
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- "5000:5000"
- "8080:8080"
EOF
fi
cat << EOF >> "$DOCKER_COMPOSE_TMP"
volumes:
- ./instance:/app/instance
- ./static/uploads:/app/static/uploads
- ./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:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=${APP_MODE}
- APP_BASE_URL=http://localhost:5000
- 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
EOF
if [ ! -f "$DOCKER_COMPOSE_TMP" ]; then
handle_error "Failed to create temporary docker-compose file ($DOCKER_COMPOSE_TMP)."
fi
# Replace the original docker-compose.yml
mv "$DOCKER_COMPOSE_TMP" "$DOCKER_COMPOSE_ORIG"
echo "docker-compose.yml updated successfully."
echo
# --- Step 6: Build Docker images ---
echo "===> Starting Docker build (Step 6)..."
docker-compose build --no-cache
if [ $? -ne 0 ]; then
handle_error "Docker Compose build failed. Check Docker installation and docker-compose.yml file."
fi
echo "Docker images built successfully."
echo
# --- Step 7: Start Docker containers ---
echo "===> Starting Docker containers (Step 7)..."
docker-compose up -d
if [ $? -ne 0 ]; then
handle_error "Docker Compose up failed. Check Docker installation and container configurations."
fi
echo "Docker containers started successfully."
echo
echo "===================================="
echo "Script finished successfully! (Mode: $APP_MODE)"
echo "===================================="
exit 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 706 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,390 +0,0 @@
body {
width: 100%;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Apply overflow and height constraints only for fire animation page
body.fire-animation-page {
overflow: hidden;
height: 100vh;
}
*/
/* Fire animation overlay */
.fire-on {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(#1d4456, #112630);
opacity: 1;
z-index: 1;
transition: all 1200ms linear;
}
.section-center {
position: relative;
width: 300px; /* Reduced size for landing page */
height: 300px;
margin: 0 auto;
display: block;
overflow: hidden;
border: 8px solid rgba(0,0,0,.2);
border-radius: 50%;
z-index: 5;
background-color: #1d4456;
box-shadow: 0 0 50px 5px rgba(255,148,0,.1);
transition: all 500ms linear;
}
/* Wood and star using local images */
.wood {
position: absolute;
z-index: 21;
left: 50%;
bottom: 12%;
width: 80px;
margin-left: -40px;
height: 30px;
background-image: url('{{ url_for('static', filename='img/wood.png') }}');
background-size: 80px 30px;
border-radius: 5px;
}
.star {
z-index: 2;
position: absolute;
top: 138px;
left: 18px;
background-image: url('{{ url_for('static', filename='img/star.png') }}');
background-size: 11px 11px;
width: 11px;
height: 11px;
opacity: 0.4;
animation: starShine 3.5s linear infinite;
transition: all 1200ms linear;
}
.wood-circle {
position: absolute;
z-index: 20;
left: 50%;
bottom: 11%;
width: 100px;
margin-left: -50px;
height: 20px;
border-radius: 100%;
background-color: #0a171d;
}
.circle {
position: absolute;
z-index: 6;
right: -225px;
bottom: -337px;
width: 562px;
height: 525px;
border-radius: 100%;
background-color: #112630;
}
/* Moon */
.moon {
position: absolute;
top: 37px;
left: 86px;
width: 60px;
height: 60px;
background-color: #b2b7bc;
border-radius: 50%;
box-shadow: inset -15px 1.5px 0 0px #c0c3c9, 0 0 7px 3px rgba(228,228,222,.4);
z-index: 1;
animation: brilla-moon 4s alternate infinite;
transition: all 2000ms linear;
}
.moon div:nth-child(1) {
position: absolute;
top: 50%;
left: 10%;
width: 12%;
height: 12%;
border-radius: 50%;
border: 1px solid #adaca2;
box-shadow: inset 1.5px -0.75px 0 0px #85868b;
opacity: 0.4;
}
.moon div:nth-child(2) {
position: absolute;
top: 20%;
left: 38%;
width: 16%;
height: 16%;
border-radius: 50%;
border: 1px solid #adaca2;
box-shadow: inset 1.5px -0.75px 0 0px #85868b;
opacity: 0.4;
}
.moon div:nth-child(3) {
position: absolute;
top: 60%;
left: 45%;
width: 20%;
height: 20%;
border-radius: 50%;
border: 1px solid #adaca2;
box-shadow: inset 1.5px -0.75px 0 0px #85868b;
opacity: 0.4;
}
@keyframes brilla-moon {
0% { box-shadow: inset -15px 1.5px 0 0px #c0c3c9, 0 0 7px 3px rgba(228,228,222,.4); }
50% { box-shadow: inset -15px 1.5px 0 0px #c0c3c9, 0 0 11px 6px rgba(228,228,222,.4); }
}
/* Shooting stars */
.shooting-star {
z-index: 2;
width: 1px;
height: 37px;
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
position: absolute;
top: 0;
left: -52px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), white);
animation: animShootingStar 6s linear infinite;
transition: all 2000ms linear;
}
@keyframes animShootingStar {
from { transform: translateY(0px) translateX(0px) rotate(-45deg); opacity: 1; height: 3px; }
to { transform: translateY(960px) translateX(960px) rotate(-45deg); opacity: 1; height: 600px; }
}
.shooting-star-2 {
z-index: 2;
width: 1px;
height: 37px;
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
position: absolute;
top: 0;
left: 150px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), white);
animation: animShootingStar-2 9s linear infinite;
transition: all 2000ms linear;
}
@keyframes animShootingStar-2 {
from { transform: translateY(0px) translateX(0px) rotate(-45deg); opacity: 1; height: 3px; }
to { transform: translateY(1440px) translateX(1440px) rotate(-45deg); opacity: 1; height: 600px; }
}
/* Stars */
.star.snd { top: 75px; left: 232px; animation-delay: 1s; }
.star.trd { top: 97px; left: 75px; animation-delay: 1.4s; }
.star.fth { top: 15px; left: 150px; animation-delay: 1.8s; }
.star.fith { top: 63px; left: 165px; animation-delay: 2.2s; }
@keyframes starShine {
0% { transform: scale(0.3) rotate(0deg); opacity: 0.4; }
25% { transform: scale(1) rotate(360deg); opacity: 1; }
50% { transform: scale(0.3) rotate(720deg); opacity: 0.4; }
100% { transform: scale(0.3) rotate(0deg); opacity: 0.4; }
}
/* Trees */
.tree-1 {
position: relative;
top: 112px;
left: 37px;
width: 0;
height: 0;
z-index: 8;
border-bottom: 67px solid #0a171d;
border-left: 22px solid transparent;
border-right: 22px solid transparent;
}
.tree-1:before {
position: absolute;
bottom: -82px;
left: 50%;
margin-left: -3px;
width: 6px;
height: 22px;
z-index: 7;
content: '';
background-color: #000;
}
.tree-2 {
position: relative;
top: 0;
left: 187px;
width: 0;
height: 0;
z-index: 8;
border-bottom: 67px solid #0a171d;
border-left: 22px solid transparent;
border-right: 22px solid transparent;
}
.tree-2:before {
position: absolute;
bottom: -82px;
left: 50%;
margin-left: -3px;
width: 6px;
height: 22px;
z-index: 7;
content: '';
background-color: #000;
}
/* Fire */
.fire {
position: absolute;
z-index: 39;
width: 2px;
margin-left: -1px;
left: 50%;
bottom: 60px;
transition: all 1200ms linear;
}
.fire span {
display: block;
position: absolute;
bottom: -11px;
margin-left: -15px;
height: 0;
width: 0;
border: 22px solid #febd08; /* Main flame: yellow-orange */
border-radius: 50%;
border-top-left-radius: 0;
left: -6px;
box-shadow: 0 0 7px 3px rgba(244,110,28,0.8), 0 0 15px 7px rgba(244,110,28,0.6), 0 0 22px 11px rgba(244,110,28,0.3);
transform: scale(0.45, 0.75) rotate(45deg);
animation: brilla-fire 2.5s alternate infinite;
z-index: 9;
transition: all 1200ms linear;
}
.fire span:nth-child(2) {
left: -16px;
border: 22px solid #e63946; /* Outside flame: red */
box-shadow: 0 0 7px 3px rgba(230,57,70,0.8), 0 0 15px 7px rgba(230,57,70,0.6), 0 0 22px 11px rgba(230,57,70,0.3);
transform: scale(0.3, 0.55) rotate(15deg);
z-index: 8;
animation: brilla-fire-red 1.5s alternate infinite;
}
.fire span:nth-child(3) {
left: 3px;
border: 22px solid #e63946; /* Outside flame: red */
box-shadow: 0 0 7px 3px rgba(230,57,70,0.8), 0 0 15px 7px rgba(230,57,70,0.6), 0 0 22px 11px rgba(230,57,70,0.3);
transform: scale(0.3, 0.55) rotate(80deg);
z-index: 8;
animation: brilla-fire-red 2s alternate infinite;
}
.fire span:after {
display: block;
position: absolute;
bottom: -22px;
content: '';
margin-left: -3px;
height: 22px;
width: 9px;
background-color: rgba(244,110,28,0.7);
border-radius: 80px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: 0 0 15px 7px rgba(244,110,28,0.7);
left: -6px;
opacity: 0.8;
transform: rotate(-50deg);
}
.fire span:nth-child(2):after {
background-color: rgba(230,57,70,0.7); /* Match red flame */
box-shadow: 0 0 15px 7px rgba(230,57,70,0.7);
}
.fire span:nth-child(3):after {
background-color: rgba(230,57,70,0.7); /* Match red flame */
box-shadow: 0 0 15px 7px rgba(230,57,70,0.7);
}
@keyframes brilla-fire {
0%, 100% { box-shadow: 0 0 7px 3px rgba(244,110,28,0.8), 0 0 15px 7px rgba(244,110,28,0.6), 0 0 22px 11px rgba(244,110,28,0.3); }
50% { box-shadow: 0 0 10px 5px rgba(244,110,28,0.8), 0 0 21px 10px rgba(244,110,28,0.6), 0 0 31px 15px rgba(244,110,28,0.3); }
}
@keyframes brilla-fire-red {
0%, 100% { box-shadow: 0 0 7px 3px rgba(230,57,70,0.8), 0 0 15px 7px rgba(230,57,70,0.6), 0 0 22px 11px rgba(230,57,70,0.3); }
50% { box-shadow: 0 0 10px 5px rgba(230,57,70,0.8), 0 0 21px 10px rgba(230,57,70,0.6), 0 0 31px 15px rgba(230,57,70,0.3); }
}
/* Smoke */
.smoke {
position: absolute;
z-index: 40;
width: 2px;
margin-left: -1px;
left: 50%;
bottom: 79px;
opacity: 0;
transition: all 800ms linear;
}
.smoke span {
display: block;
position: absolute;
bottom: -26px;
left: 50%;
margin-left: -15px;
height: 0;
width: 0;
border: 22px solid rgba(0, 0, 0, .6);
border-radius: 16px;
border-bottom-left-radius: 0;
border-top-right-radius: 0;
left: -6px;
opacity: 0;
transform: scale(0.2, 0.2) rotate(-45deg);
}
@keyframes smokeLeft {
0% { transform: scale(0.2, 0.2) translate(0, 0) rotate(-45deg); }
10% { opacity: 1; transform: scale(0.2, 0.3) translate(0, -3px) rotate(-45deg); }
60% { opacity: 0.6; transform: scale(0.3, 0.5) translate(-7px, -60px) rotate(-45deg); }
100% { opacity: 0; transform: scale(0.4, 0.8) translate(-15px, -90px) rotate(-45deg); }
}
@keyframes smokeRight {
0% { transform: scale(0.2, 0.2) translate(0, 0) rotate(-45deg); }
10% { opacity: 1; transform: scale(0.2, 0.3) translate(0, -3px) rotate(-45deg); }
60% { opacity: 0.6; transform: scale(0.3, 0.5) translate(7px, -60px) rotate(-45deg); }
100% { opacity: 0; transform: scale(0.4, 0.8) translate(15px, -90px) rotate(-45deg); }
}
.smoke .s-0 { animation: smokeLeft 7s 0s infinite; }
.smoke .s-1 { animation: smokeRight 7s 0.7s infinite; }
.smoke .s-2 { animation: smokeLeft 7s 1.4s infinite; }
.smoke .s-3 { animation: smokeRight 7s 2.1s infinite; }
.smoke .s-4 { animation: smokeLeft 7s 2.8s infinite; }
.smoke .s-5 { animation: smokeRight 7s 3.5s infinite; }
.smoke .s-6 { animation: smokeLeft 7s 4.2s infinite; }
.smoke .s-7 { animation: smokeRight 7s 4.9s infinite; }
.smoke .s-8 { animation: smokeLeft 7s 5.6s infinite; }
.smoke .s-9 { animation: smokeRight 7s 6.3s infinite; }
/* Fire-off state (light theme) */
body:not(.fire-on) .section-center { box-shadow: 0 0 50px 5px rgba(200,200,200,.2); }
body:not(.fire-on) .smoke { opacity: 1; transition-delay: 0.8s; }
body:not(.fire-on) .fire span { bottom: -26px; transform: scale(0.15, 0.15) rotate(45deg); }

View File

@ -1,223 +0,0 @@
/* /app/static/css/fire-animation.css */
/* Removed html, body, stage styles */
.minifire-container { /* Add a wrapper for positioning/sizing */
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 130px; /* Overall height of the animation area */
overflow: hidden; /* Hide parts extending beyond the container */
background-color: #270537; /* Optional: Add a background */
border-radius: 4px;
border: 1px solid #444;
margin-top: 0.5rem; /* Space above animation */
}
.minifire-campfire {
position: relative;
/* Base size significantly reduced (original was 600px) */
width: 150px;
height: 150px;
transform-origin: bottom center;
/* Scale down slightly more if needed, adjusted positioning based on origin */
transform: scale(0.8) translateY(15px); /* Pushes it down slightly */
}
/* --- Scaled Down Logs --- */
.minifire-log {
position: absolute;
width: 60px; /* 238/4 */
height: 18px; /* 70/4 */
border-radius: 8px; /* 32/4 */
background: #781e20;
overflow: hidden;
opacity: 0.99;
transform-origin: center center;
box-shadow: 0 0 1px 0.5px rgba(0,0,0,0.15); /* Scaled shadow */
}
.minifire-log:before {
content: '';
display: block;
position: absolute;
top: 50%;
left: 9px; /* 35/4 */
width: 2px; /* 8/4 */
height: 2px; /* 8/4 */
border-radius: 8px; /* 32/4 */
background: #b35050;
transform: translate(-50%, -50%);
z-index: 3;
/* Scaled box-shadows */
box-shadow: 0 0 0 0.5px #781e20, /* 2.5/4 -> 0.6 -> 0.5 */
0 0 0 2.5px #b35050, /* 10.5/4 -> 2.6 -> 2.5 */
0 0 0 3.5px #781e20, /* 13/4 -> 3.25 -> 3.5 */
0 0 0 5.5px #b35050, /* 21/4 -> 5.25 -> 5.5 */
0 0 0 6px #781e20, /* 23.5/4 -> 5.9 -> 6 */
0 0 0 8px #b35050; /* 31.5/4 -> 7.9 -> 8 */
}
.minifire-streak {
position: absolute;
height: 1px; /* Min height */
border-radius: 5px; /* 20/4 */
background: #b35050;
}
/* Scaled streaks */
.minifire-streak:nth-child(1) { top: 3px; width: 23px; } /* 10/4, 90/4 */
.minifire-streak:nth-child(2) { top: 3px; left: 25px; width: 20px; } /* 10/4, 100/4, 80/4 */
.minifire-streak:nth-child(3) { top: 3px; left: 48px; width: 8px; } /* 10/4, 190/4, 30/4 */
.minifire-streak:nth-child(4) { top: 6px; width: 33px; } /* 22/4, 132/4 */
.minifire-streak:nth-child(5) { top: 6px; left: 36px; width: 12px; } /* 22/4, 142/4, 48/4 */
.minifire-streak:nth-child(6) { top: 6px; left: 50px; width: 7px; } /* 22/4, 200/4, 28/4 */
.minifire-streak:nth-child(7) { top: 9px; left: 19px; width: 40px; } /* 34/4, 74/4, 160/4 */
.minifire-streak:nth-child(8) { top: 12px; left: 28px; width: 10px; } /* 46/4, 110/4, 40/4 */
.minifire-streak:nth-child(9) { top: 12px; left: 43px; width: 14px; } /* 46/4, 170/4, 54/4 */
.minifire-streak:nth-child(10) { top: 15px; left: 23px; width: 28px; } /* 58/4, 90/4, 110/4 */
/* Scaled Log Positions (Relative to 150px campfire) */
.minifire-log:nth-child(1) { bottom: 25px; left: 25px; transform: rotate(150deg) scaleX(0.75); z-index: 20; } /* 100/4, 100/4 */
.minifire-log:nth-child(2) { bottom: 30px; left: 35px; transform: rotate(110deg) scaleX(0.75); z-index: 10; } /* 120/4, 140/4 */
.minifire-log:nth-child(3) { bottom: 25px; left: 17px; transform: rotate(-10deg) scaleX(0.75); } /* 98/4, 68/4 */
.minifire-log:nth-child(4) { bottom: 20px; left: 55px; transform: rotate(-120deg) scaleX(0.75); z-index: 26; } /* 80/4, 220/4 */
.minifire-log:nth-child(5) { bottom: 19px; left: 53px; transform: rotate(-30deg) scaleX(0.75); z-index: 25; } /* 75/4, 210/4 */
.minifire-log:nth-child(6) { bottom: 23px; left: 70px; transform: rotate(35deg) scaleX(0.85); z-index: 30; } /* 92/4, 280/4 */
.minifire-log:nth-child(7) { bottom: 18px; left: 75px; transform: rotate(-30deg) scaleX(0.75); z-index: 20; } /* 70/4, 300/4 */
/* --- Scaled Down Sticks --- */
.minifire-stick {
position: absolute;
width: 17px; /* 68/4 */
height: 5px; /* 20/4 */
border-radius: 3px; /* 10/4 */
box-shadow: 0 0 1px 0.5px rgba(0,0,0,0.1);
background: #781e20;
transform-origin: center center;
}
.minifire-stick:before {
content: '';
display: block;
position: absolute;
bottom: 100%;
left: 7px; /* 30/4 -> 7.5 */
width: 1.5px; /* 6/4 */
height: 5px; /* 20/4 */
background: #781e20;
border-radius: 3px; /* 10/4 */
transform: translateY(50%) rotate(32deg);
}
.minifire-stick:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 5px; /* 20/4 */
height: 5px; /* 20/4 */
background: #b35050;
border-radius: 3px; /* 10/4 */
}
/* Scaled Stick Positions */
.minifire-stick:nth-child(1) { left: 40px; bottom: 41px; transform: rotate(-152deg) scaleX(0.8); z-index: 12; } /* 158/4, 164/4 */
.minifire-stick:nth-child(2) { left: 45px; bottom: 8px; transform: rotate(20deg) scaleX(0.9); } /* 180/4, 30/4 */
.minifire-stick:nth-child(3) { left: 100px; bottom: 10px; transform: rotate(170deg) scaleX(0.9); } /* 400/4, 38/4 */
.minifire-stick:nth-child(3):before { display: none; }
.minifire-stick:nth-child(4) { left: 93px; bottom: 38px; transform: rotate(80deg) scaleX(0.9); z-index: 20; } /* 370/4, 150/4 */
.minifire-stick:nth-child(4):before { display: none; }
/* --- Scaled Down Fire --- */
.minifire-fire .minifire-flame {
position: absolute;
transform-origin: bottom center;
opacity: 0.9;
}
/* Red Flames */
.minifire-fire__red .minifire-flame {
width: 12px; /* 48/4 */
border-radius: 12px; /* 48/4 */
background: #e20f00;
box-shadow: 0 0 20px 5px rgba(226,15,0,0.4); /* Scaled shadow */
}
/* Scaled positions/heights */
.minifire-fire__red .minifire-flame:nth-child(1) { left: 35px; height: 40px; bottom: 25px; animation: minifire-fire 2s 0.15s ease-in-out infinite alternate; } /* 138/4, 160/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(2) { left: 47px; height: 60px; bottom: 25px; animation: minifire-fire 2s 0.35s ease-in-out infinite alternate; } /* 186/4, 240/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(3) { left: 59px; height: 75px; bottom: 25px; animation: minifire-fire 2s 0.1s ease-in-out infinite alternate; } /* 234/4, 300/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(4) { left: 71px; height: 90px; bottom: 25px; animation: minifire-fire 2s 0s ease-in-out infinite alternate; } /* 282/4, 360/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(5) { left: 83px; height: 78px; bottom: 25px; animation: minifire-fire 2s 0.45s ease-in-out infinite alternate; } /* 330/4, 310/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(6) { left: 95px; height: 58px; bottom: 25px; animation: minifire-fire 2s 0.3s ease-in-out infinite alternate; } /* 378/4, 232/4, 100/4 */
.minifire-fire__red .minifire-flame:nth-child(7) { left: 107px; height: 35px; bottom: 25px; animation: minifire-fire 2s 0.1s ease-in-out infinite alternate; } /* 426/4, 140/4, 100/4 */
/* Orange Flames */
.minifire-fire__orange .minifire-flame {
width: 12px; border-radius: 12px; background: #ff9c00;
box-shadow: 0 0 20px 5px rgba(255,156,0,0.4);
}
.minifire-fire__orange .minifire-flame:nth-child(1) { left: 35px; height: 35px; bottom: 25px; animation: minifire-fire 2s 0.05s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(2) { left: 47px; height: 53px; bottom: 25px; animation: minifire-fire 2s 0.1s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(3) { left: 59px; height: 63px; bottom: 25px; animation: minifire-fire 2s 0.35s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(4) { left: 71px; height: 75px; bottom: 25px; animation: minifire-fire 2s 0.4s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(5) { left: 83px; height: 65px; bottom: 25px; animation: minifire-fire 2s 0.5s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(6) { left: 95px; height: 51px; bottom: 25px; animation: minifire-fire 2s 0.35s ease-in-out infinite alternate; }
.minifire-fire__orange .minifire-flame:nth-child(7) { left: 107px; height: 28px; bottom: 25px; animation: minifire-fire 2s 0.1s ease-in-out infinite alternate; }
/* Yellow Flames */
.minifire-fire__yellow .minifire-flame {
width: 12px; border-radius: 12px; background: #ffeb6e;
box-shadow: 0 0 20px 5px rgba(255,235,110,0.4);
}
.minifire-fire__yellow .minifire-flame:nth-child(1) { left: 47px; height: 35px; bottom: 25px; animation: minifire-fire 2s 0.6s ease-in-out infinite alternate; }
.minifire-fire__yellow .minifire-flame:nth-child(2) { left: 59px; height: 43px; bottom: 30px; animation: minifire-fire 2s 0.4s ease-in-out infinite alternate; } /* Adjusted bottom slightly */
.minifire-fire__yellow .minifire-flame:nth-child(3) { left: 71px; height: 60px; bottom: 25px; animation: minifire-fire 2s 0.38s ease-in-out infinite alternate; }
.minifire-fire__yellow .minifire-flame:nth-child(4) { left: 83px; height: 50px; bottom: 25px; animation: minifire-fire 2s 0.22s ease-in-out infinite alternate; }
.minifire-fire__yellow .minifire-flame:nth-child(5) { left: 95px; height: 36px; bottom: 25px; animation: minifire-fire 2s 0.18s ease-in-out infinite alternate; }
/* White Flames */
.minifire-fire__white .minifire-flame {
width: 12px; border-radius: 12px; background: #fef1d9;
box-shadow: 0 0 20px 5px rgba(254,241,217,0.4);
}
.minifire-fire__white .minifire-flame:nth-child(1) { left: 39px; width: 8px; height: 25px; bottom: 25px; animation: minifire-fire 2s 0.22s ease-in-out infinite alternate; } /* Scaled width too */
.minifire-fire__white .minifire-flame:nth-child(2) { left: 45px; width: 8px; height: 30px; bottom: 25px; animation: minifire-fire 2s 0.42s ease-in-out infinite alternate; }
.minifire-fire__white .minifire-flame:nth-child(3) { left: 59px; height: 43px; bottom: 25px; animation: minifire-fire 2s 0.32s ease-in-out infinite alternate; }
.minifire-fire__white .minifire-flame:nth-child(4) { left: 71px; height: 53px; bottom: 25px; animation: minifire-fire 2s 0.8s ease-in-out infinite alternate; }
.minifire-fire__white .minifire-flame:nth-child(5) { left: 83px; height: 43px; bottom: 25px; animation: minifire-fire 2s 0.85s ease-in-out infinite alternate; }
.minifire-fire__white .minifire-flame:nth-child(6) { left: 95px; width: 8px; height: 28px; bottom: 25px; animation: minifire-fire 2s 0.64s ease-in-out infinite alternate; }
.minifire-fire__white .minifire-flame:nth-child(7) { left: 102px; width: 8px; height: 25px; bottom: 25px; animation: minifire-fire 2s 0.32s ease-in-out infinite alternate; }
/* --- Scaled Down Sparks --- */
.minifire-spark {
position: absolute;
width: 1.5px; /* 6/4 */
height: 5px; /* 20/4 */
background: #fef1d9;
border-radius: 5px; /* 18/4 -> 4.5 */
z-index: 50;
transform-origin: bottom center;
transform: scaleY(0);
}
/* Scaled spark positions/animations */
.minifire-spark:nth-child(1) { left: 40px; bottom: 53px; animation: minifire-spark 1s 0.4s linear infinite; } /* 160/4, 212/4 */
.minifire-spark:nth-child(2) { left: 45px; bottom: 60px; animation: minifire-spark 1s 1s linear infinite; } /* 180/4, 240/4 */
.minifire-spark:nth-child(3) { left: 52px; bottom: 80px; animation: minifire-spark 1s 0.8s linear infinite; } /* 208/4, 320/4 */
.minifire-spark:nth-child(4) { left: 78px; bottom: 100px; animation: minifire-spark 1s 2s linear infinite; } /* 310/4, 400/4 */
.minifire-spark:nth-child(5) { left: 90px; bottom: 95px; animation: minifire-spark 1s 0.75s linear infinite; } /* 360/4, 380/4 */
.minifire-spark:nth-child(6) { left: 98px; bottom: 80px; animation: minifire-spark 1s 0.65s linear infinite; } /* 390/4, 320/4 */
.minifire-spark:nth-child(7) { left: 100px; bottom: 70px; animation: minifire-spark 1s 1s linear infinite; } /* 400/4, 280/4 */
.minifire-spark:nth-child(8) { left: 108px; bottom: 53px; animation: minifire-spark 1s 1.4s linear infinite; } /* 430/4, 210/4 */
/* --- Keyframes (Rename to avoid conflicts) --- */
/* Use the same keyframe logic, just rename them */
@keyframes minifire-fire {
0% { transform: scaleY(1); } 28% { transform: scaleY(0.7); } 38% { transform: scaleY(0.8); } 50% { transform: scaleY(0.6); } 70% { transform: scaleY(0.95); } 82% { transform: scaleY(0.58); } 100% { transform: scaleY(1); }
}
@keyframes minifire-spark {
0%, 35% { transform: scaleY(0) translateY(0); opacity: 0; }
50% { transform: scaleY(1) translateY(0); opacity: 1; }
/* Adjusted translateY for smaller scale */
70% { transform: scaleY(1) translateY(-3px); opacity: 1; } /* 10/4 -> 2.5 -> 3 */
75% { transform: scaleY(1) translateY(-3px); opacity: 0; }
100% { transform: scaleY(0) translateY(0); opacity: 0; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,299 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FSHing Trip Comparison</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/github.min.css" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const targetElement = document.getElementById('diff');
const diff2htmlUi = new Diff2HtmlUI(targetElement);
diff2htmlUi.fileListToggle(false);
diff2htmlUi.synchronisedScroll();
diff2htmlUi.highlightCode();
const diffs = document.getElementsByClassName('d2h-file-wrapper');
for (const diff of diffs) {
diff.innerHTML = `
<details>
<summary>${diff.getElementsByClassName('d2h-file-name')[0].innerHTML}</summary>
${diff.innerHTML}
</details>`
}
});
</script>
</head>
<body style="text-align: center; font-family: 'Source Sans Pro', sans-serif">
<h1>FSHing Trip Comparison</a></h1>
<div id="diff">
<div class="d2h-file-list-wrapper d2h-light-color-scheme">
<div class="d2h-file-list-header">
<span class="d2h-file-list-title">Files changed (1)</span>
<a class="d2h-file-switch d2h-hide">hide</a>
<a class="d2h-file-switch d2h-show">show</a>
</div>
<ol class="d2h-file-list">
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-moved" height="16" title="renamed" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z"></path>
</svg> <a href="#d2h-898460" class="d2h-file-name">../tmp/{tmpkn9pyg0k/input.json → tmpb_q8uf73/fsh-generated/data}/fsh-index.json</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+10</span>
<span class="d2h-lines-deleted">-0</span>
</span>
</span>
</li>
</ol>
</div><div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-898460" class="d2h-file-wrapper" data-lang="json">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">../tmp/{tmpkn9pyg0k/input.json → tmpb_q8uf73/fsh-generated/data}/fsh-index.json</span>
<span class="d2h-tag d2h-moved d2h-moved-tag">RENAMED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -0,0 +1,10 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
1
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">[</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
2
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> {</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
3
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;outputFile&quot;: &quot;Encounter-discharge-1.json&quot;,</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
4
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;fshName&quot;: &quot;discharge-1&quot;,</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
5
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;fshType&quot;: &quot;Instance&quot;,</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
6
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;fshFile&quot;: &quot;instances&#x2F;discharge-1.fsh&quot;,</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
7
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;startLine&quot;: 1,</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
8
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> &quot;endLine&quot;: 12</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
9
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> }</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
10
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">]</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1 +0,0 @@
Alias: $v3-ActCode = http://terminology.hl7.org/CodeSystem/v3-ActCode

View File

@ -1,2 +0,0 @@
Name Type File
banks-mia-leanne Instance instances/banks-mia-leanne.fsh

View File

@ -1,16 +0,0 @@
Instance: aspirin
InstanceOf: AllergyIntolerance
Usage: #example
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-allergyintolerance"
* clinicalStatus = $allergyintolerance-clinical#active
* clinicalStatus.text = "Active"
* verificationStatus = $allergyintolerance-verification#confirmed
* verificationStatus.text = "Confirmed"
* category = #medication
* criticality = #unable-to-assess
* code = $sct#387458008
* code.text = "Aspirin allergy"
* patient = Reference(Patient/hayes-arianne)
* recordedDate = "2024-02-10"
* recorder = Reference(PractitionerRole/specialistphysicians-swanborough-erick)
* asserter = Reference(PractitionerRole/specialistphysicians-swanborough-erick)

View File

@ -1,49 +0,0 @@
Instance: banks-mia-leanne
InstanceOf: Patient
Usage: #example
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"
* extension[0].url = "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity"
* extension[=].extension.url = "value"
* extension[=].extension.valueCodeableConcept = $sct#446141000124107 "Identifies as female gender"
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-pronouns"
* extension[=].extension.url = "value"
* extension[=].extension.valueCodeableConcept = $loinc#LA29519-8 "she/her/her/hers/herself"
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender"
* extension[=].extension[0].url = "value"
* extension[=].extension[=].valueCodeableConcept = $sct#248152002
* extension[=].extension[=].valueCodeableConcept.text = "Female"
* extension[=].extension[+].url = "type"
* extension[=].extension[=].valueCodeableConcept = $sct#1515311000168102 "Biological sex at birth"
* identifier.extension[0].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-status"
* identifier.extension[=].valueCoding = $ihi-status-1#active
* identifier.extension[+].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-record-status"
* identifier.extension[=].valueCoding = $ihi-record-status-1#verified "verified"
* identifier.type = $v2-0203#NI
* identifier.type.text = "IHI"
* identifier.system = "http://ns.electronichealth.net.au/id/hi/ihi/1.0"
* identifier.value = "8003608333647261"
* name.use = #usual
* name.family = "Banks"
* name.given[0] = "Mia"
* name.given[+] = "Leanne"
* telecom[0].system = #phone
* telecom[=].value = "0270102724"
* telecom[=].use = #work
* telecom[+].system = #phone
* telecom[=].value = "0491574632"
* telecom[=].use = #mobile
* telecom[+].system = #phone
* telecom[=].value = "0270107520"
* telecom[=].use = #home
* telecom[+].system = #email
* telecom[=].value = "mia.banks@myownpersonaldomain.com"
* telecom[+].system = #phone
* telecom[=].value = "270107520"
* telecom[=].use = #home
* gender = #female
* birthDate = "1983-08-25"
* address.line = "50 Sebastien St"
* address.city = "Minjary"
* address.state = "NSW"
* address.postalCode = "2720"
* address.country = "AU"

View File

@ -1,12 +0,0 @@
Instance: discharge-1
InstanceOf: Encounter
Usage: #example
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-encounter"
* status = #finished
* class = $v3-ActCode#EMER "emergency"
* subject = Reference(Patient/ronny-irvine)
* period
* start = "2023-02-20T06:15:00+10:00"
* end = "2023-02-20T18:19:00+10:00"
* location.location = Reference(Location/murrabit-hospital)
* serviceProvider = Reference(Organization/murrabit-hospital)

View File

@ -1,15 +0,0 @@
Instance: vkc
InstanceOf: Condition
Usage: #example
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition"
* clinicalStatus = $condition-clinical#active "Active"
* category = $condition-category#encounter-diagnosis "Encounter Diagnosis"
* severity = $sct#24484000 "Severe"
* code = $sct#317349009 "Vernal keratoconjunctivitis"
* bodySite = $sct#368601006 "Entire conjunctiva of left eye"
* subject = Reference(Patient/italia-sofia)
* onsetDateTime = "2023-10-01"
* recordedDate = "2023-10-02"
* recorder = Reference(PractitionerRole/generalpractitioner-guthridge-jarred)
* asserter = Reference(PractitionerRole/generalpractitioner-guthridge-jarred)
* note.text = "Itchy and burning eye, foreign body sensation. Mucoid discharge."

View File

@ -1 +0,0 @@
{"resourceType":"Encounter","id":"discharge-1","meta":{"profile":["http://hl7.org.au/fhir/core/StructureDefinition/au-core-encounter"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\"><p class=\"res-header-id\"><b>Generated Narrative: Encounter discharge-1</b></p><a name=\"discharge-1\"> </a><a name=\"hcdischarge-1\"> </a><a name=\"discharge-1-en-AU\"> </a><div style=\"display: inline-block; background-color: #d9e0e7; padding: 6px; margin: 4px; border: 1px solid #8da1b4; border-radius: 5px; line-height: 60%\"><p style=\"margin-bottom: 0px\"/><p style=\"margin-bottom: 0px\">Profile: <a href=\"StructureDefinition-au-core-encounter.html\">AU Core Encounter</a></p></div><p><b>status</b>: Finished</p><p><b>class</b>: <a href=\"http://terminology.hl7.org/6.2.0/CodeSystem-v3-ActCode.html#v3-ActCode-EMER\">ActCode EMER</a>: emergency</p><p><b>subject</b>: <a href=\"Patient-ronny-irvine.html\">Ronny Lawrence Irvine Male, DoB: ( DVA Number:\u00a0QX827261)</a></p><p><b>period</b>: 2023-02-20 06:15:00+1000 --&gt; 2023-02-20 18:19:00+1000</p><h3>Locations</h3><table class=\"grid\"><tr><td style=\"display: none\">-</td><td><b>Location</b></td></tr><tr><td style=\"display: none\">*</td><td><a href=\"Location-murrabit-hospital.html\">Location Murrabit Public Hospital</a></td></tr></table><p><b>serviceProvider</b>: <a href=\"Organization-murrabit-hospital.html\">Organization Murrabit Public Hospital</a></p></div>"},"status":"finished","class":{"system":"http://terminology.hl7.org/CodeSystem/v3-ActCode","code":"EMER","display":"emergency"},"subject":{"reference":"Patient/ronny-irvine"},"period":{"start":"2023-02-20T06:15:00+10:00","end":"2023-02-20T18:19:00+10:00"},"location":[{"location":{"reference":"Location/murrabit-hospital"}}],"serviceProvider":{"reference":"Organization/murrabit-hospital"}}

View File

@ -1,8 +0,0 @@
canonical: http://example.org
fhirVersion: 4.3.0
FSHOnly: true
applyExtensionMetadataToRoot: false
id: example
name: Example
dependencies:
hl7.fhir.us.core: 6.1.0

View File

@ -1,55 +0,0 @@
Alias: $sct = http://snomed.info/sct
Alias: $loinc = http://loinc.org
Alias: $ihi-status-1 = https://healthterminologies.gov.au/fhir/CodeSystem/ihi-status-1
Alias: $ihi-record-status-1 = https://healthterminologies.gov.au/fhir/CodeSystem/ihi-record-status-1
Alias: $v2-0203 = http://terminology.hl7.org/CodeSystem/v2-0203
Instance: banks-mia-leanne
InstanceOf: Patient
Usage: #example
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"
* extension[0].url = "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity"
* extension[=].extension.url = "value"
* extension[=].extension.valueCodeableConcept = $sct#446141000124107 "Identifies as female gender"
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-pronouns"
* extension[=].extension.url = "value"
* extension[=].extension.valueCodeableConcept = $loinc#LA29519-8 "she/her/her/hers/herself"
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender"
* extension[=].extension[0].url = "value"
* extension[=].extension[=].valueCodeableConcept = $sct#248152002
* extension[=].extension[=].valueCodeableConcept.text = "Female"
* extension[=].extension[+].url = "type"
* extension[=].extension[=].valueCodeableConcept = $sct#1515311000168102 "Biological sex at birth"
* identifier.extension[0].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-status"
* identifier.extension[=].valueCoding = $ihi-status-1#active
* identifier.extension[+].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-record-status"
* identifier.extension[=].valueCoding = $ihi-record-status-1#verified "verified"
* identifier.type = $v2-0203#NI
* identifier.type.text = "IHI"
* identifier.system = "http://ns.electronichealth.net.au/id/hi/ihi/1.0"
* identifier.value = "8003608333647261"
* name.use = #usual
* name.family = "Banks"
* name.given[0] = "Mia"
* name.given[+] = "Leanne"
* telecom[0].system = #phone
* telecom[=].value = "0270102724"
* telecom[=].use = #work
* telecom[+].system = #phone
* telecom[=].value = "0491574632"
* telecom[=].use = #mobile
* telecom[+].system = #phone
* telecom[=].value = "0270107520"
* telecom[=].use = #home
* telecom[+].system = #email
* telecom[=].value = "mia.banks@myownpersonaldomain.com"
* telecom[+].system = #phone
* telecom[=].value = "270107520"
* telecom[=].use = #home
* gender = #female
* birthDate = "1983-08-25"
* address.line = "50 Sebastien St"
* address.city = "Minjary"
* address.state = "NSW"
* address.postalCode = "2720"
* address.country = "AU"

View File

@ -1,50 +0,0 @@
[supervisord]
nodaemon=true
logfile=/app/logs/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
pidfile=/app/logs/supervisord.pid
[program:flask]
command=/app/venv/bin/python /app/app.py
directory=/app
environment=FLASK_APP="app.py",FLASK_ENV="development",NODE_PATH="/usr/lib/node_modules"
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=10
stdout_logfile=/app/logs/flask.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/app/logs/flask_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=5
[program:tomcat]
command=/usr/local/tomcat/bin/catalina.sh run
directory=/usr/local/tomcat
environment=SPRING_CONFIG_LOCATION="file:/usr/local/tomcat/conf/application.yaml",NODE_PATH="/usr/lib/node_modules"
autostart=true
autorestart=true
startsecs=30
stopwaitsecs=30
stdout_logfile=/app/logs/tomcat.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/app/logs/tomcat_err.log
stderr_logfile_maxbytes=10MB
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

View File

@ -1,24 +0,0 @@
{# templates/_flash_messages.html #}
{# Check if there are any flashed messages #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{# Loop through messages and display them as Bootstrap alerts #}
{% for category, message in messages %}
{# Map Flask message categories (e.g., 'error', 'success', 'warning') to Bootstrap alert classes #}
{% set alert_class = 'alert-info' %} {# Default class #}
{% if category == 'error' or category == 'danger' %}
{% set alert_class = 'alert-danger' %}
{% elif category == 'success' %}
{% set alert_class = 'alert-success' %}
{% elif category == 'warning' %}
{% set alert_class = 'alert-warning' %}
{% endif %}
<div class="alert {{ alert_class }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -1,42 +1,26 @@
{# app/templates/_form_helpers.html #} {# app/templates/_form_helpers.html #}
{% macro render_field(field, label_visible=true) %} {% macro render_field(field, label_visible=true) %}
<div class="form-group mb-3"> <div class="form-group mb-3"> {# Add margin bottom for spacing #}
{% if field.type == "BooleanField" %} {% if label_visible and field.label %}
<div class="form-check"> {{ field.label(class="form-label") }} {# Render label with Bootstrap class #}
{{ field(class="form-check-input" + (" is-invalid" if field.errors else ""), **kwargs) }} {% endif %}
{% if label_visible and field.label %}
<label class="form-check-label" for="{{ field.id }}">{{ field.label.text }}</label> {# Add is-invalid class if errors exist #}
{% endif %} {% set css_class = 'form-control ' + kwargs.pop('class', '') %}
{% if field.description %} {% if field.errors %}
<small class="form-text text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback">
{% for error in field.errors %}
{{ error }}<br>
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
{% if label_visible and field.label %}
{{ field.label(class="form-label") }}
{% endif %}
{% set css_class = 'form-control ' + kwargs.pop('class', '') %}
{% if field.errors %}
{% set css_class = css_class + ' is-invalid' %} {% set css_class = css_class + ' is-invalid' %}
{% endif %} {% endif %}
{{ field(class=css_class, **kwargs) }}
{% if field.description %} {# Render the field itself, passing any extra attributes #}
<small class="form-text text-muted">{{ field.description }}</small> {{ field(class=css_class, **kwargs) }}
{% endif %}
{% if field.errors %} {# Display validation errors #}
<div class="invalid-feedback"> {% if field.errors %}
{% for error in field.errors %} <div class="invalid-feedback">
{{ error }}<br> {% for error in field.errors %}
{% endfor %} {{ error }}<br>
</div> {% endfor %}
{% endif %} </div>
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@ -1,60 +0,0 @@
{% from "_form_helpers.html" import render_field %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<form id="fsh-converter-form" method="POST" enctype="multipart/form-data" class="form">
{{ form.hidden_tag() }}
{{ render_field(form.package) }}
{{ render_field(form.input_mode) }}
<div id="file-upload" style="display: none;">
{{ render_field(form.fhir_file) }}
</div>
<div id="text-input" style="display: none;">
{{ render_field(form.fhir_text) }}
</div>
{{ render_field(form.output_style) }}
{{ render_field(form.log_level) }}
{{ render_field(form.fhir_version) }}
{{ render_field(form.fishing_trip) }}
{{ render_field(form.dependencies, placeholder="One per line, e.g., hl7.fhir.us.core@6.1.0") }}
{{ render_field(form.indent_rules) }}
{{ render_field(form.meta_profile) }}
{{ render_field(form.alias_file) }}
{{ render_field(form.no_alias) }}
<div class="d-grid gap-2 d-sm-flex">
{{ form.submit(class="btn btn-success", id="submit-btn") }}
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% if error %}
<div class="alert alert-danger mt-4">{{ error }}</div>
{% endif %}
{% if fsh_output %}
<div class="alert alert-success mt-4">Conversion successful!</div>
<h3 class="mt-4">FSH Output</h3>
<pre class="p-3">{{ fsh_output }}</pre>
<a href="{{ url_for('download_fsh') }}" class="btn btn-primary">Download FSH</a>
{% if comparison_report %}
<h3 class="mt-4">Fishing Trip Comparison Report</h3>
<a href="{{ url_for('static', filename='uploads/fsh_output/fshing-trip-comparison.html') }}" class="badge bg-primary text-white text-decoration-none mb-3" target="_blank">Click here for SUSHI Validation</a>
<div class="card">
<div class="card-body">
{% if comparison_report.differences %}
<p class="text-warning">Differences found in round-trip validation:</p>
<ul>
{% for diff in comparison_report.differences %}
<li>{{ diff.path }}: {{ diff.description }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-success">No differences found in round-trip validation.</p>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}

View File

@ -1,113 +0,0 @@
{# templates/_search_results_table.html #}
{# This partial template renders the search results table and pagination #}
{% if packages %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Package</th>
<th scope="col">Latest</th>
<th scope="col">Author</th>
<th scope="col">FHIR</th>
<th scope="col">Versions</th>
<!-- <th scope="col">Canonical</th> -->
{# Add Dependencies header if needed based on your data #}
{# <th scope="col">Dependencies</th> #}
</tr>
</thead>
<tbody>
{% for pkg in packages %}
<tr>
<td>
<i class="bi bi-box-seam me-2"></i>
{# Link to package details page - adjust if endpoint name is different #}
<a class="text-primary"
href="{{ url_for('package_details_view', name=pkg.name) }}"
{# Optional: Add HTMX GET for inline details if desired #}
{# hx-get="{{ url_for('package_details', name=pkg.name) }}" #}
{# hx-target="#search-results" #}
>
{{ pkg.name }}
</a>
</td>
<td>{{ pkg.display_version }}</td>
<td>{{ pkg.author or '' }}</td>
<td>{{ pkg.fhir_version or '' }}</td>
<td>{{ pkg.version_count }}</td>
<!-- <td>{{ pkg.canonical or '' }}</td> -->
{# Add Dependencies data if needed #}
{# <td>{{ pkg.dependencies | join(', ') if pkg.dependencies else '' }}</td> #}
</tr>
{% endfor %}
</tbody>
</table>
{# Pagination Controls - Ensure 'pagination' object is passed from the route #}
{% if pagination and pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{# Previous Page Link #}
{% if pagination.has_prev %}
<li class="page-item">
{# Use hx-get for HTMX-powered pagination within the results area #}
<a class="page-link"
href="{{ url_for('search_and_import', page=pagination.prev_num, search=request.args.get('search', '')) }}" {# Keep standard link for non-JS fallback #}
hx-get="{{ url_for('api_search_packages', page=pagination.prev_num, search=request.args.get('search', '')) }}" {# HTMX target #}
hx-target="#search-results"
hx-indicator=".htmx-indicator"
aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">&laquo;</span>
</li>
{% endif %}
{# Page Number Links #}
{% for p in pagination.iter_pages %} {# Iterate over the pre-computed list #}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link"
href="{{ url_for('search_and_import', page=p, search=request.args.get('search', '')) }}"
hx-get="{{ url_for('api_search_packages', page=p, search=request.args.get('search', '')) }}"
hx-target="#search-results"
hx-indicator=".htmx-indicator">
{{ p }}
</a>
</li>
{% else %}
{# Ellipsis for skipped pages #}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{# Next Page Link #}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link"
href="{{ url_for('search_and_import', page=pagination.next_num, search=request.args.get('search', '')) }}"
hx-get="{{ url_for('api_search_packages', page=pagination.next_num, search=request.args.get('search', '')) }}"
hx-target="#search-results"
hx-indicator=".htmx-indicator"
aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %} {# End pagination nav #}
{% elif request and request.args.get('search') %}
{# Message when search term is present but no results found #}
<p class="text-muted text-center mt-3">No packages found matching your search term.</p>
{% else %}
{# Initial message before any search #}
<p class="text-muted text-center mt-3">Start typing in the search box above to find packages.</p>
{% endif %}

View File

@ -1,66 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="" width="192" height="192">
<h1 class="display-5 fw-bold text-body-emphasis">About FHIRFLARE IG Toolkit{% if app_mode == 'lite' %} (Lite Version){% endif %}</h1>
<div class="col-lg-8 mx-auto">
<p class="lead mb-4">
A comprehensive toolkit designed for developers and implementers working with FHIR® Implementation Guides (IGs).
</p>
</div>
</div>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-10">
<h2>Overview</h2>
<p>The FHIRFLARE IG Toolkit is a web application built to simplify the lifecycle of managing, processing, validating, and deploying FHIR Implementation Guides. It provides a central, user-friendly interface to handle common tasks associated with FHIR IGs, streamlining workflows and improving productivity.</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' %}
<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 FHIR server depending and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
</div>
{% else %}
<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 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>
{% endif %}
<h2 class="mt-4">Core Features</h2>
<ul>
<li><strong>IG Package Management:</strong> Import FHIR IG packages directly from the registry using various version formats (e.g., `1.1.0-preview`, `current`), with flexible dependency handling (Recursive, Patch Canonical, Tree Shaking). View, process, unload, or delete downloaded packages, with detection of duplicate dependencies.</li>
<li><strong>IG Processing & Viewing:</strong> Extract and display key information from processed IGs, including defined profiles, referenced resource types, must-support elements, and examples. Visualize profile relationships like `compliesWithProfile` and `imposeProfile`.</li>
<li><strong>FHIR Validation:</strong> Validate individual FHIR resources or entire Bundles against the profiles defined within a selected IG. Provides detailed error and warning feedback. (Note: Validation accuracy is still under development, especially for complex constraints).</li>
<li><strong>FHIR Server Interaction:</strong>
<ul>
<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 server (in Standalone mode) and external servers.</li>
</ul>
</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>API Support:</strong> Provides basic API endpoints for programmatic import and push operations.</li>
</ul>
<h2 class="mt-4">Technology</h2>
<p>The toolkit leverages a combination of technologies:</p>
<ul>
<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>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>
</ul>
<h2 class="mt-4">Get Involved</h2>
<p>This is an open-source project. Contributions, feedback, and bug reports are welcome!</p>
<ul>
<li><strong>GitHub Repository:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit" target="_blank" rel="noopener noreferrer">Sudo-JHare/FHIRFLARE-IG-Toolkit</a></li>
<li><strong>Report an Issue:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" target="_blank" rel="noopener noreferrer">Raise an Issue</a></li>
<li><strong>Discussions:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noopener noreferrer">Project Discussions</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,908 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="{% if request.cookies.get('theme') == 'dark' %}dark{% else %}light{% endif %}"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" xintegrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" rel="stylesheet" />
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/fire-animation.css') }}"> <title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/animation.css') }}">
<script src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
<title>{% if app_mode == 'lite' %}(Lite Version) {% endif %}{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
<style>
/* Prevent flash of unstyled content */
:root { visibility: hidden; }
[data-theme="dark"], [data-theme="light"] { visibility: visible; }
/* Default (Light Theme) Styles */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
color: #212529;
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
main {
flex-grow: 1;
}
h1, h5 {
font-weight: 600;
}
.navbar-light {
background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease;
}
.nav-link:hover {
color: #007bff !important;
}
.nav-link.active {
color: #007bff !important;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.dropdown-menu {
list-style: none !important;
position: absolute;
background-color: #ffffff;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 0;
min-width: 160px;
z-index: 1000;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #e9ecef;
}
.dropdown-item.active {
background-color: #007bff;
color: white !important;
}
.card {
background-color: #ffffff;
border-radius: 10px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.text-muted {
color: #6c757d !important;
}
.form-check-input {
width: 1.25rem !important;
height: 1.25rem !important;
margin-top: 0.25rem;
display: inline-block !important;
border: 1px solid #6c757d;
}
.form-check-input:checked {
background-color: #007bff;
border-color: #007bff;
}
.form-check-label {
font-size: 1rem;
margin-left: 0.5rem;
vertical-align: middle;
}
.form-check-label i {
font-size: 1.2rem;
margin-left: 0.5rem;
}
.form-check {
margin-bottom: 1rem;
}
.navbar-controls {
gap: 0.5rem;
padding-right: 0;
}
/* Footer Styles */
footer {
background-color: #ffffff;
height: 56px;
display: flex;
align-items: center;
padding: 0.5rem 1rem;
border-top: 1px solid #e0e0e0;
}
.footer-left,
.footer-right {
display: inline-flex;
gap: 0.75rem;
align-items: center;
}
.footer-left a,
.footer-right a {
color: #007bff;
text-decoration: none;
font-size: 0.85rem;
}
.footer-left a:hover,
.footer-right a:hover {
text-decoration: underline;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Alert Styles */
.alert {
border-radius: 8px;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border-color: #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border-color: #c3e6cb;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border-color: #bee5eb;
}
/* --- Unified Button and Badge Styles --- */
/* Add some basic adjustments for Prism compatibility if needed */
/* Ensure pre takes full width and code block behaves */
.highlight-code pre, pre[class*="language-"] {
margin: 0.5em 0;
overflow: auto; /* Allow scrolling */
border-radius: 0.3em;
}
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none; /* Adjust if theme adds text shadow */
white-space: pre-wrap; /* Wrap long lines */
word-break: break-all; /* Break long words/tokens */
}
/* Adjust padding if needed based on theme */
:not(pre) > code[class*="language-"], pre[class*="language-"] {
background: inherit; /* Ensure background comes from pre or theme */
}
/* General Button Theme (Primary, Secondary) */
.btn-primary {
background-color: #007bff;
border-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #004085;
}
.btn-outline-primary {
color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:hover {
background-color: #007bff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
background-color: #6c757d;
color: white;
}
/* --- Prism.js Theme Overrides for Light Mode --- */
/* Target code blocks only when NOT in dark theme */
html:not([data-theme="dark"]) .token.punctuation,
html:not([data-theme="dark"]) .token.operator,
html:not([data-theme="dark"]) .token.entity,
html:not([data-theme="dark"]) .token.url,
html:not([data-theme="dark"]) .token.symbol,
html:not([data-theme="dark"]) .token.number,
html:not([data-theme="dark"]) .token.boolean,
html:not([data-theme="dark"]) .token.variable,
html:not([data-theme="dark"]) .token.constant,
html:not([data-theme="dark"]) .token.property,
html:not([data-theme="dark"]) .token.regex,
html:not([data-theme="dark"]) .token.inserted,
html:not([data-theme="dark"]) .token.null, /* Target null specifically */
html:not([data-theme="dark"]) .language-css .token.string,
html:not([data-theme="dark"]) .style .token.string,
html:not([data-theme="dark"]) .token.important,
html:not([data-theme="dark"]) .token.bold {
/* Choose a darker color that works on your light code background */
/* Example: A dark grey or the default text color */
color: #333 !important; /* You might need !important to override theme */
}
/* You might need to adjust other tokens as well based on the specific theme */
/* Example: Make strings slightly darker too if needed */
html:not([data-theme="dark"]) .token.string {
color: #005cc5 !important; /* Example: A shade of blue */
}
/* Ensure background of highlighted code isn't overridden by theme in light mode */
html:not([data-theme="dark"]) pre[class*="language-"] {
background: #e9ecef !important; /* Match your light theme pre background */
}
html:not([data-theme="dark"]) :not(pre) > code[class*="language-"] {
background: #e9ecef !important; /* Match inline code background */
color: #333 !important; /* Ensure inline code text is dark */
}
/* --- End Prism.js Overrides --- */
/* --- Theme Styles for FSH Code Blocks --- */
pre, pre code {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875em;
white-space: pre-wrap; /* Allow wrapping */
word-break: break-all; /* Break long lines */
}
/* Target pre blocks specifically used for FSH output if needed */
/* If the pre block has a specific class or ID, use that */
#fsh-output pre, /* Targets any pre within the output container */
._fsh_output pre /* Targets any pre within the partial template content */
{
background-color: #e9ecef; /* Light theme background */
color: #212529; /* Light theme text */
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
max-height: 600px; /* Adjust as needed */
overflow: auto;
}
#fsh-output pre code,
._fsh_output pre code {
background-color: transparent !important; /* Ensure code tag doesn't override pre bg */
padding: 0;
color: inherit; /* Inherit text color from pre */
font-size: inherit; /* Inherit font size from pre */
display: block; /* Make code fill pre */
}
/* Dark Theme Override */
html[data-theme="dark"] #fsh-output pre,
html[data-theme="dark"] ._fsh_output pre {
background-color: #495057; /* Dark theme background */
color: #f8f9fa; /* Dark theme text */
border-color: #6c757d;
}
html[data-theme="dark"] #fsh-output pre code,
html[data-theme="dark"] ._fsh_output pre code {
color: inherit;
}
/* --- Theme Styles for FHIR UI Operations (fhir_ui_operations.html) --- */
/* Operation Block Container */
.opblock {
border: 1px solid #dee2e6; /* Light theme border */
background: #ffffff; /* Light theme background */
color: #212529; /* Light theme text */
}
html[data-theme="dark"] .opblock {
border-color: #495057; /* Dark theme border */
background: #343a40; /* Dark theme background (card bg) */
color: #f8f9fa; /* Dark theme text */
}
/* Operation Summary (Header) */
.opblock-summary {
background: #f8f9fa; /* Light theme summary bg */
border-bottom: 1px solid #dee2e6;
color: #212529; /* Default text color for summary */
}
.opblock-summary:hover {
background: #e9ecef;
}
html[data-theme="dark"] .opblock-summary {
background: #40464c; /* Darker summary bg */
border-bottom-color: #495057;
color: #f8f9fa;
}
html[data-theme="dark"] .opblock-summary:hover {
background: #495057;
}
/* Keep method badge colors fixed - DO NOT override .opblock-summary-method */
.opblock-summary-description {
color: #495057; /* Slightly muted description text */
}
html[data-theme="dark"] .opblock-summary-description {
color: #adb5bd; /* Lighter muted text for dark */
}
/* Operation Body (Expanded Section) */
.opblock-body {
border-top: 1px solid #dee2e6;
}
html[data-theme="dark"] .opblock-body {
border-top-color: #495057;
}
/* Section Headers within Body */
.opblock-section-header {
border-bottom: 1px solid #eee;
color: inherit; /* Inherit from .opblock */
}
html[data-theme="dark"] .opblock-section-header {
border-bottom-color: #495057;
}
.opblock-title {
color: inherit; /* Inherit from .opblock */
}
/* Parameters Table */
.parameters-table th,
.parameters-table td {
border: 1px solid #dee2e6;
color: inherit; /* Inherit from .opblock */
}
html[data-theme="dark"] .parameters-table th,
html[data-theme="dark"] .parameters-table td {
border-color: #495057;
}
.parameter__type,
.parameter__in {
color: #6c757d; /* Use standard muted color */
}
html[data-theme="dark"] .parameter__type,
html[data-theme="dark"] .parameter__in {
color: #adb5bd; /* Use standard dark muted color */
}
.parameters-col_description input[type="text"],
.parameters-col_description input[type="number"] {
background-color: #fff; /* Match light form controls */
border: 1px solid #ced4da;
color: #212529;
}
html[data-theme="dark"] .parameters-col_description input[type="text"],
html[data-theme="dark"] .parameters-col_description input[type="number"] {
background-color: #495057; /* Match dark form controls */
border-color: #6c757d;
color: #f8f9fa;
}
/* Request Body Textarea */
.request-body-textarea {
background-color: #fff; /* Match light form controls */
border: 1px solid #ced4da;
color: #212529;
}
html[data-theme="dark"] .request-body-textarea {
background-color: #495057; /* Match dark form controls */
border-color: #6c757d;
color: #f8f9fa;
}
/* Responses Table */
.responses-table th,
.responses-table td {
border: 1px solid #dee2e6;
color: inherit;
}
html[data-theme="dark"] .responses-table th,
html[data-theme="dark"] .responses-table td {
border-color: #495057;
}
/* Example/Schema Tabs & Controls */
.response-control-media-type {
background-color: #f8f9fa;
}
html[data-theme="dark"] .response-control-media-type {
background-color: #40464c; /* Match summary header bg */
}
.tablinks.badge { /* Default inactive tab */
background: #dee2e6 !important; /* Light grey inactive */
color: #333 !important;
}
.tablinks.badge.active { /* Default active tab */
background: #007bff !important; /* Use primary color */
color: white !important;
}
html[data-theme="dark"] .tablinks.badge { /* Dark inactive tab */
background: #6c757d !important; /* Dark grey inactive */
color: white !important;
}
html[data-theme="dark"] .tablinks.badge.active { /* Dark active tab */
background: #4dabf7 !important; /* Use dark primary color */
color: #000 !important;
}
/* Code/Output Blocks */
.highlight-code,
.request-url-output,
.curl-output,
.execute-wrapper pre.response-output-content {
background: #e9ecef; /* Light theme pre background */
color: #212529;
border: 1px solid #dee2e6;
}
html[data-theme="dark"] .highlight-code,
html[data-theme="dark"] .request-url-output,
html[data-theme="dark"] .curl-output,
html[data-theme="dark"] .execute-wrapper pre.response-output-content {
background: #495057; /* Dark theme pre background */
color: #f8f9fa;
border-color: #6c757d;
}
/* Narrative Response Box */
.execute-wrapper div.response-output-narrative {
background: #ffffff;
color: #212529;
border: 1px solid #dee2e6;
}
html[data-theme="dark"] .execute-wrapper div.response-output-narrative {
background: #343a40; /* Match card body */
color: #f8f9fa;
border: 1px solid #495057;
}
/* Response Status Text */
.execute-wrapper .response-status[style*="color: green"] { color: #198754 !important; }
.execute-wrapper .response-status[style*="color: red"] { color: #dc3545 !important; }
html[data-theme="dark"] .execute-wrapper .response-status[style*="color: green"] { color: #20c997 !important; }
html[data-theme="dark"] .execute-wrapper .response-status[style*="color: red"] { color: #e63946 !important; }
/* Specific Action Buttons (Success, Danger, Warning, Info) */
.btn-success { background-color: #198754; border-color: #198754; color: white; }
.btn-success:hover { background-color: #157347; border-color: #146c43; }
.btn-outline-success { color: #198754; border-color: #198754; }
.btn-outline-success:hover { background-color: #198754; color: white; }
.btn-danger { background-color: #dc3545; border-color: #dc3545; color: white; }
.btn-danger:hover { background-color: #bb2d3b; border-color: #b02a37; }
.btn-outline-danger { color: #dc3545; border-color: #dc3545; }
.btn-outline-danger:hover { background-color: #dc3545; color: white; }
.btn-warning { background-color: #ffc107; border-color: #ffc107; color: #000; }
.btn-warning:hover { background-color: #ffca2c; border-color: #ffc720; }
.btn-outline-warning { color: #ffc107; border-color: #ffc107; }
.btn-outline-warning:hover { background-color: #ffc107; color: #000; }
.btn-info { background-color: #0dcaf0; border-color: #0dcaf0; color: #000; }
.btn-info:hover { background-color: #31d2f2; border-color: #25cff2; }
.btn-outline-info { color: #0dcaf0; border-color: #0dcaf0; }
.btn-outline-info:hover { background-color: #0dcaf0; color: #000; }
/* General Badge Theme */
.badge {
padding: 0.4em 0.6em; /* Slightly larger padding */
font-size: 0.85em;
font-weight: 600;
}
/* Specific Badge Backgrounds (Add more as needed) */
.badge.bg-primary { background-color: #007bff !important; color: white !important; }
.badge.bg-secondary { background-color: #6c757d !important; color: white !important; }
.badge.bg-success { background-color: #198754 !important; color: white !important; }
.badge.bg-danger { background-color: #dc3545 !important; color: white !important; }
.badge.bg-warning { background-color: #ffc107 !important; color: #000 !important; }
.badge.bg-info { background-color: #0dcaf0 !important; color: #000 !important; }
.badge.bg-light { background-color: #f8f9fa !important; color: #000 !important; border: 1px solid #dee2e6; }
.badge.bg-dark { background-color: #343a40 !important; color: white !important; }
/* Copy Button Specific Style */
.btn-copy { /* Optional: Add a specific class if needed, or style .btn-outline-secondary directly */
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
}
/* --- Dark Theme Overrides --- */
html[data-theme="dark"] .btn-primary { background-color: #4dabf7; border-color: #4dabf7; color: #000; }
html[data-theme="dark"] .btn-primary:hover { background-color: #68bcef; border-color: #5fb7ea; }
html[data-theme="dark"] .btn-outline-primary { color: #4dabf7; border-color: #4dabf7; }
html[data-theme="dark"] .btn-outline-primary:hover { background-color: #4dabf7; color: #000; }
html[data-theme="dark"] .btn-secondary { background-color: #6c757d; border-color: #6c757d; color: white; }
html[data-theme="dark"] .btn-secondary:hover { background-color: #5a6268; border-color: #545b62; }
html[data-theme="dark"] .btn-outline-secondary { color: #adb5bd; border-color: #6c757d; } /* Lighter text/border */
html[data-theme="dark"] .btn-outline-secondary:hover { background-color: #6c757d; color: white; }
html[data-theme="dark"] .btn-success { background-color: #20c997; border-color: #20c997; color: #000; }
html[data-theme="dark"] .btn-success:hover { background-color: #3ce0ac; border-color: #36d8a3; }
html[data-theme="dark"] .btn-outline-success { color: #20c997; border-color: #20c997; }
html[data-theme="dark"] .btn-outline-success:hover { background-color: #20c997; color: #000; }
/* Keep danger as is */
html[data-theme="dark"] .btn-danger { background-color: #e63946; border-color: #e63946; color: white; }
html[data-theme="dark"] .btn-danger:hover { background-color: #c82b39; border-color: #bd2130; }
html[data-theme="dark"] .btn-outline-danger { color: #e63946; border-color: #e63946; }
html[data-theme="dark"] .btn-outline-danger:hover { background-color: #e63946; color: white; }
html[data-theme="dark"] .btn-warning { background-color: #ffca2c; border-color: #ffca2c; color: #000; }
html[data-theme="dark"] .btn-warning:hover { background-color: #ffd24a; border-color: #ffce3d; }
html[data-theme="dark"] .btn-outline-warning { color: #ffca2c; border-color: #ffca2c; }
html[data-theme="dark"] .btn-outline-warning:hover { background-color: #ffca2c; color: #000; }
html[data-theme="dark"] .btn-info { background-color: #22b8cf; border-color: #22b8cf; color: #000; }
html[data-theme="dark"] .btn-info:hover { background-color: #44cee3; border-color: #38cae0; }
html[data-theme="dark"] .btn-outline-info { color: #22b8cf; border-color: #22b8cf; }
html[data-theme="dark"] .btn-outline-info:hover { background-color: #22b8cf; color: #000; }
/* Dark Theme Badges */
html[data-theme="dark"] .badge.bg-primary { background-color: #4dabf7 !important; color: #000 !important; }
html[data-theme="dark"] .badge.bg-secondary { background-color: #6c757d !important; color: white !important; }
html[data-theme="dark"] .badge.bg-success { background-color: #20c997 !important; color: #000 !important; }
html[data-theme="dark"] .badge.bg-danger { background-color: #e63946 !important; color: white !important; }
html[data-theme="dark"] .badge.bg-warning { background-color: #ffca2c !important; color: #000 !important; }
html[data-theme="dark"] .badge.bg-info { background-color: #22b8cf !important; color: #000 !important; }
html[data-theme="dark"] .badge.bg-light { background-color: #495057 !important; color: #f8f9fa !important; border: 1px solid #6c757d; }
html[data-theme="dark"] .badge.bg-dark { background-color: #adb5bd !important; color: #000 !important; }
/* --- Themed Backgrounds for Code/Structure View (cp_view_processed_ig.html) --- */
/* Styling for <pre> blocks containing code */
#raw-structure-wrapper pre,
#example-content-wrapper pre {
background-color: #e9ecef; /* Light theme background */
color: #212529; /* Light theme text */
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
max-height: 400px; /* Keep existing max-height */
overflow-y: auto; /* Keep existing overflow */
white-space: pre-wrap; /* Ensure wrapping */
word-break: break-all; /* Break long strings */
}
/* Ensure code tag inside inherits background and has appropriate font */
#raw-structure-wrapper pre code,
#example-content-wrapper pre code {
background-color: transparent !important; /* Let pre handle background */
padding: 0;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875em;
color: inherit; /* Inherit text color from pre */
display: block; /* Make code block fill pre */
white-space: pre-wrap;
word-break: break-all;
}
/* Styling for the Structure Definition tree list items */
.structure-tree-root .list-group-item {
background-color: #ffffff; /* Light theme default background */
border-color: #dee2e6;
color: #212529;
}
.structure-tree-root .list-group-item-warning {
background-color: #fff3cd; /* Bootstrap light theme warning */
border-color: #ffeeba;
/*color: #856404; Ensure text is readable */
}
/* --- Dark Theme Overrides for Code/Structure View --- */
html[data-theme="dark"] #raw-structure-wrapper pre,
html[data-theme="dark"] #example-content-wrapper pre {
background-color: #495057; /* Dark theme background */
color: #f8f9fa; /* Dark theme text */
border-color: #6c757d;
}
/* Dark theme code text color already inherited, but explicit doesn't hurt */
html[data-theme="dark"] #raw-structure-wrapper pre code,
html[data-theme="dark"] #example-content-wrapper pre code {
color: #f8f9fa;
}
/* Dark theme structure list items */
html[data-theme="dark"] .structure-tree-root .list-group-item {
background-color: #343a40; /* Dark theme card background */
border-color: #495057;
color: #f8f9fa;
}
html[data-theme="dark"] .structure-tree-root .list-group-item-warning {
background-color: #664d03; /* Darker warning background */
border-color: #997404;
/*color: #ffecb5; Lighter text for contrast */
}
/* Dark Theme Styles */
html[data-theme="dark"] body {
background-color: #212529;
color: #f8f9fa;
}
html[data-theme="dark"] .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057;
}
html[data-theme="dark"] .navbar-brand {
color: #4dabf7 !important;
}
html[data-theme="dark"] .nav-link {
color: #f8f9fa !important;
}
html[data-theme="dark"] .nav-link:hover,
html[data-theme="dark"] .nav-link.active {
color: #4dabf7 !important;
border-bottom-color: #4dabf7;
}
html[data-theme="dark"] .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
html[data-theme="dark"] .dropdown-item {
color: #f8f9fa;
}
html[data-theme="dark"] .dropdown-item:hover {
background-color: #6c757d;
}
html[data-theme="dark"] .dropdown-item.active {
background-color: #4dabf7;
color: white !important;
}
html[data-theme="dark"] .card {
background-color: #343a40;
border: 1px solid #495057;
}
html[data-theme="dark"] .text-muted {
color: #adb5bd !important;
}
html[data-theme="dark"] h1,
html[data-theme="dark"] h5 {
color: #f8f9fa;
}
html[data-theme="dark"] .form-label {
color: #f8f9fa;
}
html[data-theme="dark"] .form-control {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .form-control::placeholder {
color: #adb5bd;
}
html[data-theme="dark"] .form-select {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .alert-danger {
background-color: #dc3545;
color: white;
border-color: #dc3545;
}
html[data-theme="dark"] .alert-success {
background-color: #198754;
color: white;
border-color: #198754;
}
html[data-theme="dark"] .alert-info {
background-color: #0dcaf0;
color: white;
border-color: #0dcaf0;
}
html[data-theme="dark"] .form-check-input {
border-color: #adb5bd;
}
html[data-theme="dark"] .form-check-input:checked {
background-color: #4dabf7;
border-color: #4dabf7;
}
html[data-theme="dark"] footer {
background-color: #343a40;
border-top: 1px solid #495057;
}
html[data-theme="dark"] .footer-left a,
html[data-theme="dark"] .footer-right a {
color: #4dabf7;
}
@media (max-width: 576px) {
.navbar-controls {
flex-direction: column;
align-items: stretch;
width: 100%;
}
.navbar-controls .form-check {
margin-bottom: 0.5rem;
}
footer {
height: auto;
padding: 0.5rem;
flex-direction: column;
gap: 0.5rem;
}
.footer-left,
.footer-right {
flex-direction: column;
text-align: center;
gap: 0.5rem;
}
.footer-left a,
.footer-right a {
font-size: 0.8rem;
}
}
</style>
</head> </head>
<body class="d-flex flex-column min-vh-100 {% block body_class %}{% endblock %}"> <body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}{% if app_mode == 'lite' %} (Lite){% endif %}</a> <a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse d-flex justify-content-between" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="fas fa-house me-1"></i> Home</a> <a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.path == '/search-and-import' %}active{% endif %}" href="{{ url_for('search_and_import') }}"><i class="fas fa-download me-1"></i> Search and Import</a> <a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="bi bi-journal-arrow-down me-1"></i> Manage FHIR Packages</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'manual_import_ig' else '' }}" href="{{ url_for('manual_import_ig') }}"><i class="fas fa-folder-open me-1"></i> Manual Import</a> <a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="bi bi-download me-1"></i> Import IGs</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="fas fa-folder-open me-1"></i> Manage FHIR Packages</a> <a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="bi bi-upload me-1"></i> Push IGs</a>
</li> </li>
<li class="nav-item"> </ul>
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="fas fa-upload me-1"></i> Push IGs</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'upload_test_data' else '' }}" href="{{ url_for('upload_test_data') }}"><i class="bi bi-clipboard-data me-1"></i>Upload Test Data</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="fas fa-check-circle me-1"></i> Validate FHIR Sample</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'retrieve_split_data' else '' }}" href="{{ url_for('retrieve_split_data') }}"><i class="fas bi-cloud-download me-1"></i> Retrieve & Split Data</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui' else '' }}" href="{{ url_for('fhir_ui') }}"><i class="fas fa-cloud-download-alt me-1"></i> FHIR API Explorer</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui_operations' else '' }}" href="{{ url_for('fhir_ui_operations') }}"><i class="bi bi-gear me-1"></i> FHIR UI Operations</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'ig_configurator' else '' }}" href="{{ url_for('ig_configurator') }}"><i class="fas fa-wrench me-1"></i> IG Valid-Conf Gen</a>
</li> -->
<!-- {% if app_mode != 'lite' %}
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>
</li>
{% endif %} -->
</ul>
<div class="navbar-controls d-flex align-items-center">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="themeToggle" onchange="toggleTheme()" aria-label="Toggle dark mode" {% if request.cookies.get('theme') == 'dark' %}checked{% endif %}>
<label class="form-check-label" for="themeToggle">
<i class="fas fa-sun"></i>
<i class="fas fa-moon d-none"></i>
</label>
</div>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<main class="flex-grow-1"> <main class="container flex-grow-1">
<div class="container mt-4"> {% with messages = get_flashed_messages(with_categories=true) %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% if messages %} {% for category, message in messages %}
<div class="mt-3"> <div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
{% for category, message in messages %} {{ message }}
<div class="alert <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{{ 'alert-danger' if category == 'error' else
'alert-success' if category == 'success' else
'alert-info' }}
alert-dismissible fade show" role="alert">
<i class="fas
{{ 'fa-exclamation-triangle me-2' if category == 'error' else
'fa-check-circle me-2' if category == 'success' else
'fa-info-circle me-2' }}"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div> </div>
{% endif %} {% endfor %}
{% endwith %} {% endif %}
{% block content %}{% endblock %} {% endwith %}
</div> {% block content %}{% endblock %}
</main> </main>
<footer class="main-footer" role="contentinfo"> <footer class="footer mt-auto py-3 bg-light">
<div class="container-fluid"> <div class="container text-center">
<div class="d-flex justify-content-between align-items-center"> {% set current_year = now.year %}
<div class="footer-left"> <span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit" target="_blank" rel="noreferrer" aria-label="Project Github">Project Github</a>
<a href="/about" aria-label="Learn about this project">What is FHIRFLARE</a>
<a href="https://www.hl7.org/fhir/" target="_blank" rel="noreferrer" aria-label="Visit FHIR website">FHIR</a>
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" class="text-danger text-decoration-none" aria-label="FHIRFLARE support"><i class="fas fa-exclamation-circle me-1"></i> Raise an Issue</a>
</div>
<div class="footer-right">
<a class="nav-link {{ 'active' if request.endpoint == 'flasgger.apidocs' else '' }}" href="{{ url_for('flasgger.apidocs') }}" aria-label="API Documentation"><i class="fas fa-book-open me-1"></i> API Docs</a>
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noreferrer" aria-label="Project Discussion">Project Discussions</a>
<a href="https://github.com/Sudo-JHare" aria-label="Developer">Developer</a>
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/blob/main/LICENSE.md" aria-label="License">License</a>
</div>
</div>
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
function toggleTheme() { {% block scripts %}
const html = document.documentElement; <script>
const toggle = document.getElementById('themeToggle'); document.addEventListener('DOMContentLoaded', function () {
const sunIcon = document.querySelector('.fa-sun'); var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
const moonIcon = document.querySelector('.fa-moon'); var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
const newTheme = toggle.checked ? 'dark' : 'light'; });
html.setAttribute('data-theme', newTheme); </script>
localStorage.setItem('theme', newTheme); {% endblock %}
sunIcon.classList.toggle('d-none', toggle.checked);
moonIcon.classList.toggle('d-none', !toggle.checked);
document.cookie = `theme=${newTheme}; path=/; max-age=31536000`;
if (typeof loadLottieAnimation === 'function') {
loadLottieAnimation(newTheme);
}
}
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme') || getCookie('theme') || 'light';
const themeToggle = document.getElementById('themeToggle');
const sunIcon = document.querySelector('.fa-sun');
const moonIcon = document.querySelector('.fa-moon');
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'dark';
sunIcon.classList.toggle('d-none', savedTheme === 'dark');
moonIcon.classList.toggle('d-none', savedTheme !== 'dark');
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(t => new bootstrap.Tooltip(t));
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,234 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1 class="mb-4">HAPI FHIR Configuration Manager</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div id="loading" class="text-center my-4">
<p>Loading configuration...</p>
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div id="error" class="alert alert-danger d-none" role="alert"></div>
<div id="config-form" class="d-none">
<div class="mb-3">
<button id="save-btn" class="btn btn-primary me-2">Save Configuration</button>
<button id="restart-btn" class="btn btn-success">Restart HAPI Server</button>
<span id="restart-status" class="ms-3 text-success d-none"></span>
</div>
<div id="config-sections"></div>
</div>
</div>
<script>
// Configuration descriptions from HAPI FHIR JPA documentation
const CONFIG_DESCRIPTIONS = {
'hapi.fhir.narrative_enabled': 'Enables or disables the generation of FHIR resource narratives (human-readable summaries). Set to false to reduce processing overhead. Default: false.',
'hapi.fhir.mdm_enabled': 'Enables Master Data Management (MDM) features for linking and matching resources (e.g., Patient records). Requires mdm_rules_json_location to be set. Default: false.',
'hapi.fhir.mdm_rules_json_location': 'Path to the JSON file defining MDM rules for resource matching. Example: "mdm-rules.json".',
'hapi.fhir.cors.allow_Credentials': 'Allows credentials (e.g., cookies, authorization headers) in CORS requests. Set to true to support authenticated cross-origin requests. Default: true.',
'hapi.fhir.cors.allowed_origin': 'List of allowed origins for CORS requests. Use ["*"] to allow all origins or specify domains (e.g., ["https://example.com"]). Default: ["*"].',
'hapi.fhir.tester.home.name': 'Name of the local tester configuration for HAPI FHIR testing UI. Example: "Local Tester".',
'hapi.fhir.tester.home.server_address': 'URL of the local FHIR server for testing. Example: "http://localhost:8080/fhir".',
'hapi.fhir.tester.home.refuse_to_fetch_third_party_urls': 'Prevents the tester from fetching third-party URLs. Set to true for security. Default: false.',
'hapi.fhir.tester.home.fhir_version': 'FHIR version for the local tester (e.g., "R4", "R5"). Default: R4.',
'hapi.fhir.tester.global.name': 'Name of the global tester configuration for HAPI FHIR testing UI. Example: "Global Tester".',
'hapi.fhir.tester.global.server_address': 'URL of the global FHIR server for testing. Example: "http://hapi.fhir.org/baseR4".',
'hapi.fhir.tester.global.refuse_to_fetch_third_party_urls': 'Prevents the global tester from fetching third-party URLs. Default: false.',
'hapi.fhir.tester.global.fhir_version': 'FHIR version for the global tester (e.g., "R4"). Default: R4.',
'hapi.fhir.cr.enabled': 'Enables Clinical Reasoning (CR) module for operations like evaluate-measure. Default: false.',
'hapi.fhir.cr.caregaps.reporter': 'Reporter mode for care gaps in the CR module. Default: "default".',
'hapi.fhir.cr.caregaps.section_author': 'Author identifier for care gap sections. Default: "default".',
'hapi.fhir.cr.cql.use_embedded_libraries': 'Uses embedded CQL libraries for Clinical Quality Language processing. Default: true.',
'hapi.fhir.inline_resource_storage_below_size': 'Maximum size (in bytes) for storing FHIR resources inline in the database. Default: 4000.',
'hapi.fhir.advanced_lucene_indexing': 'Enables experimental advanced Lucene/Elasticsearch indexing for enhanced search capabilities. Default: false. Note: May not support all FHIR features like _total=accurate.',
'hapi.fhir.enable_index_of_type': 'Enables indexing of resource types for faster searches. Default: true.',
// Add more descriptions as needed based on your application.yaml
};
document.addEventListener('DOMContentLoaded', () => {
const configForm = document.getElementById('config-form');
const loading = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const configSections = document.getElementById('config-sections');
const saveBtn = document.getElementById('save-btn');
const restartBtn = document.getElementById('restart-btn');
const restartStatus = document.getElementById('restart-status');
let configData = {};
// Fetch initial configuration
fetch('/api/config')
.then(res => res.json())
.then(data => {
configData = data;
renderConfigSections();
loading.classList.add('d-none');
configForm.classList.remove('d-none');
// Initialize Bootstrap tooltips
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
new bootstrap.Tooltip(el);
});
})
.catch(err => {
showError('Failed to load configuration');
loading.classList.add('d-none');
});
// Render configuration sections
function renderConfigSections() {
configSections.innerHTML = '';
Object.entries(configData).forEach(([section, values]) => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'card mb-3';
sectionDiv.innerHTML = `
<div class="card-header">
<h3 class="card-title">${section.charAt(0).toUpperCase() + section.slice(1)}</h3>
</div>
<div class="card-body">
${renderInputs(values, [section])}
</div>
`;
configSections.appendChild(sectionDiv);
});
}
// Render inputs recursively with descriptions
function renderInputs(obj, path) {
let html = '';
Object.entries(obj).forEach(([key, value]) => {
const inputId = path.concat(key).join('-');
const configPath = path.concat(key).join('.');
const description = CONFIG_DESCRIPTIONS[configPath] || 'No description available.';
if (typeof value === 'boolean') {
html += `
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="${inputId}" ${value ? 'checked' : ''}>
<label class="form-check-label" for="${inputId}">
${key}
<i class="fas fa-info-circle ms-1" data-bs-toggle="tooltip" data-bs-placement="top" title="${description}"></i>
</label>
</div>
`;
} else if (typeof value === 'string' || typeof value === 'number') {
html += `
<div class="mb-2">
<label for="${inputId}" class="form-label">
${key}
<i class="fas fa-info-circle ms-1" data-bs-toggle="tooltip" data-bs-placement="top" title="${description}"></i>
</label>
<input type="text" class="form-control" id="${inputId}" value="${value}">
</div>
`;
} else if (Array.isArray(value)) {
html += `
<div class="mb-2">
<label for="${inputId}" class="form-label">
${key} (comma-separated)
<i class="fas fa-info-circle ms-1" data-bs-toggle="tooltip" data-bs-placement="top" title="${description}"></i>
</label>
<input type="text" class="form-control" id="${inputId}" value="${value.join(',')}">
</div>
`;
} else if (typeof value === 'object' && value !== null) {
html += `
<div class="border-start ps-3 ms-3">
<h4>${key}</h4>
${renderInputs(value, path.concat(key))}
</div>
`;
}
});
return html;
}
// Update config data on input change
configSections.addEventListener('change', (e) => {
const path = e.target.id.split('-');
let value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
if (e.target.type === 'text' && e.target.value.includes(',')) {
value = e.target.value.split(',').map(v => v.trim());
}
updateConfig(path, value);
});
// Update config object
function updateConfig(path, value) {
let current = configData;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = value;
}
// Save configuration
saveBtn.addEventListener('click', () => {
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(configData)
})
.then(res => res.json())
.then(data => {
if (data.error) {
showError(data.error);
} else {
showSuccess('Configuration saved successfully');
}
})
.catch(() => showError('Failed to save configuration'));
});
// Restart HAPI server
restartBtn.addEventListener('click', () => {
restartStatus.textContent = 'Restarting...';
restartStatus.classList.remove('d-none');
fetch('/api/restart-tomcat', { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.error) {
showError(data.error);
restartStatus.classList.add('d-none');
} else {
restartStatus.textContent = 'HAPI server restarted successfully';
setTimeout(() => {
restartStatus.classList.add('d-none');
restartStatus.textContent = '';
}, 3000);
}
})
.catch(() => {
showError('Failed to restart HAPI server');
restartStatus.classList.add('d-none');
});
});
// Show error message
function showError(message) {
errorDiv.textContent = message;
errorDiv.classList.remove('d-none');
setTimeout(() => errorDiv.classList.add('d-none'), 5000);
}
// Show success message
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.querySelector('.container').insertBefore(alert, configForm);
setTimeout(() => alert.remove(), 5000);
}
});
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More