f1-circuits: become a nox mesh module

The mesh builds the CDN from this repository and a commit (novox/hq ADR
0069); registry-api.novox.be, where HAL's image came from, is retired, so
the running container can no longer be re-pulled. The HAL module.yml and
service/docker-compose.yml retire with HAL.

The Kometa metadata links every poster to this service's own address,
which was the literal https://f1-circuits.zurag.be. It is now
F1_CIRCUITS_BASE_URL, written by the mesh from the route's name
(${bound:route:name}, mesh-controller #149) - falling back to the old
address, so HAL's deployment behaves as before. The tile user agent
follows it.

The poster cache is a placed directory mounted at /app/cache, as HAL's
/services/f1-circuits/cache was. The image was built from unpinned pip
installs; service/requirements.txt now pins the exact set the running
container has (pip freeze), and the base is python 3.12.13-alpine3.24 by
digest (a later alpine 3.24 patch than the running build's 3.24.0 - the
older digest is no longer tagged). The slug "f1" keeps the route login
within 20 characters on any machine.

Verified: the manifest parses on mesh-controller main and #149 and
renders for a zurag.be node (F1_CIRCUITS_BASE_URL=https://f1-circuits.
zurag.be). Built from this commit and run on an empty cache: /circuits,
/circuits/<country> and a rendered Spa track are byte-identical to what
ace serves today; the base URL comes from the variable, and without it
falls back to the old address.

Depends on mesh-controller #149.
This commit is contained in:
2026-09-30 12:05:36 +02:00
parent 71feafa345
commit 6eb693203d
4 changed files with 125 additions and 10 deletions
+82
View File
@@ -0,0 +1,82 @@
{
"module": "f1-circuits",
"slug": "f1",
"version": "1",
"capabilities": [
"container-runtime"
],
"requires": [
"route"
],
"contributes": {
"route": {
"label": "f1-circuits",
"endpoint": "web"
}
},
"binds": {
"route": "${dir:state}/route.json"
},
"listens": [
{
"name": "web",
"port": 8000,
"protocol": "tcp",
"from": "mesh",
"why": "the track-layout CDN, the race posters and the Kometa metadata over http; its public name is a route grant and the proxy reaches it here. Kometa fetches the metadata and every poster by that public name"
}
],
"resources": [
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": "."
},
{
"id": "cache",
"type": "directory",
"mode": "0700"
},
{
"id": "site-env",
"type": "file",
"path": "${dir:state}/site.env",
"mode": "0644",
"content": "F1_CIRCUITS_BASE_URL=https://${bound:route:name}\n"
},
{
"id": "server",
"type": "container",
"name": "f1-circuits",
"artifact": "cdn",
"ports": [
"8000"
],
"env-file": [
"${dir:state}/site.env"
],
"volumes": [
"${dir:cache}:/app/cache"
],
"restart-on": [
"site-env"
]
}
],
"build": {
"on": [
{
"arg": "PYTHON_BASE",
"image": "python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
}
],
"artifacts": [
{
"name": "cdn",
"kind": "image",
"from": "service/Dockerfile"
}
]
}
}
+8 -2
View File
@@ -16,6 +16,7 @@ import io
import json import json
import logging import logging
import math import math
import os
import urllib.request import urllib.request
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
@@ -61,8 +62,13 @@ TILE_PROVIDERS = {
}, },
} }
# The address this service is reached at, which the Kometa metadata links every poster to. The
# mesh writes it from the name the route is served under (F1_CIRCUITS_BASE_URL); the fallback is
# the address HAL served it at, so a deployment that sets nothing behaves as before.
PUBLIC_BASE_URL = os.environ.get("F1_CIRCUITS_BASE_URL", "https://f1-circuits.zurag.be").rstrip("/")
# Polite UA — both providers expect one and Esri throttles aggressively without it. # Polite UA — both providers expect one and Esri throttles aggressively without it.
TILE_USER_AGENT = "f1-circuits-posters/1.0 (+https://f1-circuits.zurag.be)" TILE_USER_AGENT = f"f1-circuits-posters/1.0 (+{PUBLIC_BASE_URL})"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -641,7 +647,7 @@ class PostersService:
# ------------------------------------------------------------------ kometa metadata # ------------------------------------------------------------------ kometa metadata
BASE_URL = "https://f1-circuits.zurag.be" BASE_URL = PUBLIC_BASE_URL
def render_kometa_metadata(self, first_season: int = 1950, last_offset: int = 1) -> str: def render_kometa_metadata(self, first_season: int = 1950, last_offset: int = 1) -> str:
"""Emit the Kometa metadata YAML for the Formula 1 library, covering """Emit the Kometa metadata YAML for the Formula 1 library, covering
+8 -8
View File
@@ -1,4 +1,8 @@
FROM python:3.12-alpine # The base arrives pinned from module.json's build.on (python 3.12.13 on alpine 3.24); the mesh
# builds this image from this repository and a commit (novox/hq ADR 0069). The build context is
# the repository root, so every COPY names its path from there.
ARG PYTHON_BASE
FROM ${PYTHON_BASE}
WORKDIR /app WORKDIR /app
@@ -14,13 +18,9 @@ RUN apk add --no-cache \
# All pip deps ship musllinux wheels — no build-deps needed. # All pip deps ship musllinux wheels — no build-deps needed.
# numpy is pulled in by models/geo_json/geometry.py for vector ops at import time. # numpy is pulled in by models/geo_json/geometry.py for vector ops at import time.
RUN pip install --no-cache-dir \ # The versions are the ones the running deployment had (service/requirements.txt).
fastapi \ COPY service/requirements.txt ./requirements.txt
uvicorn \ RUN pip install --no-cache-dir -r requirements.txt
python-slugify \
pillow \
cairosvg \
numpy
COPY cdn-api.py circuits_service.py ergast_service.py posters_service.py ./ COPY cdn-api.py circuits_service.py ergast_service.py posters_service.py ./
COPY models/ ./models/ COPY models/ ./models/
+27
View File
@@ -0,0 +1,27 @@
# The exact set the deployment ace ran on 2026-09-30 was built with (pip freeze inside it).
# Pinned so a rebuild from this commit installs what was running, not whatever is newest.
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.13.0
cairocffi==1.7.1
CairoSVG==2.9.0
cffi==2.0.0
click==8.4.1
cssselect2==0.9.0
defusedxml==0.7.1
fastapi==0.137.0
h11==0.16.0
idna==3.18
numpy==2.4.6
pillow==12.2.0
pycparser==3.0
pydantic==2.13.4
pydantic_core==2.46.4
python-slugify==8.0.4
starlette==1.3.1
text-unidecode==1.3
tinycss2==1.5.1
typing-inspection==0.4.2
typing_extensions==4.15.0
uvicorn==0.49.0
webencodings==0.5.1