feat(infra): bind-mount /import for backend mass-import endpoint #526

Merged
marcel merged 7 commits from feat/prod-import-bind-mount into main 2026-05-11 20:55:43 +02:00

7 Commits

Author SHA1 Message Date
Marcel
3775f4cb52 ci(nightly): regression guard for backend /import:ro mount
Some checks failed
CI / Backend Unit Tests (pull_request) Successful in 4m13s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 2m48s
CI / OCR Service Tests (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
CI / Unit & Component Tests (push) Has been cancelled
Sara flagged that a future "compose cleanup" PR could silently drop the
backend volumes block and CI would happily pass while mass import on
staging silently broke. Adds a pre-build step that renders the staging
compose config and fails the deploy if `target: /import` or
`read_only: true` is missing.

Local verification of the guard:
- Volumes block removed → `grep -q 'target: /import'` exits 1 → step fails
- Volumes block present → both greps match → step passes

Addresses Sara's review on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:08:30 +02:00
Marcel
c2c42706c7 ci(release): wire IMPORT_HOST_DIR=/srv/familienarchiv-production/import
Mirrors the staging change. The host directory does not yet exist on
the production server — first production release that consumes this
will create an empty bind source via Docker's auto-create behaviour;
mass import then reports "no spreadsheet found" until an operator
pre-stages a payload there.

Addresses Tobias's review on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:06:33 +02:00
Marcel
9703a72e6c ci(nightly): wire IMPORT_HOST_DIR=/srv/familienarchiv-staging/import
The compose file now requires IMPORT_HOST_DIR or refuses to start
(#526). Without this line the next nightly deploy would fail with a
clear interpolation error, but it should not fail — the staging
import payload already lives at this host path (rsync'd in #526).

Addresses Tobias's review on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:05:55 +02:00
Marcel
a40267e490 docs(deployment): document IMPORT_HOST_DIR and mass-import workflow
DEPLOYMENT.md line 81 declares any compose env var missing from §2 a
blocking review comment. IMPORT_HOST_DIR (added on this branch) was
unmentioned. Adds the row and rewrites §6.4 so the staging/prod operator
workflow (rsync host → set env → trigger import) is in the runbook,
not just buried in compose comments.

Addresses review feedback from Markus and Tobias on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:05:14 +02:00
Marcel
cdb5db6c68 fix(compose): require IMPORT_HOST_DIR, no default
Tobias and Markus both flagged that a shared default (/srv/familienarchiv/
import) invites silent collision when staging and prod cohabit one host.
Switch to ${IMPORT_HOST_DIR:?...} so compose refuses to start without an
explicit per-env path — collision becomes structurally impossible.

The error message points operators at docs/DEPLOYMENT.md so the recovery
step is one click away. IMPORT_HOST_DIR moves from "Optional" to the
main required-env-vars block in the header.

Addresses review feedback from Markus, Tobias, and Nora on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:03:57 +02:00
Marcel
ff20721dee refactor(import): make import directory @Value-configurable
The hardcoded `static final String IMPORT_DIR = "/import"` was the only
non-`@Value` configurable input in MassImportService — every column
index next to it is wired through `app.import.col.*`. Lifts the
contract from infrastructure (compose bind mount) into application
config (`app.import.dir`), with `/import` as the default so the existing
bind-mount path keeps working.

Addresses review feedback from Markus and Felix on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:02:45 +02:00
Marcel
4a537d6b19 feat(infra): bind-mount /import for backend mass-import endpoint
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m55s
CI / OCR Service Tests (push) Successful in 18s
CI / Backend Unit Tests (push) Successful in 4m9s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Successful in 56s
CI / Unit & Component Tests (pull_request) Failing after 2m47s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m12s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Successful in 57s
`MassImportService` reads the ODS spreadsheet and referenced PDFs from a
hardcoded `/import` path inside the backend container. Dev compose
already bind-mounts `./import:/import`, but the prod compose had no
equivalent, so `POST /api/admin/import` would always fail on staging/prod
with "no spreadsheet found".

Mount strategy:
- Source path is env-driven (`IMPORT_HOST_DIR`), defaulting to
  `/srv/familienarchiv/import` so the host path is stable across CI
  deploys (the compose working dir is recreated each run, so `./import`
  would not persist).
- Read-only — `MassImportService` only reads (`Files.list` /
  `Files.walk`), never writes. Read-only mount makes that contract
  explicit and prevents the backend container from mutating the source
  PDFs.
- Empty / missing path is harmless: the import API just returns the
  existing "no spreadsheet found" error rather than crashing the
  container.

To use on staging: rsync the import folder to
`/srv/familienarchiv-staging/import/` on the host, set
`IMPORT_HOST_DIR=/srv/familienarchiv-staging/import` in `.env.staging`,
redeploy, trigger import from `/admin/system`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 18:57:47 +02:00