commit 30d2cf84f5a52ef953f7b6994eca97d4f29d0462 parent d1f9970bc047b12c02b8f60173a080131aa17ba5 Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st> Date: Mon, 28 Sep 2026 09:45:39 -0400 Merge r11/phase-4-s3 (follow-up O6c) — the operator's ruling: the Docker build-mode stub is dropped (the setting, the /sites toggle and the Settings select); Build all still builds in containers whenever a container engine answers; an old settings.json with the key still loads Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Diffstat:
26 files changed, 227 insertions(+), 263 deletions(-)
diff --git a/Dockerfile.build b/Dockerfile.build @@ -1,4 +1,4 @@ -# Build image for the docker export pipeline (buildPipeline.mode = "docker"). +# Build image for the docker export pipeline (Build all, whenever a container engine answers). # # Bakes the repo source + installed deps so each per-site build container is # hermetic and reproducible. The corpus, the shared LMDB index, the .export-index diff --git a/PUBLISH.md b/PUBLISH.md @@ -360,9 +360,8 @@ any limit. deploys them serially — a large speedup when you host several sites, and stronger isolation than building one site at a time in `export/`. It does so **whenever a container engine answers** (`docker version`), and builds serially on the host when -none does. The **Build pipeline → mode** (Basic / Docker, in Settings or on the -/sites toggle) is persisted and shown, but no build reads it today: it is a label, -not a switch. A single site's build always runs in `export/`, one at a time. +none does. There is no mode to set. A single site's build always runs in `export/`, +one at a time. **Prerequisites.** diff --git a/RUNNING_IN_DOCKER.md b/RUNNING_IN_DOCKER.md @@ -541,8 +541,8 @@ silently loses formats), `ffmpeg`/`ffprobe`, `whisper-cli` (statically linked), ### The multi-site build pipeline falls back inside a container -The editor can fan per-site export builds out across containers -(`buildPipeline.mode = "docker"`, see [PUBLISH.md](PUBLISH.md#building-every-site-in-containers)). Inside a container there +The editor's **Build all sites** fans per-site export builds out across containers +whenever a container engine answers (see [PUBLISH.md](PUBLISH.md#building-every-site-in-containers)). Inside a container there is no `docker` binary, so that path is unavailable. It already handles this — the build logs diff --git a/SETTINGS.md b/SETTINGS.md @@ -556,14 +556,13 @@ Default: ## `buildPipeline` -The build pipeline's settings. A single site builds in the shared export/ tree, serialized on one queue. Build all sites (and Build & deploy all) builds every site at once, each in its own container (Dockerfile.build, the image and maxParallelBuilds below), then deploys them serially, whenever a container engine answers, and serially on the host when none does — see PUBLISH.md. `mode` is persisted and shown on /sites, but no build path reads it today: it is a label, not a switch. +The build pipeline's settings. A single site builds in the shared export/ tree, serialized on one queue. Build all sites (and Build & deploy all) builds every site at once, each in its own container (Dockerfile.build, the image and maxParallelBuilds below), then deploys them serially, whenever a container engine answers, and serially on the host when none does — see PUBLISH.md. There is no mode switch; a `mode` key left in an older file is dropped on the next save. #### `buildPipeline` | Key | Default | Description | |---|---|---| -| `mode` | `"basic"` | "basic" or "docker". Persisted and shown on /sites, but no build path reads it today — a label, not a switch: Build all sites uses containers whenever a container engine answers, and builds serially on the host when none does, in either mode. | -| `maxParallelBuilds` | `2` | Cap on concurrent per-site container builds when Build all sites runs in containers (whenever a container engine answers, whatever `mode` says). Clamped to [1, BUILD_MAX_PARALLEL_MAX]. | +| `maxParallelBuilds` | `2` | Cap on concurrent per-site container builds when Build all sites runs in containers (whenever a container engine answers). Clamped to [1, BUILD_MAX_PARALLEL_MAX]. | | `dockerImage` | `"yt-dlp-transcript-browser-build"` | Tag of the reusable build image (built once, reused for every site). | | `dockerfile` | `"Dockerfile.build"` | Dockerfile path relative to the monorepo root, used to (re)build the image. | @@ -571,7 +570,6 @@ Default: ```json { - "mode": "basic", "maxParallelBuilds": 2, "dockerImage": "yt-dlp-transcript-browser-build", "dockerfile": "Dockerfile.build" diff --git a/common/lib/settingsSchema.test.ts b/common/lib/settingsSchema.test.ts @@ -171,6 +171,31 @@ function checkField( } // min / max / NaN / string / null / absent, for each clamped top-level field. +// THE RETIRED `buildPipeline.mode` (release 11, follow-up O6c). It was a label +// nothing read. A file that still has it — the operator's says "basic" — must +// load, keep the three real fields, and carry no `mode` onward. +test("a buildPipeline that still carries the retired mode loads, and the mode is gone", () => { + for (const mode of ["basic", "docker", "nonsense", 3]) { + const clean = S.sanitizeBuildPipeline({ + mode, + maxParallelBuilds: 3, + dockerImage: " my-image ", + dockerfile: "Dockerfile.build", + }); + assert.deepEqual(clean, { + maxParallelBuilds: 3, + dockerImage: "my-image", + dockerfile: "Dockerfile.build", + }); + assert.equal("mode" in clean, false, String(mode)); + } + // Through the whole-file parse, as every process reads it. + const parsed = siteSettingsSchema.parse({ buildPipeline: { mode: "basic", maxParallelBuilds: 5 } }); + assert.equal("mode" in parsed.buildPipeline, false); + assert.equal(parsed.buildPipeline.maxParallelBuilds, 5); + assert.equal("mode" in defaults().buildPipeline, false); +}); + test("maxTranscriptPageBytes clamps into [256 KiB, 20 MiB]", () => { checkField("maxTranscriptPageBytes", [ [0, S.TRANSCRIPT_PAGE_MIN_BYTES], diff --git a/common/lib/settingsSchema.ts b/common/lib/settingsSchema.ts @@ -414,28 +414,25 @@ export const DIGEST_SETTINGS_FIELD_DOCS: FieldDocs<DigestSettings> = { "fresh. Empty = default.", }; -// "basic" | "docker" — persisted and shown on /sites, but A LABEL TODAY: no -// build path reads it. Build all / Build & deploy all (editor buildAction.ts) -// fan out in containers (publish/build.ts, runDockerBuildAllPhase, capped by -// maxParallelBuilds) whenever `docker version` answers, and build serially on -// the host otherwise, whichever mode is set. Whether they should honour it is -// an open question for the operator (plans/release-11.md, slice O6). -export type BuildMode = "basic" | "docker"; - +// Build all / Build & deploy all (editor buildAction.ts, `archilyzer build all`) +// fan out in containers (publish/build.ts, runDockerBuildAllPhase) whenever +// `docker version` answers, and build serially on the host otherwise. There is +// no mode switch: the `mode` key ("basic" | "docker") was a label nothing read, +// and it was dropped on 2026-09-28 (release 11, follow-up O6c). A settings.json +// that still carries it loads, and loses it on the next save — the sanitizer +// below builds its output from the three real fields only. +// // Each field is documented in BUILD_PIPELINE_SETTINGS_FIELD_DOCS below (rendered into SETTINGS.md). export type BuildPipelineSettings = { - mode: BuildMode; maxParallelBuilds: number; dockerImage: string; dockerfile: string; }; export const BUILD_PIPELINE_SETTINGS_FIELD_DOCS: FieldDocs<BuildPipelineSettings> = { - mode: - "\"basic\" or \"docker\". Persisted and shown on /sites, but no build path reads it today — a label, not a switch: Build all sites uses containers whenever a container engine answers, and builds serially on the host when none does, in either mode.", maxParallelBuilds: "Cap on concurrent per-site container builds when Build all sites runs in" + - " containers (whenever a container engine answers, whatever `mode` says)." + + " containers (whenever a container engine answers)." + " Clamped to [1, " + "BUILD_MAX_PARALLEL_MAX].", dockerImage: @@ -1091,13 +1088,8 @@ export const BUILD_MAX_PARALLEL_MAX = 16; export const DEFAULT_BUILD_IMAGE = "yt-dlp-transcript-browser-build"; export const DEFAULT_BUILD_DOCKERFILE = "Dockerfile.build"; -export function isBuildMode(v: unknown): v is BuildMode { - return v === "basic" || v === "docker"; -} - export function defaultBuildPipeline(): BuildPipelineSettings { return { - mode: "basic", maxParallelBuilds: BUILD_MAX_PARALLEL_DEFAULT, dockerImage: DEFAULT_BUILD_IMAGE, dockerfile: DEFAULT_BUILD_DOCKERFILE, @@ -1105,7 +1097,8 @@ export function defaultBuildPipeline(): BuildPipelineSettings { } // Coerce a raw settings.buildPipeline value into a clean BuildPipelineSettings, -// falling back to defaults for missing/ill-typed fields. +// falling back to defaults for missing/ill-typed fields. Built from the three +// known fields only, so a retired key (the old `mode`) never survives. export function sanitizeBuildPipeline(value: unknown): BuildPipelineSettings { const d = defaultBuildPipeline(); if (!value || typeof value !== "object") return d; @@ -1119,7 +1112,6 @@ export function sanitizeBuildPipeline(value: unknown): BuildPipelineSettings { ? r.dockerfile.trim() : d.dockerfile; return { - mode: isBuildMode(r.mode) ? r.mode : d.mode, maxParallelBuilds: clampPositiveInt( r.maxParallelBuilds, d.maxParallelBuilds, @@ -1612,7 +1604,7 @@ export const siteSettingsSchema = z.object({ "Where a channel's downloaded media goes when it is relocated off the corpus disk. A DEFAULT ONLY: the relocate controller never reads it and always takes an explicit root, so this is the value the per-channel Storage panel prefills and the /channels bulk move falls back to. Blank = no default. See StorageSettings.", ), buildPipeline: settingsField((v): BuildPipelineSettings => sanitizeBuildPipeline(v)).describe( - "The build pipeline's settings. A single site builds in the shared export/ tree, serialized on one queue. Build all sites (and Build & deploy all) builds every site at once, each in its own container (Dockerfile.build, the image and maxParallelBuilds below), then deploys them serially, whenever a container engine answers, and serially on the host when none does — see PUBLISH.md. `mode` is persisted and shown on /sites, but no build path reads it today: it is a label, not a switch.", + "The build pipeline's settings. A single site builds in the shared export/ tree, serialized on one queue. Build all sites (and Build & deploy all) builds every site at once, each in its own container (Dockerfile.build, the image and maxParallelBuilds below), then deploys them serially, whenever a container engine answers, and serially on the host when none does — see PUBLISH.md. There is no mode switch; a `mode` key left in an older file is dropped on the next save.", ), digest: settingsField((v): DigestSettings => sanitizeDigest(v)).describe( "AI digest generation (chapters + topic tags over the existing transcripts). Local-first: the metered lane is off by default. See DigestSettings.", diff --git a/common/lib/settingsWrite.test.ts b/common/lib/settingsWrite.test.ts @@ -92,3 +92,32 @@ test("a held lane survives its own round trip", async () => { assert.equal(isGateHeld(back, "digest"), true); assert.equal(isGateHeld(back, "backfill"), false); }); + +// The retired `buildPipeline.mode` (release 11, follow-up O6c): a file on disk +// that still says it loads without error, and the next save drops it while +// keeping the three real fields. +test("a settings.json with the retired buildPipeline.mode loads, and a save drops the key", async () => { + const { writeFileSync } = await import("node:fs"); + const { getSettings } = await import("./settings"); + writeFileSync( + process.env.SETTINGS_FILE!, + JSON.stringify({ + buildPipeline: { mode: "basic", maxParallelBuilds: 3, dockerImage: "kept-image", dockerfile: "Dockerfile.build" }, + }), + ); + const loaded = getSettings(); + assert.equal("mode" in loaded.buildPipeline, false); + assert.equal(loaded.buildPipeline.maxParallelBuilds, 3); + assert.equal(loaded.buildPipeline.dockerImage, "kept-image"); + + await writeSettings(loaded); + const onDisk = JSON.parse(readFileSync(process.env.SETTINGS_FILE!, "utf8")) as { + buildPipeline: Record<string, unknown>; + }; + assert.equal("mode" in onDisk.buildPipeline, false); + assert.deepEqual(onDisk.buildPipeline, { + maxParallelBuilds: 3, + dockerImage: "kept-image", + dockerfile: "Dockerfile.build", + }); +}); diff --git a/common/publish/build.ts b/common/publish/build.ts @@ -375,7 +375,7 @@ export async function runDeployIntoLog( } // --------------------------------------------------------------------------- -// Docker export pipeline (buildPipeline.mode = "docker") +// Docker export pipeline (Build all, whenever a container engine answers) // // Three ordered phases (see PUBLISH.md, "Building every site in containers"): // A) HOST, serial: build:data (shared LMDB + .export-index) then build:archives diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -6,8 +6,8 @@ - **"Persist source video" or a whole-recording fetch that cannot get the source no longer marks the video's download failed.** When YouTube's subtitles came down but the source video did not, the video page said "Download failed" over a transcript that is fine. The download now keeps the subtitle pass's result and records only the failed media attempt, with yt-dlp's reason. The run itself now ends failed with that reason; it used to end done with no file, so `fetch_clip` could only say the job "finished but named no file". A partial source file is left for a retry to resume, and the run's log names it. - **A bucket's retry keeps its log when it empties the bucket.** On a channel's Download stage, "Download with cookies", the partial-download resume and the missing-transcript retry could lose their run log part-way: the video they fetched left the bucket, the page refreshed, and the card disappeared with the log in it. The card now stays, with its log and its button disabled, until the page is reloaded. The Transcribe stage's "Fetch audio" button does the same. (A Diagnostics card still disappears, log and all, when its retry empties it.) - **A release cut whose commit fails still refreshes the pages.** When the changelog's new heading was written but the commit after it failed, the Cut release form and `pnpm ops cut-release` answered as if nothing had happened and no page showed the new heading until a reload. Both now refresh the changelog pages, and `pnpm ops cut-release` says the file was written. Every refused cut's answer says whether anything was written (`untouched`), and a cut of both changelogs that stopped half-way names the one already cut as well as the failure. -- **`archilyzer` checks the machine, runs one operation offline, starts the MCP server, and is one command from the repo root.** `pnpm archilyzer <command>` is the short form (`pnpm archilyzer --help` lists them all). `pnpm archilyzer doctor` is a read-only report: Node, the checkout, the corpus and whether each channel's media is reachable, `settings.json`, every tool the paths name plus each enabled worker's engine and model, umtool's report-pipeline tools, and this checkout's ports; it exits 1 only for something the machine is set up to do and cannot. `pnpm archilyzer run <operation> <channel> [ids…]` runs diarization, either attribution pass or digest over one channel as the editor's job does (a job record and log under `.jobs/`, the same summary line, the same refusal for an unmounted drive); sync, the metadata scan, downloads and transcription are refused with the reason, because they run on the editor's paced download queue and worker pool. `pnpm archilyzer mcp` starts the MCP server, so it can be registered as `-- pnpm -C "$PWD" archilyzer mcp`. Every other script in `common/bin/` is a subcommand too (`duplicates`, `posts fetch`, `digest plan`, `verify transcripts`, …), and export's `detect:duplicates` script is now `archilyzer duplicates`. Every environment variable is listed, by audience, in the new `ENVIRONMENT.md`, and `DEPLOY_CLOUDFLARE.md` and `DEPLOY_DOCKER.md` are now one `PUBLISH.md`. Settings and `/sites` no longer call the Docker build mode a follow-up, and say what it is: a label. **Build all sites** builds in containers whenever a container engine answers, whichever mode is set. -- **Settings' build mode says it is a label, and the test-only environment variables start with `E2E_`.** The Build mode choices now read *Basic (a label for now)* and *Docker (a label for now)*, and the **Build all sites** section and the per-site panel on `/sites` say the same: Build all builds every site in parallel in containers whenever a container engine answers, whatever the mode, and single-site builds run one at a time. For whoever runs the test suites: every variable only a test harness reads is renamed with an `E2E_` prefix and listed in its package's `playwright.config.ts` (and in `ENVIRONMENT.md`); `SHARDS=N pnpm e2e:sharded` is now `E2E_SHARDS=N`. +- **`archilyzer` checks the machine, runs one operation offline, starts the MCP server, and is one command from the repo root.** `pnpm archilyzer <command>` is the short form (`pnpm archilyzer --help` lists them all). `pnpm archilyzer doctor` is a read-only report: Node, the checkout, the corpus and whether each channel's media is reachable, `settings.json`, every tool the paths name plus each enabled worker's engine and model, umtool's report-pipeline tools, and this checkout's ports; it exits 1 only for something the machine is set up to do and cannot. `pnpm archilyzer run <operation> <channel> [ids…]` runs diarization, either attribution pass or digest over one channel as the editor's job does (a job record and log under `.jobs/`, the same summary line, the same refusal for an unmounted drive); sync, the metadata scan, downloads and transcription are refused with the reason, because they run on the editor's paced download queue and worker pool. `pnpm archilyzer mcp` starts the MCP server, so it can be registered as `-- pnpm -C "$PWD" archilyzer mcp`. Every other script in `common/bin/` is a subcommand too (`duplicates`, `posts fetch`, `digest plan`, `verify transcripts`, …), and export's `detect:duplicates` script is now `archilyzer duplicates`. Every environment variable is listed, by audience, in the new `ENVIRONMENT.md`, and `DEPLOY_CLOUDFLARE.md` and `DEPLOY_DOCKER.md` are now one `PUBLISH.md`. +- **The build-mode toggle and setting are gone, and the test-only environment variables start with `E2E_`.** The Basic / Docker choice on `/sites` and in **Settings → Build pipeline** was a label nothing read, so it is removed; a `settings.json` that still has `buildPipeline.mode` loads as before and drops the key on the next save. **Build all sites** builds every site in parallel in containers whenever a container engine answers, and serially on the host when none does, as it always did; a single site's build runs one at a time. Max parallel builds, the image tag and the Dockerfile path stay. For whoever runs the test suites: every variable only a test harness reads is renamed with an `E2E_` prefix and listed in its package's `playwright.config.ts` (and in `ENVIRONMENT.md`); `SHARDS=N pnpm e2e:sharded` is now `E2E_SHARDS=N`. - **In high-contrast mode the sidebar's Archilyzer mark keeps its edge.** In Windows' high-contrast mode (forced colours) the reader's own background replaces the page on every ground and can be as dark as the mark's slate tile, whose thin ring is only drawn on Dark. In that mode the tile now gets a 1-pixel outline in the reader's text colour, on every ground, following its rounded corners. Nothing changes outside that mode. ## [0.9.4] - 2026-09-28 diff --git a/editor/app/settings/actions.ts b/editor/app/settings/actions.ts @@ -6,7 +6,6 @@ import { AUTO_REFRESH_INTERVAL_MIN_SECONDS, DEFAULT_REPORT_DEBOUNCE_PRESET, defaultBuildPipeline, - isBuildMode, isReportDebouncePreset, MIN_FREE_DISK_GB_MAX, RESUME_MARGIN_GB_DEFAULT, @@ -180,12 +179,9 @@ export async function saveSettingsAction( } // Build pipeline. Values are clamped/coerced by sanitizeBuildPipeline inside - // the settings schema on save, so we only read the form here (NaN/blank → default). The - // deploy-page toggle also writes `mode`; whichever saves last wins. + // the settings schema on save, so we only read the form here (NaN/blank → default). const dB = defaultBuildPipeline(); - const buildModeRaw = String(formData.get("buildMode") ?? "").trim(); const buildPipeline = { - mode: isBuildMode(buildModeRaw) ? buildModeRaw : dB.mode, maxParallelBuilds: Number.parseInt( String(formData.get("maxParallelBuilds") ?? "").trim(), 10, diff --git a/editor/app/settings/components/SettingsForm.tsx b/editor/app/settings/components/SettingsForm.tsx @@ -302,30 +302,14 @@ export function SettingsForm({ initial }: Props) { queue. <strong>Build all sites</strong> builds every site at once, each in its own container (capped by Max parallel builds), then deploys them one by one, whenever a container engine answers, and serially on the - host when none does. The mode below is a label for now: no build reads - it. See PUBLISH.md. It can also be toggled on the{" "} - <a href="/sites" className="underline"> - Sites - </a>{" "} - page. + host when none does. See PUBLISH.md. </p> - <label className="flex flex-col gap-1 text-sm"> - <span className="font-medium">Build mode</span> - <select - name="buildMode" - defaultValue={initial.buildPipeline.mode} - className="rounded border border-border bg-card px-2 py-1 text-sm" - > - <option value="basic">Basic (a label for now)</option> - <option value="docker">Docker (a label for now)</option> - </select> - </label> <Field - label="Max parallel builds (Docker mode)" + label="Max parallel builds" name="maxParallelBuilds" defaultValue={String(initial.buildPipeline.maxParallelBuilds)} type="number" - hint="Cap on concurrent per-site container builds (1–16) when Build all sites runs in containers — whenever a container engine answers, whichever mode is set." + hint="Cap on concurrent per-site container builds (1–16) when Build all sites runs in containers — whenever a container engine answers." /> <Field label="Docker image tag" diff --git a/editor/app/sites/[siteId]/publish/page.tsx b/editor/app/sites/[siteId]/publish/page.tsx @@ -1,7 +1,6 @@ import { statSync } from "node:fs"; import path from "node:path"; import type { Metadata } from "next"; -import Link from "next/link"; import { notFound } from "next/navigation"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; @@ -54,13 +53,6 @@ export default async function SitePublishPage({ ? new Date(lastDeployedAt).toLocaleString() : "never"} </p> - <p className="text-sm text-muted-foreground mt-1"> - Build mode (Basic or Docker) is set on the{" "} - <Link className="underline" href="/sites"> - Sites - </Link>{" "} - page. - </p> </div> <BuildDeployButton siteId={siteId} diff --git a/editor/app/sites/components/BuildAllSitesButton.tsx b/editor/app/sites/components/BuildAllSitesButton.tsx @@ -10,9 +10,8 @@ import { JobLane } from "./JobLane"; type Lane = { deploy: boolean; skipArchives: boolean; key: number }; // One-click orchestrated pipeline over EVERY site as a SINGLE managed job (one -// log, one Cancel): whenever a container engine answers — whatever -// buildPipeline.mode says, which nothing reads — it runs the shared data phase + -// archive warm once, fans the per-site builds out in parallel (capped by +// log, one Cancel): whenever a container engine answers it runs the shared +// data phase + archive warm once, fans the per-site builds out in parallel (capped by // maxParallelBuilds), then deploys the built sites serially. Without one it falls // back to a serial host build+deploy. Distinct from the per-site panel below, // which launches one separate job per selected site. @@ -56,8 +55,7 @@ export function BuildAllSitesButton() { </label> </div> <p className="text-xs text-muted-foreground"> - When a container engine answers (whichever build mode is set): the shared - data phase runs once, per-site builds run in parallel in containers, then + When a container engine answers: the shared data phase runs once, per-site builds run in parallel in containers, then deploys run serially. With none, a serial host build and deploy, one site at a time. </p> diff --git a/editor/app/sites/components/BuildModeToggle.tsx b/editor/app/sites/components/BuildModeToggle.tsx @@ -1,66 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import type { BuildMode } from "yt-dlp-transcript-common/lib/settings"; -import { setBuildModeAction } from "../lib/buildModeAction"; - -// Segmented Basic | Docker control. Persists the choice (setBuildModeAction → -// settings.json `buildPipeline.mode`). TODAY IT IS A LABEL: no build path reads -// it — Build all / Build & deploy all use containers whenever `docker version` -// answers (buildAction.ts, dockerAvailable) — and the note below says so rather -// than promising a switch. -export function BuildModeToggle({ mode: initialMode }: { mode: BuildMode }) { - const [mode, setMode] = useState<BuildMode>(initialMode); - const [pending, startTransition] = useTransition(); - - function choose(next: BuildMode) { - if (next === mode || pending) return; - setMode(next); - startTransition(async () => { - const res = await setBuildModeAction(next); - if (!res.ok) setMode(initialMode); - }); - } - - const options: { value: BuildMode; label: string }[] = [ - { value: "basic", label: "Basic" }, - { value: "docker", label: "Docker" }, - ]; - - return ( - <div className="flex flex-col gap-1.5"> - <span className="text-xs font-medium text-muted-foreground">Build mode</span> - <div - role="group" - aria-label="Build mode" - className="inline-flex w-fit rounded-md border border-border overflow-hidden text-sm" - > - {options.map((opt) => { - const active = mode === opt.value; - return ( - <button - key={opt.value} - type="button" - aria-pressed={active} - disabled={pending} - onClick={() => choose(opt.value)} - className={ - "px-3 py-1.5 font-medium transition-colors disabled:opacity-60 " + - (active - ? "bg-primary text-primary-foreground" - : "bg-transparent text-muted-foreground hover:bg-muted") - } - > - {opt.label} - </button> - ); - })} - </div> - <span className="text-xs text-muted-foreground"> - The mode is a label for now: Build all sites uses containers whenever a - container engine answers, and builds serially on the host when none - does, in either mode. - </span> - </div> - ); -} diff --git a/editor/app/sites/components/BuildSitesPanel.tsx b/editor/app/sites/components/BuildSitesPanel.tsx @@ -14,8 +14,8 @@ type Lane = { siteId: string; title: string; deploy: boolean; key: string }; // Batch build (and optionally deploy) several sites at once. Each selected site // launches its own managed job, rendered as its own live JobLane. The jobs share -// the build/deploy queue and run one at a time (the export/ tree is shared), -// whichever build mode is set; "Build all sites" is the parallel path. +// the build/deploy queue and run one at a time (the export/ tree is shared); +// "Build all sites" is the parallel path. export function BuildSitesPanel({ sites }: { sites: SiteOption[] }) { const [selected, setSelected] = useState<Set<string>>(new Set()); const [deployAfter, setDeployAfter] = useState(false); @@ -120,9 +120,9 @@ export function BuildSitesPanel({ sites }: { sites: SiteOption[] }) { </p> )} <p className="text-xs text-muted-foreground"> - These run one at a time (the build output tree is shared), whichever - build mode is set. “Build all sites” above builds every site in - parallel, in containers, whenever a container engine answers. + These run one at a time (the build output tree is shared). “Build all + sites” above builds every site in parallel, in containers, whenever a + container engine answers. </p> {lanes && lanes.length > 0 && ( diff --git a/editor/app/sites/lib/buildAction.ts b/editor/app/sites/lib/buildAction.ts @@ -82,8 +82,8 @@ export async function buildExportAction( if (!id) { return { ok: false, error: "Select a site to build" }; } - // Run as a managed function (not a raw command) so the build can route on the - // persisted build mode (basic vs docker) via runBuildPhase, and so the same + // Run as a managed function (not a raw command) so the build goes through + // runBuildPhase (a single site always builds in export/), and so the same // child-streaming path is shared with build-then-deploy. Cancel still works: // the abort signal is forwarded to the child by runChildIntoLog. // `skipData` composes from the existing .export-index staging without diff --git a/editor/app/sites/lib/buildModeAction.ts b/editor/app/sites/lib/buildModeAction.ts @@ -1,24 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { - getSettings, - isBuildMode, - type BuildMode, -} from "yt-dlp-transcript-common/lib/settings"; -import { saveSettings } from "../../settings/saveSettings"; - -// Persist the build-mode choice (Basic vs Docker) from the family page (/sites) -// so it becomes the default for every subsequent build. Only the mode is touched -// here; the image/concurrency knobs are edited on the Settings page. -// writeSettings re-sanitizes the whole object regardless. -export async function setBuildModeAction( - mode: BuildMode, -): Promise<{ ok: boolean }> { - if (!isBuildMode(mode)) return { ok: false }; - const settings = getSettings(); - // One key of one block: saveSettings merges it over the stored pipeline. - await saveSettings({ buildPipeline: { ...settings.buildPipeline, mode } }); - revalidatePath("/sites"); - return { ok: true }; -} diff --git a/editor/app/sites/page.tsx b/editor/app/sites/page.tsx @@ -10,7 +10,6 @@ import { import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { builtHomepageAt } from "yt-dlp-transcript-common/lib/builtExport"; import { getHomepageConfig } from "yt-dlp-transcript-common/lib/homepage"; -import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { listSites } from "yt-dlp-transcript-common/lib/site"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { @@ -21,7 +20,6 @@ import { liveJobRows } from "../jobs/active/buildActiveJobs"; import { RunningJobsList } from "../jobs/components/RunningJobsList"; import { BuildAllSitesButton } from "./components/BuildAllSitesButton"; import { BuildButtons } from "./components/BuildButtons"; -import { BuildModeToggle } from "./components/BuildModeToggle"; import { BuildSitesPanel } from "./components/BuildSitesPanel"; import { CutReleaseForm } from "./components/CutReleaseForm"; import { HomepageBuildButtons } from "./components/HomepageBuildButtons"; @@ -77,7 +75,6 @@ export default async function SitesPage() { const suggestedVersion = suggestNextVersion(lastVersion); const unreleased = source ? extractUnreleasedSection(source) : ""; const hasUnreleased = unreleased.length > 0; - const buildMode = getSettings().buildPipeline.mode; const existingQueues = getRegistry().activeQueueNames(); // Through the one builder, so this list has the same progress bars /jobs does // (it used to drop `progress`, `tasks`, `drainable` and the reorder bounds). @@ -160,9 +157,9 @@ export default async function SitesPage() { /> </section> - {/* Batch: build (and optionally deploy) several sites at once. The - build mode toggle sits here, and says it is a label: Build all fans - out in containers whenever an engine answers, whatever it says. */} + {/* Batch: build (and optionally deploy) several sites at once. Build all + fans out in containers whenever an engine answers; there is no mode + switch (dropped 2026-09-28, O6c). */} <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build all sites</h2> @@ -172,7 +169,6 @@ export default async function SitesPage() { answers), then deploys run serially. </p> </div> - <BuildModeToggle mode={buildMode} /> <BuildAllSitesButton /> <div className="mt-4"> diff --git a/editor/e2e/deploy-page.spec.ts b/editor/e2e/deploy-page.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "@playwright/test"; import type { Page } from "@playwright/test"; -import { resetData, writeSite, readJson } from "./helpers"; +import { resetData, writeSite } from "./helpers"; // The "pick specific sites" panel (BuildSitesPanel). /sites renders two // controls named "Deploy after build" — one here, one in BuildAllSitesButton — @@ -68,59 +68,18 @@ test("Build & deploy is enabled only when the active site has a Cloudflare proje ).toBeVisible(); }); -test("build-mode toggle persists the choice and says the mode is a label", async ({ +// The Basic | Docker toggle was a label nothing read, and it is gone (release +// 11, follow-up O6c): Build all uses containers whenever an engine answers. +test("/sites has no build-mode toggle, and Build all says when it uses containers", async ({ page, }) => { await writeSite("testsite", { cloudflareProject: "proj" }); await page.goto("/sites"); - - const group = page.getByRole("group", { name: "Build mode" }); - await expect(group.getByRole("button", { name: "Basic" })).toHaveAttribute( - "aria-pressed", - "true", - ); - - // Retry the click until the toggle actually flips. A click dispatched before - // React hydrates fires NOTHING — no handler, no request, no error — so a bare - // click().then(assert) is a coin flip whenever hydration lags, which is what - // happens under the sharded route's concurrent containers (seen as - // aria-pressed still "false" 5s after the click). Same idiom as - // channel-site-membership.spec's setSiteChecked. - const docker = group.getByRole("button", { name: "Docker" }); - await expect(async () => { - await docker.click(); - await expect(docker).toHaveAttribute("aria-pressed", "true", { - timeout: 1_000, - }); - }).toPass({ timeout: 15_000 }); - await expect(page.getByText(/The mode is a label for now/i)).toBeVisible(); - - // Wait for the choice to actually reach settings.json before reloading. - // BuildModeToggle updates its own state OPTIMISTICALLY — setMode(next) flips - // aria-pressed immediately and only then does startTransition run - // setBuildModeAction — so the assertions above prove nothing about - // persistence. Reloading straight after them races the write: on a fast host - // the write wins, in a loaded container it does not, which is why this test - // passed under `pnpm e2e` and failed under `pnpm e2e:sharded`. - await expect - .poll( - async () => - ( - await readJson<{ buildPipeline?: { mode?: string } }>( - "test-settings.json", - ) - ).buildPipeline?.mode, - { timeout: 10_000 }, - ) - .toBe("docker"); - - // Persisted: reload and the toggle is still on Docker. - await page.goto("/sites"); + await expect(page.getByRole("heading", { name: "Build all sites" })).toBeVisible(); + await expect(page.getByRole("group", { name: "Build mode" })).toHaveCount(0); await expect( - page - .getByRole("group", { name: "Build mode" }) - .getByRole("button", { name: "Docker" }), - ).toHaveAttribute("aria-pressed", "true"); + page.getByText(/When a container engine answers: the shared data phase runs once/i), + ).toBeVisible(); }); test("batch panel: selecting sites enables the launch button and reflects deploy intent", async ({ diff --git a/editor/e2e/settings.spec.ts b/editor/e2e/settings.spec.ts @@ -66,11 +66,13 @@ test("rejects out-of-range maxTranscriptPageBytes", async ({ page }) => { ); }); -test("saves build pipeline settings (mode, concurrency, image)", async ({ +test("saves build pipeline settings (concurrency, image), and has no mode", async ({ page, }) => { await page.goto("/settings"); - await page.getByLabel("Build mode").selectOption("docker"); + // The Basic | Docker select was a label nothing read (dropped in release 11, + // follow-up O6c). + await expect(page.getByLabel("Build mode")).toHaveCount(0); await page.getByLabel(/max parallel builds/i).fill("4"); await page.getByLabel(/docker image tag/i).fill("my-build-image"); await page.getByRole("button", { name: /save settings/i }).click(); @@ -79,21 +81,9 @@ test("saves build pipeline settings (mode, concurrency, image)", async ({ ).toBeVisible(); const saved = await readJson<{ - buildPipeline?: { - mode: string; - maxParallelBuilds: number; - dockerImage: string; - }; + buildPipeline?: Record<string, unknown>; }>("test-settings.json"); - expect(saved.buildPipeline?.mode).toBe("docker"); expect(saved.buildPipeline?.maxParallelBuilds).toBe(4); expect(saved.buildPipeline?.dockerImage).toBe("my-build-image"); - - // The Sites page reflects the persisted mode. - await page.goto("/sites"); - await expect( - page - .getByRole("group", { name: "Build mode" }) - .getByRole("button", { name: "Docker" }), - ).toHaveAttribute("aria-pressed", "true"); + expect(saved.buildPipeline && "mode" in saved.buildPipeline).toBe(false); }); diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -- **The docs' *Building several sites at once* page says what Build all does.** It called the container pipeline opt-in, turned on in the settings. Build all sites builds every site in parallel in containers whenever a container engine is available, whichever build mode is set (that choice is only a label for now), and one after another when none is. +- **The docs' *Building several sites at once* page says what Build all does.** It called the container pipeline opt-in, turned on in the settings. Build all sites builds every site in parallel in containers whenever a container engine is available, and one after another when none is; there is nothing to switch on. - **A single-colour social icon shows on every ground.** The footer's social icons are the operator's (`homepage.json`'s, else `settings.socialLinks`), normalized when they are saved (`normalizeSocialSvg`, release 11 slice O1). An icon drawn in one colour now takes the footer's colour throughout; before, a part that carried its own colour kept it, so X's official logo, which is white, was invisible on the Light ground. An icon of two or more colours, such as YouTube's red mark with its white triangle, keeps its colours as pasted. "No fill", gradients, masks, clip paths and animation timing are never changed, and a clip path's own colour does not count, so a one-colour icon exported from Figma follows the footer too. It applies when the settings are next saved, then needs a rebuild and deploy of the homepage. - **In high-contrast mode the header mark's tile keeps its edge.** In Windows' high-contrast mode (forced colours) the reader's own background replaces the page on every ground and can be as dark as the slate tile, whose ring is only drawn on Dark. In that mode the tile gets a 1-pixel outline in the reader's text colour, on every ground, following its rounded corners. Nothing changes outside that mode. - **A sixth official instance has a chart colour of its own.** The growth chart, its legend and `/stats` had five validated colours, so a sixth site fell to a pink within a degree of the fifth's magenta. There is now a sixth, a rust (`--chart-6`: `#823c10` on Light and Sepia, `#a54a08` on Dark), which is Vermilion's hue family, so Jasolyzer's card and its layer will share a hue once it is published. It clears every pair with the other five on all three grounds for colour-blind readers (the dataviz validator, all pairs; worst CVD ΔE 9.1, normal 16.3). Any six sites now wear the six validated colours; `/stats`' sixth channel gets the rust too. diff --git a/homepage/content/docs/deploy-docker.md b/homepage/content/docs/deploy-docker.md @@ -3,11 +3,8 @@ If one corpus publishes several sites, **Build all sites** builds them **in parallel**, in isolated containers, and then deploys them serially, whenever a container engine is available on the machine. With none, it builds them one after -another instead. A single site's own build always runs one at a time. - -The editor's settings and its Sites page also show a build mode, Basic or Docker. -Today that choice is only a label: Build all sites uses containers whenever it -can, whichever mode is set. +another instead. A single site's own build always runs one at a time. There is +nothing to switch on. ## What you need diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -1112,6 +1112,8 @@ The real cause is an **optimistic write raced by a reload**. `BuildModeToggle` c passes before `settings.json` has been written, and the test's `page.goto()` reload can read the pre-click settings. A fast host wins that race; a loaded container loses it. The spec now polls `test-settings.json` for `buildPipeline.mode === "docker"` before reloading. +*(Amended 2026-09-28, O6c: the toggle and `buildPipeline.mode` are gone, and so is that test; the +lesson below stands.)* **The general lesson: an optimistic control proves nothing about persistence.** Assert the persisted artifact, not the widget, before reloading — and treat "passes serially, fails @@ -3131,7 +3133,7 @@ only their routes say otherwise", is wrong for two of them): |---|---|---|---|---| | `/charts` | **per-site** templates; preview DATA = last-composed site | `readTemplates(paths, siteId)` → `sites/<id>/chart-templates.json`; `useStats`/`useSummaries` fetch `/stats/*` = `export/public/stats` = whichever site `compose-site` ran last | `saveDefaultDashboardAction`, no revalidate | yes, REQUIRED one site | | `/aliases` | **global + per-site overlay** | `readGlobalAliases` → `transcripts/search-aliases.json`; `readSiteAliases` → `sites/<id>/search-aliases.json`; merged at compose, per-site id SHADOWS global (`aliasesStore.ts:68-73`) | `saveGlobalAliasesAction`, `saveSiteAliasesAction`, no revalidate | optional | -| `/deploy` | **per-site target + global chrome** | `listSites`; `export/CHANGELOG.md`; `export/out/index.html` mtime (`resolveOutDir` ignores siteId); `settings.buildPipeline.mode` | `cutReleaseAction`, `setBuildModeAction`, `deployExportAction(siteId)` + four from `buildAction` | optional | +| `/deploy` | **per-site target + global chrome** | `listSites`; `export/CHANGELOG.md`; `export/out/index.html` mtime (`resolveOutDir` ignores siteId); `settings.buildPipeline.mode` *(gone 2026-09-28, O6c)* | `cutReleaseAction`, `setBuildModeAction`, `deployExportAction(siteId)` + four from `buildAction` | optional | | `/build` | **corpus-wide, no site notion at all** | `activeQueueNames()`, `liveJobRows(BUILD_KINDS)` — no `searchParams`, no `listSites` | eleven actions in `buildAction.ts` | **never** | | `/homepage` | **global — the hub's identity** | `getHomepageConfig` → `sites/_homepage/homepage.json` | `saveHomepageConfigAction` | **never**; page was sync | @@ -3139,7 +3141,7 @@ only their routes say otherwise", is wrong for two of them): in two directories: `build/buildAction.ts:34` imported `deploy/buildDeployCore`, and four `deploy/components/*` imported `build/buildAction`. Now: server modules in `editor/app/sites/lib/` — `buildAction.ts`, `deployAction.ts`, `cutReleaseAction.ts`, -`buildModeAction.ts`, `homepageActions.ts`, `chartsActions.ts`, `aliasesActions.ts` (all +`buildModeAction.ts` *(deleted 2026-09-28, O6c)*, `homepageActions.ts`, `chartsActions.ts`, `aliasesActions.ts` (all `"use server"`) plus `buildDeployCore.ts` (deliberately NOT, its header says why). Components FLAT in `editor/app/sites/components/`, so every moved file has the same import shape: `../lib/x`, `../../components/QueueControl`, `../../jobs/actions` — the last two byte-identical @@ -7000,6 +7002,14 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a deploy all use containers whenever `docker version` answers, else the serial host build. Settings ("Basic (a label for now)" / "Docker (a label for now)"), the schema text, SETTINGS.md, `/sites` and the homepage's Docker page say so. Whether they should honour it is an open operator question. + - *Amended 2026-09-28 (morning, follow-up O6c): ruled — dropped.* The operator: "If docker build + mode isn't ready yet, just drop it for now rather than keep the stub." `buildPipeline.mode`, + `BuildMode` / `isBuildMode`, the `/sites` toggle (`BuildModeToggle.tsx`, + `buildModeAction.ts`) and the Settings select are gone. `buildPipeline` is + `{maxParallelBuilds, dockerImage, dockerfile}`, and `sanitizeBuildPipeline` builds only those, + so a `settings.json` that still says `"mode": "basic"` loads and loses the key on the next save + (`settingsSchema.test.ts`, `settingsWrite.test.ts`). The container path is unchanged: Build all + uses containers whenever `docker version` answers. ### The homepage builds and deploys from `/sites` (O4) diff --git a/plans/STATE.md b/plans/STATE.md @@ -22,7 +22,7 @@ changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 st - **O6-A — one-core Phase 4 slice 3, checkpoint A** (`cb9d02b2`): `archilyzer doctor`, `run <operation>`, `mcp`, every bin a subcommand, `pnpm archilyzer`; `common/lib/ports.mjs` and `common/lib/envVars.ts` → generated `ENVIRONMENT.md`; `PUBLISH.md` absorbs the two deploy docs; - the build mode is a label. + the build mode is a label (then dropped outright: O6c, below). - **O1 — hub lows** (`6cd3a9fe`): `/ask` waits for the hub's list, says when there are no archives, shows a scope line; a member's missing live chat on its chip with a live-chat Retry; `normalizeSocialSvg` themes a single-colour icon (the x.com fix); `mcp/README.md`'s `fetch_clip` @@ -38,6 +38,10 @@ changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 st - **O6-B — checkpoint B** (`eb28a341`): every test-only env var `E2E_`-prefixed and declared in its playwright config; the harnesses read `ports.mjs`. **One-core Phase 4 is done** (the optional SETUP/PLAN.md consolidations aside). + - **O6c — the build-mode stub dropped** (2026-09-28 morning, on the operator's ruling; branch + `r11/phase-4-s3` tip after `5b6a66ec`): `buildPipeline.mode`, the `/sites` toggle and the + Settings select are gone; an old `settings.json` with `mode` loads and drops it on the next + save; Build all still uses containers whenever `docker version` answers. - **Gates on the merged tree** (`eb28a341`; every number in the record): tsc clean; common 2,112, editor unit 85, `test:scripts` 185 + 1 skip, mcp 269, homepage unit 2; the three generated-doc checks exit 0; editor, export site + hub, homepage and umtool builds ok; e2e export 206, `e2e:hub` @@ -55,11 +59,18 @@ changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 st No reader-visible change, so no bullet. Record: `release-11.md`, "Operator rulings and follow-up O1c". 2. **The operator questions the records raise:** - - Should Build all honour `buildPipeline.mode` (today a label; O6)? **Open.** + - ~~Should Build all honour `buildPipeline.mode` (today a label; O6)?~~ **Ruled 2026-09-28: + dropped** ("just drop it for now rather than keep the stub"). Follow-up O6c removed the mode, + the `/sites` toggle and the Settings select; an old `settings.json` with `mode` still loads. + Build all uses containers whenever `docker version` answers, as before. - ~~Is `--chart-6`'s rust near `--state-gone` acceptable (O2)?~~ **Ruled 2026-09-28: accepted; Vermilion keeps slot 6.** - - Settle the five 410'd Rumble videos with a "Full-check unexpected" on `rekietalaw-rumble` - and `the-quartering-rumble` (O3)? **Open.** + - ~~Settle the five 410'd Rumble videos with a "Full-check unexpected" on `rekietalaw-rumble` + and `the-quartering-rumble` (O3)?~~ **Done 2026-09-28 13:32Z** (the operator: "full check the + rumble channels"; run by the parent on :3001 through `platform:rumble`): all five read + deleted, 0 errors, so they publish as Deleted at the next build, not "Missing?" (jeralyzer's + and rekietalyzer's counts do not rise by 4 and 1). Record: `release-11.md`, "Operator + actions (2026-09-28 morning)". 3. **A release cut** (`archilyzer release cut all next --commit`; editor 8 and export 9 pending bullets — export's is public on every `/changelog`). 4. **The :3001 restart** on the new `main` (it brings O4's Homepage section and routes, O3's diff --git a/plans/release-11.md b/plans/release-11.md @@ -226,7 +226,8 @@ found, and slice P's review lows L3 + L4. No settings, site or channel key; noth five are Rumble probes that got `HTTP Error 410: Gone` on 2026-09-25 and were stored as `error`, hours before `6d5cdbc3` taught the classifier that 410 is `deleted`; they are really deleted, and "Missing?" is closer than "available". A "Full-check unexpected" on those two channels would - settle them as Deleted (the operator's call; nothing was run). + settle them as Deleted (the operator's call; nothing was run). *Done 2026-09-28 13:32Z, on the + operator's word: all five read deleted — "Operator actions (2026-09-28 morning)", below.* **2 — a failed forced download keeps the subtitle pass's status** (`common/ytdlp/downloadOneManaged.ts`). - The attempt-3 `else` set `status = "failed"` and `lastSucceeded = false` for a `keepTranscript` @@ -347,7 +348,8 @@ found, and slice P's review lows L3 + L4. No settings, site or channel key; noth control renders. The follow-up is to lift a ran-bucket set into both sections and keep those cards in `populated` / `listed`. - **The five 410 videos** are published as available today and will read "Missing?" after the next - build; a Full-check unexpected would make them Deleted. + build; a Full-check unexpected would make them Deleted. *Ruled and done 2026-09-28 13:32Z: the + full check read all five deleted, so they publish as Deleted, not "Missing?".* **Review fixes** (review SHIP AFTER FIXES, `o3-review.md` in the job scratch dir: one should-fix, wording only; three lows; one nit; the three questions ruled — keep `archiveSourceVideo`'s failure broad, staying @@ -610,6 +612,7 @@ the `E2E_` prefix cleanup, which is checkpoint B, after O1–O5 land. they use containers whenever `docker version` answers and the mode is a label (review S2). The parent ruled copy-only tonight; honouring it (basic → serial host even when docker answers) is a behaviour change for its own slice. + *Ruled 2026-09-28 (morning): dropped — follow-up O6c removed the mode, the toggle and the select.* - **`run` does not see the editor's lanes** (review L1). Beside the editor's lane on the same channel it does the same videos twice — wasted CPU, not damage (atomic writes). Said in the header and the usage line; a `.jobs/` check for a running job of the same kind and slug is not @@ -1691,8 +1694,9 @@ something." at least one archive is in scope. - **`--chart-6`'s rust near `--state-gone` is accepted** (O2's question). - **Vermilion keeps slot 6** (`ACCENT_CHART_SLOT.vermilion = 5`, the runbook's question). -- **Still open:** O6's Build all vs `buildPipeline.mode`, and O3's full check of the five 410'd - Rumble videos. +- **Still open** at the time: O6's Build all vs `buildPipeline.mode`, and O3's full check of the + five 410'd Rumble videos. *Both closed later that morning: the mode dropped (follow-up O6c) and + the full check run ("Operator actions (2026-09-28 morning)").* **O1c: the link is written in the line** (`a4ffa959`). Branch `r11/hub-lows`, fast-forwarded to `main` `e1cedf3d` first. @@ -1740,6 +1744,79 @@ something." shipped lines are strings with no `.text`. - **A mutation, a second link not throwing:** exactly the two-links test fails. +### Follow-up O6c — the build-mode stub dropped (2026-09-28 morning) + +**The ruling.** The operator: "If docker build mode isn't ready yet, just drop it for now rather +than keep the stub." `buildPipeline.mode` was a label nothing read (O6's review S2). Build all and +Build & deploy all use containers whenever `docker version` answers, and build serially on the host +otherwise. That is unchanged. Branch `r11/phase-4-s3`, fast-forwarded to `main` `27723d37` first. + +**What went:** +- `common/lib/settingsSchema.ts`: the `mode` key, `BuildMode` and `isBuildMode`, and `mode`'s + field doc. The buildPipeline description and the `maxParallelBuilds` doc no longer name a mode. + `SETTINGS.md` and `settings.json.example` are regenerated. +- The `/sites` Basic | Docker toggle: `BuildModeToggle.tsx`, `buildModeAction.ts`, and its use and + `getSettings` import on `/sites`. +- The Settings form's select (and "(Docker mode)" in the Max parallel builds label) and + `settings/actions.ts`' handling of it. +- The publish tab's "Build mode (Basic or Docker) is set on the Sites page" line. +- The mode's mentions in: + - `BuildAllSitesButton` (comment and paragraph) and `BuildSitesPanel` (comment and note); + - the `buildAction.ts`, `publish/build.ts:378` and `Dockerfile.build:1` comments; + - `PUBLISH.md`, `RUNNING_IN_DOCKER.md:~545` and the homepage's `deploy-docker.md`. +- Specs: + - `deploy-page.spec`'s toggle test is now "/sites has no build-mode toggle, and Build all says + when it uses containers"; + - `settings.spec` saves the three real fields and asserts no Build mode control and no `mode` + on disk. + +**What stays:** the container path, `maxParallelBuilds`, `dockerImage` and `dockerfile`. + +**An old file still loads.** The operator's `settings.json` says `"mode": "basic"`. +`sanitizeBuildPipeline` builds its output from the three real fields only, so that file loads with +no error, and the next save writes no `mode`. +- `settingsSchema.test.ts`: the sanitizer and the whole-file parse drop `mode`, for `"basic"`, + `"docker"`, junk and a number. +- `settingsWrite.test.ts`: a file carrying `mode` loads through `getSettings()`, and + `writeSettings` puts exactly `{maxParallelBuilds, dockerImage, dockerfile}` on disk. +- `settings example --check` compares the committed `settings.json.example` and `SETTINGS.md` + against what the schema generates. Both are regenerated without `mode`, so it exits 0. +- It never reads the operator's file. + +**Changelogs.** The O6 editor `[Unreleased]` bullet that said "the mode is a label" is replaced, +not stacked. It now says the toggle and setting are gone, an old file loads, and Build all uses +containers when it can, as it always did. The O6-A bullet's build-mode sentence is removed. The +homepage bullet for its Docker page says there is nothing to switch on. + +| sha | what | +|---|---| +| `5b6a66ec` | `editor, common:` the stub dropped (schema, toggle, select, copy, the two specs, two new settings tests) | +| `7262256f` | `docs:` PUBLISH, RUNNING_IN_DOCKER, the homepage's Docker page | +| _this_ | `plans:` this record, STATE, FACTS amendments; the editor and homepage changelog bullets replaced | + +**Gates**, from the worktree root on `27723d37` + these commits (`$T/o6c-*.log`): +- tsc clean (70 s). +- **common 2,114/2,114** (+2), **editor unit 85/85**, **`test:scripts` 185 + 1 skip**, **mcp + 269/269**. +- The three `--check`s: all exit 0. +- `next build`: editor ok (39 s), homepage ok (15 s). +- **e2e** (`o6c-specs.txt`: deploy-page, settings, sites-crud, sites-homepage, ops-api): **56 passed, 0 + failed, 2.2 min**. +- **They bite:** with `27723d37`'s `settingsSchema.ts` under the new tests, both new tests fail + (22 pass, 2 fail — `o6-bite.log`). + +**Operator actions (2026-09-28 morning).** +- **The five 410'd Rumble videos are settled.** On the operator's word ("full check the rumble + channels"), the parent ran **Full-check unexpected** on the live :3001 at **2026-09-28 13:32Z**, + through the editor's `platform:rumble` queue at concurrency 1 (`$T/fc-run.log`): + - `rekietalaw-rumble`, job `01M3M3EVM0M3EZH9ETK6KY1AH7`: `v7e07us` deleted (`v7emb9a` skipped, + already known deleted); + - `the-quartering-rumble`, job `01M3M3F55A7VESDMYTM5801YJK`: `v4vriou`, `v4x5o1l`, `v4yqask` and + `v501kfc` deleted; + - 0 errors. +- So O3's five read **Deleted** at the next index build, not "Missing?". Jeralyzer's and + Rekietalyzer's "Missing?" counts do not rise by 4 and 1. + ## Rollout Nothing is rolled out: no cut, no deploy and no restart happened overnight, by rule. `main` is the @@ -1751,19 +1828,21 @@ integration tip (above); the live :3001 editor still runs `BUILD_ID` `S07zTu3MKT rulings and follow-up O1c", above). The drafts' markers are gone and each link is written in its line (`` copy`…${link("…")}…` ``); no commit is owed for them before the cut. 2. **Answer the records' questions** (none blocks the rollout): - - O6: should Build all / Build & deploy all honour `buildPipeline.mode`? Today the mode is a label - and containers are used whenever `docker version` answers. + - ~~O6: should Build all / Build & deploy all honour `buildPipeline.mode`?~~ **Ruled 2026-09-28: + dropped.** Follow-up O6c removed the mode, the `/sites` toggle and the Settings select; + containers are used whenever `docker version` answers, as before. - ~~O2: is `--chart-6`'s rust acceptable beside `--state-gone`?~~ **Ruled 2026-09-28: accepted; Vermilion keeps slot 6.** - - O3: settle the five Rumble videos that got HTTP 410 on 2026-09-25 with a "Full-check unexpected" - on `rekietalaw-rumble` and `the-quartering-rumble`. They read "Missing?" after the next build; - the full check would make them Deleted. + - ~~O3: settle the five Rumble videos that got HTTP 410 on 2026-09-25 with a "Full-check + unexpected" on `rekietalaw-rumble` and `the-quartering-rumble`.~~ **Done 2026-09-28 13:32Z:** + all five read deleted, 0 errors ("Operator actions (2026-09-28 morning)"); they publish as + Deleted at the next build. 3. **Cut the release:** `archilyzer release show`, then `archilyzer release cut all next --commit` (editor 8 pending bullets, export 9; export's are public on every site's `/changelog`). The homepage changelog keeps its own `[Unreleased]` convention and is not cut. 4. **Restart :3001 on the new `main`** (one restart). It brings O4's Homepage section and the two ops - routes, O3's retry-bucket, forced-download and Cut release form fixes, O6's build-mode copy and the - digest replay that keeps `ids`, and O2's sidebar outline under forced colours. **Restart before + routes, O3's retry-bucket, forced-download and Cut release form fixes, O6c's removal of the + build-mode toggle and select, O6's digest replay that keeps `ids`, and O2's sidebar outline under forced colours. **Restart before retrying any `archilyzer run digest` job**: the old editor's replay widens it to the whole channel. 5. **Write the settings once** (any Save on Settings, or any other settings write). `writeSettings` re-normalizes every stored social link, so the x.com icon's `fill="white"` becomes `currentColor`. diff --git a/settings.json.example b/settings.json.example @@ -132,7 +132,6 @@ "defaultLocationId": "" }, "buildPipeline": { - "mode": "basic", "maxParallelBuilds": 2, "dockerImage": "yt-dlp-transcript-browser-build", "dockerfile": "Dockerfile.build"