Skip to content

Deploy FHIR API docs (Swagger UI) and HAPI FHIR sandbox infrastructure#427

Merged
aurelianware merged 5 commits intomainfrom
copilot/deploy-fhir-api-documentation
Mar 4, 2026
Merged

Deploy FHIR API docs (Swagger UI) and HAPI FHIR sandbox infrastructure#427
aurelianware merged 5 commits intomainfrom
copilot/deploy-fhir-api-documentation

Conversation

Copy link
Contributor

Copilot AI commented Mar 4, 2026

  • Explore repository structure and understand existing patterns
  • Create src/api-docs/index.html — Swagger UI landing page
  • Create src/api-docs/specs/.gitkeep
  • Create containers/hapi-fhir-sandbox/Dockerfile
  • Create containers/hapi-fhir-sandbox/application.yaml
  • Create containers/hapi-fhir-sandbox/load-synthea.sh
  • Create .github/workflows/deploy-sandbox.yml
  • Modify src/portal/CloudHealthOffice.Portal/Pages/Apis.razor
  • Address PR review comments (commit 7a1f04c)
  • Migrate API docs from Azure Static Web Apps to Azure Container Apps (commit afddec4):
    • Create containers/api-docs/Dockerfile — nginx:1.27-alpine, bakes in index.html + OpenAPI specs
    • Create containers/api-docs/nginx.conf — security headers, /fhir/r4 redirect, SPA fallback
    • Rewrite deploy-api-docs.yml — OIDC, vars.ACR_NAME/vars.ACR_LOGIN_SERVER, Container Apps deploy
    • Remove src/api-docs/staticwebapp.config.json
  • Fix yamllint CI failure: remove trailing blank line from deploy-api-docs.yml (line 82)
Original prompt

Deploy FHIR API Documentation at api.cloudhealthoffice.com and sandbox.cloudhealthoffice.com
Problem
The portal’s /apis page (Apis.razor) displays three API endpoint URLs:
∙ Production: https://api.cloudhealthoffice.com/fhir/r4
∙ Sandbox: https://sandbox.cloudhealthoffice.com/fhir/r4
∙ Local Dev: http://localhost:3000/fhir/r4
None of these resolve to running services. The subdomains have no DNS records, no hosting infrastructure, and no deployed applications. Visitors clicking these URLs get connection errors, which undermines the credibility of the entire API page.

he OpenAPI specs exist in the repo at api/openapi/ (5 specs, ~3,200 lines total), but there’s nothing serving them.
Solution: Two-Phase Approach
Phase 1: Swagger UI at api.cloudhealthoffice.com (Immediate)
Deploy a static Swagger UI instance that serves the five OpenAPI specs. This gives visitors interactive API documentation they can browse, see request/response examples, and understand the contract — without requiring live backend services.
What goes live:
https://api.cloudhealthoffice.com → Swagger UI landing page with all 5 API specs
https://api.cloudhealthoffice.com/fhir/r4 → Redirects to Swagger UI Patient Access API
∙ Each spec is browsable with “Try it out” disabled (no live backend yet)
Infrastructure:
∙ Azure Static Web App (same pattern as cloudhealthoffice.com)
∙ Custom domain: api.cloudhealthoffice.com
∙ Source: Static HTML + bundled Swagger UI + the 5 YAML specs from api/openapi/
Implementation:
1. Create src/api-docs/ directory with a Swagger UI build:

src/api-docs/
├── index.html # Landing page with API selector
├── swagger-ui/ # Swagger UI dist files (from npm/CDN)
├── specs/
│ ├── patient-access-api.yaml
│ ├── provider-access-api.yaml
│ ├── payer-to-payer-api.yaml
│ ├── prior-auth-api.yaml
│ └── claims-scrubbing-api.yaml
└── staticwebapp.config.json

  1. The index.html landing page should:
    ∙ Match Cloud Health Office branding (dark theme, cyan accents)
    ∙ Show a dropdown/tabs to switch between the 5 API specs
    ∙ Display the spec version (v4.0.0), auth requirements, and base URL
    ∙ Link back to portal.cloudhealthoffice.com/apis and cloudhealthoffice.com

  2. staticwebapp.config.json:

{
"routes": [
{
"route": "/fhir/r4",
"redirect": "/?spec=patient-access",
"statusCode": 302
},
{
"route": "/fhir/r4/*",
"redirect": "/?spec=patient-access",
"statusCode": 302
}
],
"navigationFallback": {
"rewrite": "/index.html"
},
"globalHeaders": {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "https://portal.cloudhealthoffice.com"
},
"mimeTypes": {
".yaml": "text/yaml",
".json": "application/json"
}
}

  1. GitHub Actions workflow (deploy-api-docs.yml):

name: Deploy API Docs
on:
push:
branches: ["main"]
paths:
- 'src/api-docs/'
- 'api/openapi/
'
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

  - name: Copy OpenAPI specs
    run: |
      cp api/openapi/*.yaml src/api-docs/specs/

  - name: Deploy to Azure Static Web Apps
    uses: Azure/static-web-apps-deploy@v1
    with:
      azure_static_web_apps_api_token: ${{ secrets.API_DOCS_SWA_TOKEN }}
      repo_token: ${{ secrets.GITHUB_TOKEN }}
      action: "upload"
      app_location: "src/api-docs"
      output_location: ""
      skip_app_build: true
  1. DNS: Add CNAME record for api.cloudhealthoffice.com pointing to the Azure Static Web App
    Phase 2: HAPI FHIR Sandbox with Synthetic Data (Next Sprint)
    Deploy a HAPI FHIR server loaded with Synthea-generated synthetic patient data at sandbox.cloudhealthoffice.com. This gives prospects a live, clickable API they can actually call with real (synthetic) responses.
    What goes live:
    https://sandbox.cloudhealthoffice.com/fhir/r4/metadata → FHIR CapabilityStatement
    https://sandbox.cloudhealthoffice.com/fhir/r4/Patient → Synthetic patients
    https://sandbox.cloudhealthoffice.com/fhir/r4/Claim → Synthetic claims
    ∙ All read-only, no PHI, no auth required for sandbox
    Infrastructure:
    ∙ Azure Container App in existing cho-aca-env environment
    ∙ ACR image: choacrhy6h2vdulfru6.azurecr.io/hapi-fhir-sandbox
    ∙ Custom domain: sandbox.cloudhealthoffice.com
    ∙ CosmosDB or PostgreSQL backend (evaluate cost)
    Implementation:
    1. Create containers/hapi-fhir-sandbox/:

src/api-docs/
├── index.html # Landing page with API selector
├── swagger-ui/ # Swagger UI dist files (from npm/CDN)
├── specs/
│ ├── patient-access-api.yaml
│ ├── provider-access-api.yaml
│ ├── payer-to-payer-api.yaml
│ ├── prior-auth-api.yaml
│ └── claims-scrubbing-api.yaml
└── staticwebapp.config.json

  1. The index.html landing page should:
    ∙ Match Cloud Health Office branding (dark theme, cyan accents)
    ∙ Show a dropdown/tabs to switch bet...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…ox infra

Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com>
Copilot AI changed the title [WIP] Deploy FHIR API documentation for public access Deploy FHIR API docs (Swagger UI) and HAPI FHIR sandbox infrastructure Mar 4, 2026
@aurelianware aurelianware marked this pull request as ready for review March 4, 2026 08:15
Copilot AI review requested due to automatic review settings March 4, 2026 08:15
@aurelianware
Copy link
Owner

@copilot di you already deploy the api docs statics website? I’m wondering where to get the deployment token to add to repo secret value fir API_DOCS_SWA_TOKEN

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds deployable FHIR API documentation (Swagger UI) and introduces a HAPI FHIR sandbox container + deployment workflow, then updates the portal /apis page to point users to the new docs/sandbox endpoints.

Changes:

  • Add a Cloud Health Office–branded Swagger UI static site under src/api-docs/, including SWA routing/headers and a workflow to deploy it.
  • Add a HAPI FHIR sandbox container (Dockerfile, config, Synthea loader) and a GitHub Actions workflow to deploy it to Azure Container Apps.
  • Update the portal Apis.razor endpoints + example commands to reference the new production docs and sandbox metadata/patient queries.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/portal/CloudHealthOffice.Portal/Pages/Apis.razor Updates endpoint links and replaces the “Quick Example” with sandbox curl commands.
src/api-docs/index.html New Swagger UI landing page with spec selector and CHO theming.
src/api-docs/staticwebapp.config.json Adds redirects, navigation fallback, security headers, CORS, and MIME types for SWA.
src/api-docs/specs/.gitkeep Ensures specs/ is tracked; documents specs are copied at deploy time.
.github/workflows/deploy-api-docs.yml Deploy workflow to copy OpenAPI YAMLs and upload to Azure Static Web Apps.
containers/hapi-fhir-sandbox/Dockerfile New container image definition based on hapiproject/hapi.
containers/hapi-fhir-sandbox/application.yaml HAPI FHIR configuration (R4, H2, CORS, server address).
containers/hapi-fhir-sandbox/load-synthea.sh Startup script to wait for HAPI and load Synthea bundles once.
.github/workflows/deploy-sandbox.yml Build/push sandbox image to ACR and create/update an Azure Container App.

@@ -0,0 +1,31 @@
spring:
datasource:
url: jdbc:h2:file:/data/hapi-fhir;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The H2 database is stored on the container filesystem (jdbc:h2:file:/data/...). With Azure Container Apps (and especially with --min-replicas 0 scale-to-zero), local storage is ephemeral and data will be lost when the replica is replaced. If persistence across cold starts is desired, mount durable storage (e.g., Azure Files) or use an external database.

Suggested change
url: jdbc:h2:file:/data/hapi-fhir;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL
url: ${SPRING_DATASOURCE_URL:jdbc:h2:mem:hapi-fhir;DB_CLOSE_DELAY=-1;MODE=MySQL}

Copilot uses AI. Check for mistakes.
<MudText Typo="Typo.body2" Class="mt-2" Style="font-family: 'Courier New', monospace;">
<MudLink Typo="Typo.body2" Class="mt-2 d-block"
Style="font-family: 'Courier New', monospace;"
Href="https://api.cloudhealthoffice.com"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link destination (Href) and the displayed URL don’t match: clicking takes users to https://api.cloudhealthoffice.com, but the text implies /fhir/r4. Align the Href with the displayed endpoint (or update the displayed text) to avoid sending users to an unexpected location.

Suggested change
Href="https://api.cloudhealthoffice.com"
Href="https://api.cloudhealthoffice.com/fhir/r4"

Copilot uses AI. Check for mistakes.
Comment on lines +385 to +389
function applyDarkTheme() {
var style = document.createElement('style');
style.textContent = [
'.swagger-ui { background: #000 !important; color: rgba(255,255,255,0.9) !important; }',
'.swagger-ui .wrapper { background: #000 !important; }',
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyDarkTheme() appends a new <style> element every time a spec is loaded (via onComplete). Switching specs repeatedly will accumulate duplicate style tags. Consider adding the style element once (e.g., with an id check) or updating an existing style node.

Copilot uses AI. Check for mistakes.
hapi:
fhir:
fhir_version: R4
server_address: https://sandbox.cloudhealthoffice.com/fhir/r4
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configured server_address includes /fhir/r4, but the container health check and the data-loader script in this PR target /fhir/.... These need to be consistent; otherwise the CapabilityStatement may advertise the wrong base URL or the health check may probe the wrong endpoint. Align the runtime path, server_address, and health check to the same base route.

Suggested change
server_address: https://sandbox.cloudhealthoffice.com/fhir/r4
server_address: https://sandbox.cloudhealthoffice.com/fhir

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +23
# Read-only sandbox — disable write operations for untrusted callers
# Note: write is still enabled internally so load-synthea.sh can populate data
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the sandbox is “Read-only” and will “disable write operations”, but there’s no configuration here that actually enforces read-only behavior (it only tweaks delete/expunge/narrative/validation). Either implement an actual read-only enforcement mechanism or update the comment to accurately describe what is and isn’t restricted.

Suggested change
# Read-only sandbox — disable write operations for untrusted callers
# Note: write is still enabled internally so load-synthea.sh can populate data
# Sandbox safety: limit destructive operations (no multiple delete, no expunge, no narratives)
# Note: standard FHIR write operations (create/update/patch) remain enabled; full read-only must be enforced elsewhere

Copilot uses AI. Check for mistakes.
Comment on lines +336 to +337
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swagger UI assets are loaded from unpkg using a major-only version range ("@5"), which can introduce breaking changes unexpectedly and is a supply-chain risk. Pin to an exact swagger-ui-dist version (and ideally add SRI hashes) to make deployments deterministic.

Copilot uses AI. Check for mistakes.
echo "[load-synthea] Synthetic data loaded successfully."
else
echo "[load-synthea] No Synthea bundles found in ${DATA_DIR}. Skipping data load."
touch "${LOADED_FLAG}"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This marks the data as loaded even when no Synthea bundles exist. If bundles are added later via a mounted volume (or copied in during debugging), the loader will never run again unless the flag is manually removed. Only create the loaded flag after a successful load, or use a separate “attempted” marker.

Suggested change
touch "${LOADED_FLAG}"

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +46
for bundle in "${DATA_DIR}"/*.json; do
echo "[load-synthea] POST ${bundle}"
curl -sf \
-X POST "${HAPI_BASE_URL}" \
-H "Content-Type: application/fhir+json" \
--data-binary "@${bundle}" \
-o /dev/null
done
touch "${LOADED_FLAG}"
echo "[load-synthea] Synthetic data loaded successfully."
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With set -e and curl -f, a single failed bundle POST will terminate the script and bring down the container (potentially causing crash loops). Consider handling failures per-bundle (log + continue, or collect failures and decide at the end) so the sandbox can still start even if one bundle is malformed.

Suggested change
for bundle in "${DATA_DIR}"/*.json; do
echo "[load-synthea] POST ${bundle}"
curl -sf \
-X POST "${HAPI_BASE_URL}" \
-H "Content-Type: application/fhir+json" \
--data-binary "@${bundle}" \
-o /dev/null
done
touch "${LOADED_FLAG}"
echo "[load-synthea] Synthetic data loaded successfully."
failed_bundles=0
for bundle in "${DATA_DIR}"/*.json; do
echo "[load-synthea] POST ${bundle}"
if ! curl -sf \
-X POST "${HAPI_BASE_URL}" \
-H "Content-Type: application/fhir+json" \
--data-binary "@${bundle}" \
-o /dev/null; then
echo "[load-synthea] ERROR: Failed to POST bundle ${bundle}"
failed_bundles=$((failed_bundles + 1))
fi
done
touch "${LOADED_FLAG}"
if [ "${failed_bundles}" -gt 0 ]; then
echo "[load-synthea] Synthetic data load completed with ${failed_bundles} bundle error(s). See logs for details."
else
echo "[load-synthea] Synthetic data loaded successfully."
fi

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
- name: Log in to Azure Container Registry
run: az acr login --name ${{ env.ACR_REGISTRY }}

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

az acr login --name expects the ACR resource name (e.g., choacr...) rather than the login server FQDN (...azurecr.io). In this repo other workflows pass vars.ACR_NAME (resource name). Use the registry name here (or split env into ACR_NAME + ACR_LOGIN_SERVER) to avoid login failures.

Copilot uses AI. Check for mistakes.
--resource-group "${{ env.RESOURCE_GROUP }}" \
--query "properties.configuration.ingress.fqdn" -o tsv)
echo "Container App FQDN: ${FQDN}"
curl -sf "https://${FQDN}/fhir/metadata" -o /dev/null && \
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health check probes https://${FQDN}/fhir/metadata, but elsewhere in this PR the sandbox base URL is /fhir/r4 (portal links, OpenAPI servers, and application.yaml server_address). Update the probe path to match the deployed base route so the check validates the actual public endpoint.

Suggested change
curl -sf "https://${FQDN}/fhir/metadata" -o /dev/null && \
curl -sf "https://${FQDN}/fhir/r4/metadata" -o /dev/null && \

Copilot uses AI. Check for mistakes.
@aurelianware aurelianware reopened this Mar 4, 2026
@aurelianware
Copy link
Owner

@copilot apply changes based on the comments in this thread

…source, server_address, dark theme, bundle error handling, permissions

Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot di you already deploy the api docs statics website? I’m wondering where to get the deployment token to add to repo secret value fir API_DOCS_SWA_TOKEN

The API_DOCS_SWA_TOKEN comes from the Azure portal after you create the Static Web App resource. Steps:

  1. Go to Azure Portal → Static Web Apps → Create (or open an existing one)
  2. Once created, open the resource → Manage deployment token (in the Overview blade)
  3. Copy the token value
  4. Add it to GitHub: Repo Settings → Secrets and variables → Actions → New repository secret — name it API_DOCS_SWA_TOKEN

The deploy workflow (deploy-api-docs.yml) will automatically use it on the next push to main that touches src/api-docs/** or api/openapi/**.

@copilot apply changes based on the comments in this thread

All review comments addressed in commit 7a1f04c:

  • application.yaml: H2 datasource now uses ${SPRING_DATASOURCE_URL:jdbc:h2:mem:...} env var override (in-memory default is safe for scale-to-zero; override with a real DB URL via env var for persistence). server_address aligned to /fhir/r4. Comment updated to accurately describe what is and isn't restricted.
  • load-synthea.sh: Per-bundle failures are logged and counted — script continues loading remaining bundles. Flag is only written when all bundles succeed (allowing retry on next start for partial failures). Flag is also not set when no bundles are found (allows retry after bundles are mounted later).
  • deploy-api-docs.yml: Permissions tightened to contents: read only.
  • deploy-sandbox.yml: az acr login now uses vars.ACR_NAME (registry resource name). Health check probes /fhir/r4/metadata.
  • Apis.razor: Both Production and Sandbox Href values now match the displayed URLs exactly.
  • index.html: Dark theme CSS moved to the static <style> block in <head>applyDarkTheme() and onComplete removed (no more accumulating <style> tags on spec switch). Swagger UI pinned to 5.18.2.

@aurelianware
Copy link
Owner

@copilot di you already deploy the api docs statics website? I’m wondering where to get the deployment token to add to repo secret value fir API_DOCS_SWA_TOKEN

The API_DOCS_SWA_TOKEN comes from the Azure portal after you create the Static Web App resource. Steps:

  1. Go to Azure Portal → Static Web Apps → Create (or open an existing one)
  2. Once created, open the resource → Manage deployment token (in the Overview blade)
  3. Copy the token value
  4. Add it to GitHub: Repo Settings → Secrets and variables → Actions → New repository secret — name it API_DOCS_SWA_TOKEN

The deploy workflow (deploy-api-docs.yml) will automatically use it on the next push to main that touches src/api-docs/** or api/openapi/**.

@copilot apply changes based on the comments in this thread

All review comments addressed in commit 7a1f04c:

  • application.yaml: H2 datasource now uses ${SPRING_DATASOURCE_URL:jdbc:h2:mem:...} env var override (in-memory default is safe for scale-to-zero; override with a real DB URL via env var for persistence). server_address aligned to /fhir/r4. Comment updated to accurately describe what is and isn't restricted.
  • load-synthea.sh: Per-bundle failures are logged and counted — script continues loading remaining bundles. Flag is only written when all bundles succeed (allowing retry on next start for partial failures). Flag is also not set when no bundles are found (allows retry after bundles are mounted later).
  • deploy-api-docs.yml: Permissions tightened to contents: read only.
  • deploy-sandbox.yml: az acr login now uses vars.ACR_NAME (registry resource name). Health check probes /fhir/r4/metadata.
  • Apis.razor: Both Production and Sandbox Href values now match the displayed URLs exactly.
  • index.html: Dark theme CSS moved to the static <style> block in <head>applyDarkTheme() and onComplete removed (no more accumulating <style> tags on spec switch). Swagger UI pinned to 5.18.2.

@copilot can we do this in an azure container app environment instead of the static website?

…nginx)

Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot di you already deploy the api docs statics website? I’m wondering where to get the deployment token to add to repo secret value fir API_DOCS_SWA_TOKEN
...

Done in commit afddec4. The Swagger UI now runs as a Container App in the same cho-aca-env environment as the HAPI FHIR sandbox:

  • containers/api-docs/Dockerfile — nginx:1.27-alpine base; bakes src/api-docs/index.html and all api/openapi/*.yaml specs into the image at build time. Build context is the repo root (docker build -f containers/api-docs/Dockerfile .).
  • containers/api-docs/nginx.conf — YAML MIME type, security headers (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, CORS locked to portal.cloudhealthoffice.com), /fhir/r4 → 302 redirect to /?spec=patient-access, SPA fallback to index.html.
  • deploy-api-docs.yml — now uses OIDC login + vars.ACR_NAME/vars.ACR_LOGIN_SERVER, builds and pushes to ACR, deploys/updates the api-docs Container App (--min-replicas 1 so no cold start, --target-port 80, 0.25 CPU / 0.5Gi). No API_DOCS_SWA_TOKEN secret needed.
  • src/api-docs/staticwebapp.config.json — removed; all routing is now in nginx.

The only prerequisite is setting up a CNAME api → <api-docs Container App FQDN> in GoDaddy (same as the sandbox CNAME).

@aurelianware
Copy link
Owner

@copilot apply changes based on the comments in this thread also fix the failing check Run ibiqlik/action-yamllint@v3
Run # export LOGFILE=$(mktemp yamllint-XXXXXX)

= Linting YAML files =

Error: /workflows/deploy-api-docs.yml:82:1: [error] too many blank lines (1 > 0) (empty-lines)
Error: Process completed with exit

Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot apply changes based on the comments in this thread also fix the failing check Run ibiqlik/action-yam...

Fixed in commit 2e2efe8. The trailing blank line at the end of deploy-api-docs.yml (which caused the too many blank lines (1 > 0) (empty-lines) yamllint error) has been removed. Verified with the exact CI config (extends: default, line-length: false, document-start: false, truthy: false) — all workflows pass cleanly now.

The PR review comment changes were already applied in the prior commits (7a1f04c and afddec4).

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

Code Coverage

Package Line Rate Branch Rate Health
CloudHealthOffice.Portal 2% 3%
CloudHealthOffice.Portal 2% 3%
Summary 2% (176 / 11658) 3% (70 / 2408)

@aurelianware aurelianware merged commit 6296f8a into main Mar 4, 2026
64 checks passed
@aurelianware aurelianware deleted the copilot/deploy-fhir-api-documentation branch March 4, 2026 09:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Comment on lines +3 to +12
url: ${SPRING_DATASOURCE_URL:jdbc:h2:mem:hapi-fhir;DB_CLOSE_DELAY=-1;MODE=MySQL}
username: sa
password: ""
driverClassName: org.h2.Driver
jpa:
properties:
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
hibernate:
ddl-auto: update

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sandbox is configured with an in-memory H2 DB (jdbc:h2:mem:...), so all data (including any loaded synthetic bundles) will be lost on every container restart/scale-to-zero. If you expect stable demo data, switch to a persistent datastore (file-based H2, Postgres, etc.) and/or mount persistent storage for HAPI’s data directory.

Copilot uses AI. Check for mistakes.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cloud Health Office — FHIR API Documentation</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" />
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page depends on loading Swagger UI JS/CSS from unpkg.com. That introduces an external runtime dependency (and supply-chain risk) for a production docs site; if the CDN is blocked/outage, docs won’t render. Consider vendoring swagger-ui-dist into the container image (or at least adding SRI integrity attributes + a CSP) so the API docs remain self-contained and more resilient.

Suggested change
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" />
<link rel="stylesheet" href="./swagger-ui.css" />

Copilot uses AI. Check for mistakes.
FROM nginx:1.27-alpine

# Add YAML MIME type (not included in nginx defaults)
RUN sed -i 's|text/plain\s*txt;|text/plain txt;\n text/yaml yaml yml;|' /etc/nginx/mime.types
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed regex uses \s* (e.g., text/plain\s*txt;), which isn’t supported by BusyBox/POSIX sed in nginx:alpine, so this substitution will likely never match and the YAML MIME type won’t be added. Use a POSIX character class like [[:space:]]* or add the YAML type via an explicit types { ... } block in the nginx config instead of editing /etc/nginx/mime.types with a brittle pattern.

Suggested change
RUN sed -i 's|text/plain\s*txt;|text/plain txt;\n text/yaml yaml yml;|' /etc/nginx/mime.types
RUN sed -i 's|text/plain[[:space:]]*txt;|text/plain txt;\n text/yaml yaml yml;|' /etc/nginx/mime.types

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
# OpenAPI spec files are copied here by the deploy-api-docs.yml workflow
# from api/openapi/*.yaml at deploy time.
# Do not commit spec files directly — they are sourced from api/openapi/.
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says specs are copied into src/api-docs/specs/ by deploy-api-docs.yml, but the current workflow builds an nginx container that copies specs directly from api/openapi/ and does not copy anything into this folder. Please update/remove this note so it matches the current deployment approach (and so local previews of src/api-docs/index.html aren’t misleading).

Suggested change
# OpenAPI spec files are copied here by the deploy-api-docs.yml workflow
# from api/openapi/*.yaml at deploy time.
# Do not commit spec files directly — they are sourced from api/openapi/.
# This directory is intentionally kept empty and tracked via .gitkeep.
# The API docs deployment (nginx container) serves OpenAPI specs directly
# from api/openapi/*.yaml and does not copy them into src/api-docs/specs/.
# Do not add spec files here — commit them under api/openapi/ instead.

Copilot uses AI. Check for mistakes.

DATA_DIR="/data/synthea"
LOADED_FLAG="/data/.synthea-loaded"
HAPI_BASE_URL="http://localhost:8080/fhir"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sandbox base path is inconsistent across this PR: this script talks to http://localhost:8080/fhir (e.g., readiness check and bundle POST), while the portal links and the deploy workflow health check use /fhir/r4. If HAPI is actually mounted at /fhir/r4, this script will never see readiness and will POST to the wrong endpoint; if it’s mounted at /fhir, the external URLs/health check are wrong. Align the base path across application.yaml, this script, and the workflow (and configure HAPI’s servlet/context path if /fhir/r4 is required).

Suggested change
HAPI_BASE_URL="http://localhost:8080/fhir"
HAPI_BASE_URL="http://localhost:8080/fhir/r4"

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +28
# Sandbox safety: limit destructive operations (no multiple delete, no expunge, no narratives)
# Note: standard FHIR write operations (create/update/patch) remain enabled; full read-only must be enforced elsewhere
narrative_enabled: false
cors:
allow_Credentials: false
allowed_origin:
- "*"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config explicitly notes that standard FHIR write operations remain enabled, while the PR description and portal copy indicate the sandbox should be read-only and unauthenticated. With allowed_origin: ['*'], an unauthenticated writable FHIR server is very easy to abuse (data vandalism, storage/CPU DoS, etc.). Enforce read-only at the server level (preferred) or block non-GET/HEAD methods at the ingress layer before deploying this publicly.

Suggested change
# Sandbox safety: limit destructive operations (no multiple delete, no expunge, no narratives)
# Note: standard FHIR write operations (create/update/patch) remain enabled; full read-only must be enforced elsewhere
narrative_enabled: false
cors:
allow_Credentials: false
allowed_origin:
- "*"
# Sandbox safety: limit destructive operations (no multiple delete, no expunge, no narratives) and restrict CORS exposure
# Note: standard FHIR write operations (create/update/patch) are intended for controlled sandbox UIs only
narrative_enabled: false
cors:
allow_Credentials: false
allowed_origin:
- "https://sandbox.cloudhealthoffice.com"

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
ACR_REGISTRY: choacrhy6h2vdulfru6.azurecr.io
IMAGE_NAME: hapi-fhir-sandbox
ACA_NAME: hapi-fhir-sandbox
ACA_ENV: cho-aca-env
RESOURCE_GROUP: rg-cloudhealthoffice-prod
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow hard-codes the ACR login server (choacrhy6h2vdulfru6.azurecr.io) and resource group, while other workflows standardize on GitHub vars (e.g., vars.ACR_LOGIN_SERVER, vars.AZURE_RG_NAME) to avoid drift across environments. Please switch these values to vars.* (with defaults if needed) so registry/RG changes don’t require editing the workflow.

Suggested change
ACR_REGISTRY: choacrhy6h2vdulfru6.azurecr.io
IMAGE_NAME: hapi-fhir-sandbox
ACA_NAME: hapi-fhir-sandbox
ACA_ENV: cho-aca-env
RESOURCE_GROUP: rg-cloudhealthoffice-prod
ACR_REGISTRY: ${{ vars.ACR_LOGIN_SERVER || 'choacrhy6h2vdulfru6.azurecr.io' }}
IMAGE_NAME: hapi-fhir-sandbox
ACA_NAME: hapi-fhir-sandbox
ACA_ENV: cho-aca-env
RESOURCE_GROUP: ${{ vars.AZURE_RG_NAME || 'rg-cloudhealthoffice-prod' }}

Copilot uses AI. Check for mistakes.
IMAGE_NAME: api-docs
ACA_NAME: api-docs
ACA_ENV: cho-aca-env
RESOURCE_GROUP: rg-cloudhealthoffice-prod
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RESOURCE_GROUP is hard-coded here, but the repo’s Azure workflows generally use GitHub vars (with defaults) for RG and other environment-specific values. Consider switching to something like vars.AZURE_RG_NAME || 'rg-cloudhealthoffice-prod' to keep deployments consistent across environments.

Suggested change
RESOURCE_GROUP: rg-cloudhealthoffice-prod
RESOURCE_GROUP: ${{ vars.AZURE_RG_NAME || 'rg-cloudhealthoffice-prod' }}

Copilot uses AI. Check for mistakes.
Comment on lines +318 to +328
<MudText Typo="Typo.h6" Class="mb-3">🔐 Quick Example: FHIR Sandbox API</MudText>
<MudText Typo="Typo.body2" Class="mb-3" Style="color: rgba(255, 255, 255, 0.7);">
Get an access token and query patient claims:
Try the sandbox — no auth required, 100% synthetic data:
</MudText>
<pre style="background: #000; padding: 16px; border-radius: 4px; overflow-x: auto; border: 1px solid rgba(0, 255, 255, 0.3);"><code style="color: #00ffff; font-family: 'Courier New', monospace; font-size: 13px;"># Get access token
curl -X POST https://api.cloudhealthoffice.com/auth/token \
-H "Content-Type: application/json" \
-d '{"tenantId": "demo", "clientId": "your-client-id", "clientSecret": "your-secret"}'
<pre style="background: #000; padding: 16px; border-radius: 4px; overflow-x: auto; border: 1px solid rgba(0, 255, 255, 0.3);"><code style="color: #00ffff; font-family: 'Courier New', monospace; font-size: 13px;"># Get FHIR CapabilityStatement (sandbox — no auth required)
curl https://sandbox.cloudhealthoffice.com/fhir/r4/metadata \
-H "Accept: application/fhir+json"

# Search for patient claims
curl https://api.cloudhealthoffice.com/fhir/r4/Claim?patient=12345 \
-H "Authorization: Bearer YOUR_TOKEN"</code></pre>
# Search for synthetic patients
curl https://sandbox.cloudhealthoffice.com/fhir/r4/Patient?_count=5 \
-H "Accept: application/fhir+json"</code></pre>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copy says the sandbox has “no auth required” and “100% synthetic data”, but the sandbox container currently (a) doesn’t bundle or generate any Synthea data (it only loads from /data/synthea if present) and (b) is not configured as read-only. Until the deployment guarantees seeded synthetic data and read-only behavior, this UI text is likely to be inaccurate/misleading.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +33
<MudLink Typo="Typo.body2" Class="mt-2 d-block"
Style="font-family: 'Courier New', monospace;"
Href="https://api.cloudhealthoffice.com/fhir/r4"
Target="_blank">
https://api.cloudhealthoffice.com/fhir/r4
</MudText>
</MudLink>
</MudItem>
<MudItem xs="12" md="4">
<MudChip Color="Color.Warning" Variant="Variant.Outlined">Sandbox</MudChip>
<MudText Typo="Typo.body2" Class="mt-2" Style="font-family: 'Courier New', monospace;">
<MudLink Typo="Typo.body2" Class="mt-2 d-block"
Style="font-family: 'Courier New', monospace;"
Href="https://sandbox.cloudhealthoffice.com/fhir/r4"
Target="_blank">
https://sandbox.cloudhealthoffice.com/fhir/r4
</MudText>
</MudLink>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new MudLink elements for the production and sandbox FHIR endpoints use Target="_blank" without specifying a rel attribute, which means the newly opened page can access window.opener and force your portal tab to navigate to an arbitrary URL (reverse‑tabnabbing). If either of these external endpoints (or anything they redirect to) is compromised, an attacker could abuse this to replace your portal tab with a phishing page while appearing to come from a trusted click-through. To mitigate, ensure external links opened in a new tab explicitly include a rel value that enforces noopener (and ideally noreferrer) or configure MudLink usage so that all Target="_blank" links render with rel="noopener" by default.

Copilot uses AI. Check for mistakes.
aurelianware added a commit that referenced this pull request Mar 8, 2026
…umentation

Deploy FHIR API docs (Swagger UI) and HAPI FHIR sandbox infrastructure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants