Archilyzer · Source

archilyzer

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

commit 5730940be7013765f94d1cac1f4d38c947b3311f
parent 4c16f4dc95ad78004e8bc6d0ec167e1219221559
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri,  3 Jul 2026 00:32:16 -0400

Auto-queue policy editor: add a "move to top" button

Each reorderable rule/group in the auto-queue policy tree now has a ⤒
button beside the existing ↑/↓ swap controls that jumps the node to the
front of its sibling list in one click (disabled on the first row).
Reordering stays local until "Save policy", matching the swap buttons.

Adds moveChildToTop/moveToTop mirroring moveChildIn/move, a UI e2e test,
and a changelog entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/CHANGELOG.md | 1+
Meditor/app/auto-queue/components/PolicyTreeEditor.tsx | 33+++++++++++++++++++++++++++++++++
Meditor/e2e/auto-queue.spec.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 88 insertions(+), 0 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **"Move to top" button on the auto-queue policy editor.** Each reorderable rule/group in the auto-queue policy tree gains a **⤒** button beside the existing ↑/↓ swap controls that jumps the node straight to the front of its sibling list in one click (disabled on the first row, like ↑). Reordering stays local until **Save policy**, matching the swap buttons. See `editor/app/auto-queue/components/PolicyTreeEditor.tsx` and `editor/e2e/auto-queue.spec.ts`. - **Kick VOD playback + VOD-expiry indicators.** Kick becomes a first-class platform (`Platform` union, `detectPlatform`, `platformFromMetadata` `/^kick/i`, `extractVideoId` kick branch, `defaultWebpageUrl`). Kick VODs have no iframe embed, so playback streams the HLS manifest yt-dlp resolves at download time: `summarize()` persists `manifest_url` → `hlsUrl` on the transcript summary/detail, and a new client-only `common/components/KickPlayer.tsx` plays it in a native `<video>` via the bundled **hls.js** (not react-player's file player, which loads hls.js from a CDN and would break the offline export). It exposes the same `seekTo`/`onReady`/`onProgress` handle as the YouTube player, so Kick gets full scrubbing + cue highlighting; on a fatal manifest error it falls back to an expiry notice + source link. Separately, a shared `common/lib/vodExpiry.ts` (retention: Kick 30d, Twitch 14d, tunable) drives a new `VodExpiredBadge` on search result cards for likely-deleted Kick/Twitch VODs, with a `title=` tooltip explaining each platform's retention. Cache versions bumped so stale data re-derives (`transcriptStore` `DB_VERSION` 4, `normalizeTranscript` `CUES_FILE_VERSION` 2). See `common/lib/{platform,transcripts,transcripts-server,vodExpiry,format}.ts`, `common/components/{KickPlayer,PlayerProvider,badges,TranscriptSearch}.tsx`, `common/ytdlp/runYtdlp.ts`, and `export/e2e/kick-vod.spec.ts`. - **Clicking a site on the Sites list now opens its edit page instead of bouncing back to the list.** The sidebar site selector seeds the active site into the URL (`?site=`) on mount so the scoped server pages (Dashboard, Channels, Charts, Deploy) can read it — but it was also firing on the `/sites` CRUD pages, where a mount-time `router.replace("/sites?site=<id>")` raced and clobbered the in-flight navigation to `/sites/<id>` from a list link, dumping you back on the list. The seed is now skipped on `/sites*` routes (which never consume `?site=`), so site links navigate straight to the editor; scoped-page seeding is unchanged. See `editor/app/components/SiteScopeSelect.tsx`. - **"Build static export" can now skip the data rebuild and compose from existing staging.** The `export/` build normally regenerates the pool-wide data first (its npm `prebuild` hook runs `build:data` = `build:index && build:stats && build:templates`), and the index step (heavy transcript → page-tree + LMDB processing) dominates build time. A new **Skip data rebuild (index, stats, charts)** checkbox on the Build static export control lets you rebuild a site *without* that work — it runs only `compose:site && next build` against the current `.export-index/` staging, which is exactly what you want when re-composing after a code/theme/template change or building a different site from already-staged data. It routes to a new `build:nodata` export script (same body as `build`, but a distinct name so npm's `prebuild` hook doesn't fire); the checkbox reuses the streamed-log/queue/cancel machinery of the existing managed build. Assumes a prior full build produced the staging (do a full build first if the data is stale). Only the build-only control is affected — the one-click **Build & deploy** button always does a full build. See `export/package.json` (`build:nodata`), `editor/app/deploy/buildDeployCore.ts` (`runBuildPhase` `skipData`), `editor/app/build/buildAction.ts` (`buildExportAction`), and `editor/app/deploy/components/BuildExportButton.tsx`. diff --git a/editor/app/auto-queue/components/PolicyTreeEditor.tsx b/editor/app/auto-queue/components/PolicyTreeEditor.tsx @@ -89,6 +89,25 @@ function moveChildIn( }; } +function moveChildToTop( + node: AutoQueueNode, + parentId: string, + index: number, +): AutoQueueNode { + if (!isGroup(node)) return node; + if (node.id === parentId) { + if (index <= 0 || index >= node.children.length) return node; + const next = [...node.children]; + const [item] = next.splice(index, 1); + next.unshift(item); + return { ...node, children: next }; + } + return { + ...node, + children: node.children.map((c) => moveChildToTop(c, parentId, index)), + }; +} + function makeLeaf(type: "channel" | "platform" | "all"): AutoQueueLeaf { return { id: newId(), match: { type }, weight: 1, maxWorkers: null }; } @@ -134,6 +153,8 @@ export function PolicyTreeEditor({ setRoot((r) => addChildTo(r, parentId, child) as AutoQueueGroup); const move = (parentId: string, index: number, dir: -1 | 1) => setRoot((r) => moveChildIn(r, parentId, index, dir) as AutoQueueGroup); + const moveToTop = (parentId: string, index: number) => + setRoot((r) => moveChildToTop(r, parentId, index) as AutoQueueGroup); const save = async () => { setSaving(true); @@ -191,6 +212,7 @@ export function PolicyTreeEditor({ remove={remove} addChild={addChild} move={move} + moveToTop={moveToTop} /> <div className="flex items-center gap-3"> @@ -231,6 +253,7 @@ type EditorProps = { remove: (id: string) => void; addChild: (parentId: string, child: AutoQueueNode) => void; move: (parentId: string, index: number, dir: -1 | 1) => void; + moveToTop: (parentId: string, index: number) => void; }; function NodeEditor(props: EditorProps) { @@ -298,6 +321,16 @@ function NodeEditor(props: EditorProps) { type="button" className={btnClass} disabled={index === 0} + aria-label="Move to top" + title="Move to top" + onClick={() => props.moveToTop(parentId, index)} + > + ⤒ + </button> + <button + type="button" + className={btnClass} + disabled={index === 0} onClick={() => props.move(parentId, index, -1)} > ↑ diff --git a/editor/e2e/auto-queue.spec.ts b/editor/e2e/auto-queue.spec.ts @@ -381,6 +381,60 @@ test("UI: build a policy in the editor, save it, and start the runner", async ({ .toBe(true); }); +test("UI: Move to top jumps a rule to the front of its siblings", async ({ + page, +}) => { + await resetData(null); + await makeChannel("alpha", []); + await makeChannel("beta", []); + await makeChannel("gamma", []); + await writeSettings({ + adminTitle: "Test Admin", + maxTranscriptPageBytes: 8388608, + sleepBetweenDownloadsSeconds: 0, + minFreeDiskGB: 0, + workers: ONE_WORKER, + autoQueue: { + transcription: { + enabled: false, + maxWorkers: 1, + root: { id: "root", mode: "strict", children: [] }, + }, + download: {}, + }, + }); + + await page.goto("/auto-queue"); + const section = page.locator("section", { + has: page.getByRole("heading", { name: "Auto-transcribe" }), + }); + + // Add three channel rules pointing at alpha, beta, gamma (in that order). + for (const slug of ["alpha", "beta", "gamma"]) { + await section.getByRole("button", { name: "+ Channel rule" }).click(); + const values = section.getByLabel("channel rule value"); + await values.last().selectOption(slug); + } + + // Move the last rule (gamma) straight to the top. + const topButtons = section.getByRole("button", { name: "Move to top" }); + await topButtons.last().click(); + + await section.getByLabel(/Enable auto-transcribe/).check(); + await section.getByRole("button", { name: "Save policy" }).click(); + await expect(section.getByText("Saved.")).toBeVisible(); + + // gamma is now first in the persisted sibling order. + const settings = await readJson<{ + autoQueue: { + transcription: { root: { children: { match?: { value?: string } }[] } }; + }; + }>("test-settings.json"); + expect( + settings.autoQueue.transcription.root.children.map((c) => c.match?.value), + ).toEqual(["gamma", "alpha", "beta"]); +}); + test("status: reports the runner running and snapshot-derived pending counts", async ({ request, }) => {