ILCR collects annual logging cost information from forestry licensees to support interior stumpage rate setting in British Columbia. This repository is the modernized ILCR application, which replaces the legacy ILCR 2.0.4 (Java 8 / JSF / WebADE).
- Licensees (
ILCR_SUBMITTER) enter Schedules 1–11 for their mills and submit them. - Ministry staff (
ILCR_ADMIN) review, correct, verify and reverse reports, and administer mills, users, reporting years, Home content and code tables. - Both roles print schedules and run the Mill Information and Mill Status reports; submitters see only their own mills. The data extract is for Ministry staff only.
The application keeps the existing Oracle THE database as its unchanged system of record and signs users in through modern FAM / BC Gov CSS.
- Frontend: React 19, TypeScript, Vite, TanStack Router, IBM Carbon with the BC Gov NR theme, oidc-client-ts (FAM/CSS)
- Backend: Java 21, Spring Boot 4.1.1, Maven, executable JVM JAR, Spring Security OAuth2 resource server, Spring Data JDBC + Hikari against Oracle
THE, embedded JasperReports 7 - Tests: JUnit + Testcontainers Oracle (backend), Vitest + Testing Library + MSW (frontend), Playwright + playwright-bdd + axe (
frontend/e2e) - Local platform: direct Maven/npm runs or Docker Compose with backend, frontend, optional Caddy, and sanitized Oracle env wiring
- Target platform: OpenShift Gold using the
backend,frontendanddatabasedeployment packages
backend/ Spring Boot API (one package per schedule and feature)
frontend/ React/Vite web app; frontend/e2e is the Playwright + playwright-bdd suite
database/ Oracle Free image for the CI e2e database (tools namespace)
common/ OpenShift init template, API smoke test, k6 load tests
scripts/ Local DDL helper and read-only delivery-schema probes
docs/ API inventory, decision records and delivery-schema probe output
monitoring/ Sysdig alert templates (used by the PROD monitor job)
Prerequisites:
- JDK 21
- Maven 3.9+
- Node 24+
- Docker or Podman for the full local stack
- Optional Oracle dev DB credentials in a local, ignored
.env
Run the Spring Boot backend directly:
cd backend
$env:SPRING_PROFILES_ACTIVE = "local"
mvn spring-boot:runThe backend listens on http://localhost:8080 by default.
- Health:
GET /api/health - Schedule 1 API:
GET /api/v1/schedule1
Run the frontend directly against the local backend:
cd frontend
npm ci
$env:BACKEND_URL = "http://localhost:8080"
npm run devThe frontend shell includes the NRS Carbon layout, light/dark theme toggle, side navigation, and (on localhost) a top-right mock user selector. The mock personas are:
- Alex Admin:
ILCR_ADMIN - Sam Submitter:
ILCR_SUBMITTER
Run the full local stack:
copy .env.example .env
docker compose up --build backend frontendThe frontend is available at http://localhost:3000. In compose, the backend is mapped to http://localhost:8080 and runs inside the container on port 8080.
Locally the backend starts with security off (mock principal from the X-Mock-Groups header) and no datasource: both ILCR_SECURITY_ENABLED and ILCR_DATASOURCE_ENABLED default to false in application.yml. Deployed pods always run with security on (see OpenShift Status).
To connect Oracle on backend startup, put local values in ignored .env and set ILCR_DATASOURCE_ENABLED=true for Docker Compose:
SPRING_DATASOURCE_URL=jdbc:oracle:thin:@//<host>:1521/<service-name>
SPRING_DATASOURCE_USERNAME=<username>
SPRING_DATASOURCE_PASSWORD=<password>
ILCR_DATASOURCE_ENABLED=trueFor direct Maven runs, the oracle Spring profile also enables the datasource:
cd backend
$env:SPRING_PROFILES_ACTIVE = "local,oracle"
mvn spring-boot:runDo not commit real database passwords. Put local values in .env; the file is git-ignored.
Note: This is a specific workaround for developers behind a corporate SSL-decryption/packet-inspection gateway (such as Zscaler) and is not required for all developers (e.g., if you are on a direct internet connection).
If your corporate network performs SSL decryption/packet-inspection, Maven inside the isolated Docker container may fail to connect to Maven Central (all dependencies, including JasperReports 7, resolve from Central — the build declares no custom <repositories>) with a PKIX path building failed error.
The recommended, zero-import workaround is to leverage your Windows host's trusted certificate store by caching the dependencies on Windows once, and mounting your host's .m2 repository into the container:
- Seed the cache on Windows:
Run this once inside your Windows terminal to download and cache the libraries (which automatically trusts your corporate certificate):
cd backend mvn clean install -DskipTests
- Mount the cache in your local environment:
Set the
M2_HOMEvariable inside your local, ignored.envfile pointing to your host's.m2directory:Docker Compose will automatically detect this variable and mount your local Windows Maven cache into the container'sM2_HOME=/mnt/c/Users/<your-username>/.m2/root/.m2path, bypassing the certificate handshake issues completely!
The SPA uses a public OIDC client with PKCE and signed CSS access tokens. Runtime configuration
comes from frontend/public/config.js, replaced by a ConfigMap in deployed environments.
Local mock mode is enabled only with mockUser: true on localhost and backend security off.
For real login, configure the environment's ILCR_OIDC_ISSUER_URI and ILCR_OIDC_CLIENT_ID
(GitHub Environment variables in dev, test and prod), set frontend mockUser: false, and
enable backend security with the openshift profile's SSO configuration. Register
https://<host>/authCallback as the CSS client's redirect URI and https://<host>/ as its
post-logout redirect URI.
Signed-out visitors land on a welcome page with Log in with IDIR and Log in with Business
BCeID. The choice goes to CSS as an identity-provider hint (azureidir or bceidbusiness), and
after sign-in the visitor returns to the page they first asked for. Until Business BCeID is
provisioned on ILCR's CSS clients, its button shows a notice instead of signing in, because CSS
would answer the hint with an IDIR sign-in.
Reusable building blocks and global styles that new schedule/feature pages should adopt rather than
re-implement (paths under frontend/src):
- Schedule tombstone header —
components/core/ScheduleTombstone. A two-column page header: left is the page identity (title+ asubtitlesub-page label), right is the working-context mill/status lines. It replaces the oldPageTitleheader on schedule pages and owns thedocument.titleside effect.subtitleaccepts a string or astring[]rendered breadcrumb-style, so deeper sub-pages can thread their level (e.g.["Report Tree to Truck Costs", "License", "Sample"]). - Working context —
components/core/WorkingContext.useWorkingContext(millId, year)fetchesGET /v1/mill-contextwith the stale-response guards;WorkingContextLinesrenders the three legacy lines (Mill: … - Year: …,Sch 1-10 …,Sch 11 …). Both the tombstone and any future banner reuse these. The former globalContextBannerwas removed fromLayout— mill/status now renders once, in the tombstone header. - Currency formatting —
fmtCurrencyinutils/number.ts. Thousands-separated with two decimals (1234.5 → "1,234.50",null → "—"), no$sign (the column header carries the unit). Use it for$/m³/rate and other currency read-only cells; keepfmtfor plain integer/quantity cells. - Footer —
components/Layout/Footer, rendered once byLayouton every route. Shows the app version (left) and the BC Gov copyright/disclaimer/privacy/accessibility links (centred). The version comes from__APP_VERSION__, inlined frompackage.jsonby a Vitedefine(seevite.config.ts; typed insrc/vite-env.d.ts) — reuse that global for any other build-time constant. - Global styles (
styles/_overrides.scss,styles/_custom.scss) — buttons and fields share one 48px (3rem) height app-wide, across size variants, via the--cds-layout-size-height-localtoken (Story 30.2), so controls take nosizeprop; Carbon's Dropdown ignores the token and is pinned separately. Text areas share the same light-grey field background (#f4f4f4) as the text inputs. The height contract is pinned bystyles/__tests__/controlHeightContract.test.ts.
The repository tracks source, deploy templates, and safe examples such as .env.example. It ignores local secrets, certificates, build outputs, dependency folders, coverage reports, Playwright reports, Maven target/, and Vite dist/.
If a local setting is needed by the team, add a sanitized example to .env.example or this README instead of committing a developer-specific .env.
The backend follows the CSP-style JVM deployment path: Spring Boot 4, executable JAR, Log4j2 logging, actuator health, Maven verification, and CycloneDX SBOM generation. Graal/native-image support is intentionally not part of this build.
- Package shape. Each feature package (
schedule1…schedule11,checkstatus,reporting,dataextract,millmaintenance,assignment, …) has:- a controller that implements an
api/*Apiinterface holding the request mappings; dto/records;- a service that owns the transactions;
- a Spring Data JDBC repository with explicit
@QuerySQL againstTHE.
- a controller that implements an
- No schema changes. The application runs no DDL: Flyway is test-scope only and builds the throwaway test databases.
- Authorization is server-side.
- Every endpoint requires authentication. Every data endpoint also checks a permission action with
@PreAuthorize; the exceptions are the identity and working-context reads (/me,/mills,/reporting-years,/mill-context,/home-content/mine). - Schedule writes also pass the role × status editability matrix.
- Submitters are limited to mills they are actively assigned to.
- Every endpoint requires authentication. Every data endpoint also checks a permission action with
- Errors are RFC 7807
ProblemDetail, and message text comes frommessages.propertiesunder the legacy keys.
Every endpoint and the permission it requires is listed in docs/api-inventory.md. Keep it in step when you add or change an endpoint.
- Environments. Pull requests that touch deployable paths deploy a sandbox (zone = PR number mod 50). Merges to
maindeploy to TEST. - PROD requires a manual tested-tag release. Merges deploy TEST and record immutable image digests.
- Security is always on. Deployed pods always enforce FAM/CSS access-token authentication:
ILCR_SECURITY_ENABLEDis hard-coded"true"inbackend/openshift.deploy.yml. - Datasource.
ILCR_DATASOURCE_ENABLEDdefaults totrue. Set it tofalsein an environment for a data-less smoke deployment. - Fail-closed guard. The backend also refuses to start a deployed pod with security off while the datasource is on (
DeployedSecurityGuard), so mock auth can never serve real data.
The NR User Lookup directory search (ILCR_USER_LOOKUP_ENABLED, DL-27) is a separate switch and ships false in every environment. Only its non-secret half is wired in backend/openshift.deploy.yml — the service-account credential (ILCR_USER_LOOKUP_CLIENT_ID / _SECRET) is a per-environment Vault secret that does not exist yet, and a secretKeyRef to a missing Secret would fail the pod, so it lands with the DL-27 onboarding work. The backend refuses to start with the flag on and any of the four values blank, so the switch and the credential cannot drift apart. While the flag is off the endpoint is not routed at all and GET /api/v1/users/lookup answers 404.
Deployment settings live in the dev, test and prod GitHub Environments. PR deploys, PR cleanup and the scheduled PR purge use dev. Every deploy job fails unless OC_NAMESPACE ends in -<environment>, and each environment's issuer must be its own SSO realm.
Each OpenShift token is a token for that namespace's github-deployer service account, as in LEXIS. Every job uses it only to request a 25-minute token for the pipeline service account, and does its work with that.
Repository secrets. The analysis jobs attach no environment.
| Secret | Description |
|---|---|
SONAR_TOKEN_BACKEND, SONAR_TOKEN_FRONTEND |
SonarCloud analysis |
tools environment. The shared e2e Oracle runs in the tools namespace, which PR and merge runs share. The e2e reset and shard jobs and Deploy (database) attach this environment. It needs no branch rule, because PR runs use it.
| Name | Kind | Description |
|---|---|---|
OC_NAMESPACE_TOOLS |
Secret | Tools namespace, ending in -tools |
OC_TOKEN_TOOLS |
Secret | github-deployer token for that namespace |
TOOLS_ORACLEDB_PASSWORD |
Secret | Password of the shared e2e Oracle |
OC_SERVER |
Variable | OpenShift API server URL |
Environment secrets (each of dev, test and prod unless noted)
| Secret | Description |
|---|---|
OC_NAMESPACE |
That environment's namespace, ending in -dev, -test or -prod |
OC_TOKEN |
github-deployer token for that namespace |
ORACLEDB_HOST |
Oracle hostname only. The backend builds the TCPS URL itself, so no jdbc:, // or port |
ORACLEDB_SERVICENAME |
Oracle service name only |
ORACLEDB_USERNAME, ORACLEDB_PASSWORD |
Oracle credentials |
VANITY_TLS_CERTIFICATE, VANITY_TLS_KEY, VANITY_TLS_CA_CERTIFICATE |
prod only: PEM certificate, key and issuer chain for VANITY_HOST |
SYSDIG_API_TOKEN |
prod only, optional: Sysdig alert sync |
Environment variables (each of dev, test and prod unless noted)
| Variable | Description |
|---|---|
OC_SERVER |
OpenShift API server URL |
ILCR_OIDC_ISSUER_URI |
CSS realm issuer: dev.loginproxy in dev, test.loginproxy in test, loginproxy.gov.bc.ca in prod |
ILCR_OIDC_CLIENT_ID |
CSS public client ID (resource in the installation JSON) |
VANITY_HOST |
prod only: PROD hostname without scheme or path |
ILCR_OIDC_SITEMINDER_LOGOUT_URL |
Optional: SiteMinder logoff.cgi URL; defaults to logontest7 outside PROD and logon7 in PROD |
ILCR_DATASOURCE_ENABLED |
Optional: false for a data-less smoke deployment |
The SPA uses https://<host>/authCallback as its redirect URI and https://<host>/ as its post-logout redirect URI. Both must be registered on the environment's CSS client.
Backend:
cd backend
mvn verify # unit tests, JaCoCo, Checkstyle, Spotless — integration tests are SKIPPED
mvn verify -P integration-test # integration tests only (Testcontainers Oracle; needs Docker)
mvn verify -P all-tests # bothmvn verify on its own prints BUILD SUCCESS without running any *IT; check the Tests run: count before reading a green build as integration coverage. Run mvn spotless:apply first — spotless:check fails the build.
Frontend:
cd frontend
npm ci
npm run lint
npm run test:cov
npm run buildEnd-to-end tests live in the self-contained frontend/e2e package, so they have their own
node_modules and are not installed by the frontend npm ci above. Install them once, then run:
cd frontend/e2e
npm ci
npm test # equivalently, from frontend/: npm run test:e2enpm run test:e2e (which shells out to the e2e package) requires the full running stack —
frontend :3000, backend :8080, and the seeded Oracle DB — plus a browser channel. See
frontend/e2e/README.md for the bring-up. Playwright runs against the local
Vite app by default; set E2E_BASE_URL only when intentionally testing a deployed route.