The weather-terms glossary (9 entries) and four static pages' SEO title/ description were hardcoded directly in web/content.py - a 1019-line module that also owns all SSR rendering logic - mixed in with code that changes on a completely different cadence and for different reasons. Add content/glossary.yaml and content/pages.yaml (a new repo-root content/ tree, a sibling of backend/ and frontend/ paths.py resolves the same way - the seed of a future thermograph-copy repo per the architecture decision doc's own §4) plus web/content_loader.py: a small loader that validates each file's shape at load time (required fields present and non-empty, no duplicate glossary slugs) and fails loudly on a malformed edit rather than rendering a blank glossary card or an empty <title>. content.py's GLOSSARY dict and the about/privacy/hub/glossary_index page_title/description literals now come from the loader. Scope: only content that is genuinely pure static data with no embedded template logic. cities_flavor.json (Wikipedia extracts, already its own generated file) and the homepage's title/description (embedded in home.html.j2 as Jinja block overrides, a heavily-tested product-critical template) are deliberately left as they are - a future pass, not required for this one. UI microcopy bound to frontend logic stays in frontend/, per the doc's own line between content and frontend. Verified: content/glossary.yaml generated programmatically from the live GLOSSARY dict (not hand-transcribed) and round-tripped byte-for-byte identical against it; content/pages.yaml's four entries checked field-by- field against the original hardcoded strings. Full backend suite green (362 passed, 4 skipped) with zero existing test changes needed beyond one assertion made escaping-aware (a pre-existing Jinja double-escape quirk on the one title containing "&", intentionally preserved not fixed). Built and booted the real Docker image: content/ present at /app/content, and curled /glossary, /glossary/percentine, /about, /privacy from inside the running container - all four render with the exact expected title text.
46 lines
2 KiB
Python
46 lines
2 KiB
Python
"""Canonical filesystem locations, resolved once from the repo root.
|
|
|
|
Every path the backend needs (the climate cache, the accounts DB, logs, the
|
|
Jinja templates, the frontend static dir, the bundled city data) is derived
|
|
here from the repository root rather than from each module's own ``__file__``.
|
|
That keeps the locations stable no matter how deep in the package tree a module
|
|
lives, so moving a module never silently re-points the cache or the accounts DB.
|
|
|
|
Callers that support an environment override (e.g. ``THERMOGRAPH_ACCOUNTS_DB``)
|
|
keep doing so; these are only the defaults.
|
|
"""
|
|
import os
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _repo_root() -> str:
|
|
"""Walk up from this file until a directory holds both ``backend/`` and
|
|
``frontend/`` — the repository root, in every checkout (dev, prod, worktree)."""
|
|
d = _HERE
|
|
while True:
|
|
if os.path.isdir(os.path.join(d, "backend")) and os.path.isdir(os.path.join(d, "frontend")):
|
|
return d
|
|
parent = os.path.dirname(d)
|
|
if parent == d:
|
|
raise RuntimeError(
|
|
"cannot locate the repo root (no ancestor has sibling backend/ and frontend/)"
|
|
)
|
|
d = parent
|
|
|
|
|
|
REPO_DIR = _repo_root()
|
|
BACKEND_DIR = os.path.join(REPO_DIR, "backend")
|
|
DATA_DIR = os.path.join(REPO_DIR, "data")
|
|
LOGS_DIR = os.path.join(REPO_DIR, "logs")
|
|
FRONTEND_DIR = os.path.join(REPO_DIR, "frontend")
|
|
TEMPLATES_DIR = os.path.join(BACKEND_DIR, "templates")
|
|
# Structured SSR copy (glossary, static-page SEO meta) — see
|
|
# backend/web/content_loader.py and docs/README.md. A sibling of backend/ and
|
|
# frontend/, not under either: it's the seed of a future thermograph-copy repo.
|
|
CONTENT_DIR = os.path.join(REPO_DIR, "content")
|
|
|
|
# Bundled reference data shipped alongside the code (generated by gen_cities /
|
|
# gen_flavor), not runtime state — hence under backend/, not data/.
|
|
CITIES_JSON = os.path.join(BACKEND_DIR, "cities.json")
|
|
CITIES_FLAVOR_JSON = os.path.join(BACKEND_DIR, "cities_flavor.json")
|