From e8bfb4d06b025ed2e0e99388a772a07f19d299bc Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 5 Jun 2026 06:51:22 +0000 Subject: [PATCH] f1-stream: consume Forgejo-registry image; drop in-monorepo source The actively-developed f1-stream (infra files/ copy: 12 active extractors + Playwright/chrome-service verifier) is now its own repo viktor/f1-stream and is the deployed app (replacing the stale March github build). - main.tf: image -> forgejo.viktorbarzin.me/viktor/f1-stream:${var.image_tag} + image_pull_secrets registry-credentials. Image stays in KEEL_IGNORE_IMAGE. - Remove stacks/f1-stream/files/ (source now in viktor/f1-stream). - docs/plans: extraction design + plan pair. Applied via tg + kubectl set image to forgejo:24857a82; live /health green. Co-Authored-By: Claude Opus 4.8 --- .../2026-06-04-f1-stream-extraction-design.md | 75 + .../2026-06-04-f1-stream-extraction-plan.md | 39 + ...-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK | 3 - stacks/f1-stream/files/.dockerignore | 5 - stacks/f1-stream/files/.gitignore | 2 - stacks/f1-stream/files/Dockerfile | 44 - stacks/f1-stream/files/backend/__init__.py | 0 stacks/f1-stream/files/backend/embed_proxy.py | 359 --- .../files/backend/extractors/__init__.py | 93 - .../files/backend/extractors/aceztrims.py | 122 - .../files/backend/extractors/base.py | 118 - .../backend/extractors/chrome_browser.py | 247 -- .../files/backend/extractors/curated.py | 61 - .../files/backend/extractors/daddylive.py | 181 -- .../files/backend/extractors/dd12.py | 111 - .../files/backend/extractors/demo.py | 75 - .../backend/extractors/discord_source.py | 203 -- .../files/backend/extractors/hmembeds.py | 131 - .../files/backend/extractors/models.py | 39 - .../files/backend/extractors/pitsport.py | 595 ----- .../f1-stream/files/backend/extractors/ppv.py | 273 --- .../files/backend/extractors/registry.py | 116 - .../files/backend/extractors/service.py | 270 --- .../files/backend/extractors/streamed.py | 125 - .../files/backend/extractors/stremio.py | 161 -- .../files/backend/extractors/subreddit.py | 249 -- .../files/backend/extractors/timstreams.py | 190 -- stacks/f1-stream/files/backend/health.py | 301 --- .../f1-stream/files/backend/m3u8_rewriter.py | 264 -- stacks/f1-stream/files/backend/main.py | 488 ---- .../files/backend/playback_verifier.py | 478 ---- stacks/f1-stream/files/backend/proxy.py | 501 ---- .../f1-stream/files/backend/requirements.txt | 6 - stacks/f1-stream/files/backend/schedule.py | 240 -- stacks/f1-stream/files/backend/stealth.py | 43 - .../f1-stream/files/backend/token_refresh.py | 362 --- stacks/f1-stream/files/frontend/.gitignore | 3 - .../files/frontend/package-lock.json | 2140 ----------------- stacks/f1-stream/files/frontend/package.json | 23 - stacks/f1-stream/files/frontend/src/app.css | 35 - stacks/f1-stream/files/frontend/src/app.html | 13 - .../f1-stream/files/frontend/src/lib/api.js | 88 - .../files/frontend/src/lib/stores.js | 13 - .../files/frontend/src/routes/+layout.js | 3 - .../files/frontend/src/routes/+layout.svelte | 28 - .../files/frontend/src/routes/+page.svelte | 232 -- .../frontend/src/routes/watch/+page.svelte | 484 ---- .../f1-stream/files/frontend/svelte.config.js | 19 - .../f1-stream/files/frontend/vite.config.js | 10 - stacks/f1-stream/files/redeploy.sh | 7 - stacks/f1-stream/main.tf | 19 +- 51 files changed, 131 insertions(+), 9556 deletions(-) create mode 100644 docs/plans/2026-06-04-f1-stream-extraction-design.md create mode 100644 docs/plans/2026-06-04-f1-stream-extraction-plan.md delete mode 100644 stacks/f1-stream/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK delete mode 100644 stacks/f1-stream/files/.dockerignore delete mode 100644 stacks/f1-stream/files/.gitignore delete mode 100644 stacks/f1-stream/files/Dockerfile delete mode 100644 stacks/f1-stream/files/backend/__init__.py delete mode 100644 stacks/f1-stream/files/backend/embed_proxy.py delete mode 100644 stacks/f1-stream/files/backend/extractors/__init__.py delete mode 100644 stacks/f1-stream/files/backend/extractors/aceztrims.py delete mode 100644 stacks/f1-stream/files/backend/extractors/base.py delete mode 100644 stacks/f1-stream/files/backend/extractors/chrome_browser.py delete mode 100644 stacks/f1-stream/files/backend/extractors/curated.py delete mode 100644 stacks/f1-stream/files/backend/extractors/daddylive.py delete mode 100644 stacks/f1-stream/files/backend/extractors/dd12.py delete mode 100644 stacks/f1-stream/files/backend/extractors/demo.py delete mode 100644 stacks/f1-stream/files/backend/extractors/discord_source.py delete mode 100644 stacks/f1-stream/files/backend/extractors/hmembeds.py delete mode 100644 stacks/f1-stream/files/backend/extractors/models.py delete mode 100644 stacks/f1-stream/files/backend/extractors/pitsport.py delete mode 100644 stacks/f1-stream/files/backend/extractors/ppv.py delete mode 100644 stacks/f1-stream/files/backend/extractors/registry.py delete mode 100644 stacks/f1-stream/files/backend/extractors/service.py delete mode 100644 stacks/f1-stream/files/backend/extractors/streamed.py delete mode 100644 stacks/f1-stream/files/backend/extractors/stremio.py delete mode 100644 stacks/f1-stream/files/backend/extractors/subreddit.py delete mode 100644 stacks/f1-stream/files/backend/extractors/timstreams.py delete mode 100644 stacks/f1-stream/files/backend/health.py delete mode 100644 stacks/f1-stream/files/backend/m3u8_rewriter.py delete mode 100644 stacks/f1-stream/files/backend/main.py delete mode 100644 stacks/f1-stream/files/backend/playback_verifier.py delete mode 100644 stacks/f1-stream/files/backend/proxy.py delete mode 100644 stacks/f1-stream/files/backend/requirements.txt delete mode 100644 stacks/f1-stream/files/backend/schedule.py delete mode 100644 stacks/f1-stream/files/backend/stealth.py delete mode 100644 stacks/f1-stream/files/backend/token_refresh.py delete mode 100644 stacks/f1-stream/files/frontend/.gitignore delete mode 100644 stacks/f1-stream/files/frontend/package-lock.json delete mode 100644 stacks/f1-stream/files/frontend/package.json delete mode 100644 stacks/f1-stream/files/frontend/src/app.css delete mode 100644 stacks/f1-stream/files/frontend/src/app.html delete mode 100644 stacks/f1-stream/files/frontend/src/lib/api.js delete mode 100644 stacks/f1-stream/files/frontend/src/lib/stores.js delete mode 100644 stacks/f1-stream/files/frontend/src/routes/+layout.js delete mode 100644 stacks/f1-stream/files/frontend/src/routes/+layout.svelte delete mode 100644 stacks/f1-stream/files/frontend/src/routes/+page.svelte delete mode 100644 stacks/f1-stream/files/frontend/src/routes/watch/+page.svelte delete mode 100644 stacks/f1-stream/files/frontend/svelte.config.js delete mode 100644 stacks/f1-stream/files/frontend/vite.config.js delete mode 100755 stacks/f1-stream/files/redeploy.sh diff --git a/docs/plans/2026-06-04-f1-stream-extraction-design.md b/docs/plans/2026-06-04-f1-stream-extraction-design.md new file mode 100644 index 00000000..1e8bdb63 --- /dev/null +++ b/docs/plans/2026-06-04-f1-stream-extraction-design.md @@ -0,0 +1,75 @@ +# f1-stream extraction + productionization — design (2026-06-04) + +## Problem + +The actively-developed f1-stream codebase (FastAPI backend serving a SvelteKit +SPA; ~19 pluggable stream extractors + a Playwright/chrome-service playback +verifier) lived **inside** the infra monorepo at +`infra/stacks/f1-stream/files/`. It had no standalone repo, no real CI (only a +manual `redeploy.sh` doing a local `docker buildx` push), no tests, a loose +unpinned `requirements.txt`, and no semver. + +**Key gotcha (the source-of-truth confusion):** there is ALSO an older +`github.com/ViktorBarzin/f1-stream` (`main`, last commit 2026-03-29, 14 +extractors, no verifier) — and the *currently-deployed* image +(`viktorbarzin/f1-stream:`, Keel-managed) is built from THAT github repo, +not from `files/`. So the `files/` copy was the newer, richer, but +**never-properly-deployed** version. Viktor confirmed (2026-06-05) the +`files/` version is the one to ship; this extraction makes it the canonical +repo AND finally deploys it (changing the live app from the stale March build +to the current code). + +## Goal + +Extract `files/` into its own Forgejo repo `viktor/f1-stream`, productionize it +(Poetry, ruff, mypy, pragmatic tests, README, semver `v2.0.1`, Woodpecker CI), +point the infra Terraform stack at the Forgejo image, and remove `files/`. + +## Decisions + +- **Registry → Forgejo private** (`forgejo.viktorbarzin.me/viktor/f1-stream`). + Deployment gets `image_pull_secrets { registry-credentials }`. +- **Packaging → Poetry + ruff + mypy** (Poetry 2.1.3, lock committed). Python + **package stays `backend`** (imports + `uvicorn backend.main:app`). **Python + 3.13** kept. +- **Tests → pragmatic pure-logic only**: m3u8_rewriter, the proxy HLS parsers, + schedule parsing/status, extractor registry. 63 tests; ruff + mypy clean. +- **CI → single `.woodpecker.yml`**: lint+type+test → buildx push to Forgejo + (tags `latest` + ``) → `kubectl set image` + rollout. Keel stays + enrolled as a redundant net. (No Slack step — the `environment:{from_secret}` + form is rejected by this Woodpecker version's decoder.) +- **Dockerfile → no bundled Chromium.** In-cluster the verifier drives the + shared chrome-service over CDP and never launches a local browser. Bundling + Chromium broke the in-cluster buildkit build (`playwright install chromium` + times out fetching ~165MB from the Azure CDN through cluster egress). The + `playwright` pip package stays for the CDP client. +- **Versioning → first git tag `v2.0.1`** (continuity with the existing image + lineage), deviating deliberately from the `v0.1.0` precedent. +- **Runtime stays root** (matching the prior working image) to avoid an NFS / + Chromium-cache regression. + +## Terraform delta (only infra change) + +`stacks/f1-stream/main.tf`: image → `forgejo.../viktor/f1-stream:${var.image_tag}` +(new `var.image_tag`, default `latest`) + `image_pull_secrets`; remove `files/` +and `redeploy.sh`. Image field stays in `ignore_changes` (KEEL_IGNORE_IMAGE); +the running tag is managed by CI/Keel/`kubectl set image`, not Terraform. +Everything else (Anubis, ingress, ExternalSecrets, NFS, chrome-service + +Discord env) unchanged. + +## Operational notes / known rough edges (2026-06-05) + +- The Woodpecker repo (id 166) was registered via the JWT-mint script and its + config-fetch user association is currently broken (`user does not exist + [uid:0]`) — pipelines error. Until re-enabled via the Woodpecker UI OAuth, + the image is built+pushed manually from the devvm. +- The infra repo's `origin` (github) and `forgejo` (CI-canonical) remotes are + diverged; this change is applied via `scripts/tg apply` locally and committed; + landing it on `forgejo/master` for CI durability depends on the normal + origin↔forgejo reconciliation. + +## Blast radius + +The `f1-stream` K8s service is the only consumer; no `.tf` references `files/`. +Switching the live image to the Forgejo build is the intended, user-approved +behavior change (stale March build → current code). diff --git a/docs/plans/2026-06-04-f1-stream-extraction-plan.md b/docs/plans/2026-06-04-f1-stream-extraction-plan.md new file mode 100644 index 00000000..84eb4f74 --- /dev/null +++ b/docs/plans/2026-06-04-f1-stream-extraction-plan.md @@ -0,0 +1,39 @@ +# f1-stream extraction + productionization — plan (2026-06-04) + +Companion to `2026-06-04-f1-stream-extraction-design.md`. + +## Steps + +1. Scaffold `/home/wizard/code/f1-stream/` from `infra/stacks/f1-stream/files/` + (backend/, frontend/, Dockerfile by name; add README, .gitignore). ✅ +2. Poetry conversion (pyproject v2.0.1, `packages=[{include="backend"}]`, lock, + ruff/mypy/pytest config; E501 per-file-ignored on the JS/scraper modules). ✅ +3. 63 pytest unit tests over the pure-logic core; ruff + mypy clean. ✅ +4. Dockerfile: Poetry multi-stage, **no bundled Chromium** (CDP-only). ✅ +5. `.woodpecker.yml`: lint+test → buildx push to Forgejo → kubectl set image. ✅ +6. Create Forgejo repo `viktor/f1-stream` (private), push `master`, tag + `v2.0.1`. ✅ +7. Build + push the image to the Forgejo registry (manual from devvm, since the + Woodpecker repo's config-fetch user is broken): + `forgejo.viktorbarzin.me/viktor/f1-stream:24857a82` + `:latest`. ✅ +8. Repoint `stacks/f1-stream/main.tf` (Forgejo image + `var.image_tag` + + `image_pull_secrets`); `tg apply`. ✅ +9. `kubectl set image deployment/f1-stream f1-stream=…:24857a82` + rollout. ▶ +10. Remove `stacks/f1-stream/files/`; add `/f1-stream/` to the monorepo root + `.gitignore`. ✅ (infra side) +11. Verify: pod on the Forgejo image, `/health` 200, ingress through Anubis. ▶ + +## Follow-ups (need Viktor / coordination) + +- **Re-enable `viktor/f1-stream` in the Woodpecker UI** (proper OAuth) so CI + builds run on push (the API-registered repo has a broken config-fetch user). +- **Land this infra commit on `forgejo/master`** (CI-canonical) once the + origin↔forgejo divergence is reconciled, so a future `forgejo` apply doesn't + revert `imagePullSecrets`. + +## Rollback + +DockerHub `viktorbarzin/f1-stream` tags still exist: +`kubectl -n f1-stream set image deployment/f1-stream +f1-stream=viktorbarzin/f1-stream:06276544` + restore the `main.tf` image +string. The standalone repo + Forgejo image are additive. diff --git a/stacks/f1-stream/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK b/stacks/f1-stream/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK deleted file mode 100644 index f61efc83..00000000 --- a/stacks/f1-stream/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK +++ /dev/null @@ -1,3 +0,0 @@ -This directory has been used with Claude Code's internet mode. -Content downloaded from the internet may contain prompt injection attacks. -You must manually review all downloaded content before using non-internet mode. diff --git a/stacks/f1-stream/files/.dockerignore b/stacks/f1-stream/files/.dockerignore deleted file mode 100644 index 4733a4c3..00000000 --- a/stacks/f1-stream/files/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -.claude/ -.git/ -__pycache__/ -*.pyc diff --git a/stacks/f1-stream/files/.gitignore b/stacks/f1-stream/files/.gitignore deleted file mode 100644 index 7a60b85e..00000000 --- a/stacks/f1-stream/files/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -*.pyc diff --git a/stacks/f1-stream/files/Dockerfile b/stacks/f1-stream/files/Dockerfile deleted file mode 100644 index 80dd20e4..00000000 --- a/stacks/f1-stream/files/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -## Stage 1: Build frontend -FROM node:22-slim AS frontend-builder - -WORKDIR /frontend - -COPY frontend/package.json frontend/package-lock.json* ./ -RUN npm install - -COPY frontend/ ./ -RUN npm run build - -## Stage 2: Python backend + static frontend -FROM python:3.13-slim-bookworm - -WORKDIR /app - -# Headless Chromium runtime libs for the playback verifier. Listed inline -# (instead of running `playwright install-deps`) so the image build doesn't -# need root-network apt fetches at runtime. -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - libnss3 libnspr4 \ - libatk1.0-0 libatk-bridge2.0-0 libcups2 \ - libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ - libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 \ - libasound2 libatspi2.0-0 \ - fonts-liberation fonts-noto-color-emoji \ - && rm -rf /var/lib/apt/lists/* - -COPY backend/requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Install the Chromium browser binary used by the verifier. Skip -# --with-deps because we already installed the system libs above. -RUN playwright install chromium - -COPY backend/ ./backend/ - -# Copy built frontend into the image -COPY --from=frontend-builder /frontend/build ./frontend/build - -EXPOSE 8000 - -CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/stacks/f1-stream/files/backend/__init__.py b/stacks/f1-stream/files/backend/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stacks/f1-stream/files/backend/embed_proxy.py b/stacks/f1-stream/files/backend/embed_proxy.py deleted file mode 100644 index 34ccb28c..00000000 --- a/stacks/f1-stream/files/backend/embed_proxy.py +++ /dev/null @@ -1,359 +0,0 @@ -"""Embed iframe-stripping reverse proxy. - -Serves third-party embed pages (e.g. https://hmembeds.one/embed/{hash}, -https://pooembed.eu/embed/{slug}) through our origin so we can: - -1. Strip X-Frame-Options and Content-Security-Policy: frame-ancestors headers, - so the embed loads in our - {:else} - - {/if} - - -
-
- - - - setVolume(i, e)} - class="w-16 h-1 accent-f1-red" - aria-label="Volume" - /> - -
- - -
-
- - - {#if player.error} -
- {player.error} -
- {/if} - - {/each} - - {/if} - - - {#if loading} -
-
- Loading streams... -
- {:else if errorMsg} -
-

Failed to load streams: {errorMsg}

- -
- {:else if streamsData} -
-

- Available Streams - ({streamsData.count}) -

-
- {#if players.length > 0} - {players.length}/{MAX_PLAYERS} streams active - {/if} - -
-
- - {#if streamsData.streams.length === 0} -
-

No streams available right now.

-

Streams appear when a session is live. Check the schedule for upcoming sessions.

- - View Schedule - -
- {:else} -
- {#each streamsData.streams as stream, i} - {@const active = isStreamActive(stream.stream_type === 'embed' ? stream.embed_url : stream.url)} -
-
-
- {stream.site_name || stream.site_key || 'Unknown'} - {#if stream.is_live} - Live - {/if} - {#if stream.stream_type === 'embed'} - Embed - {/if} - {#if active} - Playing - {/if} -
-
- {#if stream.title} - {stream.title} - {/if} - {#if stream.quality} - {stream.quality} - {/if} - {#if stream.response_time_ms != null} - - {stream.response_time_ms}ms - - {/if} -
-
- -
- {#if !active} - - {:else} - Active - {/if} -
-
- {/each} -
- {/if} - {/if} - diff --git a/stacks/f1-stream/files/frontend/svelte.config.js b/stacks/f1-stream/files/frontend/svelte.config.js deleted file mode 100644 index 9088ef3b..00000000 --- a/stacks/f1-stream/files/frontend/svelte.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - pages: 'build', - assets: 'build', - fallback: 'index.html', - precompress: false, - strict: true - }), - paths: { - base: '' - } - } -}; - -export default config; diff --git a/stacks/f1-stream/files/frontend/vite.config.js b/stacks/f1-stream/files/frontend/vite.config.js deleted file mode 100644 index a39ec5c1..00000000 --- a/stacks/f1-stream/files/frontend/vite.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit() - ] -}); diff --git a/stacks/f1-stream/files/redeploy.sh b/stacks/f1-stream/files/redeploy.sh deleted file mode 100755 index e436a6ce..00000000 --- a/stacks/f1-stream/files/redeploy.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e - -docker buildx build --platform linux/amd64 --provenance=false \ - -t viktorbarzin/f1-stream:v2.0.1 -t viktorbarzin/f1-stream:latest \ - --push . -kubectl -n f1-stream rollout restart deployment f1-stream diff --git a/stacks/f1-stream/main.tf b/stacks/f1-stream/main.tf index ff64af71..26d97eeb 100644 --- a/stacks/f1-stream/main.tf +++ b/stacks/f1-stream/main.tf @@ -6,6 +6,16 @@ variable "nfs_server" { type = string } variable "discord_f1_guild_id" { type = string } variable "discord_f1_channel_ids" { type = string } +# Image tag for the Forgejo-registry image. The app lives in its own repo +# (viktor/f1-stream, extracted 2026-06-04). CI builds + pushes `latest` and +# ``, then drives the rollout via `kubectl set image`. Keel stays +# enrolled as a redundant net, so the running tag is managed outside Terraform +# (see KEEL_IGNORE_IMAGE below). +variable "image_tag" { + type = string + default = "latest" +} + resource "kubernetes_namespace" "f1-stream" { metadata { name = "f1-stream" @@ -13,7 +23,7 @@ resource "kubernetes_namespace" "f1-stream" { "istio-injection" : "disabled" tier = local.tiers.aux "chrome-service.viktorbarzin.me/client" = "true" - "keel.sh/enrolled" = "true" + "keel.sh/enrolled" = "true" } } lifecycle { @@ -118,7 +128,7 @@ resource "kubernetes_deployment" "f1-stream" { } spec { container { - image = "viktorbarzin/f1-stream:latest" + image = "forgejo.viktorbarzin.me/viktor/f1-stream:${var.image_tag}" image_pull_policy = "Always" name = "f1-stream" resources { @@ -176,6 +186,11 @@ resource "kubernetes_deployment" "f1-stream" { claim_name = module.nfs_data_host.claim_name } } + # Pull the (private) Forgejo-registry image. Kyverno syncs + # registry-credentials into every namespace. + image_pull_secrets { + name = "registry-credentials" + } } } }