Tick items whose root cause already landed, and fix two doc-drift items:
- L82: align ROADMAP-PINCHFLAT Phase F Source/MediaItem sketch to the shipped
shared/ipc.ts shape (videoId/itemCount/watched/feedUrl + required
url/playlistTitle/playlistIndex).
- L89: note H8's afterAllArtifactBuild checksum hook fixes release-artifact
consistency going forward; stale dist artifacts handled by L84.
- Tick the wire-or-remove trio (M5/M6/UX1 — all wired in the DownloadBar
Advanced panel) and MAX_ENQUEUE_BATCH (M33 fixed the false comment).
- Tick UX ref-duplicates resolved by their roots (UX10=H5, UX13=M22, UX14=L76,
UX25=L78), the (ref) a11y line (UI33/UI30/UI28/UI29 all done), and the
(OK)/(non-goal) Windows-conventions line.
Docs only; no code touched. 149 -> 137 open audit items.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI31 — the native <Select> wrapper now takes a `disabled` prop that passes to
the <select> and applies disabled foreground/background/stroke tokens +
not-allowed cursor, so it can show a disabled state like Fluent controls.
UI32 — dropped the datetime-local's explicit colorScheme:'light dark' so it
inherits the resolved in-app scheme from the app root; the native calendar popup
now follows the app's Light/Dark theme, consistent with the native <Select>,
instead of tracking the OS preference.
W16 — the icon-only row-action clusters (QueueItem + HistoryView) enforce a
>=40x40px hit target via a `& button` min-size rule (glyph unchanged, subtle
button just gains padding). Labels were already non-hover-only: the shared Hint
shows on :focus-within and every action has an aria-label. LibraryView item rows
have no icon-only actions, so nothing there needed changing.
W18 — defended the two color-meaning surfaces against forced-colors: accent
swatches set forced-color-adjust:none (they're color previews; otherwise all
flatten to one system color), and the SegmentedControl active segment paints
with system Highlight/HighlightText under @media (forced-colors: active) so the
checked state stays visible. Status chips (Fluent Badge) and thumbnail tints
correctly let the system palette win.
typecheck + 253 tests + eslint + prettier + production build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI33 — render titles as real headings so Narrator heading-navigation works:
the shared ScreenHeader title is now <Subtitle2 as="h1"> (one h1 per screen in
one place — Downloads/History/Library/Settings/Terminal); DownloadsView "Queue
(N)" and all 11 settings cards are <Subtitle2 as="h2"> sections; Onboarding
"Welcome" is <Title2 as="h1">. A minimal h1–h6 margin reset in base.css zeroes
the UA heading margin (Fluent's typography classes out-specify the element
selector, so the visual ramp is unchanged) so layout matches the former spans.
UI27 — give the command palette proper combobox/listbox semantics: the input is
role="combobox" with aria-controls/aria-activedescendant/aria-autocomplete, the
list is role="listbox", and each row is role="option" with aria-selected, so the
active row is announced to screen readers via active-descendant. Focus stays on
the input (standard combobox pattern), so the rows became non-focusable divs;
onMouseEnter is kept to unify pointer + keyboard highlight on one sel state.
Also corrected UI30's checkbox in the audit (already implemented via the shared
SegmentedControl radiogroup; only the marker lagged).
typecheck + 253 tests + eslint + prettier + production build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes H4 and the remaining formatter half of H2.
- New @shared/format.ts is the single home for fmtBytes/fmtSpeed/fmtEta, imported
by both main and renderer. main/lib/formatters.ts re-exports them.
- DownloadProgress now carries raw speedBytesPerSec/etaSeconds instead of
pre-formatted speed/eta strings. main's parseProgress emits the raw numbers.
- The renderer stores the raw numbers on DownloadItem; QueueItem formats them for
per-item display, and summarizeQueue sums/maxes them directly. The lossy
string->number->string round-trip in queueStats (parseSpeed / parseEtaSeconds /
a local formatSpeed / formatEta) is deleted, along with a duplicate fmtEta in
store/downloads.ts.
- Per-item and aggregate speed now use the same 1024-based scale; the aggregate
previously used a 1000-based formatter (a latent inconsistency).
Tests updated for the raw-number contract (parseProgress, summarizeQueue).
typecheck + 248 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates the scattered YouTube-URL parsing (the clean, renderer-contained
half of H2):
- Move the complete youtubeId (watch/shorts/embed/live/youtu.be) into
lib/urlHelpers.ts as the single home.
- thumb.ts, queueStats.ts (sameVideo), and store/downloads.ts (titleFromUrl)
now import it; the three ad-hoc reimplementations are removed.
- titleFromUrl gains correctness: it previously labeled ANY host with a ?v=
param as "YouTube video (…)" and missed youtu.be/shorts; it's now host-correct
and covers every YouTube shape. sameVideo now also dedupes /shorts/ID against
/watch?v=ID.
- Unit-tested in test/clipboardLink.test.ts (+4 cases, 247 pass).
The formatter half of H2 is deferred to travel with H4: the renderer re-parses
speed/eta strings only because raw numbers aren't carried across the IPC boundary
(H4), and fmtBytes (1024) vs formatSpeed (1000) differ intentionally.
typecheck + 247 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Housekeeping: the command-preview / incognito / per-download-options features
were implemented and self-reviewed in the previous commits but their audit
checkboxes were still open. Marks M5, M6, UX1 done with fix notes. Completes the
SHOULD-fix-for-1.0 release-gate tier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Critical:
- C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS
in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all
derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx
shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts.
- C2: break the downloads<->sources circular import via a typed event bus
(store/coordinator.ts). App eagerly imports sources for side-effects so startup
load + scheduled --sync + downloadCompleted subscription still run.
Reliability:
- L140/L148: cancel/pause release the active slot synchronously; identity-guarded
releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected
by a stale entry nor run over the concurrency cap.
- L141: renderer history capped at 500 + url de-dup to match main.
- R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry
instead of an empty catch.
- R7: encryptSecret warns before the plaintext fallback.
typecheck + 243 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-best format option claimed ext:'mp4', but the real output container
for the best pick depends on the user's videoContainer setting (mp4/mkv/webm),
so it mislabeled mkv/webm outputs. ext is optional on FormatOption and the
option's label is the fixed "Best available", so the field is simply omitted.
hasAudio:true is kept (the best pick always merges an audio track).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
writeJsonAtomic always pretty-printed (JSON.stringify(value, null, 2)), which
needlessly inflates size and write time for the media-items store (up to
MAX_ITEMS=20000 rows). Added an optional `pretty` flag (default true, so
history/sources/templates/errorlog stay hand-inspectable) threaded through
createJsonStore, and set it false for media-items.json.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M23: replaceMediaItems wrote [...newItems, ...others] and the JSON store sliced
the combined list to MAX_ITEMS, so re-indexing one large source could
silently evict ANOTHER source's tail. Now other sources are always kept in
full and only the source being (re)indexed is capped to the remaining
budget (MAX_ITEMS - others.length).
M27: enqueueItems forced settings.defaultKind for every item, so a channel
couldn't be queued as audio without flipping the global default. Added an
optional `kind` override (falls back to the default) and a video/audio
SegmentedControl in the Library action row, seeded from defaultKind so
existing behavior is unchanged until toggled.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audio quality preset "Best (MP3)" named a format that's wrong whenever the
audio format is opus/flac/wav. Renamed it to "Best" so the label never
contradicts the chosen audioFormat (which is picked separately). Bitrate presets
stay as bitrates and are already ignored for lossless formats (M21).
- AUDIO_QUALITY_OPTIONS[0]: 'Best (MP3)' -> 'Best' (buildArgs already had a
`case 'Best'` returning '0', so arg generation is unchanged).
- Updated the main + renderer defaults and the preview/mock/test fixtures.
- Added a one-line migration in getSettings() that rewrites a stored legacy
'Best (MP3)' to 'Best' so existing users' dropdowns match.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LibraryView mapped url -> status by last-write, so when a URL had multiple
queue entries ("Download anyway"), the row reflected whichever item was
iterated last. Added a STATUS_PRIORITY ranking (completed > downloading >
queued > saved > paused > error > canceled) and keep the highest-priority
status per URL, so a row shows the most meaningful state of that video.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The completion side-effect (add to history unless private + markDownloaded on
the source MediaItem) was duplicated in applyEvent('done') and the preview
fake-ticker, and had already drifted (the preview copy omitted formatId/
formatHasAudio). Extracted one recordCompletion(item) helper that both call.
Also tightened a latent bug: the old applyEvent ran markDownloaded for any
'done' event with a mediaItemId, even when the item was canceled (the reducer
keeps canceled items as 'canceled'). recordCompletion now runs only when
status === 'completed', so a canceled collection item is no longer wrongly
marked downloaded (and will be retried on the next re-sync).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L119: Standardized empty-state copy on the "No X yet." pattern. Terminal
"Output will appear here." -> "No output yet."; Diagnostics
"No errors logged." -> "No errors yet."
L151: Range-dash convention is ASCII hyphen everywhere. Changed the lone
en-dash prose range ("2-3 is a good balance") to a hyphen, matching the
time-range placeholders (1:30-2:00).
L153: The three "Dismiss" close buttons now name what they dismiss --
"Dismiss suggested link" (DownloadBar + Library suggestion banners) and
"Dismiss duplicate warning" (DownloadBar dup row).
L95 left open: the Cookies-card "sign in"/"sign-in" usage is already correct
English (verb vs modifier); no change warranted.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L106: Established the convention — ArrowClockwise = retry/redo a local action
(retry-all, re-download, retry queue item); ArrowSync = network sync/
update (sync sources, reindex, check/apply updates). Flipped the two
misclassified sites (LibraryView "check for new" sync, SettingsView
"Update yt-dlp") to ArrowSync and dropped the now-unused imports.
L107: QueueItem queued-row Spinner was size="extra-tiny"; every other spinner
is "tiny". Standardized to "tiny".
L122: The app-update and yt-dlp-update buttons showed "Checking…"/"Updating…"
in the label AND a separate sibling Spinner. Folded the spinner into the
button icon (icon swaps to Spinner while busy), matching DownloadBar/
LibraryView, and removed the redundant sibling Spinner.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addressing issues found reviewing this session's audit commits:
REGRESSIONS FIXED
- updater.ts (L52): reverted the spawn(detached) installer launch. Raw
CreateProcess/spawn fails with ERROR_ELEVATION_REQUIRED if the NSIS build
flips to perMachine (the config comment invites this), and it silently
dropped shell.openPath's launch-error detection. Restored shell.openPath
(ShellExecute honors the elevation manifest) and kept the L52 goal by
replacing the 1500ms timer with setImmediate(app.quit).
- LibraryView.tsx (M15): native <button> doesn't inherit color (UA sets
ButtonText), so the source-card chevron (currentColor) would render wrong
in dark mode. Added color: colorNeutralForeground1 to cardHead.
INEFFECTIVE CHANGE REVERTED
- base.css (W18): forced-color-adjust:auto on * is the CSS default (no-op),
and [class*="backdrop"] never matches Fluent/Griffel's hashed atomic class
names. Reverted; W18 unmarked (needs real High-Contrast testing).
WEAK FIX REVERTED
- Onboarding.tsx (L67): "Skip" and "Get started" called the identical handler
on a single-screen onboarding. Removed the duplicate button; L67 unmarked
(real ask is a "show tips again" revisit affordance).
STYLE / CORRECTNESS
- Stripped UTF-8 BOMs accidentally added to Select/CommandPalette/App/
Onboarding by the PowerShell sanitizer; left pre-existing BOMs untouched.
- DownloadBar.tsx: merged the duplicate @shared/ipc import; removed a stray
double blank line.
- qualityOptions.ts (L29): restored the original `satisfies Record<MediaKind,
readonly string[]>` constraint + the noUncheckedIndexedAccess rationale
comment (the extraction had dropped both for a weaker `as const`).
- downloads.ts: removed double blank line left by the QUALITY_OPTIONS move.
- binaries.ts: corrected YTDLP_MISSING_MSG doc comment ("at startup" -> the
actual call sites).
typecheck + 242 tests + eslint all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
firstUrl() in DownloadBar was a local duplicate of looksLikeUrl iteration
(scanning multi-line text for the first valid URL). Promoted to
firstUrlInText() in src/renderer/src/lib/urlHelpers.ts alongside the other
URL utilities, re-exported from useClipboardLink.ts. DownloadBar now imports
firstUrlInText from there instead of maintaining a local copy.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L67: Onboarding now has a 'Skip' button alongside 'Get started', both in a
right-aligned flex row. Both call update({hasCompletedOnboarding:true})
so the behavior is the same — Skip just reads as optional for users who
want to get started immediately without reading the tips.
L86: HistoryView now clears any selected IDs that are no longer visible when
the filter (query or kind) changes. A useEffect on `filtered` intersects
the current selection with the new visible set and calls setSelected only
if the size actually changed (avoids re-renders when the filter doesn't
affect the selection).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.claude/launch.json was committed but VS Code reads launch configs from
.vscode/launch.json, so no tool was consuming it. Moved to the correct
location and removed the misplaced file from git tracking.
Also added .vscode/settings.json to .gitignore so local VS Code user
preferences don't end up tracked.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W18: Added explicit forced-color-adjust:auto on the universal selector so
the browser's HC mode can override all custom token colors. Added an
@media(forced-colors:active) rule that replaces the CommandPalette
backdrop's rgba(0,0,0,0.32) with the Canvas system color + a ButtonText
border, so the palette remains perceivable under Windows High Contrast.
W19: Already fixed in the SR8 commit (window title now reflects download
state via mainWindow.setTitle in the taskbarProgress handler). Marking
done here with the companion CSS change.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cleanError() strips leading 'ERROR:' from yt-dlp stderr before sending
errors to the UI and error log. The browser-preview seed entry in
store/errorlog.ts had the raw 'ERROR: [youtube]...' prefix, making the
preview inconsistent with what production shows. Removed the prefix from
the seed string so the preview and production rendering match.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window title was always 'AeroFetch' regardless of activity, so screen
readers and taskbar previews couldn't tell whether downloads were running.
The taskbarProgress IPC handler (which already fires on each progress tick)
now calls mainWindow.setTitle() alongside setProgressBar/setOverlayIcon:
- Idle: 'AeroFetch'
- Downloading: 'AeroFetch -- N downloads active'
- Error: 'AeroFetch -- Error'
This reuses the same overlay badge label string so the text is consistent
between the taskbar tooltip and the title bar.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W8: Added Ctrl+, as a standard Settings accelerator alongside the existing
Ctrl+K palette shortcut. The key handler now handles both in one listener
so there's a single window.addEventListener lifecycle.
W9: Command palette now restores focus to the previously focused element when
it closes. prePaletteRef stores document.activeElement at open time;
onClose fires a requestAnimationFrame focus-restore after unmounting the
palette, so the focus returns to the trigger (a sidebar nav button, the
URL input, etc.) rather than leaving focus stranded at the document body.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both interactive headers in LibraryView used role="button" on divs, which is
invalid when they contain child interactive elements (group header had nested
All/None and Download buttons).
- Group header (groupHead): split into an outer flex div + a native <button>
(groupToggle style) for the toggle/expand action + sibling Fluent Buttons
for All/None and Download. The toggle button gets aria-expanded and a
composite aria-label (title + item count). e.stopPropagation() removed
from the sibling buttons since they are no longer inside the toggle.
- Source card header (cardHead): converted from a role="button" div to a
native <button> with width:100%, border:none, and background:transparent.
aria-label set to source.title. tabIndex/onKeyDown removed (native button
handles keyboard by default).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both DownloadBar (renderer, drag-drop) and deeplink.ts (main, argv) had their
own regex to extract URL= from a Windows Internet Shortcut file -- with subtly
different patterns (/^\s*URL\s*=\s*(\S+)/ vs /^URL=(.+)$/).
Added parseUrlShortcutContent() to @shared/ipc. Both callers now delegate to
it and apply their own http/https guard (looksLikeUrl in renderer, asHttpUrl
in main). The local parseUrlFile wrapper in DownloadBar is kept as the
validation compositing point; it's now a one-liner.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
which assembles it directly from @shared/ipc. store/downloads.ts no longer
imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
updated to import from the new module.
L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
so the IPC response returns to the renderer before the process exits.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L19: App.tsx replaced `setTimeout(() => focus(), 60)` in the "New download"
palette action with `requestAnimationFrame(() => requestAnimationFrame(...))`
to wait exactly one React render + paint cycle rather than an arbitrary 60ms
timer that could race on slow machines.
L73: DownloadBar's probe button label changed from "Fetch" to "Check URL" (hint
and aria-label both updated). Placeholder text updated to match. This
distinguishes it from LibraryView's "Index" action, which catalogs an entire
channel/playlist, making both labels clearly meaningful in context.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L46: Introduced YTDLP_MISSING_MSG constant in binaries.ts. All four callers
(download, indexer, probe, ytdlp) now emit the same user-facing string:
"yt-dlp.exe is missing. Open Settings -> Software update to re-download it."
This removes newline-embedded messages and four divergent phrasings.
L71: Removed readOnly from videoDir/audioDir folder inputs in SettingsView.
Added onChange handler so users can paste or type a path directly without
being forced to use the Browse button. The path still flows through
applySettings() in main, which validates it.
L74: Accent color swatches were triple-labeled (aria-pressed + aria-label +
title). Replaced with a single aria-label that embeds "(selected)" state,
plus title for the hover tooltip. Removed the incorrect aria-pressed
(these are selection buttons, not toggles).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L6: Select.tsx adds a `size` prop ('medium' | 'large'). When size='large',
the select is 40px tall (matching Fluent size='large' Input/Button) with
slightly larger padding and font. DownloadBar quality/format selects now
use size='large' so they align with the row's large Input and buttons.
L12: CommandPalette keyboard navigation now scrolls the highlighted item into
view on each ArrowUp/ArrowDown press. Uses a listRef + data-sel attribute
so no DOM imperative tracking of individual buttons is needed.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L9: Extract four thumbnail box dimensions into src/renderer/src/thumbSizes.ts
(THUMB_LG 120x68, THUMB_MD 108x64, THUMB_SM 72x44, THUMB_XS 60x34).
DownloadBar, QueueItem, HistoryView, and LibraryView all import and use
them instead of hard-coded strings -- one place to change the 16:9 sizing.
L28: DownloadBar now reflects Settings changes to defaultKind/defaultQuality
in real time, but only while the URL field is empty (idle state). A URL
typed or probed in progress is never discarded by a settings change.
M11: Clipboard reads now consistently use window.api.readClipboard (IPC) in
the paste button, matching the clipboard-watcher hook. navigator.clipboard
is no longer used for reads anywhere in the renderer.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L22: DownloadOptionsForm accepts optional `kind` prop. When set, hides
audio-specific fields (Audio format) for video downloads and vice versa
(Video container, Preferred codec) for audio downloads. Settings passes
defaultKind so the defaults card shows only relevant options.
L24: Replace lone window.open('htmlUrl', '_blank') with a new openUrl IPC
channel (shell:open-url) that calls shell.openExternal after validating
the protocol is http/https. Consistent with how all other external opens
work in the app. Preload + main handler + mock updated.
L27: DownloadBar URL-field Enter is now smart -- if formats/playlist are
already loaded (or probe errored), Enter triggers download; otherwise
it probes. Eliminates the keyboard dead-end where Enter never downloads.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L14: base.css gets * { box-sizing: border-box } (removes ad-hoc swatch rule),
:focus-visible outline using the Fluent brand token color, and
-webkit-font-smoothing / -moz-osx-font-smoothing antialiasing.
L20: youtubePlayerClient Input now has a datalist of known client names
(web, web_safari, web_embedded, mweb, tv, ios, android) so users
get autocomplete suggestions. Still accepts free text for future clients.
SR10: Sidebar tagline changed from 'yt-dlp frontend' to 'Video downloader'
(plain-English, not developer shorthand).
L150: Standardized 'PO Token' capitalization -- SettingsView label now says
'YouTube PO Token (advanced)' consistently.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L8: TerminalView lines now carry a stable auto-increment id (keyed by that
instead of array index) so front-trimming at MAX_LOG_LINES doesn't cause
React to reuse wrong DOM nodes. SettingsView error list keyed by id alone
(not id+occurredAt, which was redundant since IDs are already unique).
SR9: DownloadOptionsForm -- rewrote 'Container for merged video' to a plain
description; rewrote format-sorting hint to remove raw '-S string' /
'yt-dlp syntax' jargon.
M20: All ProgressBars now carry aria-label -- QueueItem (downloading +
paused rows, keyed on title/URL), DownloadsView overall summary bar,
SettingsView app-update download bar.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sidebar collapsed state was an orphan in localStorage -- not backed up,
lost on portable installs, and inconsistent with every other preference.
Added sidebarCollapsed: boolean to the Settings type and DEFAULT_SETTINGS,
wired it through applySettings' boolean branch, and updated the renderer
store / main.tsx mock defaults. App.tsx now drives the toggle via
updateSettings({ sidebarCollapsed }) instead of localStorage.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L7: Canceled status chip now uses 'subtle' color (neutral) instead of 'warning'
(amber) -- distinguishing it from paused which remains amber.
M13: Proxy URL and YouTube PO-token inputs now use type="password" to mask
credentials, matching updateToken. Proxy URLs can carry user:pass; PO tokens
are opaque secrets.
M37: Rewrite two dev-facing hints -- youtubePlayerClient now says 'how AeroFetch
identifies itself to YouTube' instead of citing yt-dlp internals; filename
template says 'Controls how saved files are named' instead of 'yt-dlp output
template'. Remaining hints (cookie browser, aria2c, etc.) were already clean.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L23: Diagnostics error list now shows "Showing 20 of N errors." when there
are more than 20 logged errors.
L78: DownloadsView empty-state copy updated from "Paste a URL above to get
started" to "Paste a URL above, then click Download" -- matches the UI.
L79: SponsorBlock default categories expanded from ['sponsor'] to
['sponsor', 'selfpromo', 'interaction'] -- sponsor-only was silently
covering almost nothing; the three new defaults are universally unwanted.
Existing SB tests updated to pass explicit categories instead of using defaults.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L16: relTime now caps at weeks/months instead of unbounded 'N d ago' (adds w/mo
buckets). formatWhen omits the year when the timestamp is in the current year.
L75: Update-token Field in the Software-update card is hidden by default; only
shown when a token is already configured or an update-check error occurred.
L77: Adds a Re-check button next to the ffmpeg/ffprobe version display so a
previously-not-found binary can be re-detected without restarting the app.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L17: TemplateManager urlPattern field now validates the regex on each keystroke;
an error message appears below the input and Save is blocked until fixed.
L18: Terminal nav item is hidden when customCommandEnabled is false -- no more
dead-end gated view. Sidebar gains showTerminal prop; App.tsx reads the
setting and passes it through.
L56: SponsorBlock Categories Field shows a 'warning' validationMessage when the
toggle is ON but no categories are selected, making the silent no-op visible.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L54: DownloadBar preview image now tracks failed src in state (same pattern as
MediaThumb); on load error shows the kind icon fallback instead of broken img.
L61: Convert taskbarProgress from ipcMain.on/ipcRenderer.send to ipcMain.handle/
ipcRenderer.invoke so all IPC channels use the same request-response idiom.
App.tsx calls void to explicitly discard the resolved promise.
L63: audioQuality() now has an explicit 'Best' case alongside the bitrate strings;
unknown labels still fall back to '0' but the default branch is documented.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L49: Remove newline from missing-binary error message in download.ts; inline
error spans render it as literal newline, now one sentence with period.
L53: backup.ts exportBackup/importBackup/showMessageBox no longer use win!
non-null assertions -- each uses a guarded ternary (same pattern index.ts
already used for chooseFolder).
L59: Extract PAGE_BACKGROUND to src/shared/theme.ts; main/index.ts and
renderer/src/theme.ts both import from it -- no more "keep in sync" comment.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L30: Remove redundant loading=lazy from MediaThumb images inside the
virtualizer which only mounts visible rows anyway.
L44: electron-builder.yml now excludes the actual eslint.config.js and
.prettierrc rather than non-existent legacy .eslintrc/.prettierrc.yaml.
L60: IpcChannels.externalUrl renamed from external-url to app:external-url
to match every other channel name category prefix.
L90: settings.ts sanitizeOptions now uses (ARR as readonly string[]).includes
pattern consistently with setSettings; removes the as-never hack.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H3: probe.ts now imports fmtBytes from lib/formatters instead of download.ts,
breaking the circular probe->download dependency direction.
M33: ipc.ts comment corrected to reflect that no batch cap exists; notes the
known M33 issue for future implementation.
L25: App.tsx taskbar subscriber skips IPC when fraction/mode/badge unchanged,
eliminating redundant roundtrips on every progress tick.
L32: paletteActions wrapped in useMemo so CommandPalette sees a stable array
reference; only rebuilds when isDark changes.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H5: HistoryEntry now stores formatId+formatHasAudio; redownload passes them
back to addFromUrl so the original yt-dlp format is reused, not guessed.
Compound quality labels stripped to the preset prefix for old history.
L72: thumbnail now passed in redownload so re-queued rows keep their art.
H6: TerminalView log capped at MAX_LOG_LINES=2000 to prevent unbounded growth.
M21: --audio-quality omitted for lossless audioFormats (flac/wav).
L166: fmtEta hour rollover fixed in all 3 locations -- 2h00:00 not 120:00.
L165: queueStats formatSpeed precision aligned with fmtBytes.
L55: QueueItem suppresses sizeLabel when already in probed-format quality label.
L47: probeMeta null sends empty meta event so Resolving clears via SR6.
L68: notifiedBackground resets on window show for subsequent close-while-downloading.
L76: dup warning falls back to URL when title is still a placeholder.
L91: SettingsView onFormatSelect uses indexOf instead of unsafe tuple cast.
L98: Embed chapters field gets a hint text.
L108: BrowserWindow created with explicit title AeroFetch.
L37: pure formatters extracted to src/main/lib/formatters.ts and unit-tested.
L38: buildArgs test covers webm thumbnail suppression.
L39: CROP_SQUARE_PPA test is structural not exact-string.
L40: looksLikeUrl/looksLikeSingleVideo extracted to src/renderer/src/lib/urlHelpers.ts.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reformat from a one-off security report into a living checklist.
Adds the 2026-06-29 architectural review findings and a second
polish pass; all original security findings marked completed.
Items carry stable IDs for tracking across sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generate build/icon.svg — a white download glyph on the teal brand square,
mirroring the in-app brand mark (Onboarding.tsx) — and rasterise it to a
multi-size build/icon.ico (256/128/64/48/32/16) via ImageMagick. Uncomment
win.icon in electron-builder.yml so installers stop falling back to the default
Electron icon. Marks CODE-AUDIT.md M3 placeholder-done; a designed asset is
still wanted before v1.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A live smoke test of buildArgs' argv against the bundled yt-dlp + ffmpeg found
that ffprobe.exe was never bundled (resources/bin/ shipped only ffmpeg.exe).
yt-dlp resolves both from --ffmpeg-location, so duration-aware post-processing
failed at runtime for every user with "ffprobe not found": --sponsorblock-remove,
--force-keyframes-at-cuts, and --split-chapters (CODE-AUDIT C1).
- Bundle ffprobe.exe (matching n8.1.2 LGPL build, sha256-verified against the
already-bundled ffmpeg.exe) and document it in resources/bin/README.md as a
required binary; correct the stale "not committed to git" note.
- Guard startDownload against a missing ffmpeg.exe/ffprobe.exe up front so the
failure is a clear AeroFetch error, not a cryptic yt-dlp postprocessing one
(new getFfprobePath() in binaries.ts).
- Expand test/real-download.integration.test.ts from 2 to 8 live cases: crop,
sponsorblock-remove, audio opus re-encode, mkv+vp9 merge, subtitle+chapter
embed, restrict-filenames, download-archive skip, and Phase C extra-args.
All 8 pass against live yt-dlp + ffmpeg.
- ROADMAP: mark the Phase A/B/C smoke-test caveats done. CODE-AUDIT: add C1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Security:
- S1 (backup.ts): require explicit confirmation before enabling custom-command
templates from a backup file; import templates in a disabled state if declined
- S2 (cookies.ts): deny non-http/https popups in cookie login window, matching
the main window's setWindowOpenHandler defence
- S3 (download.ts): main-process concurrency cap — startDownload now rejects
spawns when active.size >= maxConcurrent (defence-in-depth; renderer already
enforces this but should not be the only gate)
- S4 (settings.ts, validation.ts): reject filenameTemplate values containing
path traversal (.. segments or absolute paths); validate outputDir is absolute
- S5 (history.ts, errorlog.ts, templates.ts, validation.ts): per-field
validation on JSON reads — invalid entries dropped rather than blindly trusted
Performance:
- P1 (settings.ts): getSettings() now only writes to disk when sanitization
actually changed a value; removes synchronous file I/O on every hot-path read
- P2 (ipc.ts, download.ts, downloads.ts): renderer forwards its pre-probed
metadata (title/channel/duration) via StartDownloadOptions.meta; main uses
it directly instead of spawning a redundant second yt-dlp probe
Maintainability:
- M1 (log.ts, download.ts, probe.ts): extract duplicated cleanError() to
src/main/log.ts; both callers import from the shared module
- M2 (buildArgs.ts): document parseExtraArgs escape-sequence limitations
- M3 (electron-builder.yml): clarify icon TODO as a pre-release action
- P3 (downloads.ts): document that lowering maxConcurrent mid-flight does
not pause active downloads (intended behaviour, now written down)
Tests:
- Add test/validation.test.ts covering isSafeFilenameTemplate, isSafeOutputDir,
isValidHistoryEntry, isValidErrorLogEntry, isTemplateLike (76 tests pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>