commit 1bb200ed4363d8372cd7d832b5486706ea9835ff
parent f5ed0e75792343f195ed071ddcb604d1e67139ba
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 22 May 2026 18:07:06 -0400
compact changelogs more
Diffstat:
2 files changed, 23 insertions(+), 52 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -2,35 +2,15 @@
## [Unreleased]
-### Added
-- New `/actionable` top-level page that aggregates pending work across all channels. Three sections: channels with undownloaded videos, channels with downloaded videos awaiting transcription, and channels whose report is stale or missing. Each row links to the channel and exposes inline "Download missing" / "Transcribe pending" / "Refresh report" buttons so action can be taken without leaving the page.
-- "Update all reports" button on `/actionable` queues a `refresh-report` job per channel (visible in `/jobs`) so the global view can be brought up to date in one click.
-- Pipeline jobs (sync, download-from-playlist, download-missing, download-missing-subs, retry-bucket, store-playlist) now automatically regenerate the channel's report when they finish. The regen runs inside the same job's log; a regen failure is logged as a `[warn]` and does not fail the job itself.
-- "Needs attention" hint card on the dashboard linking to `/actionable` when any channel has undownloaded videos or videos awaiting transcription. Hidden when both lists are empty.
-- Composable layered search (export viewer). The single search bar is now a query builder supporting AND / OR / NOT and arbitrary nesting; each layer targets transcripts, live chat, or title/channel. Per-layer results are memoised in IndexedDB so editing a deeper leaf only re-runs that layer against its already-narrowed scope. Old `?q=…&m=…&re=…` share-links auto-migrate to a single-leaf tree. See `export/CHANGELOG.md` for the user-visible details.
-- New `/deploy` page that previews `export/CHANGELOG.md` pending changes, offers a "Cut release" form (turns `## [Unreleased]` into a dated semver heading) and a "Deploy" button (runs `pnpm run deploy` in `export/` with live log streaming).
-- "Cut release" form at the bottom of the `/changelog` page for the editor's own changelog.
-- New `/changelog` page that renders the editor's `CHANGELOG.md`. Linked from the sidebar nav.
-- "Recent changes" card on the dashboard showing the most recent changelog entry, with a "View all" link to the full changelog.
-- Subtle blue dot on the sidebar **Changelog** nav item when a newer dated entry is detected and hasn't been viewed yet. The state is per-browser (localStorage) and clears when the changelog page is opened.
-- Per-heading copy-link buttons on the changelog page for permalinks to any section.
-- Per-channel "Include in Sync all" toggle on the `/channels` list. Excluded channels are skipped by **Sync all** and surfaced in the existing skipped tooltip with reason "excluded from sync all". Stored as `excludeFromSync` in `channels/<slug>/config.json`.
-- Sortable column headers on the `/channels` table. Clicking any column sorts client-side; clicking the active column flips direction. Default rendering is unsorted (input order preserved); sort state does not persist across navigation.
-
-### Changed
-- `/jobs/active` progress bars are now per-job instead of per-channel. Each running batch job (download-from-playlist, download-missing, download-missing-subs, sync, retry-bucket, whisper-all, whisper-bucket-downloaded-no-transcript) gets its own bar scaled to the job's own work. `initial` and `target` are snapshotted just before the job's work function runs (not at enqueue time), so the bar starts empty and fills exactly one unit per video downloaded or transcribed by that job. `whisper-all`'s target locks at start (since `runWhisperBatch` only acts on the directories present when it starts). Jobs without a natural metric (store-playlist, build/clean/transcode/clear/remove/refresh-report, single-video runs) get no bar. The old channel-wide bar has been removed.
-- "Recent changes" card on the dashboard now clamps to a fixed height with a gradient fade at the bottom; the entire card is a link to `/changelog`.
-- `/channels` rows are now dimmed when a channel is excluded from Sync all, matching the existing treatment for build exclusion.
-- `/jobs/active` now lists running jobs above queued jobs — both within each channel section and in the section order itself. Channels with running work appear before queued-only channels so the live jobs stay at the top of the page without scrolling. Ordering within each status group still follows the existing `queuedAt`-descending tiebreaker.
-- Sidebar nav badges are no longer redundant: **Jobs** continues to count running + queued, while **Active** now counts only running jobs.
-
-### Fixed
-- A failed download attempt that classifies a video as `deleted` / `private` / `members_only` (e.g. yt-dlp reports "removed by the uploader") is now respected even when an older `availability.json` says the video was once public. `resolveEffectiveAvailability` previously preferred `availability.json` unconditionally, so a video that went public → deleted between availability sweeps stayed classified as public until the next sweep, leaving it counted in undownloaded / awaiting-transcription lists indefinitely. The resolver now picks whichever source — `availability.json` or `download-outcome.json`'s last attempt — was written more recently.
-- Actionable lists no longer count videos that the availability check has flagged as `deleted`, `members_only`, or `private`. The "Channels with downloaded videos awaiting transcription" row count on `/actionable`, the dashboard "Needs attention" summary, the channel page's Transcribe stage count + disclosure list, and the Download stage's "dirs missing transcript & audio" list all now exclude unactionable videos. `undownloadedIds` was already filtered at snapshot generation, but the transcript buckets weren't — a deleted video with audio on disk used to inflate "awaiting transcription" indefinitely. Per-video row status in the channel video list is unchanged; excluded videos still display their true on-disk state (so the existing "needs action" filter can hide them via the `excluded` flag).
-- Channel page: clicking a pipeline stage in the side rail (or a mobile stage badge) no longer jumps up into the selected video's viewer panel. The per-video stage cards were emitting the same DOM ids as the channel-level pipeline sections, so the browser scrolled to the first match. The per-video cards now use a `video-stage-` prefix.
-- Channel page sticky/anchor offsets bumped to clear the sticky `StatusHeader`. The left video list and the pipeline stage rail previously pinned too high (`top: 1rem` and `top: 8rem` respectively) and sat partially behind the header. Stage anchors used a fixed `scroll-margin-top: 8rem` that wasn't enough on narrow widths where the header's mobile badge row wraps. The video-list and stage-rail sticky tops are now `top: 7rem` (lg+), and stage sections use a responsive `scroll-margin-top` (11rem on mobile, 7rem on lg+).
-- Clicking a mobile stage badge (or a stage rail item) now auto-expands the collapsible "Channel pipeline & settings" wrapper if the user had collapsed it. Previously the hash navigation succeeded but the target section was inside a closed `<details>` and not visible. The target is also re-scrolled into view after the expansion so it lands at the correct position.
-- Audio-check probe now flags `.part` files with heavy mid-stream codec corruption as malformed even when ffmpeg exits 0. Previously a stream could rack up hundreds of per-packet AAC decode failures (`Error submitting packet to decoder`) while ffmpeg still exited 0, and the file was promoted to a finished download. The classifier now also treats >5 decoder-error lines in stderr as malformed.
-- Audio-check pipeline now scopes its `.part` discovery and final-probe to the video being downloaded, instead of scanning the whole channel `data/` tree and picking the first match. On channels with many in-progress `.part` files (cornbreadman: ~248), the orchestrator would latch onto a stale neighbour's `.part`, probe it instead of the current download, and then the success-path fallback would resolve a totally unrelated subdir's already-finalized `audio.mp3` as "the final file" — skipping transcode and letting the real `.mp4` finalize with no integrity check, only to fail noisily during the next transcode-untranscoded sweep.
-- Audio-check pre-launch pass (`.testing` cleanup, `.good` → `.part` restore, malformed-`.part` probe) is now also scoped to the targeted video's subdir when the video id is known from the URL. Previously the pre-pass scanned every subdir under `<channel>/data/` and full-decoded each pre-existing `.part` through ffmpeg, so kicking off a single download on a channel with many interrupted prior attempts (cornbreadman) would stall for minutes emitting `Pre-check OK ... verdict=...` lines for ~30 sibling videos before yt-dlp even started. Rumble URLs (id not knowable until `metadata.info.json` is written) still scan all subdirs as before.
-- Editor's `/changelog` page no longer renders unstyled. The `@source` directive in `editor/app/globals.css` pointed at a non-existent path, so Tailwind never scanned the shared `<Changelog>` component for class usage.
+- **`/actionable` page** aggregating pending work across all channels — three sections (channels with undownloaded videos, channels with downloaded videos awaiting transcription, and channels with stale or missing reports), each row linking to the channel with inline "Download missing" / "Transcribe pending" / "Refresh report" buttons. An "Update all reports" button queues a refresh per channel. A "Needs attention" card on the dashboard links here whenever either of the first two lists has anything in it.
+- **Pipeline jobs regenerate the channel's report when they finish** (sync, download-from-playlist, download-missing, download-missing-subs, retry-bucket, store-playlist), so reports stay current without a manual refresh.
+- **`/changelog` page** rendering this changelog, linked from the sidebar nav. A subtle blue dot appears on the **Changelog** nav item when there's an entry you haven't viewed yet, clearing once the page is opened. Per-heading copy-link buttons give permalinks to any section, and a "Cut release" form at the bottom turns `## [Unreleased]` into a dated heading. The dashboard's "Recent changes" card shows the most recent entry, clamped to a fixed height with a gradient fade, and the whole card links to the full changelog.
+- **`/deploy` page** that previews `export/CHANGELOG.md` pending changes, offers a "Cut release" form for the export side, and a "Deploy" button that runs the export deploy with live log streaming.
+- **Composable layered search** in the export viewer — see `export/CHANGELOG.md` for the user-visible details. Old `?q=…&m=…&re=…` share-links keep working.
+- **Per-channel "Include in Sync all" toggle** on the `/channels` list. Excluded channels are skipped by **Sync all** with reason "excluded from sync all" in the skipped tooltip; their rows are dimmed in the table.
+- **Sortable column headers** on the `/channels` table — click any column to sort, click again to flip direction. Sort state doesn't persist across navigation.
+- **`/jobs/active` overhaul.** Each running batch job gets its own progress bar scaled to that job's own work, replacing the old channel-wide bar. Running jobs now appear above queued ones (both within each channel section and across channels), so live work stays at the top of the page. The sidebar **Jobs** badge still counts running + queued; the new **Active** badge counts only running.
+- **Deleted, private, and members-only videos no longer stick around as "awaiting transcription".** A failed download attempt that finds a video unavailable is now respected even if an older availability sweep had it as public. `/actionable` counts, the dashboard "Needs attention" summary, the channel page's Transcribe stage, and the Download stage's "dirs missing transcript & audio" list all exclude these videos. The video's row in the channel list still shows its true on-disk state.
+- **Channel page navigation fixes.** Clicking a pipeline stage in the side rail (or a mobile stage badge) no longer scrolls up into the selected video's viewer panel, sticky offsets now clear the header on every width, and clicking a stage when the "Channel pipeline & settings" wrapper is collapsed auto-expands it before scrolling into view.
+- **Audio download integrity fixes.** `.part` files with mid-stream codec corruption (hundreds of decoder errors that ffmpeg still treated as a clean exit) are now flagged as malformed instead of finalising as a finished download. A single download on a channel with many leftover `.part` files no longer stalls for minutes pre-checking unrelated videos, and no longer occasionally finalises the wrong file by latching onto a stale neighbour's `.part`.
+- **Editor's `/changelog` page no longer renders unstyled** — Tailwind now scans the shared changelog component.
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -2,23 +2,14 @@
## [Unreleased]
-### Changed
-- Reorganised the search/filter UI to put the profile selector at the top, framing both the query builder and the filters. The "Profile" row is no longer gated on having more than one channel — profiles cover the layered query *and* the filters, so the selector is meaningful even with a single channel. The "Share current search" button moved into that same row (it's the URL-shareable counterpart of a locally-saved profile). The dirty-profile indicator switched from the literal text "(unsaved)" to a small amber `•` — "(unsaved)" was doing double duty as both the placeholder option label and the dirty marker, which made the wording ambiguous.
-- The filter section (channels, Type, Audience, Availability) is now collapsible — same pattern as Advanced options. The summary stays visible whether open or closed and shows "{N} of {Total} channels" with a compact comma-separated list of the selected channel names when collapsed (truncated with an ellipsis if it overflows; full list available on hover via the row's `title`). Open/closed state persists across reloads in localStorage alongside the existing channel-group collapse state, and toggling never marks a profile dirty.
-- Removed the "slow / medium / fast" cost badges next to each layer's scope dropdown. They read as a warning on the primary mode (transcripts) without giving users any actionable choice.
-- Search results now render through a virtualised list (`@tanstack/react-virtual`), one element per matching video. Cards mount in full when they enter the viewport (header + leaf sections + hits laid out in normal CSS flow inside the card) and unmount once scrolled away — earlier iterations virtualised every row inside each card, which produced occasional empty bands between a card's header and its leaf sections while ResizeObserver caught up to row sizes. The manual "Show more videos" pagination button and the "Videos per page" advanced option are gone; all matching videos render in one continuous virtualised list. When the search pipeline still caps at its per-batch hit limit, a "Load more results" button surfaces below the list (the existing `hitLimit` flow, just relabelled now that pagination of cards is no longer needed).
-
-### Added
-- "Reset layers" button next to the Search submit collapses the query builder back to a single empty transcripts leaf in one click. Disabled when the draft is already at that empty state.
-- "Revert" button surfaces in the Profile row whenever an active profile is dirty — re-applies the saved snapshot (filters and query tree both) so users can experiment with edits and roll them back without re-selecting the profile.
-- "Reset everything" button in the filter reset row clears the layered query *and* every filter (channels, type, audience, availability) and drops the active profile pointer in one shot. The narrower "Reset channels" / "Reset all filters" buttons stay alongside it.
-- Composable layered search. The single search bar is now a query builder: any number of layers can be combined with AND / OR / NOT and arbitrary nesting. Each layer targets a scope (transcripts, live chat, or title/channel), and matches from every contributing layer are surfaced in the result list with a per-layer colour swatch. Per-layer results are memoised in IndexedDB so editing a deeper leaf only re-runs that layer against its already-narrowed scope. Simple one-keyword search still looks like a single input — the builder collapses to compact mode when there's only one layer. The composite query serialises into a new `qt=` URL parameter; legacy `?q=&m=&re=` links auto-migrate to a one-layer tree.
-- "Unwrap" button on every non-root group in the query builder. Flattens the group's children into the parent at the group's position, giving users a fast way back out of a sub-group they no longer want. If the group has NOT set, each promoted child inherits a flipped `negate` so the meaning of the subtree doesn't silently flip.
-- New `/changelog` page that renders the export's `CHANGELOG.md`. Linked from the right side of the sticky header.
-- Per-heading copy-link buttons on the changelog page for permalinks to any section.
-
-### Fixed
-- Layered search no longer drops every hit row after a page reload. `LayerHit.leafId` is set from a session-scoped counter id and was stored verbatim in the IndexedDB layer cache; on reload `parseRoot` re-allocates leaf ids from a fresh counter, so cached hits arrived carrying stale ids and got silently dropped by the renderer's bucket-by-leaf step. The "N hits" count still summed correctly because it's computed before bucketing — matching the reported symptom. `applyCached` now rewrites each cached hit's `leafId` to the current leaf's id.
-- Layered search no longer drops transcript hit rows for AND-chained leaves after certain edit sequences. The per-leaf result cache was keyed without `contributeHits`, so a leaf evaluated with "Show hits in results" off would write an empty-hits cache entry that a later same-query/scope hits-on leaf would silently inherit — the result list surfaced the video but no hit timestamps. Adding `contributeHits` to the canonical hash separates the two payload shapes. Toggling regex (which already changed the hash) was the existing workaround.
-- Filter changes survive a refresh again. When the user had any saved profile active, `commitSearch` was writing the new state to the working snapshot but leaving the profile pointer set; hydration then preferred the unchanged profile snapshot and silently reverted the commit. Committing changes that diverge from the active profile now clears the profile pointer (the selector falls back to "(unsaved)") so the next reload reads the working snapshot.
-- `build:index` no longer runs out of memory on large datasets. Both the transcripts and the subs page writers now stream each entry directly to disk and hash it incrementally instead of materialising the joined page body in memory, which previously OOMed when a single live-chat track encoded to hundreds of MB. The post-processing "is this video deleted?" pass is now an in-memory LMDB scan (the availability check is cached in the mtime record at mutation time, schema bumped to 8 to invalidate the old cache) instead of ~28k sequential `availability.json` reads on every build. The `build:index` script also pre-sets `--max-old-space-size=8192` as a backstop.
+- **Composable layered search.** The single search bar is now a query builder: combine any number of layers with AND / OR / NOT and arbitrary nesting, each layer scoped to transcripts, live chat, or title/channel. Matches from every layer surface in the result list with a per-layer colour swatch. Editing one layer doesn't re-run the others, so deep nests stay fast. Simple one-keyword search still looks like a single input — the builder collapses to compact mode when there's only one layer. Legacy `?q=&m=&re=` share-links auto-migrate to a one-layer tree.
+- **Query-builder controls.** A "Reset layers" button collapses the builder back to one empty transcripts leaf. An "Unwrap" button on every non-root group flattens its children into the parent; if the group had NOT set, the promoted children inherit a flipped NOT so the meaning of the subtree doesn't silently change. The "slow / medium / fast" cost badges next to each scope dropdown have been removed — they read as a warning on the primary mode (transcripts) without giving you anything to do about it.
+- **Profile row at the top of the search.** The profile selector now sits above both the query builder and the filters, and is shown even with a single channel since profiles cover both query and filters. The "Share current search" button moved into the same row. The dirty-profile indicator is now a small amber `•` instead of the ambiguous text "(unsaved)". A **Revert** button appears whenever the active profile is dirty, re-applying the saved snapshot (filters and query both) so you can experiment and roll back without re-selecting the profile.
+- **Collapsible filters section** (channels, Type, Audience, Availability). The summary stays visible whether open or closed and shows "{N} of {Total} channels" with a compact list of the selected channel names (truncated on overflow; full list on hover). Open/closed state persists across reloads, and toggling never marks a profile dirty.
+- **"Reset everything" button** in the filter reset row clears the layered query *and* every filter and drops the active profile pointer in one shot. The narrower "Reset channels" / "Reset all filters" buttons stay alongside.
+- **Virtualised search results.** Results now render through a virtualised list, one element per matching video. Cards mount in full when they enter the viewport and unmount once scrolled away — much smoother on long result lists, and no empty bands while sizes settle. The "Show more videos" pagination button and the "Videos per page" advanced option are gone; everything renders in one continuous list. When the search pipeline still caps at its per-batch hit limit, a "Load more results" button appears below the list.
+- **`/changelog` page** rendering this changelog, linked from the right side of the sticky header, with per-heading copy-link buttons for permalinks.
+- **Search results survive a page reload.** Previously the cached hit rows would silently disappear after refresh (the "N hits" count stayed correct, but the list of timestamps was empty).
+- **AND-chained search layers no longer drop hit timestamps** after certain edit sequences. Previously, evaluating a layer with "Show hits in results" off and then turning it on at the same query/scope could surface the matching video without any timestamps; the workaround was toggling regex on and off.
+- **Filter changes survive a refresh again** when a saved profile was active. Previously, committing a filter change with a profile selected would silently revert on the next reload.
+- **Index builds no longer crash on large datasets.** A single live-chat track encoding to hundreds of MB could previously exhaust memory; page bodies now stream to disk instead of being buffered, and the post-build availability pass no longer makes thousands of sequential file reads.