From c871dc484290f910b53d9c9e55af7c6f2e7da034 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Tue, 11 Aug 2026 20:17:53 +0800 Subject: [PATCH] Add deployment files --- .dockerignore | 28 + .env.example | 50 ++ .env_example | 113 +++ .gitea/workflows/cd.yml | 174 +++++ .gitea/workflows/ci.yml | 95 +++ VigilCareRecordsAPI/Dockerfile | 58 ++ .../appsettings.Production.json | 27 + .../appsettings.Production_example.json | 57 ++ docker-compose.prod.yml | 69 ++ docs/25-gitea-cicd-docker-deploy.md | 417 +++++++++++ docs/26-vigilcare-records-cicd.md | 83 +++ docs/vigilcare-records-clinical-overview.md | 650 ++++++++++++++++++ ...vigilcare-records-phase-10-verification.sh | 0 ...vigilcare-records-phase-11-verification.sh | 0 ...vigilcare-records-phase-13-verification.sh | 0 ...-vigilcare-records-phase-2-verification.sh | 0 ...-vigilcare-records-phase-3-verification.sh | 0 ...-vigilcare-records-phase-4-verification.sh | 0 ...-vigilcare-records-phase-5-verification.sh | 0 ...-vigilcare-records-phase-6-verification.sh | 0 ...-vigilcare-records-phase-8-verification.sh | 0 scripts/run-vigilcare-records-verification.sh | 0 vigilcare-records-web/Dockerfile | 23 + vigilcare-records-web/nginx.conf | 36 + 24 files changed, 1880 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .env_example create mode 100644 .gitea/workflows/cd.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 VigilCareRecordsAPI/Dockerfile create mode 100644 VigilCareRecordsAPI/appsettings.Production.json create mode 100644 VigilCareRecordsAPI/appsettings.Production_example.json create mode 100644 docker-compose.prod.yml create mode 100644 docs/25-gitea-cicd-docker-deploy.md create mode 100644 docs/26-vigilcare-records-cicd.md create mode 100644 docs/vigilcare-records-clinical-overview.md mode change 100755 => 100644 scripts/run-vigilcare-records-phase-10-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-11-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-13-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-2-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-3-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-4-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-5-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-6-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-phase-8-verification.sh mode change 100755 => 100644 scripts/run-vigilcare-records-verification.sh create mode 100644 vigilcare-records-web/Dockerfile create mode 100644 vigilcare-records-web/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6b786ca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# .NET build output +**/bin/ +**/obj/ +**/out/ +**/publish/ +**/TestResults/ + +# Node +**/node_modules/ +**/dist/ + +# Env / secrets +.env +.env.* +!.env.example + +# VCS / IDE +.git/ +.gitea/ +.vscode/ +.idea/ +*.user + +# Docs / misc noise not needed in build context +docs/ +scripts/ +README.md +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8a807c8 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +# Copy this file to /opt/vigilcare-records/.env on the deploy host (chmod 600). +# CD never uploads or overwrites this file — only the IMAGE_TAG line is patched +# automatically on each release. See docs/26-vigilcare-records-cicd.md. + +# ---- image coordinates ---- +REGISTRY=git.vectur45.com/trent/vigilcare-records +IMAGE_TAG=v1.0.0 + +# ---- exposed ports on the production host ---- +API_PORT=5217 +DASHBOARD_PORT=8089 +DASHBOARD_ORIGIN=https://vigilcare-records.vectur45.com + +# ---- PostgreSQL ---- +# VigilCare Records shares one PostgreSQL instance with VigilCareClinical on the +# same host (see docs/vigilcare-records-clinical-overview.md — "Integrated Database +# Deployment"). "postgres" below is the service name on the "shared-services" +# Docker network already created by VigilCareClinical's own compose project; that +# stack MUST be up before the first `docker compose -f docker-compose.prod.yml up`. +# Runtime (DML-only) connection used by the API container. +PG_CONNECTION="Host=postgres;Port=5432;Database=vigilcare_records;Username=vigilcare_records_app;Password=CHANGE_ME;SSL Mode=Disable" +# DDL-privileged connection used ONLY by the EF migration bundle (CD migrate job). +# That job runs on the act_runner host directly (not joined to shared-services), so +# it needs the externally-routable host:port, not the container network name. +# Never put this credential in the API container environment. +PG_CONNECTION_DDL="Host=postgres.site.com;Port=5432;Database=vigilcare_records;Username=vigilcare_records_migrator;Password=CHANGE_ME;SSL Mode=Require;Trust Server Certificate=false" + +# ---- Redis (shared-services network) ---- +# "redis" = the service name on the shared Redis compose project. +# Use a dedicated logical database so Records' batch-assignment locks never +# collide with VigilCareClinical's own Redis keys. +REDIS_CONNECTION=redis:6379,abortConnect=false,defaultDatabase=2 + +# ---- MinIO (shared-services network) ---- +# "minio" = the service name on the shared MinIO compose project. Records stores +# scanned documents under its own bucket, separate from any clinical buckets. +MINIO_ENDPOINT=minio:9000 +MINIO_ACCESS_KEY=admin +MINIO_SECRET_KEY=CHANGE_ME +MINIO_BUCKET_NAME=vigilcare-records-scans +MINIO_USE_SSL=false + +# ---- Seq (shared-services network) ---- +SEQ_URL=http://seq:80 +SEQ_API_KEY=CHANGE_ME + +# ---- application secrets (generate with: openssl rand -base64 48) ---- +JWT_SECRET=CHANGE_ME_AT_LEAST_32_BYTES +JWT_ISSUER=VigilCareRecords +JWT_AUDIENCE=VigilCareRecords diff --git a/.env_example b/.env_example new file mode 100644 index 0000000..c634776 --- /dev/null +++ b/.env_example @@ -0,0 +1,113 @@ +# ---- image coordinates ---- +REGISTRY=git.vectur45.com/trent/vigilcare-clinical +IMAGE_TAG=v1.0.0 + +# ---- exposed ports on the production host ---- +API_PORT=5270 +GATEWAY_PORT=5081 +DASHBOARD_PORT=8088 +DASHBOARD_ORIGIN=https://vigilcare-clinical.vectur45.com +# Gitea Actions var PROD_API_URL (dashboard build) — not read by compose: +# https://api.vigilcare-clinical.vectur45.com + + +# ---- PostgreSQL (container on the same VM, reached via the shared-service network) ---- +# Runtime (DML-only) connection used by the API container. +# "postgres" = the service name in the Postgres compose project - rename to match it exactly. +# Port 5432 is the container's internal port, NOT the 5433 published on the host. +# SSL Mode=Disable: Postgres on the shared-services Docker network has no TLS. +# Use Require only when connecting to a TLS-enabled external Postgres. +PG_CONNECTION="Host=postgres;Port=5432;Database=vigilcare;Username=admin;Password=PartyHard753!;SSL Mode=Disable" +# DDL-privileged connection used ONLY by the EF migration bundle (Step 6 / CD migrate job). +# That job currently runs an act_runner container that is NOT joined to shared-service +# (see .gitea/workflows/cd.yml), so it MUST keep using the externally-routable host:port, +# not the container network name, unless that job is later attached to shared-service too. +# Never put this credential in the API container environment. +PG_CONNECTION_DDL="Host=postgres.site.com;Port=5432;Database=vigilcare;Username=admin;Password=PartyHard753!;SSL Mode=Require;Trust Server Certificate=false" +GATEWAY_PG_CONNECTION="Host=postgres;Port=5432;Database=vigilcare_ward;Username=admin;Password=PartyHard753!;SSL Mode=Disable" + +# ---- Redis (container on the same VM, reached via the shared-service network) ---- +# "redis" = the service name in the Redis compose project - rename to match it exactly. +# Port 6379 is the container's internal port; confirm it matches (it usually does). +REDIS_CONNECTION=redis:6379,abortConnect=false +GATEWAY_REDIS_CONNECTION=redis:6379,abortConnect=false,defaultDatabase=1 + +# ---- external Seq ---- +# On vectur-home-server (Tailscale) - use the ingestion port (5341), not the web UI +# port (8080->80). No TLS is configured, so plain http, not https. +SEQ_URL=http://vectur-home-server:5341 +# The compose only sets SEQ_FIRSTRUN_ADMINUSERNAME/PASSWORD for first-run login, +# it does not provision an API key. Generate one manually via Seq's web UI +# (Settings -> API Keys) after the container's first run, then paste it here. +SEQ_API_KEY=CHANGE_ME + +# ---- external Kafka ---- +# Single-broker cluster on vectur-home-server (Tailscale) - PLAINTEXT only, no SASL. +# Traffic relies on the Tailscale mesh for encryption in transit. +KAFKA_BOOTSTRAP=vectur-home-server:9092 +KAFKA_REPLICATION_FACTOR=1 +KAFKA_SECURITY_PROTOCOL=Plaintext + +# ---- external Elasticsearch ---- +# Unauthenticated cluster: leave ES_API_KEY / ES_USERNAME / ES_PASSWORD unset. +# Program.cs only attaches auth when those values are non-empty. +ES_URI=http://vectur-home-server:9200 +ES_API_KEY= +# ES_USERNAME= +# ES_PASSWORD= + +# ---- external RabbitMQ ---- +# Plain AMQP on vectur-home-server (Tailscale) - only 5672 is exposed, no TLS listener. +# RABBITMQ_USERNAME/PASSWORD must match RABBITMQ_DEFAULT_USER/PASS in the RabbitMQ +# compose's own .env on vectur-home-server. +RABBITMQ_HOST=vectur-home-server +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=admin +RABBITMQ_PASSWORD=2va2xcLWTAlRN4abEv3wa7EVawHXk+tnRkQVOOXqs2M= +RABBITMQ_USE_SSL=false +GATEWAY_RABBITMQ_HOST=vectur-home-server +GATEWAY_RABBITMQ_PORT=5672 +GATEWAY_RABBITMQ_USERNAME=admin +GATEWAY_RABBITMQ_PASSWORD=2va2xcLWTAlRN4abEv3wa7EVawHXk+tnRkQVOOXqs2M= +GATEWAY_RABBITMQ_USE_SSL=false + +# ---- external MinIO ---- +# Host publishes the S3 API on 9002 (mapped to container's 9000), no TLS termination. +# MINIO_ACCESS_KEY/SECRET_KEY must match MINIO_ROOT_USER/MINIO_ROOT_PASSWORD in the +# MinIO compose's own .env on vectur-home-server. +MINIO_ENDPOINT=vectur-home-server:9002 +MINIO_ACCESS_KEY=admin +MINIO_SECRET_KEY=p3QUh8mXvosfFjrJYJJPd36tGiPbsOASWdIe6FKzdLI= +MINIO_USE_SSL=false + +# ---- application secrets (generate with: openssl rand -base64 48) ---- +JWT_SIGNING_KEY=T0oK2f3YhBesoMgZnEB7vmi4Dfbd7LxtuemkXI8j3xA= +# WARNING: rotating PHI_SEARCH_TOKEN_KEY invalidates every stored patient +# search token. See docs/ops/phi-encryption-runbook.md before changing it. +PHI_SEARCH_TOKEN_KEY=tbL0Nku3+bK476bv4zmfyRBiiRMTTF3To4Qq9RUOSVg= +GATEWAY_API_KEY=ZxFKE4wUChEg+VzNGM16zFSuHeM+I+IQc59rIzN0U2g= +FHIR_API_KEY=wY29TLNIzLouIEKDu+XAxmZ3T1R5cjE2IIXxaYWpNAg= +GATEWAY_JWT_SIGNING_KEY=TfxbvLz992kr8simlbr8s61W5gKQbLqjyTuPjgCWjV0= + +# ---- gateway identity ---- +GATEWAY_ID=ce985c14-42de-4db4-8538-c2a203496d9e +GATEWAY_SITE_ID=e9fba67a-fcf5-4966-acb1-dab58a68bff2 +GATEWAY_DEPARTMENT=ICU +GATEWAY_CODE=GW-ICU-1 +GATEWAY_SITE_CODE=SITE-01 +GATEWAY_SITE_NAME=Primary Site +# GATEWAY_SITE_ADDRESS= + +# ---- production bootstrap users (API seeds these when missing; not demo accounts) ---- +SEED_ADMIN_USERNAME=admin +SEED_ADMIN_PASSWORD="zx+yv8XtbxQq0E5YZ3d8kP5g" +SEED_ADMIN_DISPLAY_NAME=System Admin +SEED_NURSE_USERNAME=nurse +SEED_NURSE_PASSWORD="qcYtKfgLMezlT63AIxrPdmtK" +SEED_NURSE_DISPLAY_NAME=Charge Nurse +SEED_PHYSICIAN_USERNAME=physician +SEED_PHYSICIAN_PASSWORD="msKxpOQIGHK/StlrPIDBx9ZD" +SEED_PHYSICIAN_DISPLAY_NAME=Attending Physician + +SIMULATION_ENABLED=true +SIMULATION_RUNNER_PASSWORD=tlIxrcgEEQKh9BYdjZ6/fWwj4TFoN4zTtCUfICrQpxI= diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..e112f6a --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -0,0 +1,174 @@ +# Build images, migrate, deploy on version tags. +# Requires act_runner with docker, curl, ssh, scp, bash and label ubuntu-latest. +# +# Secrets: REGISTRY_USERNAME, REGISTRY_TOKEN, PG_CONNECTION_DDL, +# DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY +# Variables: REGISTRY (optional; defaults below) +name: CD + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + image_tag: + description: "Image tag to deploy (defaults to the pushed tag)" + required: false + +env: + REGISTRY: git.vectur45.com/trent/vigilcare-records + +jobs: + build-and-push: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.meta.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - name: Resolve tag and registry + id: meta + run: | + if [ -n "${{ vars.REGISTRY }}" ]; then + echo "REGISTRY=${{ vars.REGISTRY }}" >> "$GITHUB_ENV" + fi + if [ -n "${{ inputs.image_tag }}" ]; then + echo "tag=${{ inputs.image_tag }}" >> "$GITHUB_OUTPUT" + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + else + echo "image_tag input is required for workflow_dispatch without a tag" >&2 + exit 1 + fi + + - name: Log in to the Gitea registry + run: | + echo "${{ secrets.REGISTRY_TOKEN }}" \ + | docker login "${REGISTRY%%/*}" \ + -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Build and push vigilcare-records-api + run: | + docker build -f VigilCareRecordsAPI/Dockerfile \ + -t "${REGISTRY}/vigilcare-records-api:${{ steps.meta.outputs.tag }}" \ + -t "${REGISTRY}/vigilcare-records-api:latest" . + docker push "${REGISTRY}/vigilcare-records-api:${{ steps.meta.outputs.tag }}" + docker push "${REGISTRY}/vigilcare-records-api:latest" + + - name: Build and push vigilcare-records-dashboard + # Context is vigilcare-records-web/ — package.json and nginx.conf live there. + run: | + docker build -f vigilcare-records-web/Dockerfile \ + -t "${REGISTRY}/vigilcare-records-dashboard:${{ steps.meta.outputs.tag }}" \ + -t "${REGISTRY}/vigilcare-records-dashboard:latest" vigilcare-records-web + docker push "${REGISTRY}/vigilcare-records-dashboard:${{ steps.meta.outputs.tag }}" + docker push "${REGISTRY}/vigilcare-records-dashboard:latest" + + migrate: + needs: build-and-push + runs-on: ubuntu-latest + # Checkout must run on the job host. Build the EF bundle via Dockerfile + # --target migrate (context upload), not docker run -v — under act_runner + # bind mounts resolve on the Docker host, not the job workspace. + # NOTE: Program.cs also runs db.Database.MigrateAsync() on API startup, so + # this job is a defense-in-depth pre-deploy step using a DDL-privileged + # credential the API container never sees, not the only migration path. + steps: + - uses: actions/checkout@v4 + + - name: Build migration bundle + run: | + docker build -f VigilCareRecordsAPI/Dockerfile --target migrate \ + -t vigilcare-records-migrate-bundle:local . + cid=$(docker create vigilcare-records-migrate-bundle:local) + docker cp "$cid:/out/migrate-api" ./migrate-api + docker rm "$cid" + chmod +x ./migrate-api + + # Runs while the previous release is still serving traffic, so every + # migration must be backwards-compatible with the outgoing image + # (expand-then-contract). Self-contained linux-x64 binary — runs on the + # job host directly. + - name: Apply migrations + run: ./migrate-api --connection "${{ secrets.PG_CONNECTION_DDL }}" + + deploy: + needs: [build-and-push, migrate] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts + + - name: Copy compose file + run: | + scp -i ~/.ssh/id_ed25519 docker-compose.prod.yml \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/opt/vigilcare-records/docker-compose.prod.yml" + + - name: Deploy + env: + IMAGE_TAG: ${{ needs.build-and-push.outputs.tag }} + run: | + ssh -i ~/.ssh/id_ed25519 \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ + IMAGE_TAG="$IMAGE_TAG" bash -euo pipefail <<'EOF' + cd /opt/vigilcare-records + + # Record the currently deployed tag so a rollback has a target. + grep '^IMAGE_TAG=' .env > .env.previous || true + + if grep -q '^IMAGE_TAG=' .env; then + sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|" .env + else + echo "IMAGE_TAG=${IMAGE_TAG}" >> .env + fi + + docker compose -f docker-compose.prod.yml --env-file .env pull + docker compose -f docker-compose.prod.yml --env-file .env up -d --remove-orphans + docker image prune -f + EOF + + - name: Smoke test + run: | + ssh -i ~/.ssh/id_ed25519 \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" bash -euo pipefail <<'EOF' + cd /opt/vigilcare-records + # Do not `source` .env — compose env files are not bash (semicolons, + # spaces in "SSL Mode=...", CRLF). Read only the host ports we need. + env_val() { sed -n "s/^${1}=//p" .env | tail -n1 | tr -d '\r'; } + API_PORT="$(env_val API_PORT)"; API_PORT="${API_PORT:-5217}" + DASHBOARD_PORT="$(env_val DASHBOARD_PORT)"; DASHBOARD_PORT="${DASHBOARD_PORT:-8089}" + + for i in $(seq 1 30); do + if curl -fsS "http://localhost:${API_PORT}/health/ready" >/dev/null; then + echo "Ready check passed." + curl -fsS "http://localhost:${DASHBOARD_PORT}/" >/dev/null && echo "Dashboard serving." + exit 0 + fi + sleep 5 + done + echo "Ready check never passed — dumping API logs:" + docker compose -f docker-compose.prod.yml --env-file .env logs --tail 100 api + exit 1 + EOF + + - name: Roll back on failure + if: failure() + run: | + ssh -i ~/.ssh/id_ed25519 \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" bash -euo pipefail <<'EOF' + cd /opt/vigilcare-records + # Restores the previous image tag only. Schema changes are NOT + # reverted — this is why migrations must be backwards-compatible. + if [ -f .env.previous ]; then + PREV=$(cut -d= -f2 .env.previous) + sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${PREV}|" .env + docker compose -f docker-compose.prod.yml --env-file .env up -d + echo "Rolled back to ${PREV}" + fi + EOF diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d46ba1a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,95 @@ +# Build and test on every push/PR. Fixtures read ConnectionStrings__* / +# Redis__* directly from environment (see VigilCareRecordsAPI.Tests/Fixtures/ +# ApiFixture.cs) against the ports published by docker-compose.yml; MinIO is +# left at its docker-compose.yml defaults (localhost:9012, minioadmin/minioadmin, +# bucket auto-created on first upload by DocumentStorageService). +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start dependency stack + run: docker compose up -d postgres redis minio + + - name: Wait for Postgres + run: | + for i in $(seq 1 60); do + docker compose exec -T postgres pg_isready -U postgres && break + sleep 2 + done + docker compose exec -T postgres pg_isready -U postgres + + - name: Wait for Redis / MinIO + run: | + for i in $(seq 1 60); do + docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG \ + && docker compose exec -T minio curl -sf http://localhost:9000/minio/health/live >/dev/null \ + && echo "Redis and MinIO are ready" && exit 0 + echo "waiting for dependencies ($i)..." + sleep 2 + done + echo "Dependencies failed to become ready" >&2 + docker compose ps + docker compose logs --tail=50 redis minio + exit 1 + + - name: Create test database + run: | + docker compose exec -T postgres psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='vigilcare_records_test'" \ + | grep -q 1 \ + || docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE vigilcare_records_test" + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore + run: dotnet restore VigilCareRecords.sln + + - name: Build + run: dotnet build VigilCareRecords.sln -c Release --no-restore + + - name: Test + env: + ASPNETCORE_ENVIRONMENT: "Testing" + run: | + dotnet test VigilCareRecords.sln -c Release --no-build \ + --logger "trx;LogFileName=test-results.trx" \ + --results-directory ./TestResults + + - name: Publish test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results + path: ./TestResults + + - name: Tear down stack + if: always() + run: docker compose down -v + + frontend: + runs-on: ubuntu-latest + container: + image: node:22-alpine + steps: + - uses: actions/checkout@v4 + - name: Install + working-directory: vigilcare-records-web + run: npm ci + - name: Test + working-directory: vigilcare-records-web + run: npm run test:run + - name: Build + working-directory: vigilcare-records-web + run: npm run build diff --git a/VigilCareRecordsAPI/Dockerfile b/VigilCareRecordsAPI/Dockerfile new file mode 100644 index 0000000..f6bfbae --- /dev/null +++ b/VigilCareRecordsAPI/Dockerfile @@ -0,0 +1,58 @@ +# Build context MUST be the repo root: +# docker build -f VigilCareRecordsAPI/Dockerfile -t vigilcare-records-api . +# Single-project solution today, but keeping the repo-root context matches the +# CI/CD guide's convention and avoids a context change if a shared library is +# ever extracted alongside VigilCareRecordsAPI.Tests. + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Restore first with only the project file so the NuGet layer caches +# independently of source changes. +COPY VigilCareRecordsAPI/VigilCareRecordsAPI.csproj VigilCareRecordsAPI/ +RUN dotnet restore VigilCareRecordsAPI/VigilCareRecordsAPI.csproj + +COPY VigilCareRecordsAPI/ VigilCareRecordsAPI/ +RUN dotnet publish VigilCareRecordsAPI/VigilCareRecordsAPI.csproj \ + -c Release -o /app/publish --no-restore + +# Dev appsettings.json ships with placeholder secrets (Jwt:Secret, Minio:SecretKey). +# Blank them so a misconfigured production deploy fails fast instead of running +# with a known, publicly-committed secret. +RUN sed -i \ + -e 's/"Secret": "[^"]*"/"Secret": ""/' \ + -e 's/"SecretKey": "[^"]*"/"SecretKey": ""/' \ + /app/publish/appsettings.json + +# ---- optional target: EF migration bundle, built and extracted by the CD +# migrate job (docker build --target migrate + docker cp). Never shipped in the +# runtime image below. ---- +FROM build AS migrate +RUN dotnet tool install --global dotnet-ef --version 8.* +ENV PATH="$PATH:/root/.dotnet/tools" +RUN dotnet ef migrations bundle \ + --project VigilCareRecordsAPI/VigilCareRecordsAPI.csproj \ + --self-contained -r linux-x64 \ + --output /out/migrate-api + +# ---- runtime ---- +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/publish . + +RUN useradd --uid 1654 --user-group --no-create-home appuser \ + && chown -R appuser:appuser /app +USER appuser + +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fsS http://localhost:8080/health/live || exit 1 + +ENTRYPOINT ["dotnet", "VigilCareRecordsAPI.dll"] diff --git a/VigilCareRecordsAPI/appsettings.Production.json b/VigilCareRecordsAPI/appsettings.Production.json new file mode 100644 index 0000000..c45a2e2 --- /dev/null +++ b/VigilCareRecordsAPI/appsettings.Production.json @@ -0,0 +1,27 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Seq" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" }, + { "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "Properties": { + "Application": "VigilCareRecordsAPI" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + } +} diff --git a/VigilCareRecordsAPI/appsettings.Production_example.json b/VigilCareRecordsAPI/appsettings.Production_example.json new file mode 100644 index 0000000..6e54e53 --- /dev/null +++ b/VigilCareRecordsAPI/appsettings.Production_example.json @@ -0,0 +1,57 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Seq" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console", "Args": { "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" } }, + { "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "Properties": { + "Application": "VigilCareClinicalAPI" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "Kafka": { + "ReplicationFactor": 3, + "NumPartitions": 6, + "SecurityProtocol": "SaslSsl" + }, + "Minio": { + "UseSSL": true + }, + "RabbitMq": { + "UseSsl": true + }, + "PhiEncryption": { + "LogListAccess": true + }, + "Swagger": { "Enabled": false }, + "Seeding": { "EnableDemoData": false }, + "Simulation": { + // UAT / clinical evaluation: in-app scenario replay is on so testers can + // exercise the ward without a terminal. Patients created here are marked + // IsSimulated; Reset ward only deletes those rows. Do not enable against a + // database that also holds real care data — use a dedicated UAT DB. + "Enabled": true, + "ScenarioDirectory": "Scenarios/List", + "LoopbackBaseUrl": "http://localhost:8080", + "RunnerUsername": "simulation.runner", + "MaxConcurrentRuns": 8, + "MaxSpeed": 600, + "RunHistoryLimit": 50 + } + } + \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..28683ba --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,69 @@ +# Production overlay. Runs ONLY the two application containers — Postgres, +# Redis, MinIO, and Seq are pre-existing services shared with VigilCareClinical, +# reached through the external "shared-services" Docker network. This project +# does not create that network; VigilCareClinical's own compose stack must +# already be running the first time this stack is deployed. +# +# docker compose -f docker-compose.prod.yml --env-file .env up -d +# +# Does not extend docker-compose.yml. Local-dev infra stays in that file; this +# one ships only the deployable apps. + +name: vigilcare-records + +services: + api: + image: ${REGISTRY}/vigilcare-records-api:${IMAGE_TAG} + container_name: vigilcare_records_api + restart: unless-stopped + ports: + - "${API_PORT:-5217}:8080" + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__DefaultConnection: "${PG_CONNECTION}" + Redis__ConnectionString: "${REDIS_CONNECTION}" + Seq__ServerUrl: "${SEQ_URL}" + Serilog__WriteTo__1__Args__serverUrl: "${SEQ_URL}" + Serilog__WriteTo__1__Args__apiKey: "${SEQ_API_KEY}" + Minio__Endpoint: "${MINIO_ENDPOINT}" + Minio__AccessKey: "${MINIO_ACCESS_KEY}" + Minio__SecretKey: "${MINIO_SECRET_KEY}" + Minio__BucketName: "${MINIO_BUCKET_NAME:-vigilcare-records-scans}" + Minio__UseSsl: "${MINIO_USE_SSL:-false}" + Jwt__Secret: "${JWT_SECRET}" + Jwt__Issuer: "${JWT_ISSUER:-VigilCareRecords}" + Jwt__Audience: "${JWT_AUDIENCE:-VigilCareRecords}" + Cors__AllowedOrigins__0: "${DASHBOARD_ORIGIN}" + networks: + - vigilcare_records_prod + - shared-services + logging: + driver: json-file + options: { max-size: "50m", max-file: "5" } + deploy: + resources: + limits: { memory: 1G } + + dashboard: + image: ${REGISTRY}/vigilcare-records-dashboard:${IMAGE_TAG} + container_name: vigilcare_records_dashboard + restart: unless-stopped + ports: + - "${DASHBOARD_PORT:-8089}:80" + depends_on: + api: + condition: service_healthy + networks: + - vigilcare_records_prod + logging: + driver: json-file + options: { max-size: "20m", max-file: "3" } + +networks: + vigilcare_records_prod: + driver: bridge + # Owned by the Postgres/Redis/MinIO/Seq compose project shared with + # VigilCareClinical (it defines and creates "shared-services" via its own + # `docker compose up`). This project only consumes it. + shared-services: + external: true diff --git a/docs/25-gitea-cicd-docker-deploy.md b/docs/25-gitea-cicd-docker-deploy.md new file mode 100644 index 0000000..ca007a3 --- /dev/null +++ b/docs/25-gitea-cicd-docker-deploy.md @@ -0,0 +1,417 @@ +# Guide 25: Gitea CI/CD with Docker Compose + +How to wire a multi-service app for continuous integration and deployment on **Gitea Actions**, using VigilCare Clinical as a concrete example. The same layout works for any stack: keep local infra in one Compose file, ship only app images in production, put secrets on the host (not in git), and let workflows build, migrate, and deploy on version tags. + +Related docs in this repo: + +- [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) / [`.gitea/workflows/cd.yml`](../../.gitea/workflows/cd.yml) +- [`docker-compose.yml`](../../docker-compose.yml) (local + CI dependencies) +- [`docker-compose.prod.yml`](../../docker-compose.prod.yml) (production app stack) +- [`.env.example`](../../.env.example) +- [`docs/ops/cd-deploy-ssh-setup.md`](../ops/cd-deploy-ssh-setup.md) +- [`docs/instructions-for-env.md`](../instructions-for-env.md) + +--- + +## Mental model + +| Layer | What it is | Who owns it | +|-------|------------|-------------| +| **Source** | App code, Dockerfiles, Compose files, workflow YAML | Git repo | +| **CI** | On every push/PR: start deps, build, test | Gitea Actions + `act_runner` | +| **CD** | On `v*` tags: build/push images, migrate DB, SSH deploy | Same runner + container registry | +| **Secrets on host** | Production `.env` with DB URLs, JWT keys, etc. | Deploy VM only (never committed, never SCP’d by CD) | +| **Runtime** | Pulled images + Compose prod overlay | Deploy VM (`/opt//`) | + +``` +Developer push/PR ──► CI (compose deps + tests) +Developer git tag v1.2.3 ──► CD + ├─ build & push images → Gitea registry + ├─ apply EF migrations (DDL user) + └─ SSH → pull images, up -d, smoke test +``` + +Gitea Actions is largely compatible with GitHub Actions syntax (`on:`, `jobs:`, `uses: actions/checkout@v4`, etc.). Jobs run on a self-hosted **act_runner** that needs Docker, curl, ssh, scp, and bash, labeled `ubuntu-latest` (or whatever label you set in the workflow). + +--- + +## 1. Split Compose: local/CI vs production + +Do **not** reuse the same Compose file for laptop and production. + +### Local / CI — `docker-compose.yml` + +Defines **infrastructure** (Postgres, Redis, Kafka, RabbitMQ, Elasticsearch, MinIO, …) and optional app profiles. Developers and CI start only what tests need: + +```bash +docker compose up -d postgres redis rabbitmq kafka elasticsearch minio +docker compose --profile ward-gateway up -d ward-gateway-db ward-gateway-redis ward-gateway-rabbitmq +``` + +Useful patterns (as in this repo): + +- **Published ports** so host processes (or CI job containers via `host.docker.internal`) can reach brokers. +- **Profiles** (`ward-gateway`, `full`) so optional services stay off by default. +- **CI-friendly Kafka advertising** — override advertised host for runners: + +```yaml +KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://${KAFKA_EXTERNAL_HOST:-localhost}:9092 +``` + +CI sets `KAFKA_EXTERNAL_HOST: host.docker.internal` so the test process in a container reconnects to the published port on the Docker host, not to itself. + +### Production — `docker-compose.prod.yml` + +Runs **only the deployable apps** (here: `api`, `gateway`, `dashboard`). Databases and brokers are assumed to already exist; Compose wires them through environment variables from `.env`. + +```yaml +services: + api: + image: ${REGISTRY}/clinical-api:${IMAGE_TAG} + environment: + ConnectionStrings__DefaultConnection: "${PG_CONNECTION}" + # … Jwt, Kafka, Redis, etc. from .env + volumes: + - dp_keys:/app/data-protection-keys # durable secrets / keyrings + networks: + - vigilcare_prod + - shared-services # external network owned by infra compose +``` + +Principles: + +1. **Image coordinates** via `REGISTRY` + `IMAGE_TAG` — CD updates only `IMAGE_TAG`. +2. **External networks** for shared Postgres/Redis stacks already running on the host. +3. **No build:** on the VM — `docker compose pull` then `up -d`. +4. **Named volumes** for anything that must survive recreate (e.g. Data Protection keys). + +CD copies **only** this file to the server each release; it does not copy `.env`. + +--- + +## 2. Dockerfiles that CI and CD can trust + +### Multi-stage builds + +Keep a **SDK/build** stage and a thin **runtime** stage. Example (API): + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS src +# restore with project files only → cache NuGet layer +# then COPY source, publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +COPY --from=build /app/publish . +USER app +HEALTHCHECK CMD curl -fsS http://localhost:8080/health/live || exit 1 +ENTRYPOINT ["dotnet", "VigilCareClinicalAPI.dll"] +``` + +Frontend (Vite) must bake public API URLs at **build** time: + +```dockerfile +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL +RUN npm run build +``` + +CD passes `--build-arg VITE_API_URL="${{ vars.PROD_API_URL }}"`. + +### Build context + +Match Compose and CD: + +| Image | Dockerfile | Context | Why | +|-------|------------|---------|-----| +| clinical-api | `VigilCareClinicalAPI/Dockerfile` | **repo root** | Sibling `ProjectReference`s | +| ward-gateway | `VigilCare.WardGateway/Dockerfile` | **repo root** | Same | +| dashboard | `vigilcare-dashboard/Dockerfile` | `vigilcare-dashboard/` | `package.json`, `nginx.conf` | + +Wrong context is the most common “works on my machine, fails in CD” failure. + +### Scrub secrets from published config + +Dev `appsettings.json` often contains placeholder keys. Strip them in the image so production **must** supply env vars: + +```dockerfile +RUN sed -i \ + -e 's/"SigningKey": "[^"]*"/"SigningKey": ""/' \ + -e 's/"Gateway": "dev-[^"]*"/"Gateway": ""/' \ + /app/publish/appsettings.json +``` + +### Optional: migration target in the same Dockerfile + +```dockerfile +FROM src AS migrate +RUN dotnet ef migrations bundle ... --output /out/migrate-api +``` + +CD builds `--target migrate`, copies the binary out, and runs it with a **DDL** connection string that never enters the API container. Prefer `docker build` + `docker cp` over `docker run -v` on act_runner — bind mounts resolve on the Docker host, not the job workspace. + +### `.dockerignore` + +Exclude `bin/`, `obj/`, `node_modules/`, tests, `.env`, docs noise. Keep anything the image must ship (e.g. scenario JSON under `VigilCare.Simulator/Scenarios/`). + +--- + +## 3. `.env.example` vs production `.env` + +| File | In git? | Purpose | +|------|---------|---------| +| `.env.example` | Yes | Document every key; safe placeholders | +| `.env` (laptop) | No (`.gitignore`) | Local experimentation only | +| `/opt/vigilcare/.env` on VM | No | **Source of truth** for production | + +`.gitignore` pattern used here: + +``` +.env +.env.* +!.env.example +``` + +Group keys clearly in `.env.example`: + +1. `REGISTRY` / `IMAGE_TAG` +2. Host ports (`API_PORT`, …) +3. External service connection strings +4. App secrets (`JWT_SIGNING_KEY`, API keys) — generate with `openssl rand -base64 48` +5. Bootstrap / seed users + +**Privilege split:** runtime `PG_CONNECTION` (DML app user) vs `PG_CONNECTION_DDL` (migrator only). The DDL string is a Gitea secret for the migrate job, not an API container env var. + +### One-time place `.env` on the VM + +CD never uploads `.env`. Before the first tag deploy: + +```bash +ssh deploy@YOUR_HOST "mkdir -p /opt/vigilcare" +scp .env deploy@YOUR_HOST:/opt/vigilcare/.env +ssh deploy@YOUR_HOST "chmod 600 /opt/vigilcare/.env" +``` + +Later secret rotations: edit `/opt/vigilcare/.env` on the server (or replace your secret-management process). See [`docs/instructions-for-env.md`](../instructions-for-env.md). + +--- + +## 4. Gitea Actions — CI workflow + +Path: `.gitea/workflows/ci.yml` + +### Triggers + +```yaml +on: + push: + branches: [master] + pull_request: + branches: [master] +``` + +### Backend job pattern + +1. Checkout +2. `docker compose up -d` for dependencies +3. Wait loops (`pg_isready`, Kafka broker API, Redis `PING`, RabbitMQ diagnostics as the `rabbitmq` user — avoid root creating a bad `.erlang.cookie`) +4. Create test databases +5. `dotnet restore` / `build` / `test` with connection env vars pointing at `host.docker.internal` and published ports +6. Upload test artifacts; always tear down with `docker compose ... down -v` + +### Frontend job pattern + +```yaml +frontend: + runs-on: ubuntu-latest + container: + image: node:22-alpine + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm run test && npm run build + working-directory: vigilcare-dashboard +``` + +### Runner requirement for Compose-based tests + +The job (or its Docker sibling) must reach published ports. On Docker Desktop / many Linux runners, that means `host.docker.internal` and runner `extra_hosts: host-gateway`. Wire that into your act_runner config if tests hang on “connection refused”. + +--- + +## 5. Gitea Actions — CD workflow + +Path: `.gitea/workflows/cd.yml` + +### Triggers + +```yaml +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + image_tag: + description: "Image tag to deploy" + required: false +``` + +Release flow: merge to `master` → `git tag v1.2.3 && git push origin v1.2.3`. + +### Jobs + +``` +build-and-push ──► migrate ──► deploy (smoke + rollback on failure) +``` + +**build-and-push** + +1. Resolve tag (`GITHUB_REF_NAME` or `workflow_dispatch` input) and optional `vars.REGISTRY` +2. `docker login` to the Gitea package registry with `REGISTRY_USERNAME` / `REGISTRY_TOKEN` +3. Build each image; tag both `:v1.2.3` and `:latest`; push + +**migrate** + +1. Build `--target migrate` +2. Extract `migrate-api` +3. `./migrate-api --connection "${{ secrets.PG_CONNECTION_DDL }}"` + +Migrations must be **backwards-compatible** with the still-running previous image (expand-then-contract). Rollback restores the old image tag only — it does not reverse schema. + +**deploy** + +1. Configure SSH from `DEPLOY_SSH_KEY` (see [`cd-deploy-ssh-setup.md`](../ops/cd-deploy-ssh-setup.md)) +2. `scp docker-compose.prod.yml` → `/opt/vigilcare/` +3. On the host: save previous `IMAGE_TAG`, set new tag in `.env`, `pull`, `up -d`, prune dangling images +4. Smoke: curl `/health/ready`, gateway live, dashboard `/` +5. On failure: restore `.env.previous` and `up -d` again + +Do not `source .env` in bash — Compose env files are not shell (semicolons in connection strings, spaces, CRLF). Read individual keys with `sed` if needed. + +--- + +## 6. Gitea secrets and variables + +Repo → **Settings** → **Actions** + +### Secrets (sensitive) + +| Secret | Used by | +|--------|---------| +| `REGISTRY_USERNAME` | `docker login` | +| `REGISTRY_TOKEN` | `docker login` (access token / PAT with package write) | +| `PG_CONNECTION_DDL` | migrate job only | +| `DEPLOY_HOST` | SSH / SCP | +| `DEPLOY_USER` | SSH / SCP | +| `DEPLOY_SSH_KEY` | Private key PEM / OpenSSH private key body | + +### Variables (non-secret config) + +| Variable | Used by | +|----------|---------| +| `PROD_API_URL` | Dashboard image build-arg (public API origin) | +| `REGISTRY` | Optional override of default `gitea.example.com/org` | + +Enable **Packages** (container registry) for the org/user that owns `REGISTRY`. + +--- + +## 7. One-time infrastructure checklist + +Use this when cloning the pattern onto a new project or a fresh Gitea instance. + +### Gitea + runner + +- [ ] Gitea with Actions enabled +- [ ] act_runner registered, label matches `runs-on:` (e.g. `ubuntu-latest`) +- [ ] Runner can run Docker (socket or DinD) and has `docker compose`, `ssh`, `scp`, `curl`, `bash` +- [ ] Container registry reachable from runner and from the deploy host + +### Repo layout + +- [ ] `.gitea/workflows/ci.yml` and `cd.yml` +- [ ] Dockerfile(s) with runtime HEALTHCHECK +- [ ] `docker-compose.yml` for local/CI deps +- [ ] `docker-compose.prod.yml` for app-only deploy +- [ ] `.env.example` + `.gitignore` excluding `.env` +- [ ] `.dockerignore` with correct exceptions + +### Deploy host + +- [ ] User with Docker rights and home for SSH keys +- [ ] Directory e.g. `/opt//` with filled `.env` (`chmod 600`) +- [ ] External networks / infra Compose already up if prod overlay declares `external: true` +- [ ] Public DNS / reverse proxy pointing at published ports +- [ ] Deploy SSH key installed ([setup guide](../ops/cd-deploy-ssh-setup.md)) + +### First release + +- [ ] Secrets and variables set in Gitea +- [ ] CI green on `master` +- [ ] Tag `v0.1.0` (or `workflow_dispatch` with `image_tag`) +- [ ] Confirm images in registry, migrate succeeded, smoke passed + +--- + +## 8. Adapting this to another project + +Strip VigilCare-specific names; keep the skeleton: + +1. **CI:** start your test deps → wait healthy → run unit/integration tests with host-reachable URLs. +2. **CD build:** one `docker build`/`push` per deployable service; fix contexts. +3. **CD migrate:** optional job if you have schema (EF bundle, Flyway, Prisma migrate, etc.) using a privileged secret. +4. **CD deploy:** SCP prod Compose → patch `IMAGE_TAG` → pull/up → health curl → rollback tag on failure. +5. **Host `.env`:** all runtime config; CD touches only the image tag line. + +Minimal prod Compose for a single API: + +```yaml +services: + api: + image: ${REGISTRY}/my-api:${IMAGE_TAG} + ports: + - "${API_PORT:-8080}:8080" + environment: + ConnectionStrings__Default: "${PG_CONNECTION}" + restart: unless-stopped +``` + +Minimal CD deploy fragment: + +```bash +sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|" .env +docker compose -f docker-compose.prod.yml --env-file .env pull +docker compose -f docker-compose.prod.yml --env-file .env up -d --remove-orphans +``` + +--- + +## 9. Common pitfalls + +| Symptom | Likely cause | +|---------|----------------| +| Kafka / broker tests hang in CI | Advertised listener still `localhost` inside a job container — set `KAFKA_EXTERNAL_HOST=host.docker.internal` | +| RabbitMQ dies after health probe | Probing as root created a root-owned `.erlang.cookie` — probe as `rabbitmq` | +| `docker build` missing project references | Context not repo root | +| Dashboard calls wrong API in prod | Forgot `PROD_API_URL` / `VITE_*` build-arg | +| First CD fails at `cd /opt/...` | `.env` / directory never created on VM | +| Rollback “worked” but app errors | Schema already migrated; ensure expand-then-contract migrations | +| `source .env` breaks deploy scripts | Connection strings aren’t valid bash — don’t source Compose env files | + +--- + +## 10. Day-to-day commands + +```bash +# Local deps +docker compose up -d + +# Production-shaped run (on the VM, after images exist) +docker compose -f docker-compose.prod.yml --env-file .env up -d + +# Cut a release (after CI is green) +git tag v1.4.0 +git push origin v1.4.0 + +# Manual redeploy of an existing tag (Gitea UI → Actions → CD → Run workflow) +``` + +That is the full loop this repository uses: Compose for deps and for prod apps, env files only on the host, Gitea workflows for test and tagged releases. diff --git a/docs/26-vigilcare-records-cicd.md b/docs/26-vigilcare-records-cicd.md new file mode 100644 index 0000000..52c532b --- /dev/null +++ b/docs/26-vigilcare-records-cicd.md @@ -0,0 +1,83 @@ +# Guide 26: VigilCareRecords CI/CD (Gitea Actions + Docker Compose) + +How VigilCareRecords is built, tested, and deployed on the same Gitea Actions + `act_runner` + container registry setup already used by VigilCareClinical. See [`docs/25-gitea-cicd-docker-deploy.md`](25-gitea-cicd-docker-deploy.md) for the general pattern this guide instantiates; this doc only covers what is specific to this repo. + +Related files: + +- [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml) / [`.gitea/workflows/cd.yml`](../.gitea/workflows/cd.yml) +- [`docker-compose.yml`](../docker-compose.yml) (local + CI dependencies) +- [`docker-compose.prod.yml`](../docker-compose.prod.yml) (production app stack) +- [`.env.example`](../.env.example) +- [`VigilCareRecordsAPI/Dockerfile`](../VigilCareRecordsAPI/Dockerfile) / [`vigilcare-records-web/Dockerfile`](../vigilcare-records-web/Dockerfile) + +--- + +## 1. Topology + +VigilCareRecords ships two images: + +| Image | Dockerfile | Build context | +|---|---|---| +| `vigilcare-records-api` | `VigilCareRecordsAPI/Dockerfile` | repo root | +| `vigilcare-records-dashboard` | `vigilcare-records-web/Dockerfile` | `vigilcare-records-web/` | + +Unlike VigilCareClinical, there is no gateway service, and the frontend does not bake in an absolute API URL at build time — `src/api/client.ts` and `src/api/fhirClient.ts` use relative base URLs (`/api/v1`, `/fhir`). The dashboard's `nginx.conf` reverse-proxies those paths to the `api` container, so the same image works behind any hostname without a build-arg. + +## 2. Shared production infrastructure + +Per the "Integrated Database Deployment" decision documented in [`vigilcare-records-clinical-overview.md`](vigilcare-records-clinical-overview.md), VigilCareRecords does **not** run its own Postgres/Redis/MinIO/Seq in production. It joins the external `shared-services` Docker network already created by VigilCareClinical's own compose project and connects to those same containers, using: + +- Its own logical Postgres database (`vigilcare_records`, separate schema/tables from VigilCareClinical's `patients`/`encounters`/`observations` — see the promotion service for how the two connect at the application layer, not the infrastructure layer) +- A dedicated Redis logical database (`defaultDatabase=2` in `.env.example`) so batch-assignment locks never collide with VigilCareClinical's keys +- A dedicated MinIO bucket (`vigilcare-records-scans`) separate from any clinical document buckets + +**Before the first deploy**, confirm the VigilCareClinical infra stack (or whatever compose project owns `shared-services`) is already running on the deploy host — `docker network ls | grep shared-services` should show it. `docker compose -f docker-compose.prod.yml up` will fail to find the network otherwise. + +## 3. One-time host setup + +```bash +ssh deploy@YOUR_HOST "mkdir -p /opt/vigilcare-records" +scp .env deploy@YOUR_HOST:/opt/vigilcare-records/.env +ssh deploy@YOUR_HOST "chmod 600 /opt/vigilcare-records/.env" +``` + +Fill in `.env` from [`.env.example`](../.env.example) first — generate `JWT_SECRET` with `openssl rand -base64 48`, and get real credentials for the shared Postgres/Redis/MinIO/Seq services from whoever manages that stack. CD never uploads or overwrites `.env`; only the `IMAGE_TAG` line is patched automatically on each release. + +## 4. Gitea secrets and variables + +Repo → **Settings** → **Actions**. + +### Secrets + +| Secret | Used by | +|---|---| +| `REGISTRY_USERNAME` | `docker login` | +| `REGISTRY_TOKEN` | `docker login` (access token / PAT with package write) | +| `PG_CONNECTION_DDL` | migrate job only — DDL-privileged connection to the shared Postgres, never given to the API container | +| `DEPLOY_HOST` | SSH / SCP | +| `DEPLOY_USER` | SSH / SCP | +| `DEPLOY_SSH_KEY` | Private key PEM / OpenSSH private key body | + +### Variables + +| Variable | Used by | +|---|---| +| `REGISTRY` | Optional override of the default `git.vectur45.com/trent/vigilcare-records` | + +## 5. Release flow + +```bash +# CI green on master, then: +git tag v1.0.0 +git push origin v1.0.0 +``` + +This triggers `.gitea/workflows/cd.yml`: build & push both images → apply EF Core migrations against `PG_CONNECTION_DDL` → SSH deploy (`scp` the prod compose file, patch `IMAGE_TAG`, `pull` + `up -d`) → smoke test (`/health/ready`, dashboard `/`) → automatic rollback to the previous `IMAGE_TAG` on failure (schema changes are not reverted; see the expand/contract note in `cd.yml`). + +Manual redeploy of an existing tag: Gitea UI → Actions → CD → Run workflow, with `image_tag` input. + +## 6. Pre-production checklist (application-level, not part of this CI/CD change) + +- **Demo data seeding runs unconditionally on startup.** `Program.cs` calls `DataSeeder.SeedAsync(db)` whenever `ASPNETCORE_ENVIRONMENT` is not `Testing` — including `Production`. Unlike the VigilCareClinical example (`Seeding__EnableDemoData=false`), this app has no seeding toggle. Before a real clinical deploy, either add a guard around `DataSeeder.SeedAsync` for `Production`, or confirm the twelve seeded demo accounts (all password `password`) are acceptable/rotated before go-live. +- Confirm `Cors:AllowedOrigins` / `DASHBOARD_ORIGIN` matches the real public dashboard origin if the dashboard is ever served from a different origin than the API (not the default same-origin nginx-proxy setup described above). +- Rotate `JWT_SECRET` from any value used during testing before the first real deploy. diff --git a/docs/vigilcare-records-clinical-overview.md b/docs/vigilcare-records-clinical-overview.md new file mode 100644 index 0000000..9b60672 --- /dev/null +++ b/docs/vigilcare-records-clinical-overview.md @@ -0,0 +1,650 @@ +# VigilCare Records + +## A Clinical Guide to Paper Chart Digitization and Governance + +**Audience:** Physicians, nurse leaders, hospital administrators, and healthcare partners evaluating the platform +**Purpose:** Explain how paper-based clinical records become trusted digital clinical data—without requiring software engineering knowledge + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Why Clinical Digitization Matters](#2-why-clinical-digitization-matters) +3. [End-to-End Clinical Workflow](#3-end-to-end-clinical-workflow) +4. [System Data Flow](#4-system-data-flow) +5. [Safety and Governance](#5-safety-and-governance) +6. [Dashboard and Workstation Walkthrough](#6-dashboard-and-workstation-walkthrough) +7. [Clinical Examples](#7-clinical-examples) +8. [High-Level Architecture](#8-high-level-architecture) +9. [Relationship to Clinical Care](#9-relationship-to-clinical-care) +10. [Complete Patient Journey](#10-complete-patient-journey) + +--- + +## 1. Executive Summary + +**VigilCare Records** is a clinical records digitization and governance platform for hospitals and clinics that still rely on paper charts. It converts handwritten and printed clinical documents into structured electronic patient data—but only after that data has been checked by people, not machines alone. + +### What problem does it solve? + +Many district hospitals, island health systems, and smaller facilities still keep vital signs, laboratory results, allergies, and medication lists on paper. When those facilities introduce electronic monitoring or early warning systems, they face a hard question: + +> *How do we know the numbers in the computer are the same numbers that were written on the chart?* + +Without a governed digitization process, transcription errors, incomplete identity matching, and unreviewed OCR guesses can flow into clinical alerts and decision support. That puts patients at risk and undermines clinician trust. + +### What VigilCare Records does + +The platform provides a controlled pathway from paper to permanent electronic record: + +1. A paper chart section is **scanned and stored securely** +2. A data entry clerk **transcribes** demographics, encounters, and observations into structured drafts +3. A second person **verifies** every field against the original scan +4. For high-stakes content (vitals, labs, medications, encounters), a **clinical approver** authorizes promotion +5. Only then does the information become part of the patient’s **live clinical record** + +**Unapproved drafts never reach clinical monitoring, early warning scores, or clinician dashboards.** + +VigilCare Records is not a full electronic medical record (EMR) and does not diagnose patients. It is the governed intake layer that ensures clinical information entering downstream systems is complete, verified, and trustworthy. + +--- + +## 2. Why Clinical Digitization Matters + +### Challenges of paper charts + +| Challenge | Clinical impact | +|---|---| +| Charts live in folders, ward books, or index cards | Information is hard to find across shifts and transfers | +| Handwriting varies in clarity | Values may be misread under time pressure | +| Timestamps are often imprecise or missing | Trends and deterioration timelines become unreliable | +| Encounter boundaries are unclear | “The patient” is not distinguished from “this admission” | +| Charts can be misplaced between wards | Continuity of care is interrupted | + +### Risks of transcription errors + +Moving from paper to digital without safeguards introduces new hazards: + +- **Decimal misplacement** — potassium recorded as `52` instead of `5.2` +- **Wrong patient folder** — values linked to the incorrect medical record +- **Unit confusion** — temperature or glucose entered in the wrong scale +- **Silent “correction”** — someone overwrites a value with no record of what changed + +These errors matter most when digital values drive alerts, scoring, or handover decisions. + +### Delayed access to patient information + +Paper charts travel with the patient or sit on the ward. Consultants, night staff, and remote reviewers may wait for physical retrieval. Digitized, verified records make the same information available wherever clinical monitoring and care coordination require it—without discarding the original scan as the legal source document. + +### Regulatory and audit requirements + +Healthcare facilities must often demonstrate: + +- Who entered a clinical value +- Who checked it +- When it became part of the permanent record +- Whether it was later corrected—and how + +VigilCare Records maintains an **append-only audit history** for every digitization batch, so reviewers can reconstruct the chain from scan to live record. + +### How structured digital records improve care + +Once verified information is promoted into the live clinical database: + +| Benefit | Why it matters | +|---|---| +| **Continuity of care** | Prior vitals, labs, allergies, and medications follow the patient across encounters | +| **Clinical monitoring** | Downstream systems can score early warning signals on trustworthy observations | +| **Interoperability** | Standard FHIR representations support exchange with other clinical systems | +| **Research and quality** | Structured, timestamped data supports audit, morbidity review, and outcome analysis | + +Structured records do not replace clinical judgment. They give judgment better raw material. + +--- + +## 3. End-to-End Clinical Workflow + +### Overview of the pathway + +Paper chart digitization in VigilCare Records follows a deliberate sequence. Each stage has a clear clinical purpose. + +``` +Paper chart received + ↓ +Scan uploaded (secure storage) + ↓ +Digitization batch created + ↓ +Data entry (structured drafts) + ↓ +Verification (second human checks against scan) + ↓ +Clinical approval (when required for the document type) + ↓ +Promotion into the live clinical record + ↓ +Available for clinical monitoring and ongoing care +``` + +There are two operational tracks: + +| Track | When used | Gate model | +|---|---|---| +| **Track A — Backfill** | Historical charts, bulk digitization, past encounters | Full dual-human verification, then clinical approval | +| **Track B — Live capture** | Bedside vitals entered by a credentialed clinician | Clinician attestation replaces the dual queue; still fully audited | + +The remainder of this section focuses on Track A, which is the primary pathway for paper charts. + +### Stage-by-stage walkthrough + +#### 1. Paper chart received + +Ward or medical records staff deliver a chart section—for example a nursing observation sheet, laboratory printout, admission face sheet, allergy note, or medication list. + +#### 2. Scan uploaded + +An **Intake Clerk** scans the page(s) and uploads the image or PDF. The system: + +- Stores the scan in secure document storage +- Computes a fingerprint (hash) so the same document is not accidentally uploaded twice for the same patient within a short window +- Creates a **digitization batch**—one unit of work for that chart section + +Optional cover sheets with QR codes can pre-classify batch type and patient for high-volume intake. + +#### 3. Batch created and assigned + +The batch is typed according to clinical content: + +| Batch type | Typical content | +|---|---| +| Patient registration | Demographics, contacts, blood type | +| Encounter summary | Admission/discharge context, department, bed | +| Vitals sheet | Heart rate, blood pressure, SpO₂, temperature, etc. | +| Lab results | Laboratory observations with chart timestamps | +| Medication list | Current medications | +| Allergy update | Documented allergies and sensitivities | +| Mixed | Combined chart sections | + +A data entry clerk is assigned so two people do not work the same scan at once. + +#### 4. Data entry + +The **Data Entry Clerk** works in a split-pane workstation: + +- **Left:** the scan (zoom, pan, rotate) +- **Right:** structured forms for patient, encounter, and observations + +Timestamps for observations come from the **chart**, not the time of scanning. Built-in plausibility checks catch many transcription mistakes (for example, an impossible potassium value) before the batch leaves the entry desk. + +If optional OCR assistance is enabled, the system may pre-fill fields with confidence scores. The clerk still reviews every value against the image. OCR never replaces human entry or verification. + +#### 5. Verification + +A **Verifier**—a different person from the entry clerk—compares each drafted field to the scan. Fields are checked one by one. If anything is wrong, the batch is **rejected** with a reason and returns for correction. If everything matches, verification passes. + +#### 6. Clinical approval (when required) + +Site configuration decides which document types need an additional clinical gate after verification: + +| Usually requires clinical sign-off | Usually proceeds after verify to approver queue without the extended physician review path | +|---|---| +| Encounter summaries | Patient registration | +| Vitals sheets | Allergy updates | +| Lab results | | +| Medication lists | | +| Mixed batches | | + +In all Track A cases, a **Clinical Approver** must still authorize promotion. For high-stakes types, that review is explicit in a dedicated clinical approval queue. + +#### 7. Promotion into the live clinical record + +On approval, draft patient, encounter, and observation data are written into the live clinical database in a single, all-or-nothing step (**atomic promotion**). Either the whole approved set becomes live, or nothing does—avoiding half-updated patient records. + +#### 8. Availability for monitoring and care + +Promoted observations become visible to the companion clinical monitoring platform (VigilCareClinical). Historical backfill typically **does not** page clinicians for old critical values unless the facility deliberately enables retroactive alerts for that batch. Live bedside capture, by contrast, can alert immediately when clinically appropriate. + +### Roles and responsibilities + +| Role | Primary responsibilities | Clinical accountability | +|---|---|---| +| **Intake Clerk** | Receive charts, scan/upload, classify batch type, optionally link patient or cover sheet, assign entry work | Ensures the correct document enters the pipeline and is stored intact | +| **Data Entry Clerk** | Transcribe demographics, encounters, observations; correct rejected batches | Ensures structured fields faithfully represent the scan | +| **Verifier** | Field-level comparison of draft vs scan; pass or reject | Independent quality check—cannot verify own entry | +| **Clinical Approver** | Authorize promotion for verified batches; review high-stakes content | Clinical governance gate before data becomes permanent | +| **Clinician** | Track B live capture with attestation; may use promoted data in care | Attests that bedside values are accurate at the time of entry | +| **Administrator** | Users, site routing, queues, cancellation, supervisor oversight, interoperability browsing | Operational integrity of the digitization program | + +**Staffing note:** In a small facility one person may perform intake and entry. The system still **never** allows the same person to both enter and verify the same batch. Minimum staffing for Track A is two people. + +--- + +## 4. System Data Flow + +The diagrams below show how information moves through VigilCare Records and into clinical care systems. + +### Diagram 1 — Paper Record Digitization + +```mermaid +flowchart LR + A[Paper Chart] --> B[Scan Upload] + B --> C[Secure Storage] + C --> D[Draft Patient Data] + D --> E[Draft Encounter] + E --> F[Draft Observations] + F --> G[Verification] + G --> H[Clinical Approval] + H --> I[Promotion] + I --> J[Live Clinical Database] +``` + +**Reading the diagram clinically:** Everything to the left of Promotion is **working copy / draft**. It can be corrected, rejected, or cancelled. Only after promotion does information become part of the patient’s permanent electronic clinical record used by monitoring systems. + +### Diagram 2 — Governance Workflow + +```mermaid +flowchart TD + U[Upload] --> AS[Assignment] + AS --> DE[Data Entry] + DE --> V[Verification] + V -->|Pass| AP[Approval] + V -->|Reject| DE + AP --> AT[Audit Trail] + AP --> PCR[Permanent Clinical Record] + AT --> PCR +``` + +Every transition records **who acted, what changed, and when**. Rejection sends work back to entry with a reason; it does not erase the scan or the audit history. + +### Diagram 3 — Downstream Integration + +```mermaid +flowchart LR + L[Live Patient Record] --> M[Clinical Monitoring System] + M --> E[Early Warning Scores] + E --> A[Alert Generation] + A --> D[Clinician Dashboard] +``` + +### Critical invariant + +``` +Draft / unverified / unapproved information + ✕ + never reaches + ✕ +Clinical monitoring · Early warning scores · Alert generation · Clinician dashboards +``` + +This separation is intentional. Digitization speed must not outrun clinical trust. + +--- + +## 5. Safety and Governance + +Patient safety in digitization is less about “faster scanning” and more about **who may change what, and whether the chain of custody is reconstructable**. + +### Separation of duties + +The person who enters a batch cannot verify it. The person who entered or verified cannot approve promotion of that same batch. The system blocks these conflicts automatically. + +**Why it matters:** Dual control reduces the chance that a single transcription error—or a single person’s assumption about illegible handwriting—becomes permanent clinical fact. + +### Dual-human verification + +Track A requires two humans: one to enter, one to check against the original image. Verification is field-level, not a rubber stamp on the whole form. + +### Clinical approval routing + +High-stakes document types can be routed through an explicit clinical approval queue so a designated approver reviews content before it becomes live. Facilities configure which batch types require this extended path. + +### Immutable audit history + +Status changes, document views, rejections, OCR assists, and promotions are recorded in an append-only event history. Auditors and clinical leaders can answer: *What was entered? Who checked it? When did it go live?* + +### Controlled corrections using supersession + +Once promoted, values are **not silently edited**. A correction creates a new digitization batch linked to the original. After the new batch passes the full pipeline, corrected observations are added and originals are marked **superseded**—still visible historically, no longer treated as current. + +**Why it matters:** Regulators and morbidity reviews need the correction story, not a rewritten past. + +### Document integrity verification + +Each scan is stored with an integrity fingerprint. Rejecting a batch does not delete the scan. Duplicate uploads of the same document for the same patient within a defined window are blocked to reduce accidental double-entry. + +### Patient matching and duplicate detection + +On promotion, the system matches patients carefully (for example by name and date of birth) and warns when a near-duplicate may already exist. New patients receive a stable medical record number (MRN). This reduces split charts and merged-identity hazards. + +### Atomic promotion into live records + +Patient, encounter, and observation updates for an approved batch succeed together or not at all. Partial promotion—which could leave an encounter without its labs, or labs without a patient—is avoided by design. + +### Plausibility checks at entry + +Numeric ranges catch physically impossible values early (for example SpO₂ above 100%, or potassium far outside a plausible laboratory range). These are **digitization safety nets**, not clinical alert thresholds. + +### Why these controls are essential + +In clinical environments: + +- Alerts and scores amplify whatever data they receive +- Silent edits destroy forensic and educational value of the record +- Single-person pipelines concentrate risk on the most fatigued shift +- Paper remains the source of truth until humans have reconciled digital drafts to the scan + +Governance is not bureaucracy for its own sake—it is how a paper-to-digital program earns the right to drive monitoring and care coordination. + +--- + +## 6. Dashboard and Workstation Walkthrough + +Each workstation is designed around one job. Users see only the queues and actions appropriate to their role. + +### Intake workstation + +**Who:** Intake Clerk, Administrator + +**What they see:** Upload form, batch type selection, track (backfill vs live), optional patient search, optional cover-sheet barcode entry. + +**Decisions:** +- Is this the correct document type? +- Should this link to an existing patient MRN or wait for registration? +- Who should perform data entry? + +**Outcome:** A batch in secure storage, ready for transcription. + +### Data entry screen + +**Who:** Data Entry Clerk, Administrator + +**What they see:** Split view—scan on one side, structured fields on the other. Observation rows with codes, values, units, and chart timestamps. Optional OCR confidence highlights when enabled. + +**Decisions:** +- What does the handwriting actually say? +- Are encounter details complete for this visit? +- Are all required sections for this batch type filled? + +**Outcome:** Draft patient / encounter / observations submitted for verification—or returned to entry after rejection. + +### Verification screen + +**Who:** Verifier (and roles permitted to review the verification queue) + +**What they see:** Same scan viewer plus checkboxes or field-level status for each drafted value, with progress toward complete review. + +**Decisions:** +- Does each field match the scan? +- If not, what rejection reason will help the entry clerk fix it? + +**Outcome:** Verified (or awaiting clinical approval) — or rejected for rework. + +### Clinical approval queue + +**Who:** Clinical Approver, Administrator + +**What they see:** Batches awaiting promotion authorization; scan plus structured summary; option related to retroactive alerting for backfill when clinically appropriate. + +**Decisions:** +- Is this content ready to become part of the permanent record? +- For historical values, should downstream alerting be enabled? + +**Outcome:** Approval triggers promotion, or rejection returns the work with reason. + +### Patient history + +**Who:** Authenticated clinical and administrative users + +**What they see:** Timeline of digitization batches for a patient, correction chains (which batch superseded which), and audit events. + +**Decisions:** +- Has this patient’s paper record been fully backfilled? +- Was a value corrected, and when? + +**Outcome:** Situational awareness for governance and continuity—not a replacement for the full clinical charting EMR. + +### Supervisor dashboard + +**Who:** Administrator + +**What they see:** Work-queue overview—counts by status, aging work, rejection patterns, bottlenecks in entry vs verification vs approval. + +**Decisions:** +- Where should staffing be redirected today? +- Is rejection rate signaling scan quality or training issues? +- Which batches need cancellation or reassignment? + +### FHIR Explorer + +**Who:** Administrator + +**What they see:** A browser for promoted clinical data exposed in HL7 FHIR R4 form (Patient, Encounter, Observation), including standard search and patient “everything” views. + +**Decisions:** +- Can partner systems consume our promoted record correctly? +- Do observation codes map as expected for interoperability testing? + +**Outcome:** Technical assurance for exchange—still based only on **promoted** data. + +### Live capture (clinician workstation) + +**Who:** Clinician + +**What they see:** Bedside entry for current encounter vitals/observations, with attestation and password confirmation. + +**Decisions:** +- Are these values measured now, for this active encounter? +- Do I attest that they are accurate? + +**Outcome:** Immediate promotion with full audit; may trigger synchronous clinical alerts when thresholds are crossed. + +--- + +## 7. Clinical Examples + +### Example A — New patient registration + +**Situation:** A new admission arrives with a handwritten face sheet. + +1. Intake scans the face sheet as **Patient registration** +2. Entry clerk transcribes name, date of birth, sex, blood type, emergency contact +3. Verifier confirms each demographic field against the scan +4. Clinical approver authorizes promotion +5. Live patient record is created with a new MRN (for example `VCR-000042`) + +Subsequent vitals and labs can now link to a stable identity. + +### Example B — Historical paper chart backfill + +**Situation:** Before turning on ward alerting, the hospital digitizes 200 active paper charts. + +1. Intake uploads vitals sheets and lab reports as **Backfill** batches +2. Entry and verification proceed through the dual-human gate +3. High-stakes types route through clinical approval +4. On promotion, historical observations enter the live database with **chart timestamps** +5. By default, a potassium of 6.2 from three days ago does **not** page today’s on-call clinician + +Backfill builds a trustworthy baseline without flooding the ward with retrospective noise. + +### Example C — Laboratory result digitization + +**Situation:** A paper lab report shows potassium `5.2 mEq/L`. + +1. Intake uploads as **Lab results** +2. Entry clerk records potassium with the laboratory’s reported time +3. If `52` is typed by mistake, plausibility validation blocks the impossible value +4. Verifier confirms `5.2` against the scan +5. Clinical approver authorizes promotion +6. The observation becomes available to monitoring—still subject to facility alert policy for backfill vs live + +### Example D — Vital signs entry + +**Situation:** Nursing observation chart for an inpatient with pneumonia. + +1. Vitals sheet batch created and assigned +2. Entry clerk records heart rate, SpO₂, temperature, respiratory rate, blood pressure with times from the chart +3. Verifier checks each row +4. Clinical approval and promotion follow +5. Scores such as NEWS2 in the companion monitoring system can use these observations once live + +### Example E — Medication reconciliation (list digitization) + +**Situation:** Ward medication list must be captured before transfer. + +1. Intake uploads **Medication list** +2. Entry captures the structured medication set from the scan +3. Verification and clinical approval treat this as high-stakes content +4. On promotion, medications merge into the live patient context used for ongoing care review + +### Example F — Allergy updates + +**Situation:** Chart documents Codeine, iodine contrast, and latex allergies. + +1. **Allergy update** batch is digitized +2. Entry and verification confirm the allergy list against the scan +3. After approval and promotion, allergies are part of the live patient record visible to downstream care workflows + +(Default site routing may treat allergy updates as lower complexity than labs/vitals for the extended clinical queue, but promotion still requires authorized approval.) + +### Example G — Correcting an already promoted record + +**Situation:** SpO₂ was promoted as `94%`; the chart clearly shows `95%`. + +1. A **correction batch** is created linked to the original promoted batch +2. Correct values are entered, verified, and approved through the full pipeline +3. New live observations are inserted; originals are marked **superseded** +4. Patient history shows both the original error and the correction, with actors and times + +No one “quietly fixes” the permanent record. The clinical story remains auditable. + +--- + +## 8. High-Level Architecture + +Think of VigilCare Records as several cooperating parts, each with a clinical reason to exist. + +| Component | What it is (plain language) | Why it exists | +|---|---|---| +| **Secure document storage** | Locked vault for scans (PDF/JPEG/PNG) | Preserves the image everyone verifies against; supports integrity checks | +| **Structured clinical database** | Organized tables for drafts and live patients, encounters, observations | Separates “working drafts” from “permanent clinical facts” | +| **Digitization workflow engine** | Rules for statuses, roles, queues, and allowed transitions | Prevents skipping safety gates or approving out of order | +| **User workstations** | Role-specific screens for intake, entry, verify, approve, live capture | Puts the right decisions in front of the right people | +| **Audit system** | Permanent diary of actions and document access | Supports governance, training, and regulatory review | +| **Optional OCR assistance** | Computer suggestion of text from the scan | Speeds entry when enabled; never replaces human review | +| **HL7 FHIR interface** | Standard read-only clinical data format | Lets other systems retrieve promoted Patient / Encounter / Observation data | +| **Integration with clinical monitoring** | Handoff of promoted data to VigilCareClinical | Powers early warning, alerts, and ward views—only after approval | + +``` +┌──────────────────────────────────────────────────────────┐ +│ VigilCare Records │ +│ Scan → Enter → Verify → Approve → Promote │ +│ Drafts (never alert) │ +└───────────────────────────┬──────────────────────────────┘ + │ approved data only + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Clinical Monitoring Platform │ +│ Live record → Scores → Alerts → Clinician dashboard │ +└──────────────────────────────────────────────────────────┘ +``` + +Implementation details (servers, containers, programming languages) matter to IT teams. Clinicians need only know that **drafts and live records are separated by design**, and that promotion is the controlled bridge between them. + +--- + +## 9. Relationship to Clinical Care + +### What VigilCare Records supports + +| Clinical need | How the platform helps | +|---|---| +| **Accurate patient histories** | Demographics and prior observations are verified against source documents before they become permanent | +| **Reliable clinical observations** | Dual review, plausibility checks, and approval reduce garbage-in / garbage-out | +| **Better continuity of care** | Structured encounters and observations remain available across shifts and services | +| **Clinical decision support systems** | Downstream scoring and alerts receive only promoted data | +| **Future interoperability** | FHIR representations of promoted records support partner exchange | +| **Research-quality data** | Timestamped, coded observations with known provenance | +| **Regulatory compliance** | Audit trails, supersession, separation of duties, retained scans | + +### What the platform does not do + +VigilCare Records: + +- Does **not** diagnose disease +- Does **not** replace bedside clinical judgment +- Does **not** replace a full EMR for billing, scheduling, or comprehensive charting +- Does **not** allow unapproved drafts to drive alerts or surveillance + +Its clinical contribution is foundational: **ensure that what enters electronic care systems is complete, verified, and trustworthy.** + +--- + +## 10. Complete Patient Journey + +The following narrative follows one patient from paper chart to ongoing digital care. + +### Maria Santos — pneumonia admission + +**Day 0 — Paper chart creation** +Maria Santos is admitted to Internal Medicine with community-acquired pneumonia. Nursing staff record vital signs on a paper observation chart. The laboratory prints a white blood cell count onto a paper report. The face sheet lists demographics and emergency contacts. All of this lives in a physical folder at the bedside and nursing station. + +**Hospital intake for digitization** +As the facility prepares to activate electronic clinical monitoring, Medical Records and ward clerks assemble Maria’s active chart sections for digitization. An Intake Clerk scans the face sheet, a recent vitals page, and the lab report. Each upload becomes its own digitization batch in secure storage. Where a patient already exists, intake links the MRN; for first-time digitization, registration proceeds first. + +**Scanning and identity** +The scans are fingerprint-checked and retained. Cover sheets may accelerate classification on busy digitization days. Nothing clinical has entered the live database yet—only documents and empty or pre-filled draft shells awaiting human work. + +**Data entry** +A Data Entry Clerk opens Maria’s vitals batch. On the left, the handwritten chart; on the right, structured fields. Heart rate, SpO₂, temperature, and other observations are entered with the times written on the chart. A separate lab batch captures the white blood cell count. If OCR suggested values, the clerk corrects any misreads before submitting. + +**Verification** +A Verifier who did not enter the data opens the same scans. Each field is compared. A misread SpO₂ would be rejected with a clear reason. When all fields match, verification passes. Vitals and labs route into the clinical approval pathway appropriate for high-stakes observations. + +**Clinical approval** +A Clinical Approver reviews the structured content against the scans and authorizes promotion. For historical backfill, retroactive alerting remains off unless the facility explicitly chooses otherwise—Maria’s yesterday values should not create today’s false urgency. + +**Promotion into the electronic record** +In one governed step, Maria’s patient identity, encounter context, and observations become live clinical data. She now has a stable MRN and a digitization history that records every actor in the chain. + +**Availability in the clinical monitoring platform** +Promoted observations are visible to VigilCareClinical. Early warning scores and alert logic can use them according to facility policy. Unfinished drafts from other patients still in the entry queue remain invisible to scoring. + +**Use during ongoing patient care** +On subsequent rounds, physicians and nurses consult the clinician dashboard and patient history with greater confidence that electronic vitals and labs match the paper source that was verified. If SpO₂ was later found to be one point off, a correction batch supersedes the error without erasing the audit trail. When a clinician measures new vitals at the bedside (Track B), attestation promotes them immediately for real-time monitoring. + +**Closing the loop** +Maria’s care still depends on clinical judgment. What VigilCare Records contributed was quieter but essential: her paper chart did not leap unchecked into electronic alerting. It crossed a governed bridge—scan, entry, verification, approval, promotion—so that digital care rests on information the organization is prepared to defend. + +--- + +## Appendix A — Status vocabulary (clinical reading) + +| Status | Meaning in practice | +|---|---| +| Uploaded | Scan received; awaiting or ready for entry | +| In entry | Clerk actively drafting structured data | +| Pending verification | Submitted; waiting for second-person check | +| Rejected | Returned to entry with a reason | +| Verified | Passed data-quality check; awaiting promotion authorization | +| Awaiting clinical approval | High-stakes path; in clinical approver queue | +| Approved | Authorized; promotion in progress or completing | +| Promoted | Live in the permanent clinical database (terminal success) | +| Cancelled | Permanently stopped before promotion (terminal) | + +--- + +## Appendix B — Quick reference for clinical leaders + +| Question | Answer | +|---|---| +| Can OCR alone put labs in the chart? | No. Humans must review; verification still required. | +| Can the entry clerk verify their own work? | No. Separation of duties is enforced. | +| Do drafts trigger NEWS2 or sepsis screens? | No. Only promoted live observations do. | +| Can we fix a promoted wrong value by editing it? | No. Use a correction (supersession) batch. | +| Will backfilling old critical labs page the ward? | Not by default. Retroactive alerts are opt-in per batch. | +| Is this a full EMR? | No. It is the governed digitization precursor to clinical monitoring. | + +--- + +*This guide is intended for clinical and administrative audiences evaluating VigilCare Records. For operator step-by-step instructions, see [digitization-workstation-guide.md](digitization-workstation-guide.md). For product requirements and implementation status, see [vigilcare-records-prd.md](vigilcare-records-prd.md).* diff --git a/scripts/run-vigilcare-records-phase-10-verification.sh b/scripts/run-vigilcare-records-phase-10-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-11-verification.sh b/scripts/run-vigilcare-records-phase-11-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-13-verification.sh b/scripts/run-vigilcare-records-phase-13-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-2-verification.sh b/scripts/run-vigilcare-records-phase-2-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-3-verification.sh b/scripts/run-vigilcare-records-phase-3-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-4-verification.sh b/scripts/run-vigilcare-records-phase-4-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-5-verification.sh b/scripts/run-vigilcare-records-phase-5-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-6-verification.sh b/scripts/run-vigilcare-records-phase-6-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-phase-8-verification.sh b/scripts/run-vigilcare-records-phase-8-verification.sh old mode 100755 new mode 100644 diff --git a/scripts/run-vigilcare-records-verification.sh b/scripts/run-vigilcare-records-verification.sh old mode 100755 new mode 100644 diff --git a/vigilcare-records-web/Dockerfile b/vigilcare-records-web/Dockerfile new file mode 100644 index 0000000..aae400e --- /dev/null +++ b/vigilcare-records-web/Dockerfile @@ -0,0 +1,23 @@ +# Build context is vigilcare-records-web/ (package.json, nginx.conf live here): +# docker build -f vigilcare-records-web/Dockerfile -t vigilcare-records-dashboard vigilcare-records-web + +FROM node:22-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +# No VITE_API_URL build-arg needed: src/api/client.ts and src/api/fhirClient.ts +# use relative base URLs ("/api/v1", "/fhir") and rely on the nginx reverse proxy +# below to reach the api container at runtime — same origin in every environment. +RUN npm run build + +FROM nginx:1.27-alpine AS runtime +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:80/ >/dev/null || exit 1 + +EXPOSE 80 diff --git a/vigilcare-records-web/nginx.conf b/vigilcare-records-web/nginx.conf new file mode 100644 index 0000000..ef1e72a --- /dev/null +++ b/vigilcare-records-web/nginx.conf @@ -0,0 +1,36 @@ +# Serves the built Vue SPA and reverse-proxies API/FHIR calls to the api +# container so the frontend's relative-URL Axios clients (baseURL "/api/v1" and +# "/fhir") work unmodified in production — mirrors the Vite dev server proxy in +# vite.config.ts, just pointed at the "api" service name instead of localhost. + +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 30m; # scanned document uploads (25 MB max) plus overhead + + location /api/ { + proxy_pass http://api:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /fhir/ { + proxy_pass http://api:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SPA history-mode fallback + location / { + try_files $uri $uri/ /index.html; + } +}