From 6eb693203dfb70d0f8159f1c19cad36332946790 Mon Sep 17 00:00:00 2001 From: jochens Date: Wed, 30 Sep 2026 12:05:36 +0200 Subject: [PATCH] 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/ 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. --- module.json | 82 ++++++++++++++++++++++++++++++++++++++++ posters_service.py | 10 ++++- service/Dockerfile | 16 ++++---- service/requirements.txt | 27 +++++++++++++ 4 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 module.json create mode 100644 service/requirements.txt diff --git a/module.json b/module.json new file mode 100644 index 0000000..8f2b612 --- /dev/null +++ b/module.json @@ -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" + } + ] + } +} diff --git a/posters_service.py b/posters_service.py index 9df3981..51509a6 100644 --- a/posters_service.py +++ b/posters_service.py @@ -16,6 +16,7 @@ import io import json import logging import math +import os import urllib.request from enum import Enum 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. -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__) @@ -641,7 +647,7 @@ class PostersService: # ------------------------------------------------------------------ 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: """Emit the Kometa metadata YAML for the Formula 1 library, covering diff --git a/service/Dockerfile b/service/Dockerfile index 9cdeb11..385eb00 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -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 @@ -14,13 +18,9 @@ RUN apk add --no-cache \ # 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. -RUN pip install --no-cache-dir \ - fastapi \ - uvicorn \ - python-slugify \ - pillow \ - cairosvg \ - numpy +# The versions are the ones the running deployment had (service/requirements.txt). +COPY service/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt COPY cdn-api.py circuits_service.py ergast_service.py posters_service.py ./ COPY models/ ./models/ diff --git a/service/requirements.txt b/service/requirements.txt new file mode 100644 index 0000000..4f78ed7 --- /dev/null +++ b/service/requirements.txt @@ -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