- Use loop index as each key (handles duplicate filenames)
- Increase skipped filename font from text-xs to text-sm
- Add motion-safe guard to details chevron transition
- Replace text-warning with text-amber-900 to meet WCAG AA contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @Schema(requiredMode = REQUIRED) to SkippedFile and ImportStatus
record components so TypeScript codegen produces non-optional fields
when generate:api is next run
- Extract openFileStream(File) as package-private method so the
IOException path can be tested deterministically without relying on
OS-level file permissions (which are bypassed when running as root)
- Replace assumeTrue-based IOException test with Mockito spy that stubs
openFileStream — test now runs in CI unconditionally (45 tests, 0 skipped)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setReadable(false) silently no-ops as root; check canRead() to guard
the assumption correctly so the test is skipped in Docker CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove duplicate List import in AdminControllerTest
- derive skipped() from skippedFiles.size() — drop redundant int field
- use machine codes for SkippedFile.reason (INVALID_PDF_SIGNATURE, FILE_READ_ERROR)
- map reason codes to i18n strings in ImportStatusCard (de/en/es)
- replace raw amber Tailwind classes with warning semantic token
- fix <summary> accessibility: replace list-none with rotating chevron SVG
- replace <p> with <span> inside <summary> (phrasing content rule)
- extract setupOneValidOneFakeImport() helper — remove 3x copy-paste
- add lenient mock to short-file test for defensive coverage
- add IOException path test for isPdfMagicBytes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SkippedFile to the local ImportStatus type and updates
ImportStatusCard to show an amber skipped-count section with a
collapsible filename list in the DONE state. Only rendered when
skipped > 0. i18n keys added for de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads first 4 bytes of each candidate file before upload; rejects any
file whose header does not match %PDF (0x25 0x50 0x44 0x46). Skipped
files are counted and collected in ImportStatus.skippedFiles so
operators can see what was rejected without querying Loki.
Breaking: ImportStatus record gains skipped + skippedFiles fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Explains what ocr-volume-init does (chown volumes + create TMPDIR), how to
verify it succeeded (docker logs), and what failure looks like. Addresses
reviewer concerns from @mkeller and @tobiwendt on PR #615.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alpine:3 is a moving tag — pinning to 3.21 makes builds reproducible and
rollbacks possible. networks: [] removes the init container from the project
network since it only needs volume access, not network access (least privilege).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- entrypoint.sh: replace "cross-job ground-truth leakage" with plain
"Remove stale partial downloads left by a previous docker-kill"
- test_tmpdir_is_inside_persistent_cache_volume: add docker exec command
so future developers know how to run this deployment-contract test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_entrypoint_removes_day_old_orphans and test_entrypoint_preserves_fresh_files
verify the find -mtime +1 -delete logic using os.utime() to fabricate old mtimes
without mocking system time. Also extracts _run_entrypoint helper to remove
subprocess setup duplication.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A silent non-zero exit would previously cause the test to pass incorrectly
because only directory creation was checked. Exit code is now the first
assertion, catching regressions before the filesystem check runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_validate_zip_entry has no ML-stack dependency; importing it via main.py
pulled in surya/torch and caused the test to be skipped in CI. Moving it
to utils.py (fastapi only) and adding fastapi to the CI lightweight install
lets test_zipslip_still_anchors_under_custom_tmpdir run on every push.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ocr-service/README.md: add HF_HOME, XDG_CACHE_HOME, TORCH_HOME, TMPDIR rows
to the environment variables table
- ocr-service/CLAUDE.md: LLM reminder — TMPDIR must stay on the cache volume
- docs/adr/021-tmpdir-persistent-volume-staging.md: records the decision,
trade-offs, and rejected alternatives (Approach B / C) for issue #614
- ci.yml: add test_tmpdir.py to the OCR CI run (stdlib-only tests, no ML stack)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TMPDIR=/app/cache/.tmp routes Surya model staging to the SSD-backed cache
volume instead of the 512 MB /tmp tmpfs. The ocr-volume-init one-shot service
runs first to ensure correct ownership (uid 1000) and creates /app/cache/.tmp
on fresh volumes, making AC #6 ("fresh volume still works") a permanent
infrastructure-as-code guarantee rather than a manual chown step.
Both docker-compose.yml and docker-compose.prod.yml are updated in the same
commit to prevent the silent drift that occurred with the 512 MB tmpfs comment.
Fixes#614. See ADR-021.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On a fresh ocr_cache volume /app/cache/.tmp does not exist yet. The mkdir
ensures the first Surya model download can proceed without ENOSPC on the
512 MB /tmp tmpfs. The find cleanup removes fragments left by docker-kill
mid-download, preventing cross-job ground-truth leakage.
Fixes#614. See ADR-021.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, running the image outside compose loses the TMPDIR redirect
and Surya model downloads fall back to the 512 MB /tmp tmpfs (ENOSPC).
See issue #614, ADR-021.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the stale Basic-Auth picture with the post-#523 model:
AuthSessionController + AuthService (the new auth/ package), Spring Session
JDBC (spring_session*, 8h idle timeout, fa_session cookie), and the
ChangeSessionIdAuthenticationStrategy bean used by login to defend against
session fixation. Addresses PR #612 / Markus M3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spring Session JDBC's spring_session/spring_session_attributes (introduced in
V67 / ADR-020) and Flyway's own history table are framework-managed and
opaque to app code — modelling them on db-orm.puml would mislead future
readers into thinking they participate in domain relationships. Codify the
exclusion in the doc-currency tables of architect.md and developer.md, with
a pointer to "the relevant ADR" so a future exclusion still carries
justification. Addresses PR #612 / Markus M2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #523 moved login/logout into a new auth/ package (AuthSessionController,
AuthService, LoginRequest) — register the row in both CLAUDE.md trees
alphabetically and strip the stale "auth controllers" line from the user/
description so the next LLM reading either file finds the right home.
Addresses PR #612 / Markus M1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
text-xs (12px) is below Leonie's body-copy floor for the senior reader cohort
who hit /login?reason=expired on a phone in sunlight after being logged out.
text-sm (14px) restores legibility without breaking the visual hierarchy with
the heading. Addresses PR #612 / Leonie L3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Color-blind reader cohort (8% of men) on a phone in sunlight cannot rely on
amber alone to parse the banner as a warning. Add a Heroicons-style
exclamation-triangle SVG, aria-hidden because the heading text already
conveys the meaning to assistive tech. Addresses PR #612 / Leonie L2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace text-amber-900/text-amber-800 with the existing --color-warning
utility from layout.css. The amber soft fill stays (matching the precedent
of the green "registered" banner; a full surface-token pair is out of scope
for this PR). Addresses PR #612 / Leonie L1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four new tests against the composed handle (with sequence stubbed to return
the head function): backend 401 on a private path redirects to
/login?reason=expired; backend 401 on /login does NOT redirect (no loop);
missing fa_session passes through without a backend call; 200 attaches the
user to event.locals. Closes the hook-coverage gap flagged by Sara S1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three tests: happy path POSTs to backend with the session cookie and clears
both fa_session and legacy auth_token; cookies are cleared even when the
backend call rejects (best-effort logout); skips the backend call when no
session cookie is present. Addresses PR #612 / Sara S1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six tests covering: load() exposes ?registered and ?reason; action returns 400
on missing email; 401 with INVALID_CREDENTIALS on backend reject; success
re-emits fa_session and deletes legacy auth_token; 500 when backend omits
fa_session in Set-Cookie. Closes the frontend coverage gap on the credential-
handling logic that moved out of the Java side. Addresses PR #612 / Sara S1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the duck-typed `status in error && location in error` check with the
official SvelteKit guard. Fragile against minor-version error-shape changes
becomes a one-liner against a typed helper. Addresses PR #612 / Felix F1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the inline parser; reuse the now-shared helper. Pure rewire, no behaviour
change. Addresses PR #612 / Felix F2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move the Set-Cookie parser out of login/+page.server.ts into a shared module
with its own Vitest coverage (single-header, multi-header getSetCookie path,
missing-header, attribute-stripping, prefix-match-rejection). An Undici or
Node upgrade that changes header shape now trips its own test instead of
silently breaking login. Addresses PR #612 / Felix F2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure-comment change: spell out that resolveClientIp's leftmost-X-Forwarded-For
strategy is safe only because Caddy strips client-supplied XFF before
forwarding. Future readers swapping the ingress have a tripwire. Addresses PR
#612 / Nora concern (XFF trust documentation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pin the @JsonProperty(WRITE_ONLY) invariant on AppUser.password. If the
annotation is ever dropped — or a new field aliases the hash — the CI run that
ships the regression flags it the next morning rather than waiting for a
security review. Addresses PR #612 / Nora concern (regression test).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reorder AuthSessionController.logout so HttpSession.invalidate runs before
AuthService.logout, and wrap the audit call in try/catch so an exception (e.g.
the user was deleted between login and logout, making the audit-time
findByEmail throw) cannot leave the session row alive in spring_session.
The user's intent — "log me out" — is honoured even when audit fails.
Addresses PR #612 / Nora B2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inject Spring Security's SessionAuthenticationStrategy
(ChangeSessionIdAuthenticationStrategy) into AuthSessionController and invoke
onAuthentication at the credential boundary. The strategy calls
HttpServletRequest.changeSessionId() to invalidate any pre-auth session ID an
attacker may have planted and mint a fresh ID before the SecurityContext is
attached. Addresses PR #612 / Nora B1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes the cookie-promotion step (auth_token → Authorization: Basic) and
splits the diagram into three labelled phases: Login, Authenticated
request, Logout. Adds the spring_session DB round-trip on every
authenticated request and the alt branch for an expired session
returning 401 → /login?reason=expired.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Amber aria-live banner when ?reason=expired (set by hooks.server.ts
after the backend rejects an expired fa_session) with a one-line
explainer about the 8h idle window.
- autofocus on email so users returning after a session-expired kick
can immediately retype credentials.
- min-h-[44px] on the submit button hits the iOS HIG / WCAG 2.1 AAA
touch target minimum — relevant for the reader cohort on phones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With the Spring Session model the browser forwards fa_session itself —
the proxy no longer needs to translate auth_token → Authorization: Basic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
userGroup: GET /api/users/me with Cookie: fa_session=<id>. On 401, drop
the stale cookie and redirect to /login?reason=expired (unless already
on a public path) so the user sees an explainer instead of a silent kick.
handleFetch: forward fa_session as a Cookie header on every API call
except the public auth endpoints. Drops the old auth_token injection.
Also adds a one-off cleanup of any lingering auth_token cookie from
pre-migration sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend POST invalidates the spring_session row and writes the
LOGOUT audit entry; the client cookie is deleted unconditionally so a
network blip during logout still logs the user out locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the Basic-credentials-in-cookie flow with the Spring Session model:
1. POST {email, password} as JSON to /api/auth/login
2. Map 401 → INVALID_CREDENTIALS (or SESSION_EXPIRED if the backend returns it)
3. Parse Set-Cookie for fa_session=<opaque> and re-emit to the browser
4. Drop the legacy auth_token cookie
load() now also exposes ?reason= so the page can show the
session-expired banner (Task 21 wires it into the .svelte file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the backend ErrorCode additions from commit 393a3c25.
Adds error_session_expired_explainer for the login-page banner that
will surface when ?reason=expired.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also switches pom.xml to spring-boot-starter-session-jdbc (Spring Boot 4.x
split the session auto-config into a separate starter; spring-session-jdbc
alone does not register JdbcSessionAutoConfiguration).
Adds SpringSessionConfig#cookieSerializer bean to configure fa_session name
and SameSite=Strict (spring.session.cookie.* properties are no longer
supported by the Boot 4.x auto-configuration layer).
Cleans up application.yaml / application-dev.yaml: removes store-type: jdbc
and the unsupported cookie.* keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-introduces tables dropped by V2. Canonical DDL from Spring
Session 3.x schema-postgresql.sql.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns with the block sequence style used in docker-compose.prod.yml and
the rest of the compose file, removing the inline [ALL] inconsistency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>