Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 37fe0b1e8c5c99455a6d803a289aa293d55953c9
parent 59e0c068657849a520cac2c1664d22a27a1dc247
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 18:25:26 -0400

plans: keep the release 7/8 rollout scripts in plans/tools/rollout (scratch dies with the job); fix the redeploy log names and the smoke DEFERRED check

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Aplans/tools/rollout/README.md | 19+++++++++++++++++++
Aplans/tools/rollout/builds.sh | 10++++++++++
Aplans/tools/rollout/cancel-scan.mjs | 13+++++++++++++
Aplans/tools/rollout/e-live-check.mjs | 19+++++++++++++++++++
Aplans/tools/rollout/gate.sh | 11+++++++++++
Aplans/tools/rollout/home-preview.sh | 8++++++++
Aplans/tools/rollout/home-publish.sh | 6++++++
Aplans/tools/rollout/md5.sh | 6++++++
Aplans/tools/rollout/restart.sh | 17+++++++++++++++++
Aplans/tools/rollout/sites-redeploy.sh | 10++++++++++
Aplans/tools/rollout/smoke.sh | 37+++++++++++++++++++++++++++++++++++++
Aplans/tools/rollout/sync.sh | 7+++++++
Aplans/tools/rollout/ytdlp-live-check.mjs | 13+++++++++++++
13 files changed, 176 insertions(+), 0 deletions(-)

diff --git a/plans/tools/rollout/README.md b/plans/tools/rollout/README.md @@ -0,0 +1,19 @@ +# Rollout tooling (from the release 7/8 rollouts) + +Every script reads `CLAUDE_JOB_DIR` (the job scratch; logs land in `$CLAUDE_JOB_DIR/tmp`) and +`REL` (a label prefixing every log, default `r9`). Run them from the primary checkout, detached +(`setsid nohup sh plans/tools/rollout/<x>.sh > $T/<x>.log 2>&1 < /dev/null &`) and wait on +`exit=` in the log. The procedure they implement is `plans/release-8.md` "## Rollout". + +| Script | Step | +|---|---| +| `md5.sh <label>` | md5 sweep of `settings.json`, every `site.json`/`config.json`, `homepage.json` — before the build, before the restart, after boot; the three must match. | +| `gate.sh` (`WT=<worktree>`, specs in `$T/$REL-gate-specs.txt`) | tsc, common, editor unit, scripts, editor build, e2e on the merged worktree. | +| `builds.sh` | editor build into the live `.next`, then umtool. Comment out umtool when untouched. | +| `restart.sh` | ONE editor restart then umtool; `RESTART_FAIL` if the port stays bound. Ask before it kills an operator-started job. | +| `smoke.sh` | live smoke on :3001 — old/new API pairs, pages, `DEFERRED_OK` asserts a well-formed deferral list (not empty), `SMOKE_FAIL=0` is the verdict. | +| `sync.sh` | one `pnpm ops sync` proof (teamrcn). | +| `sites-redeploy.sh` | the five official sites BY NAME (`build-deploy` with `siteIds`; never `all` until the docker build image is rebuilt), then `build-hub` + `deploy-hub`. | +| `home-preview.sh` / `home-publish.sh` | homepage build + deploy to the `refresh` preview branch / to production. | +| `cancel-scan.mjs <jobId>` | cancels one job through the live `/jobs` Cancel button (Playwright). | +| `ytdlp-live-check.mjs [site]` / `e-live-check.mjs [site]` | live proofs of slices Z and E on a published site (Playwright). | diff --git a/plans/tools/rollout/builds.sh b/plans/tools/rollout/builds.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Release 7 rollout step 5: editor build into the live .next, then umtool build. Primary checkout on main. +R=/home/user/Projects/yt-dlp-transcript-browser; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +cd $R || exit 1 +echo "START $(date +%T) $(git rev-parse --short HEAD) editor BUILD_ID before: $(cat editor/.next/BUILD_ID) umtool: $(cat umtool/.next/BUILD_ID)" +find export/public -xtype l +s=$(date +%s); pnpm --filter editor exec next build > $T/$REL-build-editor.log 2>&1; echo "editor_build_exit=$? $(( $(date +%s)-s ))s BUILD_ID $(cat editor/.next/BUILD_ID)" +echo "client-s3 resolves: $(readlink -f editor/.next/node_modules/@aws-sdk/client-s3-* 2>/dev/null | head -1)" +s=$(date +%s); (cd umtool && pnpm exec next build > $T/$REL-build-umtool.log 2>&1); echo "umtool_build_exit=$? $(( $(date +%s)-s ))s BUILD_ID $(cat umtool/.next/BUILD_ID)" +echo "exit=0 $(date +%T)" diff --git a/plans/tools/rollout/cancel-scan.mjs b/plans/tools/rollout/cancel-scan.mjs @@ -0,0 +1,13 @@ +// Cancel one job on the LIVE /jobs page through its own Cancel button (the existing surface). +import { chromium } from "/home/user/Projects/yt-dlp-transcript-browser/node_modules/.pnpm/@playwright+test@1.59.1/node_modules/@playwright/test/index.mjs"; +const id = process.argv[2]; const base = process.env.EDITOR_URL ?? "http://localhost:3001"; +const browser = await chromium.launch(); const page = await browser.newPage(); page.setDefaultTimeout(120_000); +await page.goto(`${base}/jobs`, { waitUntil: "domcontentloaded" }); +let row = page.locator(`[data-job-id="${id}"]`); +if ((await row.count()) === 0) row = page.locator(`a[href*="${id}"]`).first().locator("xpath=ancestor::*[self::li or self::tr or self::article][1]"); +console.log("rows matched:", await row.count()); +const text = (await row.first().innerText()).replace(/\s+/g, " ").slice(0, 160); console.log("row:", text); +const btn = row.first().getByRole("button", { name: /cancel/i }); +console.log("cancel buttons:", await btn.count(), await btn.count() ? await btn.first().getAttribute("aria-label") ?? await btn.first().innerText() : ""); +if (await btn.count()) { await btn.first().click(); console.log("clicked"); await page.waitForTimeout(3000); } +await browser.close(); diff --git a/plans/tools/rollout/e-live-check.mjs b/plans/tools/rollout/e-live-check.mjs @@ -0,0 +1,19 @@ +import { chromium } from "/home/user/Projects/yt-dlp-transcript-browser/node_modules/.pnpm/@playwright+test@1.59.1/node_modules/@playwright/test/index.mjs"; +const site = process.argv[2] ?? "https://jeralyzer.pages.dev"; +const SHARD = /\/transcripts\/[^/]+\/page-\d+\.json$/; +const browser = await chromium.launch(); const page = await browser.newPage(); page.setDefaultTimeout(60_000); +let shards = 0; page.on("request", (r) => { if (SHARD.test(new URL(r.url()).pathname)) shards += 1; }); +await page.goto(site + "/", { waitUntil: "domcontentloaded" }); +await page.evaluate(() => window.localStorage.setItem("ytdlp-tb:export-filters", JSON.stringify({ v: 1, working: { channels: { included: [], excluded: [] }, nol: true, query: JSON.stringify({ k: "g", o: "AND", c: [{ k: "l", q: "lawsuit", s: "transcripts" }] }) }, profiles: {}, activeProfileName: null }))); +shards = 0; +await page.reload({ waitUntil: "domcontentloaded" }); +const input = page.locator('input[data-testid^="leaf-query-"]').first(); +await input.waitFor(); +await page.waitForTimeout(3_000); +const restored = await input.inputValue(); +const onLoad = shards; +const fetched = page.waitForRequest((r) => SHARD.test(new URL(r.url()).pathname), { timeout: 30_000 }).then(() => true).catch(() => false); +await input.press("Enter"); +const afterEnter = await fetched; +console.log(`${site}: restored="${restored}" shards-on-load=${onLoad} shard-after-enter=${afterEnter}`); +await browser.close(); diff --git a/plans/tools/rollout/gate.sh b/plans/tools/rollout/gate.sh @@ -0,0 +1,11 @@ +#!/bin/sh +W=${WT:?set WT to the worktree path}; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +cd $W || exit 1 +echo "START $(date +%T) $(git rev-parse --short HEAD)" +pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit > $T/$REL-gate-tsc.log 2>&1; echo "tsc_exit=$? errors=$(grep -c 'error TS' $T/$REL-gate-tsc.log)" +pnpm --filter yt-dlp-transcript-common test > $T/$REL-gate-common.log 2>&1; echo "common_exit=$? $(grep -E '^# (pass|fail)' $T/$REL-gate-common.log | tr '\n' ' ')" +(cd editor && pnpm exec tsx --test "app/**/*.test.ts" > $T/$REL-gate-editor-unit.log 2>&1); echo "editor_unit_exit=$? $(grep -E '^# (pass|fail)' $T/$REL-gate-editor-unit.log | tr '\n' ' ')" +pnpm run test:scripts > $T/$REL-gate-scripts.log 2>&1; echo "scripts_exit=$? $(grep -E '^# (pass|fail|skip)' $T/$REL-gate-scripts.log | tr '\n' ' ')" +pnpm --filter editor exec next build > $T/$REL-gate-editor-build.log 2>&1; echo "editor_build_exit=$?" +s=$(date +%s); pnpm e2e $(cat $T/$REL-gate-specs.txt) > $T/$REL-gate-e2e.log 2>&1; echo "e2e_exit=$? $(( $(date +%s)-s ))s $(sed 's/\x1b\[[0-9;]*m//g' $T/$REL-gate-e2e.log | grep -E '[0-9]+ passed|[0-9]+ failed|[0-9]+ flaky|[0-9]+ skipped' | tail -3 | tr '\n' ' ')" +echo "exit=0 $(date +%T)" diff --git a/plans/tools/rollout/home-preview.sh b/plans/tools/rollout/home-preview.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Slice H preview: compose + build the homepage from the primary's real data, deploy to the Pages preview branch "refresh". +cd /home/user/Projects/yt-dlp-transcript-browser || exit 1 +T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +echo "START $(date +%T) $(git rev-parse --short HEAD)" +s=$(date +%s); pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build homepage > $T/$REL-home-preview-build.log 2>&1; echo "build_homepage_exit=$? $(( $(date +%s)-s ))s summary_version=$(jq -r '.version // "?"' homepage/public/homepage-summary.json) monthly=$(jq -r '.monthly|length' homepage/public/homepage-summary.json 2>/dev/null)" +s=$(date +%s); pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts deploy homepage --preview refresh > $T/$REL-home-preview-deploy.log 2>&1; echo "deploy_preview_exit=$? $(( $(date +%s)-s ))s $(grep -o 'https://[a-z0-9.-]*archilyzer.pages.dev' $T/$REL-home-preview-deploy.log | tail -2 | tr '\n' ' ')" +echo "exit=0 $(date +%T)" diff --git a/plans/tools/rollout/home-publish.sh b/plans/tools/rollout/home-publish.sh @@ -0,0 +1,6 @@ +#!/bin/sh +cd /home/user/Projects/yt-dlp-transcript-browser || exit 1 +T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +s=$(date +%s); pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build homepage > $T/$REL-home-build.log 2>&1; echo "build_homepage_exit=$? $(( $(date +%s)-s ))s" +s=$(date +%s); pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts deploy homepage > $T/$REL-home-deploy.log 2>&1; echo "deploy_homepage_exit=$? $(( $(date +%s)-s ))s $(grep -o 'https://[a-z0-9.-]*archilyzer.pages.dev' $T/$REL-home-deploy.log | tail -1)" +echo "exit=0" diff --git a/plans/tools/rollout/md5.sh b/plans/tools/rollout/md5.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# md5 sweep over settings.json + every site.json + every config.json + homepage.json; argv[1] = label +T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +cd /home/user/Projects/yt-dlp-transcript-browser || exit 1 +md5sum settings.json transcripts/sites/*/site.json transcripts/channels/*/config.json transcripts/sites/_homepage/homepage.json > $T/$REL-md5-$1.txt +echo "$1: $(wc -l < $T/$REL-md5-$1.txt) files" diff --git a/plans/tools/rollout/restart.sh b/plans/tools/rollout/restart.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# ONE editor restart then umtool restart (release 8 rollout step 6). Logs $T/{editor,umtool}-start-r8.log +R=/home/user/Projects/yt-dlp-transcript-browser; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +for app in editor umtool; do + case $app in editor) port=3001;; umtool) port=3050;; esac + for p in $(pgrep -f "pnpm run start") $(pgrep -f "next-server"); do + [ "$(readlink /proc/$p/cwd 2>/dev/null)" = "$R/$app" ] && kill -TERM "$p" && echo "killed $app $p" + done + for i in $(seq 1 40); do ss -ltn | grep -q ":$port " || break; sleep 1; done + ss -ltn | grep -q ":$port " && { echo "RESTART_FAIL $app port $port still bound"; exit 1; } + cd "$R/$app" && setsid nohup pnpm run start -H 0.0.0.0 > "$T/$app-start-r8.log" 2>&1 < /dev/null & + for i in $(seq 1 90); do curl -s -o /dev/null --max-time 5 "http://localhost:$port/" && break; sleep 1; done + code=$(curl -s -o /dev/null --max-time 30 -w "%{http_code}" "http://localhost:$port/") + echo "RESTART_DONE $app / $code after ${i}s" +done +code=$(curl -s -o /dev/null --max-time 30 -w "%{http_code}" http://localhost:3001/tags); echo "editor /tags $code" +echo "exit=0" diff --git a/plans/tools/rollout/sites-redeploy.sh b/plans/tools/rollout/sites-redeploy.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Release 8 slice E (by name, never `all` until the docker build image is rebuilt): rebuild + deploy the five official sites by name (per-site host path), then the hub. +R=/home/user/Projects/yt-dlp-transcript-browser; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +cd $R || exit 1 +export WORKER_TOKEN=$(grep ^WORKER_TOKEN= editor/.env | cut -d= -f2); export ARCHILYZER_AGENT=fable-rollout +echo "START $(date +%T) $(git rev-parse --short HEAD)" +s=$(date +%s); pnpm -s ops build-deploy --json '{"siteIds":["jeralyzer","anilyzer","bonnellyzer","hasanalyzer","rekietalyzer"],"skipArchives":true}' --wait --wait-timeout 7200 > $T/$REL-ops-build-deploy-sites.log 2>&1; echo "sites_exit=$? $(( $(date +%s)-s ))s $(tr -d '\033' < $T/$REL-ops-build-deploy-sites.log | grep -o '"jobId": "[^"]*"\|"error": "[^"]*"\|\[deployed\] [^ ]*' | tr '\n' ' ' | cut -c1-400)" +s=$(date +%s); pnpm -s ops build-hub --json '{}' --wait > $T/$REL-ops-build-hub.log 2>&1; echo "build_hub_exit=$? $(( $(date +%s)-s ))s" +s=$(date +%s); pnpm -s ops deploy-hub --json '{}' --wait > $T/$REL-ops-deploy-hub.log 2>&1; echo "deploy_hub_exit=$? $(( $(date +%s)-s ))s $(grep -o 'https://[a-z0-9.-]*archilyzer-hub.pages.dev' $T/$REL-ops-deploy-hub.log | tail -1)" +echo "exit=0 $(date +%T)" diff --git a/plans/tools/rollout/smoke.sh b/plans/tools/rollout/smoke.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Release 8 rollout smoke against the live editor on :3001 (from smoke7.sh + smoke7b.sh). +B=http://localhost:3001; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9}; R=/home/user/Projects/yt-dlp-transcript-browser; fail=0 +echo "SMOKE_START $(date +%T)" +strip() { jq -S 'walk(if type=="object" then del(.heldMs,.fraction,.detail,.now,.rev,.builtAt,.updatedAt,.elapsedMs,.etaMs) else . end) | (if type=="object" and (.disk|type)=="object" then del(.disk.freeBytes) else . end)'; } +for pair in "/api/pulse /api/view/pulse" "/api/workers /api/view/workers" "/api/jobs/active /api/view/activeJobs" "/api/auto-queue/status /api/view/autoQueueStatus" "/api/scheduler/status /api/view/schedulerStatus" "/api/widget/sync /api/view/widgetSync" "/api/widget/actionable /api/view/widgetActionable" "/api/widget/cleanable /api/view/cleanable"; do + set -- $pair + s=$(date +%s) + a=$(curl -s --max-time 300 "$B$1" | strip); b=$(curl -s --max-time 300 "$B$2" | strip) + e=$(( $(date +%s) - s )) + if [ -n "$a" ] && [ "$a" = "$b" ]; then echo "PAIR_OK(live-stripped) $1 = $2 ($(printf %s "$a" | wc -c) bytes, ${e}s)"; else echo "PAIR_DIFF $1 vs $2 (a=$(printf %s "$a" | wc -c) b=$(printf %s "$b" | wc -c), ${e}s)"; fail=1; fi + printf %s "$a" > "$T/$REL-smoke-old$(echo $1|tr / _).json"; printf %s "$b" > "$T/$REL-smoke-new$(echo $2|tr / _).json" +done +for p in /api/view/bogus /api/view/invalidate-cache; do c=$(curl -s -o /dev/null --max-time 300 -w %{http_code} "$B$p"); [ "$c" = 404 ] && echo "404_OK $p" || { echo "404_FAIL $p $c"; fail=1; }; done +c=$(curl -s -o /dev/null --max-time 300 -w %{http_code} "$B/api/widget/presets"); [ "$c" = 200 ] && echo "PRESETS_OK" || { echo "PRESETS_FAIL $c"; fail=1; } +rev=$(curl -s --max-time 300 "$B/api/view/pulse" | jq -r '.rev|@uri') +for p in "/api/view/pulse?rev=$rev" "/api/pulse?rev=$rev"; do ch=$(curl -s --max-time 300 "$B$p" | jq -r .changed); [ "$ch" = false ] && echo "PULSE_OK $p changed=false" || echo "PULSE_CHANGED $p changed=$ch"; done +echo "active downloads: $(curl -s --max-time 300 "$B/api/view/activeJobs" | jq -c '[.. | objects | select(has("kind")) | .kind] | group_by(.) | map({(.[0]):length}) | add' 2>/dev/null)" +cb=$(curl -s --max-time 300 "$B/api/widget/cleanable" | jq -r .cleanableBytes); echo "cleanableBytes=$cb" +for p in / /channels "/channels?sort=size" /jobs /operations/diarization /settings /storage /tags /channels/FearAnd /channels/FearAnd/videos/03Bgz7vkgbs; do + f="$T/$REL-page$(echo "$p" | tr '/?=' '___').html" + s=$(date +%s); c=$(curl -s -o "$f" --max-time 300 -w %{http_code} "$B$p"); e=$(( $(date +%s) - s )) + [ "$c" = 200 ] && echo "PAGE_OK $p ${e}s $(wc -c < "$f")B" || { echo "PAGE_FAIL $p $c ${e}s"; fail=1; } +done +echo "BUILD_ID: $(cat $R/editor/.next/BUILD_ID)" +echo "ZodError in start log: $(grep -c ZodError $T/editor-start-r8.log 2>/dev/null)" +# release 7 additions +d=$(curl -s --max-time 300 "$B/api/view/autoQueueStatus" | jq -c '.download.deferred | if type=="array" and all(.[]; type=="object" and has("videoId") and has("until")) then "ok \(length)" else "bad" end' 2>/dev/null); case "$d" in '"ok '*) echo "DEFERRED_OK download.deferred well-formed ($d)";; *) echo "DEFERRED_UNEXPECTED $d"; fail=1;; esac +f="$T/$REL-page_operations_download.html"; c=$(curl -s -o "$f" --max-time 300 -w %{http_code} "$B/operations/download"); n=$(grep -c 'aria-label="Rate-limit cooldown"' "$f") +[ "$c" = 200 ] && echo "PAGE_OK /operations/download rate-limit-regions=$n" || { echo "PAGE_FAIL /operations/download $c"; fail=1; } +f="$T/$REL-page_sites.html"; c=$(curl -s -o "$f" --max-time 300 -w %{http_code} "$B/sites"); echo "PAGE $c /sites Build hub=$(grep -c 'Build hub' "$f") Deploy hub=$(grep -c 'Deploy hub' "$f")" +c=$(curl -s -o /dev/null --max-time 60 -w %{http_code} http://localhost:3050/); [ "$c" = 200 ] && echo "UMTOOL_OK / $c BUILD_ID $(cat $R/umtool/.next/BUILD_ID)" || { echo "UMTOOL_FAIL $c"; fail=1; } +echo "videoDeferrals keys: $(jq -c '.download|keys' $R/transcripts/.auto-queue/state.json)" +echo "SMOKE_FAIL=$fail $(date +%T)" +# release 8 additions: a channel list renders a title; the undownloaded video page shows the scan note (paramount-tactical has scan-only ids once scanned) +f="$T/$REL-page_channels_paramount-tactical_videos.html"; c=$(curl -s -o "$f" --max-time 300 -w %{http_code} "$B/channels/paramount-tactical/videos"); echo "PAGE $c /channels/paramount-tactical/videos $(wc -c < "$f")B open-rows=$(grep -o 'aria-label="open [^"]*"' "$f" | wc -l)" +echo "SMOKE8_END $(date +%T)" diff --git a/plans/tools/rollout/sync.sh b/plans/tools/rollout/sync.sh @@ -0,0 +1,7 @@ +#!/bin/sh +R=/home/user/Projects/yt-dlp-transcript-browser; T=${CLAUDE_JOB_DIR:?set CLAUDE_JOB_DIR}/tmp; REL=${REL:-r9} +cd $R || exit 1 +export WORKER_TOKEN=$(grep ^WORKER_TOKEN= editor/.env | cut -d= -f2); export ARCHILYZER_AGENT=fable-rollout +echo "START $(date +%T)" +s=$(date +%s); pnpm -s ops sync --json '{"slug":"teamrcn"}' --wait --wait-timeout 7200 > $T/$REL-ops-sync.log 2>&1; echo "sync_exit=$? $(( $(date +%s)-s ))s $(tr -d '\033' < $T/$REL-ops-sync.log | grep -o '"error": "[^"]*"\|"jobId": "[^"]*"' | head -2 | tr '\n' ' ')" +echo "exit=0 $(date +%T)" diff --git a/plans/tools/rollout/ytdlp-live-check.mjs b/plans/tools/rollout/ytdlp-live-check.mjs @@ -0,0 +1,13 @@ +import { chromium } from "/home/user/Projects/yt-dlp-transcript-browser/node_modules/.pnpm/@playwright+test@1.59.1/node_modules/@playwright/test/index.mjs"; +const site = process.argv[2] ?? "https://jeralyzer.pages.dev"; +const browser = await chromium.launch(); const page = await browser.newPage(); page.setDefaultTimeout(60_000); +await page.goto(site + "/", { waitUntil: "domcontentloaded" }); +const first = page.locator("[data-result-slug]").first(); +await first.waitFor({ timeout: 60_000 }); +await first.click(); +await page.getByRole("button", { name: /Mark both clip start and end first|Copy yt-dlp download command|Copy share link/i }).first().waitFor({ timeout: 60_000 }); +const ytdlp = page.getByRole("button", { name: /Mark both clip start and end first|Copy yt-dlp download command/i }); +const dl = page.getByRole("button", { name: /Download this transcript as a file/i }); +const md = page.getByRole("button", { name: /Copy this transcript as Markdown/i }); +console.log(`${site}: yt-dlp=${await ytdlp.count()} download-file=${await dl.count()} copy-md=${await md.count()}`); +await browser.close();