Compare commits

..

113 Commits
V1.8 ... main

Author SHA1 Message Date
47b746c4b1
Merge pull request #24 from Sudo-JHare/V4-Alpha
V4.1 - Merge release
2025-09-08 19:48:13 +10:00
92fe759b0f V4.3 - Advanced Validation
V4.3  Advanced Validation

added full gamut of options and configuration for fully fleshed out validation
2025-08-29 00:11:24 +10:00
356914f306 ui hotfix 2025-08-28 10:59:58 +10:00
9f3748cc49 UI Change
Hotfix UI change for displaying larger and wider IG package names.
2025-08-28 07:22:38 +10:00
f7063484d9 docker builds 2025-08-27 21:28:30 +10:00
b8014de7c3 Hotfix
added download validation links and fixed CORS issues
2025-08-27 21:08:01 +10:00
f4c457043a hotfix 2025-08-27 19:21:53 +10:00
dffe43beee rollback 2025-08-27 16:42:52 +10:00
3f1ac10290 hotfix 2025-08-27 16:19:52 +10:00
85f980be0a Update app.py 2025-08-27 14:16:52 +10:00
d37af01b3a Update README.md 2025-08-27 13:07:48 +10:00
6ae0e56118 V4.1 Prep
Build Path options

added Candle
Added Custom
2025-08-27 12:47:16 +10:00
f1478561c1 Remove Hapi references
remove Hapi references
Disabled Legacy Hapi controls
2025-08-26 22:00:44 +10:00
236390e57b V4-Alpha
Remove Hapi validaotr
replace with Hl7 Validator
restructure validation report
provide Json and Html validation reports
remove hardcoded local references and couple to ENV allowing custom env Fhir Server URl to be set for local ""instance:
2025-08-26 21:15:24 +10:00
5725f8a22f update 2025-08-13 08:12:47 +10:00
57d05eab38 hotfix 2025-08-13 07:31:32 +10:00
62ff8e8a28 fix 2025-08-12 23:26:05 +10:00
91fdaa89f9 fix 2025-08-12 23:14:09 +10:00
27f9a397b2 update 2025-08-12 23:10:52 +10:00
a76180fd48 hotfix 2025-08-12 23:00:44 +10:00
ffeef91166 Hotfix 2025-08-12 22:33:05 +10:00
9cf99ec78d updates 2025-08-12 21:55:28 +10:00
5f4e1b7207 Update services.py 2025-08-12 21:29:13 +10:00
9729004982 Hotfix - proxy pathing 2025-08-12 20:48:52 +10:00
5a6f2072d7 Hotifx
fix context relative path.
2025-08-12 19:28:47 +10:00
189ba1d18c fix. 2025-08-12 16:39:29 +10:00
Jörn Guy Süß
3474d4e7e5 set up the base url 2025-08-04 23:05:52 +10:00
Jörn Guy Süß
f1f69ad32f only add to existing files 2025-08-04 22:46:38 +10:00
Jörn Guy Süß
b10d9a8cec fix: typo - underscore should be dash 2025-08-04 22:29:05 +10:00
Jörn Guy Süß
5277cd16f4 use gh pages 2025-08-04 22:18:58 +10:00
Jörn Guy Süß
850257f977 update ruby 2025-08-04 22:03:46 +10:00
Jörn Guy Süß
6826b7ba59 use v3 2025-08-04 21:59:56 +10:00
Jörn Guy Süß
dac129d16c update upload action to v4 due to deprecation 2025-08-04 21:58:31 +10:00
Jörn Guy Süß
2af2fd9b11 Add website and website build 2025-08-04 21:52:16 +10:00
Jörn Guy Süß
777dddbcaf ignore build output folder 2025-08-04 20:19:29 +10:00
Jörn Guy Süß
3a34212899 Add website build 2025-08-04 20:17:57 +10:00
Jörn Guy Süß
6447047b86 more bump. 2025-08-04 15:52:37 +10:00
Jörn Guy Süß
cebed936ae Bump helm version 2025-08-04 15:46:14 +10:00
3744123361 updates 2025-08-04 15:15:46 +10:00
Jörn Guy Süß
7eaa94a705 fix: Removed as sub-chart for HAPI FHIR has been removed. 2025-08-04 14:34:03 +10:00
399249faa3 adding in build files 2025-08-04 14:19:12 +10:00
26f095cdd2 Adding back in build scripts
auto user run build scripts
2025-08-04 14:13:56 +10:00
Jörn Guy Süß
ff366fa6ba
Merge pull request #21 from Sudo-JHare/Validation_PH_2Pass
2 Pass Validation and Helm Charts
2025-08-04 13:48:51 +10:00
Jörn Guy Süß
d547ca12a1 feat: use ephemeral volumes and variable image
* use ephemeral volumes to avoid root mounts in user space
* use image definition using environment variable with default.
2025-08-04 12:30:39 +10:00
Jörn Guy Süß
8df84579a8 Build multi-arch 2025-08-04 11:26:33 +10:00
Jörn Guy Süß
35221a7495 Update the dockerfile
* Allow build on click
* expect the dockerfile in subdirectory
2025-08-04 11:21:20 +10:00
Jörn Guy Süß
a6a8427eed Add some upload samples
These are versions PH core
2025-08-04 11:17:33 +10:00
Jörn Guy Süß
ea9ae2abf3 remove the lods and instance directories
They are dynamically created and should not be checked in.
2025-08-04 11:16:50 +10:00
Jörn Guy Süß
505e78404e move the Dockerfile into a subdirectory to clean up. 2025-08-04 11:16:05 +10:00
Jörn Guy Süß
4c451962ae Merge remote-tracking branch 'upstream/Validation_PH_2Pass' into
1-basic-helm-chart-for-light-deployment
2025-08-04 11:15:12 +10:00
Jörn Guy Süß
d740ac8b9e Initial commit 2025-08-04 10:30:38 +10:00
8e7a272ee7 2 pass validation
PH - enhancements - feedback from PH project and JG
2025-08-01 11:00:48 +10:00
Jörn Guy Süß
8931e921be update chart version 2025-07-18 14:09:44 +10:00
Jörn Guy Süß
81d4f775e9 Add architecture affinity 2025-07-18 13:37:54 +10:00
Jörn Guy Süß
9a379e74f2 Bump chart to 0.2.0 2025-07-16 16:59:29 +10:00
Jörn Guy Süß
a5b442cd2a Merge branch 'main' of git@github.com:jgsuess/FHIRFLARE-IG-Toolkit.git into main 2025-07-16 16:54:55 +10:00
Jörn Guy Süß
81919bface ignore rendered files 2025-07-16 16:54:42 +10:00
Jörn Guy Süß
a78d33fd5f simplify the ingress for test purposes 2025-07-16 16:54:28 +10:00
Jörn Guy Süß
26685633ce simplify the ingress for test purposes 2025-07-16 16:54:20 +10:00
Jörn Guy Süß
3b75177a4c remove rendered content 2025-07-16 14:49:09 +10:00
Jörn Guy Süß
9a0d419e18 trigger page push 2025-07-16 14:41:51 +10:00
Jörn Guy Süß
c4f5f2c1fd explicity add repo 2025-07-16 14:01:42 +10:00
Jörn Guy Süß
3e4982425d Revert chart lock 2025-07-16 14:00:28 +10:00
Jörn Guy Süß
992ab6f168 Remove chart lock for workflow test 2025-07-16 13:59:27 +10:00
Jörn Guy Süß
1d9a263d23 trigger build 2025-07-16 13:50:20 +10:00
Jörn Guy Süß
ea402ceaa9 move gitignore from cache directory 2025-07-16 13:45:21 +10:00
Jörn Guy Süß
55856a35dc update safe directory 2025-07-16 13:42:35 +10:00
Jörn Guy Süß
3413b9ba71 add keyword 2025-07-16 13:40:01 +10:00
Jörn Guy Süß
ebb8f31974 Trigger on branch main 2025-07-16 13:38:50 +10:00
Jörn Guy Süß
43921790fa Add chart and chart workflow 2025-07-16 13:35:34 +10:00
Jörn Guy Süß
83ec579214 Update supervisor configuration not to autostart catalina 2025-07-16 13:08:04 +10:00
Jörn Guy Süß
0bb3ba7e82 fix: updated label match, added test 2025-07-16 11:06:00 +10:00
Jörn Guy Süß
410dd003f7 Initial commit for chart 2025-07-16 10:01:45 +10:00
Jörn Guy Süß
7d69ca27ae refactor: Move docker compose to its own directory 2025-07-15 14:02:24 +10:00
Jörn Guy Süß
d662e8509a remove related build artifacts 2025-07-15 12:09:27 +10:00
Jörn Guy Süß
928a331a40 fix: remove webapp volume mount, it is not required. 2025-07-15 12:09:05 +10:00
Jörn Guy Süß
0674128568 feat: Add helper scripts for startup and shutdown 2025-07-15 12:08:34 +10:00
Jörn Guy Süß
ea5752f59b feat: Add compose using off-the-shelf HAPI 2025-07-15 11:28:50 +10:00
Jörn Guy Süß
8d078e672b feat: reduce and optimize for python 2025-07-15 11:28:20 +10:00
Jörn Guy Süß
75639c176c refactoring: factor out full image build components. 2025-07-15 11:08:38 +10:00
Jörn Guy Süß
3fe12a8030 feat: Add a shell script for Linux users 2025-07-15 11:01:27 +10:00
Jörn Guy Süß
e2a6b19a2e fix: two tag extensions
Run docker build -t
ghcr.io/ghcr.io/jgsuess/fhirflare-ig-toolkit:main:latest .
ERROR: failed to build: invalid tag
"ghcr.io/ghcr.io/jgsuess/fhirflare-ig-toolkit:main:latest": invalid
reference format
2025-07-15 10:21:32 +10:00
Jörn Guy Süß
6ede139084 fix: work around naming convention issue
container registry name must be lowercase, repo can be anything
2025-07-15 10:18:20 +10:00
Jörn Guy Süß
366917c768 Enable container build 2025-07-15 10:06:56 +10:00
d38e29160c update
New Feature - Manual Upload IG file / or from URL.

this allows users to use IG's tat are not yet Published to registries
2025-06-22 22:36:28 +10:00
847a5b2a6d
Update animation.css 2025-06-17 10:52:19 +10:00
1bd387b848
Update fire-animation.css 2025-06-17 10:50:30 +10:00
bf6c0887d6
Update fire-animation.css 2025-06-17 10:28:58 +10:00
5c559f9652
Merge pull request #17 from Sudo-JHare/Patch
QOL Changes
2025-05-22 20:08:26 +10:00
fa091db463 QOL Changes
added Basic Auth options for custom server  usage across multiple modules.

new footer option - OpenAPI docs - integrated swagger UI for open API documentation
2025-05-22 20:07:11 +10:00
0506d76ea9 patch
Typo fix in template
2025-05-14 10:35:15 +10:00
d9ac3dc05b Merge branch 'main' of https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit 2025-05-14 10:25:50 +10:00
431bfb5a46 patch
Typo fix in template
2025-05-14 10:23:37 +10:00
d2e05ee4a0
Merge pull request #15 from Sudo-JHare/Patch
Patched
2025-05-14 10:06:08 +10:00
6412e537a8 Patched
Patched for New IG processing, to handle non standard structures, names, and versions formats in IG's
2025-05-14 09:59:53 +10:00
b6f9d2697e Update app.py 2025-05-13 15:38:02 +10:00
11e8569c56 patch 2025-05-13 14:03:29 +10:00
f150e7baaf path patch 2025-05-13 14:00:48 +10:00
f91af1148d patch 2025-05-13 13:43:23 +10:00
179982dedc Dynamic Hostname fix
Patch for dynamic host names
2025-05-13 12:37:04 +10:00
5efae0c64e Revert "Add files via upload"
This reverts commit f172b00737092fdf4040bffd2f8c6c2742eb7a44.
2025-05-11 22:32:57 +10:00
f172b00737
Add files via upload 2025-05-11 21:14:27 +10:00
b08f4e5790
Update setup_linux.sh 2025-05-08 21:27:00 +10:00
7368f42e8b
Create setup_linux.sh 2025-05-08 21:06:18 +10:00
7049a902a4
Merge pull request #14 from Sudo-JHare/Patch
Update README.md
2025-05-07 07:41:18 +10:00
84b3df524f Update README.md 2025-05-07 07:40:43 +10:00
6bca03aa7c
Merge pull request #12 from Sudo-JHare/Patch
V2.0 release.
Hapi Config Page.
retrieve bundles from server
split bundles to resources.
new upload page, and Package Registry CACHE
2025-05-04 22:58:12 +10:00
ca8668e5e5 V2.0 release.
Hapi Config Page.
retrieve bundles from server
split bundles to resources.
new upload page, and Package Registry CACHE
2025-05-04 22:55:36 +10:00
ea97f49ead
Merge pull request #11 from Sudo-JHare/Patch
Easter egg patch
2025-05-01 09:23:35 +10:00
cd06b1b216 Easter egg patch
easter egg patch

Co-Authored-By: Sudo-Xops <209843921+Sudo-Xops@users.noreply.github.com>
2025-05-01 09:22:17 +10:00
2efe4cbb2f
Merge pull request #10 from Sudo-JHare/Patch
Patch
2025-05-01 07:17:52 +10:00
dfa0ef31b2 Update README_INTEGRATION FHIRVINE as Moduel in FLARE.md
Co-Authored-By: Sudo-Xops <209843921+sudo-xops@users.noreply.github.com>
2025-05-01 07:15:59 +10:00
ca8613ff1b Tidy-up patch 2025-05-01 07:00:57 +10:00
140 changed files with 63275 additions and 154739 deletions

23
.github/ct/chart-schema.yaml vendored Normal file
View File

@ -0,0 +1,23 @@
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)

15
.github/ct/config.yaml vendored Normal file
View File

@ -0,0 +1,15 @@
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

84
.github/workflows/build-images.yaml vendored Normal file
View File

@ -0,0 +1,84 @@
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

41
.github/workflows/chart-release.yaml vendored Normal file
View File

@ -0,0 +1,41 @@
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 }}"

73
.github/workflows/chart-test.yaml vendored Normal file
View File

@ -0,0 +1,73 @@
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' }}

58
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,58 @@
# 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 }}

40
.github/workflows/jekyll.yml vendored Normal file
View File

@ -0,0 +1,40 @@
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 Normal file
View File

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

23
.project Normal file
View File

@ -0,0 +1,23 @@
<?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>

7
.settings/.jsdtscope Normal file
View File

@ -0,0 +1,7 @@
<?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

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

View File

@ -0,0 +1 @@
Global

View File

@ -2,57 +2,130 @@
setlocal enabledelayedexpansion
REM --- Configuration ---
set REPO_URL=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
set CLONE_DIR=hapi-fhir-jpaserver
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%\target\classes\%CONFIG_FILE%
set DEST_CONFIG_PATH=%CLONE_DIR_HAPI%\target\classes\%CONFIG_FILE%
REM === CORRECTED: Prompt for Version ===
REM --- NEW: Define a variable for the custom FHIR URL and server type ---
set "CUSTOM_FHIR_URL_VAL="
set "SERVER_TYPE="
set "CANDLE_FHIR_VERSION="
REM === MODIFIED: Prompt for Installation Mode ===
:GetModeChoice
SET "APP_MODE=" REM Clear the variable first
echo.
echo Select Installation Mode:
echo 1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)
echo 2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)
CHOICE /C 12 /N /M "Enter your choice (1 or 2):"
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
)
IF ERRORLEVEL 1 (
SET APP_MODE=standalone
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 CORRECTION ===
REM === END MODIFICATION ===
REM === Conditionally Execute HAPI Setup ===
IF "%APP_MODE%"=="standalone" (
echo Running Standalone setup including HAPI FHIR...
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%
if exist "%CLONE_DIR%" (
echo Checking for existing directory: %CLONE_DIR_HAPI%
if exist "%CLONE_DIR_HAPI%" (
echo Found existing directory, removing it...
rmdir /s /q "%CLONE_DIR%"
rmdir /s /q "%CLONE_DIR_HAPI%"
if errorlevel 1 (
echo ERROR: Failed to remove existing directory: %CLONE_DIR%
echo ERROR: Failed to remove existing directory: %CLONE_DIR_HAPI%
goto :error
)
echo Existing directory removed.
@ -62,8 +135,8 @@ IF "%APP_MODE%"=="standalone" (
echo.
REM --- Step 1: Clone the HAPI FHIR server repository ---
echo Cloning repository: %REPO_URL% into %CLONE_DIR%...
git clone "%REPO_URL%" "%CLONE_DIR%"
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
@ -72,10 +145,10 @@ IF "%APP_MODE%"=="standalone" (
echo.
REM --- Step 2: Navigate into the cloned directory ---
echo Changing directory to %CLONE_DIR%...
cd "%CLONE_DIR%"
echo Changing directory to %CLONE_DIR_HAPI%...
cd "%CLONE_DIR_HAPI%"
if errorlevel 1 (
echo ERROR: Failed to change directory to %CLONE_DIR%.
echo ERROR: Failed to change directory to %CLONE_DIR_HAPI%.
goto :error
)
echo Current directory: %CD%
@ -118,45 +191,141 @@ IF "%APP_MODE%"=="standalone" (
echo Current directory: %CD%
echo.
) ELSE (
echo Running Lite setup, skipping HAPI FHIR build...
REM Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen after a standalone attempt
if exist "%CLONE_DIR%" (
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
rmdir /s /q "%CLONE_DIR%"
) 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.
)
REM Create empty target directories expected by Dockerfile COPY, even if not used
mkdir "%CLONE_DIR%\target\classes" 2> nul
mkdir "%CLONE_DIR%\custom" 2> nul
REM Create a placeholder empty WAR file to satisfy Dockerfile COPY
echo. > "%CLONE_DIR%\target\ROOT.war"
echo. > "%CLONE_DIR%\target\classes\application.yaml"
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 === Modify docker-compose.yml to set APP_MODE ===
echo Updating docker-compose.yml with APP_MODE=%APP_MODE%...
REM === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL ===
echo Updating docker-compose.yml with APP_MODE=%APP_MODE% and HAPI_FHIR_URL...
(
echo version: '3.8'
echo services:
echo fhirflare:
echo build:
echo context: .
echo dockerfile: Dockerfile
IF "%SERVER_TYPE%"=="hapi" (
echo dockerfile: Dockerfile.hapi
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo dockerfile: Dockerfile.candle
) ELSE (
echo dockerfile: Dockerfile.lite
)
echo ports:
echo - "5000:5000"
echo - "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
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
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
IF "%SERVER_TYPE%"=="hapi" (
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
)
echo - ./logs:/app/logs
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

View File

@ -3,17 +3,30 @@ 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 \
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/*
# 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
# REMOVED pip install fhirpath from this line
RUN npm install -g gofsh fsh-sushi
# Set up Python environment
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
@ -29,6 +42,7 @@ 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/
@ -43,6 +57,9 @@ COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
COPY hapi-fhir-jpaserver/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
@ -50,7 +67,7 @@ RUN pip install supervisor
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 8080
EXPOSE 5000 8080 5001
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

64
Dockerfile.candle Normal file
View File

@ -0,0 +1,64 @@
# Base image with Python and Dotnet
FROM mcr.microsoft.com/dotnet/sdk:9.0
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
default-jre-headless \
curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
RUN pip uninstall -y fhirpath || true
# ADDED: Install the new fhirpathpy library
RUN pip install --no-cache-dir fhirpathpy
# Copy Flask files
COPY requirements.txt .
# Install requirements (including Pydantic - check version compatibility if needed)
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
# Copy pre-built Candle DLL files
COPY fhir-candle/publish/ /app/fhir-candle-publish/
# Install supervisord
RUN pip install supervisor
# Configure supervisord
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 5001
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

66
Dockerfile.hapi Normal file
View File

@ -0,0 +1,66 @@
# Base image with Python, Java, and Maven
FROM tomcat:10.1-jdk17
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv curl coreutils git maven \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
RUN pip uninstall -y fhirpath || true
# ADDED: Install the new fhirpathpy library
RUN pip install --no-cache-dir fhirpathpy
# Copy Flask files
COPY requirements.txt .
# Install requirements (including Pydantic - check version compatibility if needed)
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
# Copy pre-built HAPI WAR and configuration
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
# Install supervisord
RUN pip install supervisor
# Configure supervisord
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 8080
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

60
Dockerfile.lite Normal file
View File

@ -0,0 +1,60 @@
# Base image with Python and Node.js
FROM python:3.9-slim
# Install JRE and other dependencies for the validator
# We need to install `default-jre-headless` to get a minimal Java Runtime Environment
RUN apt-get update && apt-get install -y --no-install-recommends \
default-jre-headless \
curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
RUN pip uninstall -y fhirpath || true
# ADDED: Install the new fhirpathpy library
RUN pip install --no-cache-dir fhirpathpy
# Copy Flask files
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# Ensure necessary directories are writable
RUN mkdir -p /app/static/uploads /app/logs && chmod 777 /app/static/uploads /app/logs
# Install supervisord
RUN pip install supervisor
# Configure supervisord
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

426
README.md
View File

@ -1,25 +1,69 @@
# FHIRFLARE IG Toolkit
![FHIRFLARE Logo](static/FHIRFLARE.png)
## 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), and uploading complex test data sets with dependency management. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
The application can run in two modes:
The application can run in four modes:
* **Standalone:** Includes a Dockerized Flask frontend, SQLite database, and an embedded HAPI FHIR server for local validation and interaction.
* **Lite:** Includes only the Dockerized Flask frontend and SQLite database, excluding the local HAPI FHIR server. Requires connection to external FHIR servers for certain features.
* Installation Modes (Lite, Custom URL, Hapi, and Candle)
This toolkit now offers four primary installation modes, configured via a simple command-line prompt during setup:
Lite Mode
Includes the Flask frontend, SQLite database, and core tooling (GoFSH, SUSHI) without an embedded FHIR server.
Requires you to provide a URL for an external FHIR server when using features like the FHIR API Explorer and validation.
Does not require Git, Maven, or .NET for setup.
Ideal for users who will always connect to an existing external FHIR server.
Custom URL Mode
Similar to Lite Mode, but prompts you for a specific external FHIR server URL to use as the default for the toolkit.
The application will be pre-configured to use your custom URL for all FHIR-related operations.
Does not require Git, Maven, or .NET for setup.
Hapi Mode
Includes the full FHIRFLARE toolkit and an embedded HAPI FHIR server.
The docker-compose configuration will proxy requests to the internal HAPI server, which is accessible via http://localhost:8080/fhir.
Requires Git and Maven during the initial build process to compile the HAPI FHIR server.
Ideal for users who want a self-contained, offline development and testing environment.
Candle Mode
Includes the full FHIRFLARE toolkit and an embedded FHIR Candle server.
The docker-compose configuration will proxy requests to the internal Candle server, which is accessible via http://localhost:5001/fhir/<version>.
Requires Git and the .NET SDK during the initial build process.
Ideal for users who want to use the .NET-based FHIR server for development and testing.
## Installation Modes (Lite vs. Standalone)
This toolkit offers two primary installation modes to suit different needs:
* **Standalone Version:**
* **Standalone Version - Hapi / Candle:**
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
* 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.
@ -30,6 +74,12 @@ This toolkit offers two primary installation modes to suit different needs:
## Features
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking).
* **Enhanced Package Search and Import:**
* Interactive page (`/search-and-import`) to search for FHIR IG packages from configured registries.
* Displays package details, version history, dependencies, and dependents.
* Utilizes a local database cache (`CachedPackage`) for faster subsequent searches.
* Background task to refresh the package cache from registries (`/api/refresh-cache-task`).
* 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.*
@ -55,9 +105,19 @@ This toolkit offers two primary installation modes to suit different needs:
* 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.*
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, and uploading test data.
* **Live Console:** Real-time logs for push, validation, upload test data, and FSH conversion operations.
* **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.
@ -101,9 +161,10 @@ docker run -d \
-v ./logs:/app/logs \
--name fhirflare-lite \
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
Standalone Version (Includes local HAPI FHIR):
Bash
# Pull the latest Standalone image
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
@ -119,7 +180,6 @@ docker run -d \
-v ./logs:/app/logs \
--name fhirflare-standalone \
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
Building from Source (Developers)
Using Windows .bat Scripts (Standalone Version Only):
@ -127,258 +187,252 @@ First Time Setup:
Run Build and Run for first time.bat:
Code snippet
cd "<project folder>"
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
copy .\\hapi-fhir-Setup\\target\\classes\\application.yaml .\\hapi-fhir-jpaserver\\target\\classes\\application.yaml
mvn clean package -DskipTests=true -Pboot
docker-compose build --no-cache
docker-compose up -d
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
Subsequent Runs:
Run Run.bat:
Code snippet
cd "<project folder>"
docker-compose up -d
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
Access the Application:
Flask UI: http://localhost:5000
HAPI FHIR server: http://localhost:8080
Manual Setup (Linux/MacOS/Windows):
Preparation (Standalone Version Only):
Bash
cd <project folder>
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
Build:
Bash
# Build HAPI FHIR (Standalone Version Only)
mvn clean package -DskipTests=true -Pboot
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version)
docker-compose build --no-cache
Run:
docker-compose up -d
Bash
docker-compose up -d
Access the Application:
Flask UI: http://localhost:5000
HAPI FHIR server (Standalone only): http://localhost:8080
Local Development (Without Docker):
Clone the Repository:
Bash
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git)
cd FHIRFLARE-IG-Toolkit
Install Dependencies:
Bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
Bash
# Example for Debian/Ubuntu
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
sudo apt-get install -y nodejs
# Install globally
npm install -g gofsh fsh-sushi
Set Environment Variables:
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
Navigate to Import IG (/import-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).
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" \
@ -393,10 +447,11 @@ curl -X POST http://localhost:5000/api/push-ig \
"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" \
@ -410,10 +465,29 @@ curl -X POST http://localhost:5000/api/upload-test-data \
-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.
Returns a streaming NDJSON response with progress and final summary.
Retrieve Bundles
Bash
Uses multipart/form-data for file uploads.
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.
@ -422,181 +496,132 @@ 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
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.
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data.
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, 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.
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, and dependency-aware test data uploading, making it a versatile platform for FHIR developers.
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).
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.
(Keep older updates listed here if desired)
(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
@ -608,7 +633,7 @@ FHIRFLARE-IG-Toolkit/
├── README.md # Project documentation
├── requirements.txt # Python dependencies
├── Run.bat # Windows script for running Docker
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload
├── 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
@ -649,38 +674,31 @@ FHIRFLARE-IG-Toolkit/
│ ├── 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.
## 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.
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.

2529
app.py

File diff suppressed because it is too large Load Diff

1
charts/.gitignore vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,152 @@
{{/*
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

@ -0,0 +1,91 @@
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

@ -0,0 +1,36 @@
{{- 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

@ -0,0 +1,18 @@
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

@ -0,0 +1,41 @@
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

@ -0,0 +1,89 @@
# 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

23
charts/install.sh Executable file
View File

@ -0,0 +1,23 @@
#!/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

@ -3,18 +3,19 @@ services:
fhirflare:
build:
context: .
dockerfile: Dockerfile
dockerfile: Dockerfile.lite
ports:
- "5000:5000"
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
- "8080:8080"
volumes:
- ./instance:/app/instance
- ./static/uploads:/app/static/uploads
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
- ./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

@ -0,0 +1,22 @@
# 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

@ -0,0 +1,5 @@
#!/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

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

View File

@ -0,0 +1,18 @@
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

@ -0,0 +1,50 @@
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

@ -0,0 +1,5 @@
#!/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

@ -0,0 +1,19 @@
# 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

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

View File

@ -0,0 +1,25 @@
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

@ -0,0 +1,5 @@
#!/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

@ -0,0 +1,19 @@
# 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

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

66
docker/Dockerfile Normal file
View File

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

7
docker/build-docker.sh Executable file
View File

@ -0,0 +1,7 @@
#!/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"

281
forms.py
View File

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

Binary file not shown.

Binary file not shown.

View File

@ -1,22 +0,0 @@
{
"package_name": "hl7.fhir.au.base",
"version": "5.1.0-preview",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
},
{
"name": "hl7.terminology.r4",
"version": "6.2.0"
},
{
"name": "hl7.fhir.uv.extensions.r4",
"version": "5.2.0"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:12.567847+00:00"
}

View File

@ -1,34 +0,0 @@
{
"package_name": "hl7.fhir.au.core",
"version": "1.1.0-preview",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
},
{
"name": "hl7.terminology.r4",
"version": "6.2.0"
},
{
"name": "hl7.fhir.uv.extensions.r4",
"version": "5.2.0"
},
{
"name": "hl7.fhir.au.base",
"version": "5.1.0-preview"
},
{
"name": "hl7.fhir.uv.smart-app-launch",
"version": "2.1.0"
},
{
"name": "hl7.fhir.uv.ipa",
"version": "1.0.0"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:55:42.789700+00:00"
}

View File

@ -1,9 +0,0 @@
{
"package_name": "hl7.fhir.r4.core",
"version": "4.0.1",
"dependency_mode": "recursive",
"imported_dependencies": [],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:55:57.375318+00:00"
}

View File

@ -1,14 +0,0 @@
{
"package_name": "hl7.fhir.uv.extensions.r4",
"version": "5.2.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:08.781015+00:00"
}

View File

@ -1,22 +0,0 @@
{
"package_name": "hl7.fhir.uv.ipa",
"version": "1.0.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
},
{
"name": "hl7.terminology.r4",
"version": "5.0.0"
},
{
"name": "hl7.fhir.uv.smart-app-launch",
"version": "2.0.0"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:16.763567+00:00"
}

View File

@ -1,14 +0,0 @@
{
"package_name": "hl7.fhir.uv.smart-app-launch",
"version": "2.0.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:23.123899+00:00"
}

View File

@ -1,18 +0,0 @@
{
"package_name": "hl7.fhir.uv.smart-app-launch",
"version": "2.1.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
},
{
"name": "hl7.terminology.r4",
"version": "5.0.0"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:14.378183+00:00"
}

View File

@ -1,14 +0,0 @@
{
"package_name": "hl7.terminology.r4",
"version": "5.0.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:21.577007+00:00"
}

View File

@ -1,14 +0,0 @@
{
"package_name": "hl7.terminology.r4",
"version": "6.2.0",
"dependency_mode": "recursive",
"imported_dependencies": [
{
"name": "hl7.fhir.r4.core",
"version": "4.0.1"
}
],
"complies_with_profiles": [],
"imposed_profiles": [],
"timestamp": "2025-04-30T10:56:04.853495+00:00"
}

View File

@ -1,6 +0,0 @@
#FileLock
#Wed Apr 30 12:04:24 UTC 2025
server=172.19.0.2\:43507
hostName=999cce957cc1
method=file
id=196869576a9d4c77e61cf01b462a28e4b182a3e00bd

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,607 +0,0 @@
2025-04-24 12:53:20,569 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 12:53:20,575 INFO supervisord started with pid 1
2025-04-24 12:53:21,596 INFO spawned: 'flask' with pid 7
2025-04-24 12:53:21,602 INFO spawned: 'tomcat' with pid 8
2025-04-24 12:53:23,413 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:53:24,422 INFO spawned: 'flask' with pid 39
2025-04-24 12:53:25,576 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:53:27,589 INFO spawned: 'flask' with pid 47
2025-04-24 12:53:28,513 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:53:31,542 INFO spawned: 'flask' with pid 48
2025-04-24 12:53:32,775 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:53:33,780 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-24 12:53:52,023 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 12:55:05,132 WARN received SIGTERM indicating exit request
2025-04-24 12:55:05,142 INFO waiting for tomcat to die
2025-04-24 12:55:07,490 WARN stopped: tomcat (exit status 143)
2025-04-24 12:55:19,591 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 12:55:19,604 INFO supervisord started with pid 1
2025-04-24 12:55:20,610 INFO spawned: 'flask' with pid 7
2025-04-24 12:55:20,621 INFO spawned: 'tomcat' with pid 8
2025-04-24 12:55:22,555 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:55:23,565 INFO spawned: 'flask' with pid 39
2025-04-24 12:55:24,225 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:55:26,239 INFO spawned: 'flask' with pid 47
2025-04-24 12:55:27,490 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:55:30,500 INFO spawned: 'flask' with pid 48
2025-04-24 12:55:31,273 WARN exited: flask (exit status 1; not expected)
2025-04-24 12:55:32,275 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-24 12:55:50,730 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 12:59:49,405 WARN received SIGTERM indicating exit request
2025-04-24 12:59:49,408 INFO waiting for tomcat to die
2025-04-24 12:59:52,896 INFO waiting for tomcat to die
2025-04-24 12:59:54,202 WARN stopped: tomcat (exit status 143)
2025-04-24 13:00:22,084 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:00:22,099 INFO supervisord started with pid 1
2025-04-24 13:00:23,143 INFO spawned: 'flask' with pid 7
2025-04-24 13:00:23,154 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:00:26,260 WARN exited: flask (exit status 1; not expected)
2025-04-24 13:00:27,270 INFO spawned: 'flask' with pid 39
2025-04-24 13:00:28,548 WARN exited: flask (exit status 1; not expected)
2025-04-24 13:00:30,696 INFO spawned: 'flask' with pid 43
2025-04-24 13:00:33,861 WARN exited: flask (exit status 1; not expected)
2025-04-24 13:00:36,875 INFO spawned: 'flask' with pid 49
2025-04-24 13:00:38,494 WARN exited: flask (exit status 1; not expected)
2025-04-24 13:00:39,499 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-24 13:00:53,383 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 13:01:56,879 WARN received SIGTERM indicating exit request
2025-04-24 13:01:56,880 INFO waiting for tomcat to die
2025-04-24 13:01:59,200 WARN stopped: tomcat (exit status 143)
2025-04-24 13:02:26,588 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:02:26,602 INFO supervisord started with pid 1
2025-04-24 13:02:27,616 INFO spawned: 'flask' with pid 7
2025-04-24 13:02:27,625 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:02:37,957 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-24 13:02:58,407 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 13:11:42,994 WARN received SIGTERM indicating exit request
2025-04-24 13:11:43,001 INFO waiting for flask, tomcat to die
2025-04-24 13:11:46,095 INFO waiting for flask, tomcat to die
2025-04-24 13:11:47,585 WARN stopped: tomcat (exit status 143)
2025-04-24 13:11:48,632 WARN stopped: flask (terminated by SIGTERM)
2025-04-24 13:12:07,464 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:12:07,475 INFO supervisord started with pid 1
2025-04-24 13:12:08,488 INFO spawned: 'flask' with pid 7
2025-04-24 13:12:08,496 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:12:18,696 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-24 13:12:39,092 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 13:14:59,175 WARN received SIGTERM indicating exit request
2025-04-24 13:14:59,178 INFO waiting for flask, tomcat to die
2025-04-24 13:15:00,617 WARN received SIGTERM indicating exit request
2025-04-24 13:15:02,344 WARN stopped: tomcat (exit status 143)
2025-04-24 13:15:02,347 INFO waiting for flask to die
2025-04-24 13:15:03,354 WARN stopped: flask (terminated by SIGTERM)
2025-04-24 13:15:28,023 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:15:28,035 INFO supervisord started with pid 1
2025-04-24 13:15:29,049 INFO spawned: 'flask' with pid 7
2025-04-24 13:15:29,085 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:15:39,240 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-24 13:15:59,284 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 13:17:28,582 WARN received SIGTERM indicating exit request
2025-04-24 13:17:28,583 INFO waiting for flask, tomcat to die
2025-04-24 13:17:29,955 WARN stopped: tomcat (exit status 143)
2025-04-24 13:17:30,969 WARN stopped: flask (terminated by SIGTERM)
2025-04-24 13:17:48,425 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:17:48,436 INFO supervisord started with pid 1
2025-04-24 13:17:49,445 INFO spawned: 'flask' with pid 7
2025-04-24 13:17:49,452 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:17:59,706 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-24 13:18:19,818 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-24 13:25:26,199 WARN received SIGTERM indicating exit request
2025-04-24 13:25:26,212 INFO waiting for flask, tomcat to die
2025-04-24 13:25:27,654 WARN stopped: tomcat (exit status 143)
2025-04-24 13:25:28,678 WARN stopped: flask (terminated by SIGTERM)
2025-04-24 13:25:52,381 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-24 13:25:52,388 INFO supervisord started with pid 1
2025-04-24 13:25:53,393 INFO spawned: 'flask' with pid 7
2025-04-24 13:25:53,402 INFO spawned: 'tomcat' with pid 8
2025-04-24 13:26:03,915 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-24 13:26:23,694 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 10:58:58,306 WARN received SIGTERM indicating exit request
2025-04-25 10:58:58,338 INFO waiting for flask, tomcat to die
2025-04-25 10:59:01,538 INFO waiting for flask, tomcat to die
2025-04-25 10:59:04,692 WARN stopped: tomcat (exit status 143)
2025-04-25 10:59:04,740 INFO waiting for flask to die
2025-04-25 10:59:05,802 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 10:59:31,502 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 10:59:31,512 INFO supervisord started with pid 1
2025-04-25 10:59:32,532 INFO spawned: 'flask' with pid 7
2025-04-25 10:59:32,538 INFO spawned: 'tomcat' with pid 8
2025-04-25 10:59:42,663 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:00:03,350 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:13:49,313 WARN received SIGTERM indicating exit request
2025-04-25 11:13:49,316 INFO waiting for flask, tomcat to die
2025-04-25 11:13:52,578 WARN stopped: tomcat (exit status 143)
2025-04-25 11:13:52,582 INFO waiting for flask to die
2025-04-25 11:13:53,593 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:14:17,467 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:14:17,478 INFO supervisord started with pid 1
2025-04-25 11:14:18,487 INFO spawned: 'flask' with pid 7
2025-04-25 11:14:18,494 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:14:20,261 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:14:21,580 INFO spawned: 'flask' with pid 39
2025-04-25 11:14:23,183 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:14:25,231 INFO spawned: 'flask' with pid 48
2025-04-25 11:14:27,998 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:14:31,012 INFO spawned: 'flask' with pid 51
2025-04-25 11:14:32,140 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:14:33,144 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-25 11:14:38,150 WARN received SIGTERM indicating exit request
2025-04-25 11:14:38,151 INFO waiting for tomcat to die
2025-04-25 11:14:39,191 WARN stopped: tomcat (exit status 143)
2025-04-25 11:16:55,759 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:16:55,767 INFO supervisord started with pid 1
2025-04-25 11:16:56,782 INFO spawned: 'flask' with pid 7
2025-04-25 11:16:56,787 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:17:07,143 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:17:26,965 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:22:46,985 WARN received SIGTERM indicating exit request
2025-04-25 11:22:46,986 INFO waiting for flask, tomcat to die
2025-04-25 11:22:48,031 WARN stopped: tomcat (exit status 143)
2025-04-25 11:22:49,048 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:23:06,664 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:23:06,671 INFO supervisord started with pid 1
2025-04-25 11:23:07,675 INFO spawned: 'flask' with pid 7
2025-04-25 11:23:07,686 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:23:17,936 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:23:38,271 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:27:35,932 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:27:35,942 INFO supervisord started with pid 1
2025-04-25 11:27:36,948 INFO spawned: 'flask' with pid 7
2025-04-25 11:27:36,953 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:27:47,019 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:28:07,048 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:31:23,895 WARN received SIGTERM indicating exit request
2025-04-25 11:31:23,896 INFO waiting for flask, tomcat to die
2025-04-25 11:31:25,050 WARN stopped: tomcat (exit status 143)
2025-04-25 11:31:25,054 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:39:03,546 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:39:03,552 INFO supervisord started with pid 1
2025-04-25 11:39:04,569 INFO spawned: 'flask' with pid 7
2025-04-25 11:39:04,572 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:39:14,792 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:39:34,800 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:41:11,436 WARN received SIGTERM indicating exit request
2025-04-25 11:41:11,437 INFO waiting for flask, tomcat to die
2025-04-25 11:41:12,528 WARN stopped: tomcat (exit status 143)
2025-04-25 11:41:12,533 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:41:34,275 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:41:34,285 INFO supervisord started with pid 1
2025-04-25 11:41:35,319 INFO spawned: 'flask' with pid 7
2025-04-25 11:41:35,324 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:41:46,071 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:41:47,900 WARN received SIGTERM indicating exit request
2025-04-25 11:41:47,901 INFO waiting for flask, tomcat to die
2025-04-25 11:41:48,935 WARN stopped: tomcat (exit status 143)
2025-04-25 11:41:48,940 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:42:03,154 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:42:03,159 INFO supervisord started with pid 1
2025-04-25 11:42:04,166 INFO spawned: 'flask' with pid 7
2025-04-25 11:42:04,170 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:42:14,412 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:42:34,252 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:44:19,667 WARN received SIGTERM indicating exit request
2025-04-25 11:44:19,668 INFO waiting for flask, tomcat to die
2025-04-25 11:44:20,808 WARN stopped: tomcat (exit status 143)
2025-04-25 11:44:20,812 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:44:40,799 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:44:40,807 INFO supervisord started with pid 1
2025-04-25 11:44:41,813 INFO spawned: 'flask' with pid 7
2025-04-25 11:44:41,824 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:44:43,064 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:44:44,073 INFO spawned: 'flask' with pid 48
2025-04-25 11:44:45,296 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:44:47,306 INFO spawned: 'flask' with pid 53
2025-04-25 11:44:48,167 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:44:51,178 INFO spawned: 'flask' with pid 55
2025-04-25 11:44:51,934 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:44:52,937 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-25 11:45:11,860 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:47:25,513 WARN received SIGTERM indicating exit request
2025-04-25 11:47:25,513 INFO waiting for tomcat to die
2025-04-25 11:47:26,630 WARN stopped: tomcat (exit status 143)
2025-04-25 11:48:19,775 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:48:19,782 INFO supervisord started with pid 1
2025-04-25 11:48:20,786 INFO spawned: 'flask' with pid 7
2025-04-25 11:48:20,795 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:48:21,467 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:48:22,508 INFO spawned: 'flask' with pid 42
2025-04-25 11:48:23,009 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:48:25,019 INFO spawned: 'flask' with pid 47
2025-04-25 11:48:25,381 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:48:28,401 INFO spawned: 'flask' with pid 59
2025-04-25 11:48:29,045 WARN exited: flask (exit status 1; not expected)
2025-04-25 11:48:30,000 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-25 11:48:51,365 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:53:09,317 WARN received SIGTERM indicating exit request
2025-04-25 11:53:09,319 INFO waiting for tomcat to die
2025-04-25 11:53:11,346 WARN stopped: tomcat (exit status 143)
2025-04-25 11:53:49,588 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:53:49,595 INFO supervisord started with pid 1
2025-04-25 11:53:50,604 INFO spawned: 'flask' with pid 7
2025-04-25 11:53:50,608 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:54:00,901 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:54:20,619 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:55:01,514 WARN received SIGTERM indicating exit request
2025-04-25 11:55:01,515 INFO waiting for flask, tomcat to die
2025-04-25 11:55:02,357 WARN stopped: tomcat (exit status 143)
2025-04-25 11:55:02,361 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:56:05,943 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:56:05,951 INFO supervisord started with pid 1
2025-04-25 11:56:06,957 INFO spawned: 'flask' with pid 7
2025-04-25 11:56:06,961 INFO spawned: 'tomcat' with pid 8
2025-04-25 11:56:17,835 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 11:56:37,396 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 11:59:16,851 WARN received SIGTERM indicating exit request
2025-04-25 11:59:16,852 INFO waiting for flask, tomcat to die
2025-04-25 11:59:18,045 WARN stopped: tomcat (exit status 143)
2025-04-25 11:59:18,051 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 11:59:51,749 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 11:59:51,757 INFO supervisord started with pid 1
2025-04-25 11:59:52,766 INFO spawned: 'flask' with pid 7
2025-04-25 11:59:52,772 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:00:01,958 WARN received SIGTERM indicating exit request
2025-04-25 12:00:01,960 INFO waiting for flask, tomcat to die
2025-04-25 12:00:02,986 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:00:02,987 WARN stopped: tomcat (exit status 143)
2025-04-25 12:00:02,994 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:00:25,888 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:00:25,893 INFO supervisord started with pid 1
2025-04-25 12:00:26,906 INFO spawned: 'flask' with pid 8
2025-04-25 12:00:26,909 INFO spawned: 'tomcat' with pid 9
2025-04-25 12:00:37,092 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:00:57,539 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 12:12:29,121 WARN received SIGTERM indicating exit request
2025-04-25 12:12:29,129 INFO waiting for flask, tomcat to die
2025-04-25 12:12:32,499 WARN stopped: tomcat (exit status 143)
2025-04-25 12:12:32,503 INFO waiting for flask to die
2025-04-25 12:12:33,510 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:13:13,667 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:13:13,674 INFO supervisord started with pid 1
2025-04-25 12:13:14,680 INFO spawned: 'flask' with pid 7
2025-04-25 12:13:14,685 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:13:16,181 WARN exited: flask (exit status 1; not expected)
2025-04-25 12:13:17,200 INFO spawned: 'flask' with pid 40
2025-04-25 12:13:18,463 WARN exited: flask (exit status 1; not expected)
2025-04-25 12:13:20,487 INFO spawned: 'flask' with pid 48
2025-04-25 12:13:21,529 WARN exited: flask (exit status 1; not expected)
2025-04-25 12:13:24,540 INFO spawned: 'flask' with pid 50
2025-04-25 12:13:25,478 WARN exited: flask (exit status 1; not expected)
2025-04-25 12:13:26,481 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-25 12:13:44,834 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 12:23:53,921 WARN received SIGTERM indicating exit request
2025-04-25 12:23:53,934 INFO waiting for tomcat to die
2025-04-25 12:23:57,299 WARN stopped: tomcat (exit status 143)
2025-04-25 12:24:32,364 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:24:32,376 INFO supervisord started with pid 1
2025-04-25 12:24:33,384 INFO spawned: 'flask' with pid 7
2025-04-25 12:24:33,390 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:24:44,218 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:25:04,301 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 12:31:25,175 WARN received SIGTERM indicating exit request
2025-04-25 12:31:25,177 INFO waiting for flask, tomcat to die
2025-04-25 12:31:28,374 WARN stopped: tomcat (exit status 143)
2025-04-25 12:31:28,379 INFO waiting for flask to die
2025-04-25 12:31:29,389 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:31:49,504 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:31:49,512 INFO supervisord started with pid 1
2025-04-25 12:31:50,518 INFO spawned: 'flask' with pid 7
2025-04-25 12:31:50,523 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:32:00,852 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:32:21,057 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 12:49:44,415 WARN received SIGTERM indicating exit request
2025-04-25 12:49:44,432 INFO waiting for flask, tomcat to die
2025-04-25 12:49:48,411 INFO waiting for flask, tomcat to die
2025-04-25 12:49:49,464 WARN stopped: tomcat (exit status 143)
2025-04-25 12:49:50,493 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:50:14,025 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:50:14,034 INFO supervisord started with pid 1
2025-04-25 12:50:15,064 INFO spawned: 'flask' with pid 7
2025-04-25 12:50:15,086 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:50:25,385 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:50:30,391 WARN received SIGTERM indicating exit request
2025-04-25 12:50:30,392 INFO waiting for flask, tomcat to die
2025-04-25 12:50:31,427 WARN stopped: tomcat (exit status 143)
2025-04-25 12:50:31,435 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:52:24,814 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:52:24,825 INFO supervisord started with pid 1
2025-04-25 12:52:25,833 INFO spawned: 'flask' with pid 7
2025-04-25 12:52:25,839 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:52:35,983 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:52:56,005 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 12:56:07,983 WARN received SIGTERM indicating exit request
2025-04-25 12:56:07,994 INFO waiting for flask, tomcat to die
2025-04-25 12:56:11,273 WARN stopped: tomcat (exit status 143)
2025-04-25 12:56:11,314 INFO waiting for flask to die
2025-04-25 12:56:12,403 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 12:56:48,029 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 12:56:48,037 INFO supervisord started with pid 1
2025-04-25 12:56:49,049 INFO spawned: 'flask' with pid 7
2025-04-25 12:56:49,061 INFO spawned: 'tomcat' with pid 8
2025-04-25 12:56:59,229 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 12:57:19,563 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 13:01:49,576 WARN received SIGTERM indicating exit request
2025-04-25 13:01:49,585 INFO waiting for flask, tomcat to die
2025-04-25 13:01:50,897 WARN stopped: tomcat (exit status 143)
2025-04-25 13:01:51,917 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 13:38:31,018 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 13:38:31,042 INFO supervisord started with pid 1
2025-04-25 13:38:32,075 INFO spawned: 'flask' with pid 7
2025-04-25 13:38:32,125 INFO spawned: 'tomcat' with pid 8
2025-04-25 13:38:42,115 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 13:39:02,430 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 21:37:13,481 WARN received SIGTERM indicating exit request
2025-04-25 21:37:13,512 INFO waiting for flask, tomcat to die
2025-04-25 21:37:16,663 INFO waiting for flask, tomcat to die
2025-04-25 21:37:19,722 INFO waiting for flask, tomcat to die
2025-04-25 21:37:22,844 INFO waiting for flask, tomcat to die
2025-04-25 21:37:57,094 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 21:37:57,107 INFO supervisord started with pid 1
2025-04-25 21:37:58,118 INFO spawned: 'flask' with pid 7
2025-04-25 21:37:58,127 INFO spawned: 'tomcat' with pid 8
2025-04-25 21:38:02,035 WARN exited: flask (exit status 1; not expected)
2025-04-25 21:38:03,047 INFO spawned: 'flask' with pid 41
2025-04-25 21:38:06,099 WARN exited: flask (exit status 1; not expected)
2025-04-25 21:38:08,126 INFO spawned: 'flask' with pid 50
2025-04-25 21:38:11,124 WARN exited: flask (exit status 1; not expected)
2025-04-25 21:38:14,136 INFO spawned: 'flask' with pid 54
2025-04-25 21:38:15,907 WARN exited: flask (exit status 1; not expected)
2025-04-25 21:38:16,911 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-25 21:38:28,264 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 21:44:55,684 WARN received SIGTERM indicating exit request
2025-04-25 21:44:55,709 INFO waiting for tomcat to die
2025-04-25 21:44:58,801 INFO waiting for tomcat to die
2025-04-25 21:44:59,813 WARN stopped: tomcat (exit status 143)
2025-04-25 21:45:23,313 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 21:45:23,323 INFO supervisord started with pid 1
2025-04-25 21:45:24,329 INFO spawned: 'flask' with pid 7
2025-04-25 21:45:24,333 INFO spawned: 'tomcat' with pid 8
2025-04-25 21:45:34,334 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 21:45:54,338 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 21:46:48,550 WARN received SIGTERM indicating exit request
2025-04-25 21:46:48,551 INFO waiting for flask, tomcat to die
2025-04-25 21:46:49,793 WARN stopped: tomcat (exit status 143)
2025-04-25 21:46:49,802 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 21:47:10,753 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 21:47:10,763 INFO supervisord started with pid 1
2025-04-25 21:47:11,769 INFO spawned: 'flask' with pid 8
2025-04-25 21:47:11,777 INFO spawned: 'tomcat' with pid 9
2025-04-25 21:47:21,957 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 21:47:41,876 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 21:53:21,452 WARN received SIGTERM indicating exit request
2025-04-25 21:53:21,476 INFO waiting for flask, tomcat to die
2025-04-25 21:53:24,100 WARN stopped: tomcat (exit status 143)
2025-04-25 21:53:25,125 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 21:53:38,324 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 21:53:38,334 INFO supervisord started with pid 1
2025-04-25 21:53:39,340 INFO spawned: 'flask' with pid 7
2025-04-25 21:53:39,346 INFO spawned: 'tomcat' with pid 8
2025-04-25 22:44:33,924 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 22:44:33,928 INFO supervisord started with pid 1
2025-04-25 22:44:34,941 INFO spawned: 'flask' with pid 7
2025-04-25 22:44:34,946 INFO spawned: 'tomcat' with pid 8
2025-04-25 22:44:44,961 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 22:45:05,700 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-25 22:48:32,580 WARN received SIGTERM indicating exit request
2025-04-25 22:48:32,582 INFO waiting for flask, tomcat to die
2025-04-25 22:48:35,957 WARN stopped: tomcat (exit status 143)
2025-04-25 22:48:35,961 INFO waiting for flask to die
2025-04-25 22:48:36,966 WARN stopped: flask (terminated by SIGTERM)
2025-04-25 22:48:46,205 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-25 22:48:46,215 INFO supervisord started with pid 1
2025-04-25 22:48:47,223 INFO spawned: 'flask' with pid 8
2025-04-25 22:48:47,229 INFO spawned: 'tomcat' with pid 9
2025-04-25 22:48:58,098 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-25 22:49:17,502 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 02:42:59,958 WARN received SIGTERM indicating exit request
2025-04-26 02:42:59,974 INFO waiting for flask, tomcat to die
2025-04-26 02:43:02,041 WARN stopped: tomcat (exit status 143)
2025-04-26 02:43:03,053 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 02:43:12,563 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 02:43:12,567 INFO supervisord started with pid 1
2025-04-26 02:43:13,573 INFO spawned: 'flask' with pid 7
2025-04-26 02:43:13,588 INFO spawned: 'tomcat' with pid 8
2025-04-26 02:43:24,337 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 02:43:44,046 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 02:48:20,402 WARN received SIGTERM indicating exit request
2025-04-26 02:48:20,403 INFO waiting for flask, tomcat to die
2025-04-26 02:48:21,337 WARN stopped: tomcat (exit status 143)
2025-04-26 02:48:21,343 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 02:48:31,678 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 02:48:31,683 INFO supervisord started with pid 1
2025-04-26 02:48:32,689 INFO spawned: 'flask' with pid 7
2025-04-26 02:48:32,693 INFO spawned: 'tomcat' with pid 8
2025-04-26 02:48:43,291 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 02:49:03,175 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 03:11:20,401 WARN received SIGTERM indicating exit request
2025-04-26 03:11:20,402 INFO waiting for flask, tomcat to die
2025-04-26 03:11:23,066 WARN stopped: tomcat (exit status 143)
2025-04-26 03:11:24,075 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 03:11:33,054 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 03:11:33,059 INFO supervisord started with pid 1
2025-04-26 03:11:34,065 INFO spawned: 'flask' with pid 8
2025-04-26 03:11:34,069 INFO spawned: 'tomcat' with pid 9
2025-04-26 03:11:44,220 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 03:12:04,722 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 03:14:42,212 WARN received SIGTERM indicating exit request
2025-04-26 03:14:42,213 INFO waiting for flask, tomcat to die
2025-04-26 03:14:43,411 WARN stopped: tomcat (exit status 143)
2025-04-26 03:14:43,415 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 03:14:54,625 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 03:14:54,632 INFO supervisord started with pid 1
2025-04-26 03:14:55,637 INFO spawned: 'flask' with pid 7
2025-04-26 03:14:55,641 INFO spawned: 'tomcat' with pid 8
2025-04-26 03:15:06,398 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 03:15:25,714 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 04:25:12,125 WARN received SIGTERM indicating exit request
2025-04-26 04:25:12,126 INFO waiting for flask, tomcat to die
2025-04-26 04:25:13,146 WARN stopped: tomcat (exit status 143)
2025-04-26 04:25:13,151 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 04:25:23,591 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 04:25:23,597 INFO supervisord started with pid 1
2025-04-26 04:25:24,601 INFO spawned: 'flask' with pid 7
2025-04-26 04:25:24,605 INFO spawned: 'tomcat' with pid 8
2025-04-26 04:25:35,590 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 04:25:55,281 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 04:28:14,105 WARN received SIGTERM indicating exit request
2025-04-26 04:28:14,106 INFO waiting for flask, tomcat to die
2025-04-26 04:28:15,433 WARN stopped: tomcat (exit status 143)
2025-04-26 04:28:15,440 WARN stopped: flask (terminated by SIGTERM)
2025-04-26 04:28:24,074 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 04:28:24,079 INFO supervisord started with pid 1
2025-04-26 04:28:25,084 INFO spawned: 'flask' with pid 7
2025-04-26 04:28:25,088 INFO spawned: 'tomcat' with pid 8
2025-04-26 04:28:36,046 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 04:28:55,993 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-26 10:03:36,178 WARN received SIGTERM indicating exit request
2025-04-26 10:03:36,237 INFO waiting for flask, tomcat to die
2025-04-26 10:03:39,837 INFO waiting for flask, tomcat to die
2025-04-26 10:03:43,260 INFO waiting for flask, tomcat to die
2025-04-26 12:18:50,367 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-26 12:18:50,378 INFO supervisord started with pid 1
2025-04-26 12:18:51,395 INFO spawned: 'flask' with pid 7
2025-04-26 12:18:51,403 INFO spawned: 'tomcat' with pid 8
2025-04-26 12:19:01,644 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-26 12:19:21,957 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-27 08:17:45,931 WARN received SIGTERM indicating exit request
2025-04-27 08:17:45,961 INFO waiting for flask, tomcat to die
2025-04-27 08:17:49,064 INFO waiting for flask, tomcat to die
2025-04-27 08:17:50,979 WARN stopped: tomcat (exit status 143)
2025-04-27 08:17:52,033 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 10:23:53,793 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 10:23:53,799 INFO supervisord started with pid 1
2025-04-30 10:23:54,803 INFO spawned: 'flask' with pid 7
2025-04-30 10:23:54,809 INFO spawned: 'tomcat' with pid 8
2025-04-30 10:24:05,021 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 10:24:25,099 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 10:38:47,304 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 10:38:47,308 INFO supervisord started with pid 1
2025-04-30 10:38:48,314 INFO spawned: 'flask' with pid 7
2025-04-30 10:38:48,319 INFO spawned: 'tomcat' with pid 8
2025-04-30 10:38:58,312 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 10:39:19,224 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 10:48:30,304 WARN received SIGTERM indicating exit request
2025-04-30 10:48:30,306 INFO waiting for flask, tomcat to die
2025-04-30 10:48:31,221 WARN stopped: tomcat (exit status 143)
2025-04-30 10:48:31,229 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 10:48:48,967 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 10:48:48,973 INFO supervisord started with pid 1
2025-04-30 10:48:49,980 INFO spawned: 'flask' with pid 7
2025-04-30 10:48:49,993 INFO spawned: 'tomcat' with pid 8
2025-04-30 10:48:50,939 WARN exited: flask (exit status 1; not expected)
2025-04-30 10:48:51,948 INFO spawned: 'flask' with pid 42
2025-04-30 10:48:53,630 WARN exited: flask (exit status 1; not expected)
2025-04-30 10:48:55,642 INFO spawned: 'flask' with pid 48
2025-04-30 10:48:56,893 WARN exited: flask (exit status 1; not expected)
2025-04-30 10:48:59,903 INFO spawned: 'flask' with pid 50
2025-04-30 10:49:00,921 WARN exited: flask (exit status 1; not expected)
2025-04-30 10:49:01,925 INFO gave up: flask entered FATAL state, too many start retries too quickly
2025-04-30 10:49:20,108 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 10:50:51,233 WARN received SIGTERM indicating exit request
2025-04-30 10:50:51,234 INFO waiting for tomcat to die
2025-04-30 10:50:52,273 WARN stopped: tomcat (exit status 143)
2025-04-30 10:51:10,940 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 10:51:10,945 INFO supervisord started with pid 1
2025-04-30 10:51:11,950 INFO spawned: 'flask' with pid 7
2025-04-30 10:51:11,961 INFO spawned: 'tomcat' with pid 8
2025-04-30 10:51:22,850 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 10:51:42,564 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 10:54:54,839 WARN received SIGTERM indicating exit request
2025-04-30 10:54:54,840 INFO waiting for flask, tomcat to die
2025-04-30 10:54:56,240 WARN stopped: tomcat (exit status 143)
2025-04-30 10:54:56,248 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 10:55:15,801 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 10:55:15,808 INFO supervisord started with pid 1
2025-04-30 10:55:16,813 INFO spawned: 'flask' with pid 7
2025-04-30 10:55:16,817 INFO spawned: 'tomcat' with pid 8
2025-04-30 10:55:26,889 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 10:55:46,902 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:28:01,890 WARN received SIGTERM indicating exit request
2025-04-30 11:28:01,892 INFO waiting for flask, tomcat to die
2025-04-30 11:28:04,299 WARN stopped: tomcat (exit status 143)
2025-04-30 11:28:05,309 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:28:23,096 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:28:23,104 INFO supervisord started with pid 1
2025-04-30 11:28:24,109 INFO spawned: 'flask' with pid 7
2025-04-30 11:28:24,113 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:28:34,157 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:28:54,148 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:31:15,173 WARN received SIGTERM indicating exit request
2025-04-30 11:31:15,174 INFO waiting for flask, tomcat to die
2025-04-30 11:31:16,320 WARN stopped: tomcat (exit status 143)
2025-04-30 11:31:16,326 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:32:02,823 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:32:02,830 INFO supervisord started with pid 1
2025-04-30 11:32:03,836 INFO spawned: 'flask' with pid 7
2025-04-30 11:32:03,840 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:32:13,957 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:32:34,251 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:39:35,550 WARN received SIGTERM indicating exit request
2025-04-30 11:39:35,551 INFO waiting for flask, tomcat to die
2025-04-30 11:39:36,383 WARN stopped: tomcat (exit status 143)
2025-04-30 11:39:36,389 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:39:54,475 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:39:54,485 INFO supervisord started with pid 1
2025-04-30 11:39:55,643 INFO spawned: 'flask' with pid 7
2025-04-30 11:39:55,686 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:40:06,202 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:40:26,438 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:41:09,107 WARN received SIGTERM indicating exit request
2025-04-30 11:41:09,108 INFO waiting for flask, tomcat to die
2025-04-30 11:41:10,318 WARN stopped: tomcat (exit status 143)
2025-04-30 11:41:10,323 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:41:27,816 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:41:27,821 INFO supervisord started with pid 1
2025-04-30 11:41:28,831 INFO spawned: 'flask' with pid 7
2025-04-30 11:41:28,834 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:41:39,062 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:41:59,574 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:52:23,663 WARN received SIGTERM indicating exit request
2025-04-30 11:52:23,667 INFO waiting for flask, tomcat to die
2025-04-30 11:52:26,278 WARN stopped: tomcat (exit status 143)
2025-04-30 11:52:27,286 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:52:44,337 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:52:44,342 INFO supervisord started with pid 1
2025-04-30 11:52:45,347 INFO spawned: 'flask' with pid 7
2025-04-30 11:52:45,350 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:52:55,759 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:53:16,085 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:56:41,422 WARN received SIGTERM indicating exit request
2025-04-30 11:56:41,423 INFO waiting for flask, tomcat to die
2025-04-30 11:56:42,293 WARN stopped: tomcat (exit status 143)
2025-04-30 11:56:42,299 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:56:56,156 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:56:56,162 INFO supervisord started with pid 1
2025-04-30 11:56:57,166 INFO spawned: 'flask' with pid 7
2025-04-30 11:56:57,170 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:57:07,183 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:57:27,999 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 11:59:08,707 WARN received SIGTERM indicating exit request
2025-04-30 11:59:08,708 INFO waiting for flask, tomcat to die
2025-04-30 11:59:09,919 WARN stopped: tomcat (exit status 143)
2025-04-30 11:59:09,928 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 11:59:27,085 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 11:59:27,092 INFO supervisord started with pid 1
2025-04-30 11:59:28,099 INFO spawned: 'flask' with pid 7
2025-04-30 11:59:28,102 INFO spawned: 'tomcat' with pid 8
2025-04-30 11:59:38,539 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 11:59:58,828 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 12:01:48,503 WARN received SIGTERM indicating exit request
2025-04-30 12:01:48,504 INFO waiting for flask, tomcat to die
2025-04-30 12:01:49,394 WARN stopped: tomcat (exit status 143)
2025-04-30 12:01:49,399 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 12:02:01,864 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 12:02:01,872 INFO supervisord started with pid 1
2025-04-30 12:02:02,878 INFO spawned: 'flask' with pid 7
2025-04-30 12:02:02,882 INFO spawned: 'tomcat' with pid 8
2025-04-30 12:02:13,021 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 12:02:33,482 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
2025-04-30 12:03:53,485 WARN received SIGTERM indicating exit request
2025-04-30 12:03:53,486 INFO waiting for flask, tomcat to die
2025-04-30 12:03:54,882 WARN stopped: tomcat (exit status 143)
2025-04-30 12:03:54,890 WARN stopped: flask (terminated by SIGTERM)
2025-04-30 12:04:06,241 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-04-30 12:04:06,248 INFO supervisord started with pid 1
2025-04-30 12:04:07,252 INFO spawned: 'flask' with pid 7
2025-04-30 12:04:07,256 INFO spawned: 'tomcat' with pid 8
2025-04-30 12:04:17,410 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
2025-04-30 12:04:38,224 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)

View File

@ -1 +0,0 @@
1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

123
package.py Normal file
View File

@ -0,0 +1,123 @@
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,5 +5,10 @@ requests==2.31.0
Flask-WTF==1.2.1
WTForms==3.1.2
Pytest
pyyaml==6.0.1
fhir.resources==8.0.0
Flask-Migrate==4.1.0
cachetools
beautifulsoup4
feedparser==6.0.11
flasgger

File diff suppressed because it is too large Load Diff

395
setup_linux.sh Normal file
View File

@ -0,0 +1,395 @@
#!/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

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,10 +1,14 @@
body {
width: 100%;
overflow: hidden;
height: 100vh;
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;
@ -380,8 +384,7 @@ body {
.smoke .s-8 { animation: smokeLeft 7s 5.6s infinite; }
.smoke .s-9 { animation: smokeRight 7s 6.3s infinite; }
/* Dark Theme (fire-off) */
html[data-theme="dark"] .fire-on { opacity: 0; }
html[data-theme="dark"] .section-center { box-shadow: 0 0 50px 5px rgba(200,200,200,.2); }
html[data-theme="dark"] .smoke { opacity: 1; transition-delay: 0.8s; }
html[data-theme="dark"] .fire span { bottom: -26px; transform: scale(0.15, 0.15) rotate(45deg); }
/* 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,225 +1,223 @@
/* Scoped to .fire-loading-overlay to prevent conflicts */
.fire-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(39, 5, 55, 0.8); /* Semi-transparent gradient background */
/* /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;
z-index: 1050; /* Above Bootstrap modals */
}
.fire-loading-overlay .campfire {
position: relative;
width: 400px; /* Reduced size for better fit */
height: 400px;
transform-origin: center center;
transform: scale(0.6); /* Scaled down for responsiveness */
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 */
}
.fire-loading-overlay .log {
.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: 238px;
height: 70px;
border-radius: 32px;
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 */
}
.fire-loading-overlay .log:before {
.minifire-log:before {
content: '';
display: block;
position: absolute;
top: 50%;
left: 35px;
width: 8px;
height: 8px;
border-radius: 32px;
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;
box-shadow: 0 0 0 2.5px #781e20, 0 0 0 10.5px #b35050, 0 0 0 13px #781e20, 0 0 0 21px #b35050, 0 0 0 23.5px #781e20, 0 0 0 31.5px #b35050;
/* 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 */
}
.fire-loading-overlay .streak {
.minifire-streak {
position: absolute;
height: 2px;
border-radius: 20px;
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 */
/* Streak positioning (unchanged from original) */
.fire-loading-overlay .streak:nth-child(1) { top: 10px; width: 90px; }
.fire-loading-overlay .streak:nth-child(2) { top: 10px; left: 100px; width: 80px; }
.fire-loading-overlay .streak:nth-child(3) { top: 10px; left: 190px; width: 30px; }
.fire-loading-overlay .streak:nth-child(4) { top: 22px; width: 132px; }
.fire-loading-overlay .streak:nth-child(5) { top: 22px; left: 142px; width: 48px; }
.fire-loading-overlay .streak:nth-child(6) { top: 22px; left: 200px; width: 28px; }
.fire-loading-overlay .streak:nth-child(7) { top: 34px; left: 74px; width: 160px; }
.fire-loading-overlay .streak:nth-child(8) { top: 46px; left: 110px; width: 40px; }
.fire-loading-overlay .streak:nth-child(9) { top: 46px; left: 170px; width: 54px; }
.fire-loading-overlay .streak:nth-child(10) { top: 58px; left: 90px; width: 110px; }
/* 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 */
.fire-loading-overlay .log {
transform-origin: center center;
box-shadow: 0 0 2px 1px rgba(0,0,0,0.15);
}
.fire-loading-overlay .log:nth-child(1) { bottom: 100px; left: 100px; transform: rotate(150deg) scaleX(0.75); z-index: 20; }
.fire-loading-overlay .log:nth-child(2) { bottom: 120px; left: 140px; transform: rotate(110deg) scaleX(0.75); z-index: 10; }
.fire-loading-overlay .log:nth-child(3) { bottom: 98px; left: 68px; transform: rotate(-10deg) scaleX(0.75); }
.fire-loading-overlay .log:nth-child(4) { bottom: 80px; left: 220px; transform: rotate(-120deg) scaleX(0.75); z-index: 26; }
.fire-loading-overlay .log:nth-child(5) { bottom: 75px; left: 210px; transform: rotate(-30deg) scaleX(0.75); z-index: 25; }
.fire-loading-overlay .log:nth-child(6) { bottom: 92px; left: 280px; transform: rotate(35deg) scaleX(0.85); z-index: 30; }
.fire-loading-overlay .log:nth-child(7) { bottom: 70px; left: 300px; transform: rotate(-30deg) scaleX(0.75); z-index: 20; }
.fire-loading-overlay .stick {
/* --- Scaled Down Sticks --- */
.minifire-stick {
position: absolute;
width: 68px;
height: 20px;
border-radius: 10px;
box-shadow: 0 0 2px 1px rgba(0,0,0,0.1);
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;
}
.fire-loading-overlay .stick:before {
.minifire-stick:before {
content: '';
display: block;
position: absolute;
bottom: 100%;
left: 30px;
width: 6px;
height: 20px;
left: 7px; /* 30/4 -> 7.5 */
width: 1.5px; /* 6/4 */
height: 5px; /* 20/4 */
background: #781e20;
border-radius: 10px;
border-radius: 3px; /* 10/4 */
transform: translateY(50%) rotate(32deg);
}
.fire-loading-overlay .stick:after {
.minifire-stick:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
width: 5px; /* 20/4 */
height: 5px; /* 20/4 */
background: #b35050;
border-radius: 10px;
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; }
.fire-loading-overlay .stick {
transform-origin: center center;
}
.fire-loading-overlay .stick:nth-child(1) { left: 158px; bottom: 164px; transform: rotate(-152deg) scaleX(0.8); z-index: 12; }
.fire-loading-overlay .stick:nth-child(2) { left: 180px; bottom: 30px; transform: rotate(20deg) scaleX(0.9); }
.fire-loading-overlay .stick:nth-child(3) { left: 400px; bottom: 38px; transform: rotate(170deg) scaleX(0.9); }
.fire-loading-overlay .stick:nth-child(3):before { display: none; }
.fire-loading-overlay .stick:nth-child(4) { left: 370px; bottom: 150px; transform: rotate(80deg) scaleX(0.9); z-index: 20; }
.fire-loading-overlay .stick:nth-child(4):before { display: none; }
.fire-loading-overlay .fire .flame {
/* --- Scaled Down Fire --- */
.minifire-fire .minifire-flame {
position: absolute;
transform-origin: bottom center;
opacity: 0.9;
}
.fire-loading-overlay .fire__red .flame {
width: 48px;
border-radius: 48px;
/* Red Flames */
.minifire-fire__red .minifire-flame {
width: 12px; /* 48/4 */
border-radius: 12px; /* 48/4 */
background: #e20f00;
box-shadow: 0 0 80px 18px rgba(226,15,0,0.4);
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 */
.fire-loading-overlay .fire__red .flame:nth-child(1) { left: 138px; height: 160px; bottom: 100px; animation: fire 2s 0.15s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(2) { left: 186px; height: 240px; bottom: 100px; animation: fire 2s 0.35s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(3) { left: 234px; height: 300px; bottom: 100px; animation: fire 2s 0.1s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(4) { left: 282px; height: 360px; bottom: 100px; animation: fire 2s 0s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(5) { left: 330px; height: 310px; bottom: 100px; animation: fire 2s 0.45s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(6) { left: 378px; height: 232px; bottom: 100px; animation: fire 2s 0.3s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__red .flame:nth-child(7) { left: 426px; height: 140px; bottom: 100px; animation: fire 2s 0.1s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame {
width: 48px;
border-radius: 48px;
background: #ff9c00;
box-shadow: 0 0 80px 18px rgba(255,156,0,0.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; }
.fire-loading-overlay .fire__orange .flame:nth-child(1) { left: 138px; height: 140px; bottom: 100px; animation: fire 2s 0.05s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(2) { left: 186px; height: 210px; bottom: 100px; animation: fire 2s 0.1s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(3) { left: 234px; height: 250px; bottom: 100px; animation: fire 2s 0.35s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(4) { left: 282px; height: 300px; bottom: 100px; animation: fire 2s 0.4s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(5) { left: 330px; height: 260px; bottom: 100px; animation: fire 2s 0.5s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(6) { left: 378px; height: 202px; bottom: 100px; animation: fire 2s 0.35s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__orange .flame:nth-child(7) { left: 426px; height: 110px; bottom: 100px; animation: fire 2s 0.1s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__yellow .flame {
width: 48px;
border-radius: 48px;
background: #ffeb6e;
box-shadow: 0 0 80px 18px rgba(255,235,110,0.4);
/* 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; }
.fire-loading-overlay .fire__yellow .flame:nth-child(1) { left: 186px; height: 140px; bottom: 100px; animation: fire 2s 0.6s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__yellow .flame:nth-child(2) { left: 234px; height: 172px; bottom: 120px; animation: fire 2s 0.4s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__yellow .flame:nth-child(3) { left: 282px; height: 240px; bottom: 100px; animation: fire 2s 0.38s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__yellow .flame:nth-child(4) { left: 330px; height: 200px; bottom: 100px; animation: fire 2s 0.22s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__yellow .flame:nth-child(5) { left: 378px; height: 142px; bottom: 100px; animation: fire 2s 0.18s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame {
width: 48px;
border-radius: 48px;
background: #fef1d9;
box-shadow: 0 0 80px 18px rgba(254,241,217,0.4);
/* 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; }
.fire-loading-overlay .fire__white .flame:nth-child(1) { left: 156px; width: 32px; height: 100px; bottom: 100px; animation: fire 2s 0.22s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(2) { left: 181px; width: 32px; height: 120px; bottom: 100px; animation: fire 2s 0.42s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(3) { left: 234px; height: 170px; bottom: 100px; animation: fire 2s 0.32s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(4) { left: 282px; height: 210px; bottom: 100px; animation: fire 2s 0.8s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(5) { left: 330px; height: 170px; bottom: 100px; animation: fire 2s 0.85s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(6) { left: 378px; width: 32px; height: 110px; bottom: 100px; animation: fire 2s 0.64s ease-in-out infinite alternate; }
.fire-loading-overlay .fire__white .flame:nth-child(7) { left: 408px; width: 32px; height: 100px; bottom: 100px; animation: fire 2s 0.32s ease-in-out infinite alternate; }
.fire-loading-overlay .spark {
/* --- Scaled Down Sparks --- */
.minifire-spark {
position: absolute;
width: 6px;
height: 20px;
width: 1.5px; /* 6/4 */
height: 5px; /* 20/4 */
background: #fef1d9;
border-radius: 18px;
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 */
.fire-loading-overlay .spark:nth-child(1) { left: 160px; bottom: 212px; animation: spark 1s 0.4s linear infinite; }
.fire-loading-overlay .spark:nth-child(2) { left: 180px; bottom: 240px; animation: spark 1s 1s linear infinite; }
.fire-loading-overlay .spark:nth-child(3) { left: 208px; bottom: 320px; animation: spark 1s 0.8s linear infinite; }
.fire-loading-overlay .spark:nth-child(4) { left: 310px; bottom: 400px; animation: spark 1s 2s linear infinite; }
.fire-loading-overlay .spark:nth-child(5) { left: 360px; bottom: 380px; animation: spark 1s 0.75s linear infinite; }
.fire-loading-overlay .spark:nth-child(6) { left: 390px; bottom: 320px; animation: spark 1s 0.65s linear infinite; }
.fire-loading-overlay .spark:nth-child(7) { left: 400px; bottom: 280px; animation: spark 1s 1s linear infinite; }
.fire-loading-overlay .spark:nth-child(8) { left: 430px; bottom: 210px; animation: spark 1s 1.4s linear infinite; }
@keyframes 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 (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 spark {
@keyframes minifire-spark {
0%, 35% { transform: scaleY(0) translateY(0); opacity: 0; }
50% { transform: scaleY(1) translateY(0); opacity: 1; }
70% { transform: scaleY(1) translateY(-10px); opacity: 1; }
75% { transform: scaleY(1) translateY(-10px); opacity: 0; }
/* 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; }
}

1
static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -34,3 +34,17 @@ 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

@ -0,0 +1,24 @@
{# 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

@ -0,0 +1,113 @@
{# 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

@ -19,11 +19,11 @@
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
{% 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 HAPI FHIR server and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in FHIR server depending and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
</div>
{% 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 HAPI FHIR server (accessible via the `/fhir` proxy) for local validation and exploration.
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in FHIR server based on your build choice (accessible via the `/fhir` proxy) for local validation and exploration, if you configured the Server ENV variable this could also be whatever customer server you have pointed the local ENV variable at allowing you to use any fhir server of your choice.
</div>
{% endif %}
@ -36,7 +36,7 @@
<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 HAPI server (in Standalone mode) and external servers.</li>
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local server (in Standalone mode) and external servers.</li>
</ul>
</li>
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
@ -48,7 +48,7 @@
<ul>
<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 HAPI FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the Local FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
</ul>

View File

@ -1,17 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="{% if request.cookies.get('theme') == 'dark' %}dark{% else %}light{% endif %}">
<head>
<meta charset="utf-8">
<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 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" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<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="stylesheet" href="{{ url_for('static', filename='css/fire-animation.css') }}">
<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;
@ -88,7 +93,7 @@
height: 1.25rem !important;
margin-top: 0.25rem;
display: inline-block !important;
border: 1px solide #6c757d;
border: 1px solid #6c757d;
}
.form-check-input:checked {
background-color: #007bff;
@ -289,20 +294,18 @@
display: block; /* Make code fill pre */
}
/* Dark Theme Override */
html[data-theme="dark"] #fsh-output pre,
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;
}
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) --- */
@ -477,7 +480,6 @@
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; }
@ -752,7 +754,7 @@
}
</style>
</head>
<body class="d-flex flex-column min-vh-100">
<body class="d-flex flex-column min-vh-100 {% block body_class %}{% endblock %}">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}{% if app_mode == 'lite' %} (Lite){% endif %}</a>
@ -765,10 +767,13 @@
<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>
</li>
<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 {% 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>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="fas fa-download me-1"></i> Import IGs</a>
<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>
</li>
<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>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="fas fa-upload me-1"></i> Push IGs</a>
@ -779,6 +784,9 @@
<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>
@ -788,7 +796,15 @@
<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>
</ul>
<!-- <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 %}>
@ -804,7 +820,6 @@
<main class="flex-grow-1">
<div class="container mt-4">
<!-- Flashed Messages Section -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mt-3">
@ -839,6 +854,7 @@
<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>
@ -849,41 +865,38 @@
<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() {
const html = document.documentElement;
const toggle = document.getElementById('themeToggle');
const sunIcon = document.querySelector('.fa-sun');
const moonIcon = document.querySelector('.fa-moon');
if (toggle.checked) {
html.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
sunIcon.classList.add('d-none');
moonIcon.classList.remove('d-none');
} else {
html.removeAttribute('data-theme');
localStorage.setItem('theme', 'light');
sunIcon.classList.remove('d-none');
moonIcon.classList.add('d-none');
}
const newTheme = toggle.checked ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
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') {
const newTheme = toggle.checked ? 'dark' : 'light';
loadLottieAnimation(newTheme);
}
// Set cookie to persist theme
document.cookie = `theme=${toggle.checked ? 'dark' : 'light'}; path=/; max-age=31536000`;
}
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme') || (document.cookie.includes('theme=dark') ? 'dark' : 'light');
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');
if (savedTheme === 'dark' && themeToggle) {
document.documentElement.setAttribute('data-theme', 'dark');
themeToggle.checked = true;
sunIcon.classList.add('d-none');
moonIcon.classList.remove('d-none');
}
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));
});

234
templates/config_hapi.html Normal file
View File

@ -0,0 +1,234 @@
{% 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 %}

View File

@ -10,7 +10,7 @@
</p>
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
<a href="{{ url_for('search_and_import') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
</div>
@ -34,13 +34,13 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
<div>
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
<a href="{{ url_for('search_and_import') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
</div>
</div>
<div class="row g-4">
<!-- LEFT: Downloaded Packages -->
<div class="col-md-6">
<div class="col-md-5">
<div class="card h-100">
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
<div class="card-body">
@ -65,20 +65,20 @@
</td>
<td>{{ pkg.version }}</td>
<td>
<div class="btn-group btn-group-sm">
<div class="btn-group-vertical btn-group-sm w-80">
{% if is_processed %}
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
{% else %}
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
<button type="submit" class="btn btn-outline-primary w-80"><i class="bi bi-gear"></i> Process</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
<button type="submit" class="btn btn-outline-danger w-80" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
</form>
</div>
</td>
@ -105,7 +105,7 @@
</div>
<!-- RIGHT: Processed Packages -->
<div class="col-md-6">
<div class="col-md-7">
<div class="card h-100">
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
<div class="card-body">
@ -139,12 +139,12 @@
{% endif %}
</td>
<td>
<div class="btn-group-vertical btn-group-sm w-100">
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
<div class="btn-group-vertical btn-group-sm w-80">
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-80" title="View Details"><i class="bi bi-search"></i> View</a>
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
<button type="submit" class="btn btn-outline-warning w-80 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
</form>
</div>
</td>

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Import form helpers if needed, e.g., for CSRF token #}
{# Import form helpers for CSRF token and field rendering #}
{% from "_form_helpers.html" import render_field %}
{% block content %}
@ -48,135 +48,138 @@
<p class="text-muted">No packages downloaded yet. Use the "Import IG" tab.</p>
{% endif %}
{# --- MOVED: Push Response Area --- #}
{# Push Response Area #}
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h4><i class="bi bi-file-earmark-text me-2"></i>Push Report</h4>
{# --- NEW: Report Action Buttons --- #}
<div id="reportActions" style="display: none;">
<button id="copyReportBtn" class="btn btn-sm btn-outline-secondary me-2" title="Copy Report Text">
<i class="bi bi-clipboard"></i> Copy
</button>
<button id="downloadReportBtn" class="btn btn-sm btn-outline-secondary" title="Download Report as Text File">
<i class="bi bi-download"></i> Download
</button>
</div>
{# --- END NEW --- #}
<h4><i class="bi bi-file-earmark-text me-2"></i>Push Report</h4>
<div id="reportActions" style="display: none;">
<button id="copyReportBtn" class="btn btn-sm btn-outline-secondary me-2" title="Copy Report Text">
<i class="bi bi-clipboard"></i> Copy
</button>
<button id="downloadReportBtn" class="btn btn-sm btn-outline-secondary" title="Download Report as Text File">
<i class="bi bi-download"></i> Download
</button>
</div>
</div>
<div id="pushResponse" class="border p-3 rounded bg-light" style="min-height: 100px;">
<span class="text-muted">Report summary will appear here after pushing...</span>
{# Flash messages can still appear here if needed #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<span class="text-muted">Report summary will appear here after pushing...</span>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
{# --- END MOVED --- #}
</div>{# End Left Column #}
{# Right Column: Push IGs Form and Console #}
<div class="col-md-6">
<h2><i class="bi bi-upload me-2"></i>Push IGs to FHIR Server</h2>
<form id="pushIgForm">
{{ form.csrf_token if form else '' }} {# Use form passed from route #}
<form id="pushIgForm">
{{ form.csrf_token if form else '' }}
{# Package Selection #}
<div class="mb-3">
<label for="packageSelect" class="form-label">Select Package to Push</label>
<select class="form-select" id="packageSelect" name="package_id" required>
<option value="" disabled selected>Select a package...</option>
{% for pkg in packages %}
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
{% endfor %}
</select>
</div>
{# Package Selection #}
<div class="mb-3">
<label for="packageSelect" class="form-label">Select Package to Push</label>
<select class="form-select" id="packageSelect" name="package_id" required>
<option value="" disabled selected>Select a package...</option>
{% for pkg in packages %}
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
{% endfor %}
</select>
</div>
{# Dependency Mode Display #}
<div class="mb-3">
<label for="dependencyMode" class="form-label">Dependency Mode Used During Import</label>
<input type="text" class="form-control" id="dependencyMode" readonly placeholder="Select package to view mode...">
</div>
{# Dependency Mode Display #}
<div class="mb-3">
<label for="dependencyMode" class="form-label">Dependency Mode Used During Import</label>
<input type="text" class="form-control" id="dependencyMode" readonly placeholder="Select package to view mode...">
</div>
{# FHIR Server URL #}
<div class="mb-3">
<label for="fhirServerUrl" class="form-label">Target FHIR Server URL</label>
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://localhost:8080/fhir" required>
</div>
{# FHIR Server URL #}
<div class="mb-3">
<label for="fhirServerUrl" class="form-label">Target FHIR Server URL</label>
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://localhost:8080/fhir" required>
</div>
{# --- RESTRUCTURED: Auth and Checkboxes --- #}
<div class="row g-3 mb-3 align-items-end">
{# Authentication Dropdown & Token Input #}
<div class="col-md-5">
<label for="authType" class="form-label">Authentication</label>
<select class="form-select" id="authType" name="auth_type">
<option value="none" selected>None</option>
<option value="apiKey">Toolkit API Key (Internal)</option>
<option value="bearerToken">Bearer Token</option>
</select>
</div>
<div class="col-md-7" id="authTokenGroup" style="display: none;">
<label for="authToken" class="form-label">Bearer Token</label>
<input type="password" class="form-control" id="authToken" name="auth_token" placeholder="Enter Bearer Token">
</div>
</div>
{# Authentication Section #}
<div class="row g-3 mb-3 align-items-end">
<div class="col-md-5">
<label for="authType" class="form-label">Authentication</label>
<select class="form-select" id="authType" name="auth_type">
<option value="none" selected>None</option>
<option value="apiKey">Toolkit API Key (Internal)</option>
<option value="bearerToken">Bearer Token</option>
<option value="basic">Basic Authentication</option>
</select>
</div>
<div class="col-md-7" id="authInputsGroup" style="display: none;">
{# Bearer Token Input #}
<div id="bearerTokenInput" style="display: none;">
<label for="authToken" class="form-label">Bearer Token</label>
<input type="password" class="form-control" id="authToken" name="auth_token" placeholder="Enter Bearer Token">
</div>
{# Basic Auth Inputs #}
<div id="basicAuthInputs" style="display: none;">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Basic Auth Username">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Basic Auth Password">
</div>
</div>
</div>
{# Checkboxes Row #}
<div class="row g-3 mb-3">
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="includeDependencies" name="include_dependencies" checked>
<label class="form-check-label" for="includeDependencies">Include Dependencies</label>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="forceUpload" name="force_upload">
<label class="form-check-label" for="forceUpload">Force Upload</label>
<small class="form-text text-muted d-block">Force upload all resources.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="dryRun" name="dry_run">
<label class="form-check-label" for="dryRun">Dry Run</label>
<small class="form-text text-muted d-block">Simulate only.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="verbose" name="verbose">
<label class="form-check-label" for="verbose">Verbose Log</label>
<small class="form-text text-muted d-block">Show detailed Log.</small>
</div>
</div>
</div>
{# --- END RESTRUCTURED --- #}
{# Checkboxes Row #}
<div class="row g-3 mb-3">
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="includeDependencies" name="include_dependencies" checked>
<label class="form-check-label" for="includeDependencies">Include Dependencies</label>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="forceUpload" name="force_upload">
<label class="form-check-label" for="forceUpload">Force Upload</label>
<small class="form-text text-muted d-block">Force upload all resources.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="dryRun" name="dry_run">
<label class="form-check-label" for="dryRun">Dry Run</label>
<small class="form-text text-muted d-block">Simulate only.</small>
</div>
</div>
<div class="col-6 col-sm-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="verbose" name="verbose">
<label class="form-check-label" for="verbose">Verbose Log</label>
<small class="form-text text-muted d-block">Show detailed log.</small>
</div>
</div>
</div>
{# Resource Type Filter #}
<div class="mb-3">
<label for="resourceTypesFilter" class="form-label">Filter Resource Types <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="resourceTypesFilter" name="resource_types_filter" rows="1" placeholder="Comma-separated, e.g., StructureDefinition, ValueSet"></textarea>
</div>
{# Resource Type Filter #}
<div class="mb-3">
<label for="resourceTypesFilter" class="form-label">Filter Resource Types <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="resourceTypesFilter" name="resource_types_filter" rows="1" placeholder="Comma-separated, e.g., StructureDefinition, ValueSet"></textarea>
</div>
{# Skip Files Filter #}
<div class="mb-3">
<label for="skipFilesFilter" class="form-label">Skip Specific Files <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="skipFilesFilter" name="skip_files" rows="2" placeholder="Enter full file paths within package (e.g., package/examples/bad.json), one per line or comma-separated."></textarea>
</div>
{# Skip Files Filter #}
<div class="mb-3">
<label for="skipFilesFilter" class="form-label">Skip Specific Files <small class="text-muted">(Optional)</small></label>
<textarea class="form-control" id="skipFilesFilter" name="skip_files" rows="2" placeholder="Enter full file paths within package (e.g., package/examples/bad.json), one per line or comma-separated."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100" id="pushButton">
<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server
</button>
</form>
<button type="submit" class="btn btn-primary w-100" id="pushButton">
<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server
</button>
</form>
{# Live Console #}
<div class="mt-4">
@ -185,7 +188,6 @@
<span class="text-muted">Console output will appear here...</span>
</div>
</div>
</div> {# End Right Column #}
</div> {# End row #}
</div> {# End container-fluid #}
@ -198,13 +200,17 @@ document.addEventListener('DOMContentLoaded', function() {
const pushIgForm = document.getElementById('pushIgForm');
const pushButton = document.getElementById('pushButton');
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('pushResponse'); // Area for final report
const reportActions = document.getElementById('reportActions'); // Container for report buttons
const copyReportBtn = document.getElementById('copyReportBtn'); // New copy button
const downloadReportBtn = document.getElementById('downloadReportBtn'); // New download button
const responseDiv = document.getElementById('pushResponse');
const reportActions = document.getElementById('reportActions');
const copyReportBtn = document.getElementById('copyReportBtn');
const downloadReportBtn = document.getElementById('downloadReportBtn');
const authTypeSelect = document.getElementById('authType');
const authTokenGroup = document.getElementById('authTokenGroup');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const authTokenInput = document.getElementById('authToken');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const resourceTypesFilterInput = document.getElementById('resourceTypesFilter');
const skipFilesFilterInput = document.getElementById('skipFilesFilter');
const dryRunCheckbox = document.getElementById('dryRun');
@ -226,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (packageSelect && dependencyModeField) {
packageSelect.addEventListener('change', function() {
const packageId = this.value;
dependencyModeField.value = ''; // Clear on change
dependencyModeField.value = '';
if (packageId) {
const [packageName, version] = packageId.split('#');
fetch(`/get-package-metadata?package_name=${packageName}&version=${version}`)
@ -240,24 +246,32 @@ document.addEventListener('DOMContentLoaded', function() {
if (packageSelect.value) { packageSelect.dispatchEvent(new Event('change')); }
}
// Show/Hide Bearer Token Input
if (authTypeSelect && authTokenGroup) {
// Show/Hide Auth Inputs
if (authTypeSelect && authInputsGroup && bearerTokenInput && basicAuthInputs) {
authTypeSelect.addEventListener('change', function() {
authTokenGroup.style.display = this.value === 'bearerToken' ? 'block' : 'none';
authInputsGroup.style.display = (this.value === 'bearerToken' || this.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = this.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = this.value === 'basic' ? 'block' : 'none';
// Clear inputs when switching
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
if (this.value !== 'basic' && usernameInput) usernameInput.value = '';
if (this.value !== 'basic' && passwordInput) passwordInput.value = '';
});
authTokenGroup.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
authInputsGroup.style.display = (authTypeSelect.value === 'bearerToken' || authTypeSelect.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = authTypeSelect.value === 'basic' ? 'block' : 'none';
} else {
console.error("Auth elements not found.");
}
// --- NEW: Report Action Button Listeners ---
// Report Action Button Listeners
if (copyReportBtn && responseDiv) {
copyReportBtn.addEventListener('click', () => {
const reportAlert = responseDiv.querySelector('.alert'); // Get the alert div inside
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : ''; // Get text content
const reportAlert = responseDiv.querySelector('.alert');
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : '';
if (reportText && navigator.clipboard) {
navigator.clipboard.writeText(reportText)
.then(() => {
// Optional: Provide feedback (e.g., change button text/icon)
const originalIcon = copyReportBtn.innerHTML;
copyReportBtn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
setTimeout(() => { copyReportBtn.innerHTML = originalIcon; }, 2000);
@ -267,9 +281,9 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Failed to copy report text.');
});
} else if (!navigator.clipboard) {
alert('Clipboard API not available in this browser.');
alert('Clipboard API not available in this browser.');
} else {
alert('No report content found to copy.');
alert('No report content found to copy.');
}
});
}
@ -292,27 +306,27 @@ document.addEventListener('DOMContentLoaded', function() {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up
URL.revokeObjectURL(url);
} else {
alert('No report content found to download.');
}
});
}
// --- END NEW ---
// --- Form Submission ---
// Form Submission
if (pushIgForm) {
pushIgForm.addEventListener('submit', async function(event) {
event.preventDefault();
// Get form values (with null checks for elements)
// Get form values
const packageId = packageSelect ? packageSelect.value : null;
const fhirServerUrl = fhirServerUrlInput ? fhirServerUrlInput.value.trim() : null;
if (!packageId || !fhirServerUrl) { alert('Please select package and enter FHIR Server URL.'); return; }
const [packageName, version] = packageId.split('#');
const auth_type = authTypeSelect ? authTypeSelect.value : 'none';
const auth_token = (auth_type === 'bearerToken' && authTokenInput) ? authTokenInput.value : null;
const username = (auth_type === 'basic' && usernameInput) ? usernameInput.value.trim() : null;
const password = (auth_type === 'basic' && passwordInput) ? passwordInput.value : null;
const resource_types_filter_raw = resourceTypesFilterInput ? resourceTypesFilterInput.value.trim() : '';
const resource_types_filter = resource_types_filter_raw ? resource_types_filter_raw.split(',').map(s => s.trim()).filter(s => s) : null;
const skip_files_raw = skipFilesFilterInput ? skipFilesFilterInput.value.trim() : '';
@ -322,12 +336,23 @@ document.addEventListener('DOMContentLoaded', function() {
const include_dependencies = includeDependenciesCheckbox ? includeDependenciesCheckbox.checked : true;
const force_upload = forceUploadCheckbox ? forceUploadCheckbox.checked : false;
// UI Updates & API Key
if (pushButton) { pushButton.disabled = true; pushButton.textContent = 'Processing...'; }
if (liveConsole) { liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting ${dry_run ? 'DRY RUN ' : ''}${force_upload ? 'FORCE ' : ''}push for ${packageName}#${version}...</div>`; }
if (responseDiv) { responseDiv.innerHTML = '<span class="text-muted">Processing...</span>'; } // Clear previous report
if (reportActions) { reportActions.style.display = 'none'; } // Hide report buttons initially
const internalApiKey = {{ api_key | default("") | tojson }}; // Use tojson filter
// Validate Basic Auth inputs
if (auth_type === 'basic') {
if (!username) { alert('Please enter a username for Basic Authentication.'); return; }
if (!password) { alert('Please enter a password for Basic Authentication.'); return; }
}
// UI Updates
if (pushButton) {
pushButton.disabled = true;
pushButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
}
if (liveConsole) {
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting ${dry_run ? 'DRY RUN ' : ''}${force_upload ? 'FORCE ' : ''}push for ${packageName}#${version}...</div>`;
}
if (responseDiv) { responseDiv.innerHTML = '<span class="text-muted">Processing...</span>'; }
if (reportActions) { reportActions.style.display = 'none'; }
const internalApiKey = {{ api_key | default("") | tojson }};
try {
// API Fetch
@ -335,10 +360,19 @@ document.addEventListener('DOMContentLoaded', function() {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
body: JSON.stringify({
package_name: packageName, version: version, fhir_server_url: fhirServerUrl,
include_dependencies: include_dependencies, auth_type: auth_type, auth_token: auth_token,
resource_types_filter: resource_types_filter, skip_files: skip_files,
dry_run: dry_run, verbose: isVerboseChecked, force_upload: force_upload
package_name: packageName,
version: version,
fhir_server_url: fhirServerUrl,
include_dependencies: include_dependencies,
auth_type: auth_type,
auth_token: auth_token,
username: username,
password: password,
resource_types_filter: resource_types_filter,
skip_files: skip_files,
dry_run: dry_run,
verbose: isVerboseChecked,
force_upload: force_upload
})
});
@ -361,25 +395,28 @@ document.addEventListener('DOMContentLoaded', function() {
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-light'; let prefix = '[INFO]'; let shouldDisplay = false;
switch (data.type) { // Determine if message should display based on verbose
switch (data.type) {
case 'start': case 'error': case 'complete': shouldDisplay = true; break;
case 'success': case 'warning': case 'info': case 'progress': if (isVerboseChecked) { shouldDisplay = true; } break;
default: if (isVerboseChecked) { shouldDisplay = true; console.warn("Unknown type:", data.type); prefix = '[UNKNOWN]'; } break;
}
if (shouldDisplay && liveConsole) { // Set prefix/class and append to console
if (shouldDisplay && liveConsole) {
if(data.type==='error'){prefix='[ERROR]';messageClass='text-danger';}
else if(data.type==='complete'){const s=data.data?.status||'info';if(s==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(s==='partial'){prefix='[PARTIAL]';messageClass='text-warning';}else{prefix='[ERROR]';messageClass='text-danger';}}
else if(data.type==='start'){prefix='[START]';messageClass='text-info';}
else if(data.type==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(data.type==='warning'){prefix='[WARNING]';messageClass='text-warning';}else if(data.type==='info'){prefix='[INFO]';messageClass='text-info';}else{prefix='[PROGRESS]';messageClass='text-light';}
else if(data.type==='success'){prefix='[SUCCESS]';messageClass='text-success';}
else if(data.type==='warning'){prefix='[WARNING]';messageClass='text-warning';}
else if(data.type==='info'){prefix='[INFO]';messageClass='text-info';}
else{prefix='[PROGRESS]';messageClass='text-light';}
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
messageDiv.textContent = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
}
if (data.type === 'complete' && responseDiv) { // Update final summary box
if (data.type === 'complete' && responseDiv) {
const summaryData = data.data || {}; let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
@ -391,28 +428,74 @@ document.addEventListener('DOMContentLoaded', function() {
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
if (reportActions) { reportActions.style.display = 'block'; } // Show report buttons
if (reportActions) { reportActions.style.display = 'block'; }
}
} catch (parseError) { /* (Handle JSON parse errors) */ console.error('Stream parse error:', parseError); if(liveConsole){/*...add error to console...*/} }
} // end for loop
} // end while loop
// Process Final Buffer (if any)
if (buffer.trim()) {
try { /* (Parsing logic for final buffer, similar to above) */ }
catch (parseError) { /* (Handle final buffer parse error) */ }
} catch (parseError) {
console.error('Stream parse error:', parseError);
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Stream parse error: ${sanitizeText(parseError.message)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
}
}
}
} catch (error) { // Handle overall fetch/network errors
// Process Final Buffer
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
if (data.type === 'complete' && responseDiv) {
// Same summary rendering as above
const summaryData = data.data || {};
let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
const fileFilterUsed = summaryData.skip_files_filter ? summaryData.skip_files_filter.join(', ') : 'None';
if (summaryData.pushed_packages_summary?.length > 0) { pushedPkgs = summaryData.pushed_packages_summary.map(p => `${sanitizeText(p.id)} (${sanitizeText(p.resource_count)} resources)`).join('<br>'); }
if (summaryData.status === 'success') { alertClass = 'alert-success'; statusText = 'Success';} else if (summaryData.status === 'partial') { alertClass = 'alert-warning'; statusText = 'Partial Success'; } else { alertClass = 'alert-danger'; statusText = 'Error'; }
if (summaryData.failed_details?.length > 0) { failHtml = '<hr><strong>Failures:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.failed_details.forEach(f => {failHtml += `<li><strong>${sanitizeText(f.resource)}:</strong> ${sanitizeText(f.error)}</li>`;}); failHtml += '</ul>';}
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
if (reportActions) { reportActions.style.display = 'block'; }
}
} catch (parseError) {
console.error('Final buffer parse error:', parseError);
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Final buffer parse error: ${sanitizeText(parseError.message)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
}
}
} catch (error) {
console.error("Push operation failed:", error);
if (liveConsole) { /* ... add error to console ... */ }
if (responseDiv) { responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message || error)}</div>`; }
if (reportActions) { reportActions.style.display = 'none'; } // Hide buttons on error
} finally { // Re-enable button
if (pushButton) { pushButton.disabled = false; pushButton.textContent = 'Push to FHIR Server'; }
if (liveConsole) {
const errDiv = document.createElement('div');
errDiv.className = 'text-danger';
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] ${sanitizeText(error.message || error)}`;
liveConsole.appendChild(errDiv);
liveConsole.scrollTop = liveConsole.scrollHeight;
}
if (responseDiv) {
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message || error)}</div>`;
}
if (reportActions) { reportActions.style.display = 'none'; }
} finally {
if (pushButton) {
pushButton.disabled = false;
pushButton.innerHTML = '<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server';
}
}
}); // End form submit listener
});
} else { console.error("Push IG Form element not found."); }
}); // End DOMContentLoaded listener
});
</script>
{% endblock %}

View File

@ -8,13 +8,6 @@
<p class="lead mb-4">
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local HAPI or a custom server to explore resources or perform searches.
</p>
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR UI Operations</a>
</div>
-------------------------------------------------------------------------------------------------------------------------------------------->
</div>
</div>
@ -28,12 +21,37 @@
<label class="form-label">FHIR Server</label>
<div class="input-group">
<button type="button" class="btn btn-outline-primary" id="toggleServer">
<span id="toggleLabel">Use Local HAPI</span>
<span id="toggleLabel">Use Local Server</span>
</button>
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
</div>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
</div>
<div class="mb-3" id="authSection" style="display: none;">
<label class="form-label">Authentication</label>
<div class="row g-3 align-items-end">
<div class="col-md-5">
<select class="form-select" id="authType" name="auth_type">
<option value="none" selected>None</option>
<option value="bearer">Bearer Token</option>
<option value="basic">Basic Authentication</option>
</select>
</div>
<div class="col-md-7" id="authInputsGroup" style="display: none;">
<div id="bearerTokenInput" style="display: none;">
<label for="bearerToken" class="form-label">Bearer Token</label>
<input type="password" class="form-control" id="bearerToken" name="bearer_token" placeholder="Enter Bearer Token">
</div>
<div id="basicAuthInputs" style="display: none;">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Username">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Password">
</div>
</div>
</div>
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
</div>
<div class="mb-3">
<label for="fhirPath" class="form-label">FHIR Path</label>
<input type="text" class="form-control" id="fhirPath" name="fhir_path" placeholder="e.g., Patient/wang-li" required aria-describedby="fhirPathHelp">
@ -44,13 +62,10 @@
<div class="d-flex gap-2 flex-wrap">
<input type="radio" class="btn-check" name="method" id="get" value="GET" checked>
<label class="btn btn-outline-success" for="get"><span class="badge bg-success">GET</span></label>
<input type="radio" class="btn-check" name="method" id="post" value="POST">
<label class="btn btn-outline-primary" for="post"><span class="badge bg-primary">POST</span></label>
<input type="radio" class="btn-check" name="method" id="put" value="PUT">
<label class="btn btn-outline-warning" for="put"><span class="badge bg-warning text-dark">PUT</span></label>
<input type="radio" class="btn-check" name="method" id="delete" value="DELETE">
<label class="btn btn-outline-danger" for="delete"><span class="badge bg-danger">DELETE</span></label>
</div>
@ -108,218 +123,281 @@ document.addEventListener('DOMContentLoaded', function() {
const copyRequestBodyButton = document.getElementById('copyRequestBody');
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
const copyResponseBodyButton = document.getElementById('copyResponseBody');
const authSection = document.getElementById('authSection');
const authTypeSelect = document.getElementById('authType');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
// Basic check for critical elements
if (!form || !sendButton || !fhirPathInput || !responseCard || !toggleServerButton || !fhirServerUrlInput || !responseStatus || !responseHeaders || !responseBody || !toggleLabel) {
console.error("One or more critical UI elements could not be found. Script execution halted.");
alert("Error initializing UI components. Please check the console.");
return; // Stop script execution
return;
}
console.log("All critical elements checked/found.");
// --- State Variable ---
// Default assumes standalone, will be forced otherwise by appMode check below
let useLocalHapi = true;
// --- Get App Mode from Flask Context ---
// Ensure this variable is correctly passed from Flask using the context_processor
const appMode = '{{ app_mode | default("standalone") | lower }}';
console.log('App Mode Detected:', appMode);
// --- DEFINE HELPER FUNCTIONS ---
// Validates request body, returns null on error, otherwise returns body string or empty string
// --- Helper Functions ---
function validateRequestBody(method, path) {
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined;
const bodyValue = requestBodyInput.value.trim();
requestBodyInput.classList.remove('is-invalid'); // Reset validation
requestBodyInput.classList.remove('is-invalid');
jsonError.style.display = 'none';
if (!bodyValue) return ''; // Empty body is valid for POST/PUT
if (!bodyValue) return '';
const isSearch = path && path.endsWith('_search');
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
const isXml = bodyValue.startsWith('<');
const isForm = !isJson && !isXml;
if (method === 'POST' && isSearch && isForm) { // POST Search with form params
if (method === 'POST' && isSearch && isForm) {
return bodyValue;
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON/XML
} else if (method === 'POST' || method === 'PUT') {
if (isJson) {
try { JSON.parse(bodyValue); return bodyValue; }
catch (e) { jsonError.textContent = `Invalid JSON: ${e.message}`; }
} else if (isXml) {
// Basic XML check is difficult in JS, accept it for now
// Backend or target server will validate fully
return bodyValue;
} else { // Neither JSON nor XML, and not a POST search form
} else {
jsonError.textContent = 'Request body must be valid JSON or XML for PUT/POST (unless using POST _search with form parameters).';
}
requestBodyInput.classList.add('is-invalid');
jsonError.style.display = 'block';
return null; // Indicate validation error
return null;
}
return undefined; // Indicate no body should be sent for GET/DELETE
return undefined;
}
// Cleans path for proxying
function cleanFhirPath(path) {
if (!path) return '';
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
return path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
}
// Copies text to clipboard
async function copyToClipboard(text, button) {
if (text === null || text === undefined || !button || !navigator.clipboard) return;
try {
await navigator.clipboard.writeText(String(text));
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 1500);
} catch (err) { console.error('Copy failed:', err); /* Optionally alert user */ }
setTimeout(() => { if (button.isConnected) button.innerHTML = originalIcon; }, 1500);
} catch (err) { console.error('Copy failed:', err); }
}
// Updates the UI elements related to the server toggle button/input
function updateServerToggleUI() {
if (appMode === 'lite') {
// LITE MODE: Force Custom URL, disable toggle
useLocalHapi = false; // Force state
useLocalHapi = false;
toggleServerButton.disabled = true;
toggleServerButton.classList.add('disabled');
toggleServerButton.style.pointerEvents = 'none'; // Make unclickable
toggleServerButton.style.pointerEvents = 'none';
toggleServerButton.setAttribute('aria-disabled', 'true');
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
toggleLabel.textContent = 'Use Custom URL'; // Always show this label
fhirServerUrlInput.style.display = 'block'; // Always show input
toggleLabel.textContent = 'Use Custom URL';
fhirServerUrlInput.style.display = 'block';
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
fhirServerUrlInput.required = true; // Make required in lite mode
fhirServerUrlInput.required = true;
authSection.style.display = 'block';
} else {
// STANDALONE MODE: Allow toggle
toggleServerButton.disabled = false;
toggleServerButton.classList.remove('disabled');
toggleServerButton.style.pointerEvents = 'auto';
toggleServerButton.removeAttribute('aria-disabled');
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
toggleLabel.textContent = useLocalHapi ? 'Using Local Server' : 'Using Custom URL';
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
fhirServerUrlInput.required = !useLocalHapi; // Required only if custom is selected
fhirServerUrlInput.required = !useLocalHapi;
authSection.style.display = useLocalHapi ? 'none' : 'block';
}
fhirServerUrlInput.classList.remove('is-invalid'); // Clear validation state on toggle
fhirServerUrlInput.classList.remove('is-invalid');
updateAuthInputsUI();
console.log(`UI Updated: useLocalHapi=${useLocalHapi}, Button Disabled=${toggleServerButton.disabled}, Input Visible=${fhirServerUrlInput.style.display !== 'none'}, Input Required=${fhirServerUrlInput.required}`);
}
// Toggles the server selection state (only effective in Standalone mode)
function updateAuthInputsUI() {
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
const authType = authTypeSelect.value;
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
if (authType !== 'bearer' && bearerTokenInput) bearerTokenInput.value = '';
if (authType !== 'basic' && usernameInput) usernameInput.value = '';
if (authType !== 'basic' && passwordInput) passwordInput.value = '';
}
function toggleServer() {
if (appMode === 'lite') {
console.log("Toggle ignored: Lite mode active.");
return; // Do nothing in lite mode
return;
}
useLocalHapi = !useLocalHapi;
if (useLocalHapi && fhirServerUrlInput) {
fhirServerUrlInput.value = ''; // Clear custom URL when switching to local
fhirServerUrlInput.value = '';
}
updateServerToggleUI(); // Update UI based on new state
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
updateServerToggleUI();
updateAuthInputsUI();
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local Server' : 'Custom URL'}`);
}
// Updates visibility of the Request Body textarea based on selected HTTP method
function updateRequestBodyVisibility() {
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
if (!requestBodyGroup || !selectedMethod) return;
const showBody = (selectedMethod === 'POST' || selectedMethod === 'PUT');
requestBodyGroup.style.display = showBody ? 'block' : 'none';
if (!showBody) { // Clear body and errors if body not needed
if (!showBody) {
if (requestBodyInput) requestBodyInput.value = '';
if (jsonError) jsonError.style.display = 'none';
if (requestBodyInput) requestBodyInput.classList.remove('is-invalid');
}
}
// --- INITIAL SETUP & MODE CHECK ---
updateServerToggleUI(); // Set initial UI based on detected mode and default state
updateRequestBodyVisibility(); // Set initial visibility based on default method (GET)
// --- Initial Setup ---
updateServerToggleUI();
updateRequestBodyVisibility();
// --- ATTACH EVENT LISTENERS ---
// --- Event Listeners ---
toggleServerButton.addEventListener('click', toggleServer);
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody(document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value)); }
if (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
if (authTypeSelect) { authTypeSelect.addEventListener('change', updateAuthInputsUI); }
// --- Send Request Button Listener ---
sendButton.addEventListener('click', async function() {
console.log("Send Request button clicked.");
// --- UI Reset ---
sendButton.disabled = true; sendButton.textContent = 'Sending...';
sendButton.disabled = true;
sendButton.textContent = 'Sending...';
responseCard.style.display = 'none';
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
responseStatus.className = 'badge'; // Reset badge class
fhirServerUrlInput.classList.remove('is-invalid'); // Reset validation
if(requestBodyInput) requestBodyInput.classList.remove('is-invalid');
if(jsonError) jsonError.style.display = 'none';
responseStatus.textContent = '';
responseHeaders.textContent = '';
responseBody.textContent = '';
responseStatus.className = 'badge';
fhirServerUrlInput.classList.remove('is-invalid');
if (requestBodyInput) requestBodyInput.classList.remove('is-invalid');
if (jsonError) jsonError.style.display = 'none';
if (bearerTokenInput) bearerTokenInput.classList.remove('is-invalid');
if (usernameInput) usernameInput.classList.remove('is-invalid');
if (passwordInput) passwordInput.classList.remove('is-invalid');
// --- Get Values ---
const path = fhirPathInput.value.trim();
const method = document.querySelector('input[name="method"]:checked')?.value;
const customUrl = fhirServerUrlInput.value.trim();
let body = undefined;
const authType = authTypeSelect ? authTypeSelect.value : 'none';
const bearerToken = document.getElementById('bearerToken').value.trim();
const username = usernameInput ? usernameInput.value.trim() : '';
const password = passwordInput ? passwordInput.value : '';
// --- Basic Input Validation ---
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
if (!useLocalHapi && !customUrl) { // Custom URL mode needs a URL
alert('Please enter a custom FHIR Server URL.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
if (!path) {
alert('Please enter a FHIR Path.');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
if (!useLocalHapi && customUrl) { // Validate custom URL format
try { new URL(customUrl); }
catch (_) { alert('Invalid custom FHIR Server URL format.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
if (!method) {
alert('Please select a Request Type.');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
if (!useLocalHapi && !customUrl) {
alert('Please enter a custom FHIR Server URL.');
fhirServerUrlInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
if (!useLocalHapi && customUrl) {
try { new URL(customUrl); }
catch (_) {
alert('Invalid custom FHIR Server URL format.');
fhirServerUrlInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
}
// --- Validate & Get Body (if needed) ---
// --- Validate Authentication ---
if (!useLocalHapi) {
if (authType === 'bearer' && !bearerToken) {
alert('Please enter a Bearer Token.');
document.getElementById('bearerToken').classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
if (authType === 'basic' && (!username || !password)) {
alert('Please enter both Username and Password for Basic Authentication.');
if (!username) usernameInput.classList.add('is-invalid');
if (!password) passwordInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
}
// --- Validate & Get Body ---
if (method === 'POST' || method === 'PUT') {
body = validateRequestBody(method, path);
if (body === null) { // null indicates validation error
alert('Request body contains invalid JSON/Format.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
}
// If body is empty string, ensure it's treated as such for fetch
if (body === '') body = '';
body = validateRequestBody(method, path);
if (body === null) {
alert('Request body contains invalid JSON/Format.');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
if (body === '') body = '';
}
// --- Determine Fetch URL and Headers ---
const cleanedPath = cleanFhirPath(path);
const finalFetchUrl = '/fhir/' + cleanedPath; // Always send to the backend proxy endpoint
const finalFetchUrl = '/fhir/' + cleanedPath;
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
// Determine Content-Type if body exists
if (body !== undefined) {
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
else if (body) { headers['Content-Type'] = 'application/fhir+json'; } // Default if unknown but present
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
else if (body) { headers['Content-Type'] = 'application/fhir+json'; }
}
// Add Custom Target Header if needed
if (!useLocalHapi && customUrl) {
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, ''); // Send custom URL without trailing slash
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, '');
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
if (authType === 'bearer') {
headers['Authorization'] = `Bearer ${bearerToken}`;
console.log("Adding header Authorization: Bearer <truncated>");
} else if (authType === 'basic') {
const credentials = btoa(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
console.log("Adding header Authorization: Basic <redacted>");
}
}
// Add CSRF token ONLY if sending to local proxy (for modifying methods)
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
// Include DELETE method for CSRF check
if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) {
headers['X-CSRFToken'] = csrfToken;
console.log("CSRF Token added for local request.");
headers['X-CSRFToken'] = csrfToken;
console.log("CSRF Token added for local request.");
} else if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && !csrfToken) {
console.warn("CSRF token input not found for local modifying request.");
console.warn("CSRF token input not found for local modifying request.");
}
console.log(`Executing Fetch: Method=${method}, URL=${finalFetchUrl}, LocalHAPI=${useLocalHapi}`);
console.log("Request Headers:", headers);
console.log("Request Headers:", { ...headers, Authorization: headers.Authorization ? '<redacted>' : undefined });
if (body !== undefined) console.log("Request Body (first 300 chars):", (body || '').substring(0, 300) + ((body?.length ?? 0) > 300 ? "..." : ""));
// --- Make the Fetch Request ---
@ -327,10 +405,9 @@ document.addEventListener('DOMContentLoaded', function() {
const response = await fetch(finalFetchUrl, {
method: method,
headers: headers,
body: body // Will be undefined for GET/DELETE
body: body
});
// --- Process Response ---
responseCard.style.display = 'block';
responseStatus.textContent = `${response.status} ${response.statusText}`;
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
@ -343,60 +420,55 @@ document.addEventListener('DOMContentLoaded', function() {
let responseBodyText = await response.text();
let displayBody = responseBodyText;
// Attempt to pretty-print JSON
if (responseContentType.includes('json') && responseBodyText.trim()) {
try { displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2); }
catch (e) { console.warn("Failed to pretty-print JSON response:", e); /* Show raw text */ }
}
// Attempt to pretty-print XML (basic indentation)
else if (responseContentType.includes('xml') && responseBodyText.trim()) {
try {
let formattedXml = '';
let indent = 0;
const xmlLines = responseBodyText.replace(/>\s*</g, '><').split(/(<[^>]+>)/);
for (let i = 0; i < xmlLines.length; i++) {
const node = xmlLines[i];
if (!node || node.trim().length === 0) continue;
if (node.match(/^<\/\w/)) indent--; // Closing tag
formattedXml += ' '.repeat(Math.max(0, indent)) + node.trim() + '\n';
if (node.match(/^<\w/) && !node.match(/\/>$/) && !node.match(/^<\?/)) indent++; // Opening tag
}
displayBody = formattedXml.trim();
} catch(e) { console.warn("Basic XML formatting failed:", e); /* Show raw text */ }
catch (e) { console.warn("Failed to pretty-print JSON response:", e); }
} else if (responseContentType.includes('xml') && responseBodyText.trim()) {
try {
let formattedXml = '';
let indent = 0;
const xmlLines = responseBodyText.replace(/>\s*</g, '><').split(/(<[^>]+>)/);
for (let i = 0; i < xmlLines.length; i++) {
const node = xmlLines[i];
if (!node || node.trim().length === 0) continue;
if (node.match(/^<\/\w/)) indent--;
formattedXml += ' '.repeat(Math.max(0, indent)) + node.trim() + '\n';
if (node.match(/^<\w/) && !node.match(/\/>$/) && !node.match(/^<\?/)) indent++;
}
displayBody = formattedXml.trim();
} catch (e) { console.warn("Basic XML formatting failed:", e); }
}
responseBody.textContent = displayBody;
// Highlight response body if Prism.js is available
if (typeof Prism !== 'undefined') {
responseBody.className = 'border p-2 bg-light'; // Reset base classes
if (responseContentType.includes('json')) {
responseBody.classList.add('language-json');
} else if (responseContentType.includes('xml')) {
responseBody.classList.add('language-xml');
} // Add other languages if needed
Prism.highlightElement(responseBody);
}
if (typeof Prism !== 'undefined') {
responseBody.className = 'border p-2 bg-light';
if (responseContentType.includes('json')) {
responseBody.classList.add('language-json');
} else if (responseContentType.includes('xml')) {
responseBody.classList.add('language-xml');
}
Prism.highlightElement(responseBody);
}
} catch (error) {
console.error('Fetch error:', error);
responseCard.style.display = 'block';
responseStatus.textContent = `Network Error`;
responseStatus.textContent = 'Network Error';
responseStatus.className = 'badge bg-danger';
responseHeaders.textContent = 'N/A';
// Provide a more informative error message
let errorDetail = `Error: ${error.message}\n\n`;
if (useLocalHapi) {
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server and the local HAPI FHIR server (at http://localhost:8080/fhir) are running.`;
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server and the local HAPI FHIR server (at http://localhost:8080/fhir) are running.`;
} else {
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl} or the proxy could not connect to the target server (${customUrl}). Ensure the toolkit server is running and the target server is accessible.`;
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl} or the proxy could not connect to the target server (${customUrl}). Ensure the toolkit server is running and the target server is accessible.`;
}
responseBody.textContent = errorDetail;
} finally {
sendButton.disabled = false; sendButton.textContent = 'Send Request';
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
}
}); // End sendButton listener
}); // End DOMContentLoaded Listener
});
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -12,149 +12,32 @@
</div>
</div>
<!-- Fire Animation Loading Overlay -->
<div id="fire-loading-overlay" class="fire-loading-overlay d-none">
<div class="campfire">
<div class="sparks">
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
<div class="spark"></div>
</div>
<div class="logs">
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="campfire-scaler">
<div class="sparks">
<div class="spark"></div> <div class="spark"></div> <div class="spark"></div> <div class="spark"></div>
<div class="spark"></div> <div class="spark"></div> <div class="spark"></div> <div class="spark"></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="logs">
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
<div class="log"><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div><div class="streak"></div></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="sticks">
<div class="stick"></div><div class="stick"></div><div class="stick"></div><div class="stick"></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="fire">
<div class="fire__red"><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div></div>
<div class="fire__orange"><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div></div>
<div class="fire__yellow"><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div></div>
<div class="fire__white"><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div><div class="flame"></div></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
</div>
<div class="log">
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
<div class="streak"></div>
</div>
</div>
<div class="sticks">
<div class="stick"></div>
<div class="stick"></div>
<div class="stick"></div>
<div class="stick"></div>
</div>
<div class="fire">
<div class="fire__red">
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
</div>
<div class="fire__orange">
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
</div>
<div class="fire__yellow">
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
</div>
<div class="fire__white">
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
<div class="flame"></div>
</div>
</div>
</div>
<div class="text-center mt-3">
</div> </div> <div class="loading-text text-center">
<p class="text-white mt-3">Importing Implementation Guide... Please wait.</p>
<p id="log-display" class="text-white mb-0"></p>
</div>
@ -183,82 +66,80 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('import-ig-form');
const submitBtn = document.getElementById('submit-btn');
const loadingOverlay = document.getElementById('fire-loading-overlay');
const logDisplay = document.getElementById('log-display');
// Your existing JS here (exactly as provided before)
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('import-ig-form');
const submitBtn = document.getElementById('submit-btn');
const loadingOverlay = document.getElementById('fire-loading-overlay');
const logDisplay = document.getElementById('log-display');
if (form && submitBtn && loadingOverlay && logDisplay) {
form.addEventListener('submit', async function(event) {
event.preventDefault();
submitBtn.disabled = true;
logDisplay.textContent = 'Starting import...';
loadingOverlay.classList.remove('d-none');
if (form && submitBtn && loadingOverlay && logDisplay) {
form.addEventListener('submit', async function(event) {
event.preventDefault();
submitBtn.disabled = true;
logDisplay.textContent = 'Starting import...';
loadingOverlay.classList.remove('d-none'); // Show the overlay
let eventSource;
try {
// Start SSE connection for logs
eventSource = new EventSource('{{ url_for('stream_import_logs') }}');
let eventSource;
try {
eventSource = new EventSource('{{ url_for('stream_import_logs') }}');
eventSource.onmessage = function(event) {
if (event.data === '[DONE]') {
eventSource.close();
return;
}
logDisplay.textContent = event.data;
};
eventSource.onerror = function() {
console.error('SSE connection error');
if(eventSource) eventSource.close();
logDisplay.textContent = 'Log streaming interrupted.';
};
} catch (e) {
console.error('Failed to initialize SSE:', e);
logDisplay.textContent = 'Unable to stream logs: ' + e.message;
}
eventSource.onmessage = function(event) {
if (event.data === '[DONE]') {
const formData = new FormData(form);
try {
const response = await fetch('{{ url_for('import_ig') }}', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData
});
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
eventSource.close();
return;
}
logDisplay.textContent = event.data;
};
eventSource.onerror = function() {
console.error('SSE connection error');
eventSource.close();
logDisplay.textContent = 'Log streaming interrupted. Import may still be in progress.';
};
} catch (e) {
console.error('Failed to initialize SSE:', e);
logDisplay.textContent = 'Unable to stream logs: ' + e.message;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: `HTTP error: ${response.status}` }));
throw new Error(errorData.message || `HTTP error: ${response.status}`);
}
// Submit form via AJAX
const formData = new FormData(form);
try {
const response = await fetch('{{ url_for('import_ig') }}', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData
});
if (eventSource) eventSource.close();
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error: ${response.status}`);
}
const data = await response.json();
if (data.redirect) {
window.location.href = data.redirect;
} else {
const data = await response.json();
if (data.redirect) {
window.location.href = data.redirect;
} else {
// Optional: display success message
}
} catch (error) {
console.error('Import error:', error);
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
eventSource.close();
}
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success mt-3';
alertDiv.textContent = data.message || 'Import successful!';
form.parentElement.appendChild(alertDiv);
alertDiv.className = 'alert alert-danger mt-3';
alertDiv.textContent = `Import failed: ${error.message}`;
form.closest('.card-body').appendChild(alertDiv);
} finally {
loadingOverlay.classList.add('d-none'); // Hide overlay when done
submitBtn.disabled = false;
}
} catch (error) {
console.error('Import error:', error);
if (eventSource) eventSource.close();
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger mt-3';
alertDiv.textContent = `Import failed: ${error.message}`;
form.parentElement.appendChild(alertDiv);
} finally {
loadingOverlay.classList.add('d-none');
submitBtn.disabled = false;
}
});
} else {
console.error('Required elements missing: form, submit button, loading overlay, or log display.');
}
});
});
} else {
console.error('Required elements missing for import script.');
}
});
</script>
{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends "base.html" %}
{% block body_class %}fire-animation-page{% endblock %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<div id="logo-container" class="mb-4">
@ -59,7 +61,8 @@
<h5 class="card-title">IG Management</h5>
<p class="card-text">Import and manage FHIR Implementation Guides.</p>
<div class="d-grid gap-2">
<a href="{{ url_for('import_ig') }}" class="btn btn-primary">Import FHIR IG</a>
<!-- <a href="{{ url_for('import_ig') }}" class="btn btn-primary">Import FHIR IG</a> -->
<a href="{{ url_for('search_and_import') }}" class="btn btn-primary">Search & Import IGs</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary">Manage FHIR Packages</a>
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary">Push IGs</a>
</div>
@ -75,6 +78,7 @@
<div class="d-grid gap-2">
<a href="{{ url_for('upload_test_data') }}" class="btn btn-outline-secondary">Upload Test Data</a>
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary">Validate FHIR Sample</a>
<a href="{{ url_for('retrieve_split_data') }}" class="btn btn-outline-secondary">Retrieve & Split Data</a>
</div>
</div>
</div>
@ -128,10 +132,11 @@ document.addEventListener('DOMContentLoaded', () => {
animationContainer.style.opacity = '1';
}, 30000);
// Sync animation with theme (inverted: light = fire-off, dark = fire-on)
// Sync animation with theme (reversed: dark = fire-on, light = fire-off)
const updateAnimationState = () => {
const isDark = htmlElement.getAttribute('data-theme') === 'dark';
document.body.classList.toggle('fire-off', !isDark); // Light theme: fire-off, Dark theme: fire-on
const savedTheme = localStorage.getItem('theme') || (document.cookie.includes('theme=dark') ? 'dark' : 'light');
const isDark = savedTheme === 'dark';
document.body.classList.toggle('fire-on', isDark); // Dark theme: fire-on, Light theme: fire-off
};
// Initial state

View File

@ -0,0 +1,194 @@
{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<div class="container">
{% include "_flash_messages.html" %}
<h1 class="mt-4 mb-3">Import FHIR Implementation Guides</h1>
<div class="row">
<div class="col-md-12">
<p class="lead">Import new FHIR Implementation Guides to the system via file or URL.</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h2>Import a New IG</h2>
<form id="manual-import-ig-form" method="POST" action="{{ url_for('manual_import_ig') }}" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ render_field(form.import_mode, class="form-select") }}
</div>
<div id="file-field" class="{% if form.import_mode.data != 'file' %}d-none{% endif %}">
<div class="mb-3">
<label for="tgz_file" class="form-label">IG Package File (.tgz)</label>
<div class="input-group">
<input type="file" name="tgz_file" id="tgz_file" accept=".tgz" class="form-control custom-file-input">
<span id="file-label" class="custom-file-label">No file selected</span>
</div>
{% if form.tgz_file.errors %}
<div class="form-error">
{% for error in form.tgz_file.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div id="url-field" class="{% if form.import_mode.data != 'url' %}d-none{% endif %}">
<div class="mb-3">
{{ render_field(form.tgz_url, class="form-control url-input") }}
</div>
</div>
<div class="mb-3">
{{ render_field(form.dependency_mode, class="form-select") }}
</div>
<div class="form-check mb-3">
{{ form.resolve_dependencies(type="checkbox", class="form-check-input") }}
<label class="form-check-label" for="resolve_dependencies">Resolve Dependencies</label>
{% if form.resolve_dependencies.errors %}
<div class="form-error">
{% for error in form.resolve_dependencies.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-sm-flex mt-3">
{{ 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>
<style>
.form-error {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.custom-file-input {
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.25rem;
cursor: pointer;
}
.custom-file-input::-webkit-file-upload-button {
display: none;
}
.custom-file-input::before {
content: 'Choose File';
display: inline-block;
background: #007bff;
color: #fff;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
margin-right: 0.5rem;
}
.custom-file-input:hover::before {
background: #0056b3;
}
.custom-file-label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
max-width: 60%;
}
#file-field, #url-field {
display: none;
}
#file-field:not(.d-none), #url-field:not(.d-none) {
display: block;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.debug('DOMContentLoaded fired for manual_import_ig.html');
const importForm = document.getElementById('manual-import-ig-form');
const importModeSelect = document.querySelector('#import_mode');
const fileField = document.querySelector('#file-field');
const urlField = document.querySelector('#url-field');
const fileInput = document.querySelector('#tgz_file');
const fileLabel = document.querySelector('#file-label');
const urlInput = document.querySelector('#tgz_url');
// Debug DOM elements
console.debug('importForm:', !!importForm);
console.debug('importModeSelect:', !!importModeSelect);
console.debug('fileField:', !!fileField);
console.debug('urlField:', !!urlField);
console.debug('fileInput:', !!fileInput);
console.debug('fileLabel:', !!fileLabel);
console.debug('urlInput:', !!urlInput);
function toggleFields() {
if (!importModeSelect || !fileField || !urlField) {
console.error('Required elements for toggling not found');
return;
}
const mode = importModeSelect.value;
console.debug(`Toggling fields for mode: ${mode}`);
fileField.classList.toggle('d-none', mode !== 'file');
urlField.classList.toggle('d-none', mode !== 'url');
console.debug(`file-field d-none: ${fileField.classList.contains('d-none')}, url-field d-none: ${urlField.classList.contains('d-none')}`);
}
function updateFileLabel() {
if (fileInput && fileLabel) {
const fileName = fileInput.files.length > 0 ? fileInput.files[0].name : 'No file selected';
fileLabel.textContent = fileName;
console.debug(`Updated file label to: ${fileName}`);
} else {
console.warn('File input or label not found');
}
}
if (importModeSelect) {
importModeSelect.addEventListener('change', function() {
console.debug('Import mode changed to:', importModeSelect.value);
toggleFields();
});
toggleFields();
} else {
console.error('Import mode select not found');
}
if (fileInput) {
fileInput.addEventListener('change', function() {
console.debug('File input changed');
updateFileLabel();
});
updateFileLabel();
}
if (urlInput) {
urlInput.addEventListener('input', function() {
console.debug(`URL input value: ${urlInput.value}`);
});
}
if (importForm) {
importForm.addEventListener('submit', function(evt) {
console.debug('Form submission triggered');
if (!importForm.checkValidity()) {
evt.preventDefault();
console.error('Form validation failed');
importForm.reportValidity();
}
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,7 @@
<ul class="list-group">
{% for canonical in canonicals %}
<li class="list-group-item">
<a href="{{ canonical }}" target="_blank">{{ canonical }}</a>
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,31 @@
{% if dependents %}
<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>
</tr>
</thead>
<tbody>
{% for dep in dependents %}
<tr>
<td>
<i class="bi bi-box-seam me-2"></i>
<a class="text-primary" href="{{ url_for('package_details_view', name=dep.name) }}">{{ dep.name }}</a>
</td>
<td>{{ dep.version }}</td>
<td>{{ dep.author }}</td>
<td>{{ dep.fhir_version }}</td>
<td>{{ dep.version_count }}</td>
<td>{{ dep.canonical }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No dependent packages found for this package.</p>
{% endif %}

View File

@ -0,0 +1,22 @@
{% if logs %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Version</th>
<th scope="col">Publication Date</th>
<th scope="col">When</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.version }}</td>
<td>{{ log.pubDate }}</td>
<td>{{ log.when }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No version history found for this package.</p>
{% endif %}

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