commit 9f6beed2c115142e5b3036411c5133f4ebf877fd
parent 8119cfacbdbd3870d80a509df9e5ed9066222639
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 21:56:14 -0400
sites: form checkbox for per-video transcript downloads
Beside archives, same opt-out idiom: an unticked box saves
transcriptDownloads: false, a ticked one drops the key. sites-crud gains a
round-trip for both opt-outs (save, file, reload, tick back, key gone).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 74 insertions(+), 0 deletions(-)
diff --git a/editor/app/sites/actions.ts b/editor/app/sites/actions.ts
@@ -76,6 +76,8 @@ export async function saveSiteAction(
const archives = formData.get("archives") === "on";
// Duplicates default on, same opt-out idiom as archives.
const duplicates = formData.get("duplicates") === "on";
+ // Per-video transcript downloads default on, same opt-out idiom.
+ const transcriptDownloads = formData.get("transcriptDownloads") === "on";
const archiveMaxMBRaw = String(formData.get("archiveMaxMB") ?? "").trim();
let archiveMaxBytes: number | undefined;
if (archiveMaxMBRaw) {
@@ -181,6 +183,7 @@ export async function saveSiteAction(
...(pwa ? { pwa: true } : {}),
...(archives ? {} : { archives: false }),
...(duplicates ? {} : { duplicates: false }),
+ ...(transcriptDownloads ? {} : { transcriptDownloads: false }),
...(archiveMaxBytes !== undefined ? { archiveMaxBytes } : {}),
...(relatedSites.length > 0 ? { relatedSites } : {}),
};
diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx
@@ -293,6 +293,22 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) {
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
+ name="transcriptDownloads"
+ defaultChecked={initial.transcriptDownloads !== false}
+ className="accent-brand"
+ />
+ Per-video transcript downloads (Download menu, Copy Markdown, Copy download
+ command)
+ </label>
+ <p className="-mt-2 text-xs text-muted-foreground">
+ On by default: a visitor's transcript viewer offers the transcript as a
+ txt/srt/json file, as Markdown, and a yt-dlp command for a marked clip.
+ Turn off to hide all three on this site. The machine contract
+ (corpus.json, manifests, shards) is published either way.
+ </p>
+ <label className="flex items-center gap-2 text-sm">
+ <input
+ type="checkbox"
name="duplicates"
defaultChecked={initial.duplicates !== false}
className="accent-brand"
diff --git a/editor/e2e/sites-crud.spec.ts b/editor/e2e/sites-crud.spec.ts
@@ -290,6 +290,61 @@ test("inline-chips flag round-trips; seeded default group has it on", async ({
await expect(chipToggles.nth(1)).not.toBeChecked();
});
+test("archives + per-video transcript downloads opt-outs round-trip", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await writeSite("optout", { siteTitle: "Opt Out" });
+
+ type OptOutSiteFile = { archives?: boolean; transcriptDownloads?: boolean };
+ const file = "test-transcripts/sites/optout/site.json";
+ const archives = page.getByRole("checkbox", {
+ name: /generate downloadable archive zips/i,
+ });
+ const downloads = page.getByRole("checkbox", {
+ name: /per-video transcript downloads/i,
+ });
+ const save = async () => {
+ await page.getByRole("button", { name: /save site/i }).click();
+ await expect(
+ page.getByRole("status").filter({ hasText: "Saved" }),
+ ).toBeVisible();
+ };
+
+ // Both default ON: absent from the file, ticked in the form.
+ await page.goto("/sites/optout");
+ await expect(archives).toBeChecked();
+ await expect(downloads).toBeChecked();
+
+ await archives.uncheck();
+ await downloads.uncheck();
+ await save();
+ // Poll: the "Saved" status and the on-disk write can land slightly apart.
+ await expect(async () => {
+ const site = await readJson<OptOutSiteFile>(file);
+ expect(site.archives).toBe(false);
+ expect(site.transcriptDownloads).toBe(false);
+ }).toPass({ timeout: 10_000 });
+
+ await page.goto("/sites/optout");
+ await expect(archives).not.toBeChecked();
+ await expect(downloads).not.toBeChecked();
+
+ // Ticking back on removes the key — only the non-default is persisted.
+ await archives.check();
+ await downloads.check();
+ await save();
+ await expect(async () => {
+ const site = await readJson<OptOutSiteFile>(file);
+ expect("archives" in site).toBe(false);
+ expect("transcriptDownloads" in site).toBe(false);
+ }).toPass({ timeout: 10_000 });
+
+ await page.goto("/sites/optout");
+ await expect(archives).toBeChecked();
+ await expect(downloads).toBeChecked();
+});
+
test("first-run migrate button appears only when no sites exist", async ({
page,
}) => {