phase: 148_pdf_pages_in_sources
**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:
@@ -21,6 +21,15 @@ Frontend side:
|
||||
import-safe page guard, the modal skeleton on chat + Sources, and the
|
||||
modal module's close/focus/URL contract.
|
||||
|
||||
Phase 148 (task 02) — the content payload's rendered-page routes
|
||||
(the viewer strip, task 03, reads from them): the ``DocContent``
|
||||
omission rule — ``page_urls`` ABSENT on the wire when ``None`` (never
|
||||
``null``; a non-qualifying doc's payload is byte-identical to
|
||||
pre-phase) — and the content endpoint (stubbed session) — a
|
||||
page-qualifying PDF row (``page_count`` set) adds ``page_urls`` = the
|
||||
exact 1-based ``/api/documents/<id>/pages/<i>`` list over
|
||||
``1..page_count``.
|
||||
|
||||
Phase 145 (task 03) — the content payload's git origin fields + the
|
||||
``renderDocument`` "Original" anchor (modal AND /document.html, one
|
||||
site):
|
||||
@@ -46,6 +55,7 @@ import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
@@ -279,10 +289,13 @@ def _text_doc() -> Document:
|
||||
|
||||
|
||||
def _text_doc_content(
|
||||
origin_url: str | None = None, origin_branch: str | None = None
|
||||
origin_url: str | None = None,
|
||||
origin_branch: str | None = None,
|
||||
page_urls: list[str] | None = None,
|
||||
) -> DocContent:
|
||||
"""One text doc's content payload (the pre-phase shape) with the
|
||||
phase-145 origin fields optionally set."""
|
||||
phase-145 origin fields + the phase-148 ``page_urls`` optionally
|
||||
set."""
|
||||
return DocContent(
|
||||
source="S",
|
||||
path="a.md",
|
||||
@@ -294,6 +307,7 @@ def _text_doc_content(
|
||||
chunks=1,
|
||||
origin_url=origin_url,
|
||||
origin_branch=origin_branch,
|
||||
page_urls=page_urls,
|
||||
)
|
||||
|
||||
|
||||
@@ -316,6 +330,64 @@ def test_doc_content_origin_fields_absent_on_the_wire_when_unset() -> None:
|
||||
assert dumped["origin_branch"] == "docs"
|
||||
|
||||
|
||||
def test_doc_content_page_urls_absent_on_the_wire_when_unset() -> None:
|
||||
"""Phase 148 (task 02) — the model-level omission rule (the
|
||||
``image_url`` precedent): a non-qualifying document (``page_urls``
|
||||
``None`` — a text doc, an image, a text-heavy PDF) serializes with
|
||||
the key ABSENT — never ``null`` — so its payload is byte-identical
|
||||
to pre-phase. A page-qualifying PDF keeps the exact route list."""
|
||||
dumped = _text_doc_content().model_dump()
|
||||
assert "page_urls" not in dumped # absent — never null
|
||||
urls = ["/api/documents/d/pages/1", "/api/documents/d/pages/2"]
|
||||
dumped = _text_doc_content(page_urls=urls).model_dump()
|
||||
assert dumped["page_urls"] == urls # the exact 1-based route list
|
||||
|
||||
|
||||
def _page_pdf_doc() -> Document:
|
||||
"""A page-QUALIFYING PDF row (phase 148): ``page_count`` set — the
|
||||
endpoint derives ``page_urls`` from it (the exact 1-based list)."""
|
||||
doc = Document(
|
||||
id=uuid.uuid4(),
|
||||
source="Homelab",
|
||||
path="ikea/besta.pdf",
|
||||
full_path="/tmp/besta.pdf",
|
||||
title="BESTA frame (white) assembly",
|
||||
content="[Page 1 illustration: the parts tray]…",
|
||||
content_hash="e" * 64,
|
||||
is_image=False, # phase 122: the stub session never applies defaults
|
||||
page_count=2,
|
||||
)
|
||||
doc.indexed_at = datetime(2026, 8, 22, 1, 2, 3, tzinfo=UTC)
|
||||
doc.created_at = datetime(2026, 8, 20, 9, 0, 0, tzinfo=UTC)
|
||||
return doc
|
||||
|
||||
|
||||
def test_content_page_urls_carried_for_a_page_qualifying_pdf() -> None:
|
||||
"""Phase 148 (task 02) — the endpoint (stubbed session, no
|
||||
database): a page-qualifying PDF row (``page_count`` set) carries
|
||||
``page_urls`` = the EXACT 1-based
|
||||
``/api/documents/<id>/pages/<i>`` list over ``1..page_count``
|
||||
(the viewer strip's source, task 03 — a per-page render gap is
|
||||
still listed; the route 404s that N). Every pre-phase key stands
|
||||
untouched (``is_image`` false, ``image_url`` ABSENT — the PDF is
|
||||
not an image doc; the document's TEXT stays user-gated, only the
|
||||
page bytes are public)."""
|
||||
doc = _page_pdf_doc()
|
||||
with _client_with_row((doc, 4)) as client:
|
||||
r = client.get(
|
||||
"/api/documents/content",
|
||||
params={"source": "Homelab", "path": "ikea/besta.pdf"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["page_urls"] == [
|
||||
f"/api/documents/{doc.id}/pages/1",
|
||||
f"/api/documents/{doc.id}/pages/2",
|
||||
] # the exact 1-based list, count == page_count
|
||||
assert body["is_image"] is False
|
||||
assert "image_url" not in body # absent — never null (not an image doc)
|
||||
|
||||
|
||||
def test_content_origin_url_carried_for_a_matching_git_row() -> None:
|
||||
"""The endpoint (stubbed session, no database): a ``kind='git'``
|
||||
row whose URL basename matches the document's source adds
|
||||
|
||||
Reference in New Issue
Block a user