phase: 148_pdf_pages_in_sources
Build and Push Containers / build-and-push-app (push) Successful in 2m28s
Build and Push Containers / build-and-push-db (push) Successful in 16s

**Phase 148 — final verification pass: complete, 100% green.**

- **Verified (tasks 01–04 already implemented):** page render/persist at index (`_persist_page_copies`, fail-soft, stale-replace, qualifier-loss, prune hook via `_delete_page_copies`); public `GET /api/documents/{id}/pages/{n}` (uuid4-credential, 404-all-non-servable, `image/png` + cache header); `DocContent.page_urls` additive-omitted; shared `renderDocument` strip (lazy imgs, "Page N" captions, text + Summary suppressed, onerror note, createElement-only); migration 0026; `pdf_page_dir` setting; `.env.example` entry
- **Completion criteria:** (1) pictorial + fallback classes get `p{N}.png` + `page_count`, text-heavy gets neither — pinned, pass; (2) unchanged re-sync renders zero (fast path) — pass; (3) changed re-index replaces copies, qualifier-loss/prune delete — pass; (4) route 404 matrix + payload key absent-never-null — pass; (5) modal AND /document.html strip with suppression, text-heavy byte-identical — E2E pass; (6) see below; (7) no completed-phase drift — full suite green
- **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → **3461 passed, 0 failed**, `app/` coverage **99%** (>90%); `uv run pytest tests/e2e/test_pdf_pages_in_sources.py -v --no-cov` → **4 passed** in isolation; `uv run ruff check .` clean; `uv run pyright` → 0 errors, 0 warnings
- **Documentation:** README had **1 pre-existing inconsistency** — the Production Deployment section's standalone `podman run` example (and compose note) omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the fail-loud boot gate the README's own Admin section documents — the container would refuse to start); fixed with the two `-e` flags + a note for the compose path. All else verified consistent: 70/70 `.env.example` vars documented, all defaults match `config.py`, pages table matches routes, Containerfile/esbuild/uid-10001/healthcheck/entrypoint claims accurate, QA commands match pytest config. **AGENTS.md: verified consistent — no changes needed**
- **Notable:** repo uses `Containerfile` (Podman) — README's `podman build` example is valid as-is; no live infrastructure touched
- **Next pending phase:** `149_sync_progress_box`
This commit is contained in:
2026-10-01 16:58:01 -04:00
parent 863519e980
commit b233c8d714
39 changed files with 3426 additions and 56 deletions
+89
View File
@@ -40,6 +40,20 @@ traversal/UUID-guessing has no filesystem surface to hit). The
document's TEXT (the content endpoint) stays user-gated — only the
bytes are public.
GET /api/documents/{doc_id}/pages/{n} — the phase-148 (task 02) PDF
page BYTES route: ONE rendered page PNG (``<doc-id>-p{N}.png`` in the
importer's ``pdf_page_dir`` copy — task 01) of a page-QUALIFYING
document (the phase-133 fallback class or the phase-140 pictorial
class). PUBLIC — the image route's phase-124 A4 trust model verbatim
(a shared chat can cite a page figure, so the bytes serve
ANONYMOUSLY; the unguessable ``uuid4`` document id IS the credential —
the document's TEXT stays user-gated). ``image/png`` +
``Cache-Control: private, max-age=3600`` (the image route's header);
404 ``document not found`` for EVERY non-servable case (an
unparseable id, an unknown doc, ``page_count`` NULL, ``n`` outside
``1..page_count``, or a copy lost on disk). The path is built from the
ROW's id + ``n`` only — no request value reaches a filesystem path.
PATCH /api/folders/summary — the admin folder-description editor
(phase 97, task 03): update / create / clear a stored
``folder_summaries`` row, marking every non-empty save
@@ -228,6 +242,18 @@ def get_document_content(
),
origin_url=origin[0] if origin else None,
origin_branch=origin[1] if origin else None,
# Phase 148 (task 02): the rendered-page BYTES routes for a
# page-qualifying PDF — the exact 1-based list over
# ``1..page_count`` (the viewer's strip source, task 03; a
# per-page render failure leaves a GAP the route 404s). ABSENT
# (never null — the ``image_url`` omission rule) for every
# other document, so its payload is byte-identical to
# pre-phase.
page_urls=(
[f"/api/documents/{doc.id}/pages/{i}" for i in range(1, doc.page_count + 1)]
if doc.page_count is not None
else None
),
)
@@ -290,6 +316,69 @@ def get_document_image(
)
@router.get("/documents/{doc_id}/pages/{n}", response_class=FileResponse)
def get_document_page(
doc_id: str,
n: int,
db: Session = Depends(get_db), # noqa: B008
) -> FileResponse:
"""The phase-148 (task 02) PDF page BYTES route — ONE rendered page
PNG (``<doc-id>-p{N}.png`` in the importer's ``pdf_page_dir`` copy,
task 01) of a page-QUALIFYING document.
PUBLIC (the phase-124 A4 trust model verbatim — the image route's
precedent): a shared chat can cite a page FIGURE (the viewer's
strip, task 03), so the bytes serve ANONYMOUSLY. The document id
is an unguessable server-minted ``uuid4`` — IT IS the credential,
with no enumeration value — exactly like the image route and the
saved-chat id (phase 55). The document's TEXT stays user-gated (the
content endpoint): only the page bytes are public.
404 ``document not found`` (the router's unknown-document shape) in
EVERY non-servable case — an unparseable ``doc_id`` (the
``uuid.UUID`` try/except maps here, not to a 422 — the image
route's mapping: a guessed id is an unknown document), an unknown
doc, a doc whose ``page_count`` is NULL (a non-qualifying doc — the
route knows nothing of its pages), ``n`` outside ``1..page_count``
(out of range — a GAP, a page whose render failed, hits the
missing-file arm below), or a file on disk that is missing
(defensive — the row exists, the bytes don't; the viewer's onerror
note covers it).
``n`` is an ``int`` path parameter (FastAPI parses it — a non-int
422s at the router), and the path is built ONLY from the row's id
+ ``n`` (``pdf_page_dir/<doc.id>-p{n}.png``) — no request value
reaches a filesystem path (no traversal surface, the image route's
property).
Servable pages stream the exact stored bytes as ``image/png``
(``render_pdf_pages``'s ``tobytes("png")`` — a page is always a
PNG) with ``Cache-Control: private, max-age=3600`` (the image
route's header — the bytes are content-addressed by the document
id + page number, bustable by re-index).
"""
try:
uid = uuid.UUID(doc_id)
except ValueError:
raise HTTPException(status_code=404, detail="document not found") from None
doc = db.scalar(select(Document).where(Document.id == uid))
if doc is None or doc.page_count is None:
raise HTTPException(status_code=404, detail="document not found")
if not 1 <= n <= doc.page_count:
raise HTTPException(status_code=404, detail="document not found")
page_file = Path(get_settings().pdf_page_dir).expanduser() / f"{doc.id}-p{n}.png"
if not page_file.is_file():
# Defensive: the row qualifies but the copy was lost (the owner
# cleaned the page dir, the disk was wiped) — the viewer's
# onerror note renders "Page N unavailable".
raise HTTPException(status_code=404, detail="document not found")
return FileResponse(
page_file,
media_type="image/png",
headers={"Cache-Control": "private, max-age=3600"},
)
@router.patch("/documents/summary", response_model=SummaryResult)
async def update_document_summary(
payload: SummaryUpdate,