Standalone frontend: no-walk paths.py, own Dockerfile, own CI

paths.py drops the sibling-directory walk (frontend is now the repo
root: static/ and content/ are its own subdirectories, no more
sibling walk needed). New Dockerfile (much smaller deps -- fastapi,
uvicorn, httpx, jinja2, PyYAML -- no entrypoint script needed at all,
frontend has no migrations/pre-boot logic).

content/ is a committed starter copy for now, not yet real cross-repo
vendoring from thermograph-copy (that repo doesn't exist yet) -- noted
in paths.py's own docstring.

Real, known gap flagged rather than silently skipped: this process
cannot boot standalone (content.register() fetches the IndexNow key
from backend at import time with no retry, by design), so build.yml
only verifies the image builds, not a live boot+healthz check -- that
needs a genuine cross-repo contract-test job (booting a real backend
too), separate follow-up work. Same reason tests/ doesn't run here yet
(its conftest.py still imports backend's own test fixtures via a
sibling path that no longer exists) -- noted directly in the file.
This commit is contained in:
emi 2026-07-21 15:59:29 -07:00
parent d6056afabe
commit f97df715d1
6 changed files with 100 additions and 36 deletions

View file

@ -0,0 +1,34 @@
name: Build check
# Proves the split Dockerfile actually builds (repo-split Stage 7b) -- NOT a
# live boot+healthz check. content.register() fetches the IndexNow key from
# backend at import time with no retry (documented Stage 4 behavior,
# unchanged here); without a real, reachable backend this process crashes
# immediately on import, by design -- so a standalone boot check would either
# be fake (skip the real import path) or require checking out and booting
# thermograph-backend too. That's the real fix (a genuine cross-repo
# contract-test job, same pattern the original repo-split plan already
# scoped for the /cell + ETag contract), not yet built -- flagged as a real
# gap, not silently skipped. Full pytest-suite migration is separate,
# deferred follow-up work too, same category as thermograph-copy's deferred
# vendoring.
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: docker
steps:
- uses: actions/checkout@v4
- name: Install Docker CLI
run: |
apt-get update -qq
apt-get install -y -qq docker.io
- name: Build
run: docker build -t thermograph-frontend:ci .

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv/
__pycache__/
*.pyc
.DS_Store
.env

31
Dockerfile Normal file
View file

@ -0,0 +1,31 @@
# Thermograph frontend: server-rendered content pages, the interactive tool's
# SPA shells, and every static asset. Split from the monorepo (repo-split
# Stage 7). No migrations, no DB, no pre-boot logic -- a plain shell-form CMD
# is enough (unlike backend, no separate entrypoint script needed).
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
COPY . /app/
RUN useradd --system --create-home --uid 10001 thermograph \
&& chown -R thermograph:thermograph /app
USER thermograph
WORKDIR /app
ENV PORT=8080 \
THERMOGRAPH_BASE=/ \
PYTHONUNBUFFERED=1
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD curl -fsS http://127.0.0.1:${PORT}/healthz || exit 1
CMD uvicorn app:app --host 0.0.0.0 --port ${PORT}

View file

@ -1,32 +1,12 @@
"""Canonical filesystem locations for the SSR service, resolved from the repo """Canonical filesystem locations for the SSR service, resolved once from the
root. Stage-3-transitional: this package lives at `frontend_ssr/`, a sibling repo root (repo-split Stage 7). content/ is committed here as a starter copy
of the static asset tree `frontend/` (not nested inside it -- nesting would extracted alongside frontend/frontend_ssr -- real cross-repo vendoring from
let StaticFiles serve this package's own source/templates as downloadable thermograph-copy (a build-time clone at a pinned ref) is deferred follow-up
files once app.py mounts frontend/). Still walks up to find `content/` as a work, not yet wired up.
sibling too, same as backend/paths.py does today, since both still live in
one repo. Once frontend is its own repo (Stage 7), content/ is vendored at
build time instead and this goes back to a no-walk resolution, same as
backend's.
""" """
import os import os
_HERE = os.path.dirname(os.path.abspath(__file__)) REPO_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(REPO_DIR, "static")
TEMPLATES_DIR = os.path.join(REPO_DIR, "templates")
def _repo_root() -> str:
d = _HERE
while True:
if os.path.isdir(os.path.join(d, "frontend")) and os.path.isdir(os.path.join(d, "content")):
return d
parent = os.path.dirname(d)
if parent == d:
raise RuntimeError(
"cannot locate the repo root (no ancestor has sibling frontend/ and content/)"
)
d = parent
REPO_DIR = _repo_root()
STATIC_DIR = os.path.join(REPO_DIR, "frontend")
TEMPLATES_DIR = os.path.join(_HERE, "templates")
CONTENT_DIR = os.path.join(REPO_DIR, "content") CONTENT_DIR = os.path.join(REPO_DIR, "content")

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
httpx==0.28.1
jinja2==3.1.6
PyYAML==6.0.3

View file

@ -1,13 +1,22 @@
"""Shared test setup for the SSR service. """Shared test setup for the SSR service.
Stage-3-transitional: this package still lives in the same repo as the KNOWN GAP (repo-split Stage 7b): this suite does NOT run standalone in this
backend, so a `client` fixture can fake api_client by calling the REAL repo. It still assumes the layout from before the split -- a sibling
backend payload builders (api.content_payloads) against the same synthetic `backend/` checkout with its own `tests/conftest.py` (`make_history`/
history/recent fixtures backend/tests/conftest.py uses, instead of hand- `make_recent`) and `api.content_payloads` module, which this repo no longer
maintaining a parallel set of literal JSON fixtures. This means a shape drift has. Extracted verbatim as reference material for the real fix (a genuine
between content_payloads.py and how content.py consumes it fails here, not cross-repo contract-test job, or a self-contained set of fixtures owned
just in backend/tests/web/test_content.py -- and the two suites assert on here), not yet built -- see this repo's `.forgejo/workflows/build.yml` for
identical numbers, so they can never quietly diverge. the same gap on the boot-check side. Do not assume `pytest tests` passes
here until that follow-up lands.
Original docstring, still accurate for what the suite is trying to prove:
this package used to live in the same repo as the backend, so a `client`
fixture could fake api_client by calling the REAL backend payload builders
(api.content_payloads) against the same synthetic history/recent fixtures
backend/tests/conftest.py uses, instead of hand-maintaining a parallel set of
literal JSON fixtures -- so a shape drift between content_payloads.py and how
content.py consumes it failed here too, not just in the backend's own suite.
""" """
import importlib.util import importlib.util
import os import os