ASP.NET Core minimal-API container that renders the NeoIPC Surveillance Toolkit's Quarto reports (PDF / HTML) and exposes the underlying datasets as JSON. Authenticates via DHIS2 session cookies; gates admin endpoints with claims-based authorization derived from the user's DHIS2 authorities.
src/NeoIPC.Reporting/— the service (and itsDockerfile).src/NeoIPC.Reporting.Generators/— Roslyn incremental source generator that emits<Report>RenderParametersrecords andSchemadata from the toolkit's master QMDparams:blocks.tests/NeoIPC.Reporting.Tests/— NUnit test project.Category=UnitandCategory=Generatorrun on every PR;Category=Integrationspins up the built image via Testcontainers and runs only onworkflow_dispatch(see.github/workflows/build-and-test.yml).compose.yml— minimal stack: Postgres + DHIS2 + this service + Traefik.
# Unit + generator tests (no Docker needed)
dotnet test --filter "Category!=Integration"
# Integration tests (require a built image; uses NEOIPC_REPORTING_IMAGE_TAG
# env var, defaults to neoipc-reporting:smoke-test)
dotnet test --filter "Category=Integration"Double-clicking NeoIPC-Reporting.sln and pressing F5 in Visual Studio
runs the Workspace profile out of the box: the .NET service starts
locally and points at repos/Surveillance-Toolkit/reports and
repos/neoipcr from the parent workspace checkout. Edits to the toolkit
or to neoipcr show up on the next render — no rebuild needed.
This works only when the project is opened inside the workspace
checkout (i.e. via the workspace's nested repos/NeoIPC-Reporting/).
The workspace-relative paths in appsettings.Development.json are
resolved at startup against the host's ContentRoot.
For DHIS2 authentication and live data, run a DHIS2 stack on the side
(typically the workspace's own repos/neoipc-dhis2/compose.yml) and
override Reporting:Dhis2BaseUrl via env var or
appsettings.Development.local.json.
Three reports-source modes via --build-arg REPORTS_SOURCE:
| Mode | What it does | Build-args (defaults shown) |
|---|---|---|
github-branch (default) |
Clone ${REPORTS_REPO} at branch ${REPORTS_BRANCH} (with submodules) |
REPORTS_REPO=https://github.com/NeoIPC/Surveillance-Toolkit.git, REPORTS_BRANCH=main |
github-tag |
Clone a tagged release | REPORTS_REPO=…, REPORTS_TAG=vX.Y.Z (required) |
workspace |
COPY from the named build context surveillance-toolkit (passed via --build-context) |
— |
Three neoipcr-source modes via --build-arg NEOIPCR_SOURCE:
| Mode | What it does | Build-args (defaults shown) |
|---|---|---|
github-branch (default) |
pak::pkg_install("${NEOIPCR_REPO}@${NEOIPCR_BRANCH}") |
NEOIPCR_REPO=Brar/neoipcr, NEOIPCR_BRANCH=TowardsReferenceReport |
github-tag |
pak::pkg_install("${NEOIPCR_REPO}@${NEOIPCR_TAG}") |
NEOIPCR_REPO=…, NEOIPCR_TAG=vX.Y.Z (required) |
workspace |
COPY from the named build context neoipcr to /neoipcr, install devtools |
— |
The two axes are independent: pick any source mode for each, and any
*_REPO / *_BRANCH / *_TAG value on the toolkit side is unrelated
to the neoipcr side. Build a fork-of-toolkit + upstream-branch-neoipcr
image (or vice versa, or both forks on different feature branches) by
setting only the args you care about. The repo + branch overrides exist
so a dev image can pull from a fork or a feature branch before the work
merges to upstream main; once merged, drop the override and the
defaults become correct again.
The main build context stays the same across all modes — this repo's
root. Workspace mode adds named build contexts (BuildKit --build-context)
for the sibling checkouts, instead of widening the main context.
# Standalone build against the current dev branches on a fork.
cd repos/NeoIPC-Reporting
docker build -f src/NeoIPC.Reporting/Dockerfile \
--build-arg REPORTS_REPO=https://github.com/Brar/Surveillance-Toolkit.git \
--build-arg REPORTS_BRANCH=PartnerReport \
--build-arg NEOIPCR_REPO=Brar/neoipcr \
--build-arg NEOIPCR_BRANCH=PartnerReport \
-t neoipc-reporting:dev .
# Tagged toolkit + tagged neoipcr (upstream).
docker build -f src/NeoIPC.Reporting/Dockerfile \
--build-arg REPORTS_SOURCE=github-tag --build-arg REPORTS_TAG=v1.2.0 \
--build-arg NEOIPCR_SOURCE=github-tag --build-arg NEOIPCR_TAG=v0.1.0 \
-t neoipc-reporting:v1.2.0 .
# Workspace build (sibling checkouts supplied via named build contexts).
cd repos/NeoIPC-Reporting
docker build -f src/NeoIPC.Reporting/Dockerfile \
--build-context surveillance-toolkit=../Surveillance-Toolkit \
--build-context neoipcr=../neoipcr \
--build-arg REPORTS_SOURCE=workspace --build-arg NEOIPCR_SOURCE=workspace \
-t neoipc-reporting:workspace .The matching Reporting:BuildMode runtime env is baked into the image
per NEOIPCR_SOURCE mode — github-branch and github-tag images
ignore NEOIPCR_DEV_PATH; workspace images export it as /neoipcr.
cd repos/NeoIPC-Reporting
cp .env.sample .env # if present in your checkout
docker compose up --buildBrings up Postgres + DHIS2 + this service + Traefik on localhost:${TRAEFIK_PORT:-8080}.
- Network policy. The
Reporting:Dhis2BaseUrlvalue drives both the in-process auth call to/api/meand the R-subprocess data fetch via neoipcr. Egress from the reporting container must be restricted to the in-cluster DHIS2 service. In Compose this is the default-bridge network behaviour; on Kubernetes apply aNetworkPolicythat allows only that destination. - Read-only resource trees. The image's
/toolkit/and (in workspace mode)/neoipcr/trees arechmod -R a-wat build time. The render path only ever creates symlinks pointing into them, so the immutability is the load-bearing defense; runtimeread_only: trueon the whole container is not used because the Quarto + R subprocesses write to a number of cache paths under/home/app/.*that would each need explicit tmpfs mounts. The Compose service mounts a tmpfs at/tmpso per-render workdir state doesn't accumulate in the container's layered filesystem. Dhis2BaseUrlis part of the trusted computing base. An attacker who can flip it would harvest forwarded JSESSIONIDs. Treat it as deployment configuration with the same trust level as the image itself; do not surface it as a runtime knob to untrusted callers.- Storage volumes. The Compose stack mounts named volumes for admin-uploaded reference datasets and validation-exception files. These survive container restarts but live with the host operator's backup discipline.