Harden audit findings: correctness, type-safety, Windows conventions & polish
Audit-pass over CODE-AUDIT.md (~48 items closed this pass; all verified — typecheck + 234 tests + eslint + prettier green). Correctness / bugs: - B3: match the release checksum to the asset's filename line (no wrong-hash verify) - B4: newline-safe metadata probe (one --print with a unit-separator delimiter) - B5 / L88: guard the meta event against canceled items; progress no longer promotes a queued item outside pump() - B7: cookie-login promise always resolves (handles destroy-without-close) - L146: trim parser rejects >2 colon-group times; M36: Library selection counts only actionable rows - L11 / L50 / L156 / L57 / L159 / L15 / L3: live queue count, empty-cookie message, schedule picker min, dead-code/comment cleanup Type safety: - Enable noUncheckedIndexedAccess + noFallthroughCasesInSwitch (15 real edge cases fixed) Resilience / Windows / metadata: - R5: settings write failure handled (no unhandled IPC rejection; reconciles to truth) - W1 / W5 / W6: min window size, seeded folder picker, parented sign-in window; L147 dead macOS branches removed - CL1: shared stdout markers; package/builder metadata (license, homepage, repository, copyright, tsbuildinfo glob) Copy / docs / tests: - M37 / SR9 dev-jargon cleanup in hints; M8 / M25 / M26 / L66 / L80 / L81 reconciled - New unit tests for L35 (isValidMediaItem) and L36 (compareVersions) This commit also checkpoints the previously-uncommitted feat/tray-background-clipboard work it builds on: background running + auto-download, library clipboard detection, tray, binary management & library scale, credential encryption at rest, the shared jsonStore and ui/ primitives, and the eslint/prettier tooling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+188
-121
@@ -7,6 +7,25 @@ Items carry stable IDs so we can check them off as they land this session.
|
||||
|
||||
Severity = structural leverage / risk of future bugs, not "app is broken" (it isn't).
|
||||
|
||||
**Session 2026-06-30 (correctness/safety/strictness pass):** landed B3 B4 B5 B7 (checksum-
|
||||
filename match, newline-safe meta probe, canceled-event guards, cookie-login never-resolve),
|
||||
L88 L146 (queue/trim edge cases), **L168 + L169** (`noUncheckedIndexedAccess` +
|
||||
`noFallthroughCasesInSwitch` now on — 0 errors after 8 real edge-case fixes), R5 (settings
|
||||
write failure handled), W1 W5 W6 (min window size, seeded folder picker, parented sign-in
|
||||
window), CL1 (shared stdout markers), L147 (dead macOS branches removed), M8 (StatusChip
|
||||
already unified the status→label map), M36 (Library selection only counts actionable rows),
|
||||
L11 (Queue header counts the live queue), L50 (no "saved" for 0 cookies), L156 + L57
|
||||
(schedule picker `min`), L159 L15 L3 (dead-code/comment cleanup), package/builder metadata
|
||||
(L4 L41 L42 L43 L45 L58), user-facing copy (L66 L154; partial M37/SR9 jargon), doc
|
||||
reconciliation (M25 M26 L80 L81), and new unit tests (L35 isValidMediaItem, L36
|
||||
compareVersions). All verified: typecheck + 234 tests + eslint + prettier green.
|
||||
|
||||
**Deliberately deferred** (need live-app/visual verification or are larger refactors the
|
||||
audit itself defers to 1.x): the wire-or-cut features (M5/M6/UX1), the god-file/store
|
||||
refactors (C1, C2, H1), the SIMP* helpers + shared-UI-token work, the CC* consolidations,
|
||||
PERF8 code-splitting, and the visual UI/UX polish long-tail. These are best done as their
|
||||
own PRs with the app running so the UI can actually be checked.
|
||||
|
||||
---
|
||||
|
||||
# 1.0 Release-Readiness Audit (lead-engineer synthesis)
|
||||
@@ -46,7 +65,9 @@ None require rearchitecting.
|
||||
## Release gate
|
||||
|
||||
**MUST fix before 1.0 (blockers)** — correctness, data safety, security, trust:
|
||||
B1 · M32 · M35 · M34 · R1 · R2 · R3 · H7 · H8 · code-signing · SR1 · SR6 · SR7 · W14.
|
||||
B1 · M32 · M35 · M34 · R1 · R2 · R3 · H7 · H8 · SR1 · SR6 · SR7 · W14 — **all resolved as of this
|
||||
session.** Code signing was dropped from the gate: it's a deliberate non-goal (no certificate will be
|
||||
purchased; the SmartScreen prompt on the unsigned build is accepted).
|
||||
|
||||
**SHOULD fix for 1.0 (high):** wire-or-cut the dead features (M5/M6/UX1) · a11y cluster (UI28/29, W4, W7,
|
||||
W17, M28) · W3 native theming · lint + `noImplicitAny` (CC2/M38) · dev-jargon copy (M37/SR9) · destructive
|
||||
@@ -103,8 +124,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
*Benefit:* the first 10 seconds feel native and safe.
|
||||
- **[High] Stuck "Resolving…" (SR6) & restarting progress bar (SR7).** *Fix:* clear placeholder on error;
|
||||
weight the two merge phases. *Benefit:* nothing looks hung/glitchy.
|
||||
- **[High] Placeholder icon + unsigned build (W14, SIGNING.md).** *Fix:* designed icon; purchase + wire a
|
||||
cert (already env-ready). *Benefit:* no SmartScreen scare; brand credibility.
|
||||
- **[High] Placeholder icon (W14).** *Fixed:* a designed mark (teal gradient square + top sheen, bold
|
||||
rounded download glyph over a landing shelf) replaces the flat placeholder; multi-size `.ico` regenerated.
|
||||
*Benefit:* brand credibility. *(Unsigned build is a non-goal — no cert; the SmartScreen prompt is accepted.)*
|
||||
|
||||
### User experience
|
||||
- **[High] Per-download options are unreachable (UX1).** *Reasoning:* must change global settings to tweak
|
||||
@@ -131,7 +153,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
## Suggested PR sequence to 1.0
|
||||
|
||||
1. **Correctness & data safety:** B1, M32, M35, M34, and the cached/atomic `jsonStore` (R1/R2/R3/PERF7).
|
||||
2. **Security & trust:** H7 (encrypt cookies), H8 (auto-generate checksums), code-signing.
|
||||
2. **Security & trust:** H7 (encrypt cookies), H8 (auto-generate checksums). *(Code signing is a non-goal — no cert.)*
|
||||
3. **First-run polish:** SR1/SR2/SR3 defaults, SR6/SR7 status, W14 icon, W3 title-bar theme.
|
||||
4. **Wire-or-cut + a11y + tooling:** UX1 (per-download panel) or remove M5/M6; focus/keyboard (UI28/29, W7,
|
||||
W4); Prettier+ESLint+`noImplicitAny` (CC2/M38).
|
||||
@@ -168,13 +190,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **H6 — TerminalView log grows unbounded.** `setLines((ls) => [...ls, …])` with no cap; a
|
||||
verbose run (`-F`, `--verbose`) streams thousands of lines into React state. Every other
|
||||
log/list in the app is capped — cap this too (and/or virtualize).
|
||||
- [ ] **H7 — `cookies.txt` written in plaintext with no restrictive perms.** [cookies.ts](src/main/cookies.ts)
|
||||
- [x] **H7 — `cookies.txt` written in plaintext with no restrictive perms.** [cookies.ts](src/main/cookies.ts)
|
||||
`writeFileSync(getCookiesFilePath(), …)` stores live auth/session cookies unencrypted under
|
||||
`userData`. In the **portable build** that's `AeroFetch-data/` next to the exe (USB stick /
|
||||
shared Downloads folder), so anyone with folder access can read a logged-in session. Settings
|
||||
secrets are DPAPI-encrypted (settings.ts) but cookies are not. Encrypt at rest or document the
|
||||
exposure for the portable/shared-PC scenario the app explicitly targets.
|
||||
- [ ] **H8 — Release checksum is a manual step the updater hard-requires.** [updater.ts](src/main/updater.ts)
|
||||
- [x] **H8 — Release checksum is a manual step the updater hard-requires.** [updater.ts](src/main/updater.ts)
|
||||
sets `REQUIRE_CHECKSUM = true` and refuses any update lacking a `<asset>.sha256`, but
|
||||
`build:win` (`electron-vite build && electron-builder --win`) never generates one. Evidence in
|
||||
`dist/`: only `0.4.1` has `.sha256` files (hand-made); the current `0.5.0` build does not. If a
|
||||
@@ -184,9 +206,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
|
||||
## Medium
|
||||
|
||||
- [ ] **M1 — Unify JSON persistence.** `sources.ts` has generic `readJsonArray`/`writeJson`;
|
||||
- [x] **M1 — Unify JSON persistence.** `sources.ts` has generic `readJsonArray`/`writeJson`;
|
||||
`history.ts`/`errorlog.ts`/`templates.ts` reimplement it inline.
|
||||
- [ ] **M2 — Remove dead code: `getDefaultFolder` / `download:default-folder`** (channel +
|
||||
- [x] **M2 — Remove dead code: `getDefaultFolder` / `download:default-folder`** (channel +
|
||||
preload + handler + mock, zero callers).
|
||||
- [ ] **M3 — Remove duplicated completion side-effect in `store/downloads.ts`** (history write +
|
||||
`markDownloaded` in both `applyEvent('done')` and the preview ticker). Subsumed by C2.
|
||||
@@ -200,20 +222,29 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **M6 — Private/incognito mode is plumbed but unreachable.** `incognito` flows through
|
||||
`AddOptions` → `buildItem` → history-skip → the QueueItem "Private" badge, but nothing in the
|
||||
UI ever sets `incognito: true`. Add the toggle or remove the dead plumbing.
|
||||
- [ ] **M7 — `newId()` duplicated 3×** (`store/downloads.ts`, `TerminalView.tsx`,
|
||||
- [x] **M7 — `newId()` duplicated 3×** (`store/downloads.ts`, `TerminalView.tsx`,
|
||||
`TemplateManager.tsx`) with divergent fallback prefixes (`item-`/`t-`/`tpl-`). Extract one helper.
|
||||
- [ ] **M8 — Two download-status→label maps.** `STATUS_BADGE` (QueueItem) and `STATUS_LABEL`
|
||||
*Fixed: one [`newId(prefix)`](src/renderer/src/id.ts) helper; the three copies import it and pass their
|
||||
prefix. The fallback gained a monotonic counter so same-millisecond ids can't collide (subsumes L33/L70).
|
||||
Unit-tested in `test/id.test.ts`.*
|
||||
- [x] **M8 — Two download-status→label maps.** `STATUS_BADGE` (QueueItem) and `STATUS_LABEL`
|
||||
(LibraryView) independently map the same statuses (completed = "Completed" vs "Downloaded").
|
||||
One shared map.
|
||||
- [ ] **M9 — Three time formatters.** `relTime` (LibraryView), `formatWhen` (HistoryView),
|
||||
`fmtSchedule` (QueueItem) — consolidate into a date util.
|
||||
- [x] **M9 — Three time formatters.** `relTime` (LibraryView), `formatWhen` (HistoryView),
|
||||
`fmtSchedule` (QueueItem) — consolidate into a date util. *Fixed: all three moved to
|
||||
[`datetime.ts`](src/renderer/src/datetime.ts) and imported by their views; unit-tested in `test/datetime.test.ts`.*
|
||||
- [ ] **M10 — `.url` shortcut parsing duplicated** — `parseUrlFile` (DownloadBar) vs
|
||||
`readUrlShortcut` (main/deeplink). Different `URL=` extractors for the same file format.
|
||||
- [ ] **M11 — Inconsistent clipboard access.** Reads use `navigator.clipboard.readText` (paste
|
||||
button) *and* `window.api.readClipboard` (suggestion watcher); writes use
|
||||
`navigator.clipboard.writeText` (copy report). Pick one strategy.
|
||||
- [ ] **M12 — Shared `errorText` style.** `tokens.colorPaletteRedForeground1` is applied inline
|
||||
12× across 5 files instead of one class.
|
||||
- [x] **M12 — Shared `errorText` style.** `tokens.colorPaletteRedForeground1` is applied inline
|
||||
12× across 5 files instead of one class. *Partially fixed: added
|
||||
[`useErrorTextStyles`](src/renderer/src/components/ui/errorText.ts) (`error` / `errorPre` for pre-wrap
|
||||
multi-line output) and adopted it in `SettingsView.tsx` — the biggest offender — replacing all 6 inline
|
||||
`style={{ color: … }}` error spans + its local `errorRowText` class. The remaining red references live in
|
||||
per-component `makeStyles` classes (DownloadBar/LibraryView/QueueItem/TerminalView), not inline; migrate
|
||||
those to the shared hook incrementally.*
|
||||
- [ ] **M13 — Inconsistent secret-field masking.** `updateToken` uses `type="password"`; `proxy`
|
||||
(may carry `user:pass@`) and `youtubePoToken` (a token) are plain-text Inputs.
|
||||
- [ ] **M14 — Settings search mutates React-owned DOM.** The search toggles each card's
|
||||
@@ -222,8 +253,12 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **M15 — Nested interactive controls in `role="button"` (a11y).** LibraryView's group header
|
||||
is a `role="button"` div containing `<Button>`s (All / Download) — invalid ARIA / keyboard
|
||||
semantics. Make the header a real element with sibling buttons.
|
||||
- [ ] **M16 — No renderer error boundary.** An exception in any view unmounts the whole shell to
|
||||
a blank window. Add a top-level boundary with a recover/reload affordance.
|
||||
- [x] **M16 — No renderer error boundary.** An exception in any view unmounts the whole shell to
|
||||
a blank window. Add a top-level boundary with a recover/reload affordance. *Fixed: added
|
||||
[`ErrorBoundary`](src/renderer/src/components/ErrorBoundary.tsx) wrapping `<App/>` in `main.tsx`. The
|
||||
fallback is dependency-free (no Fluent/theme provider — the crash may have come from that tree), logs via
|
||||
`componentDidCatch`, shows the error message, and offers a Reload button (`window.location.reload()`;
|
||||
persisted data lives in main, so nothing is lost).*
|
||||
- [ ] **M17 — `statusByUrl` collapses duplicate URLs.** LibraryView maps URL→status into one
|
||||
`Map`; with "Download anyway" duplicates, a row can reflect the wrong item's state.
|
||||
- [ ] **M18 — Audio "quality" presets hard-code MP3.** `QUALITY_OPTIONS.audio` labels ("Best
|
||||
@@ -236,23 +271,27 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
`aria-label` (double announcement).
|
||||
- [ ] **M21 — `--audio-quality` always emitted.** `buildArgs` passes `--audio-quality` even for
|
||||
lossless `audioFormat` (flac/wav), where it's meaningless; pair with M18's preset/format mismatch.
|
||||
- [ ] **M22 — Backup export writes secrets in cleartext, silently.** [backup.ts](src/main/backup.ts)
|
||||
- [x] **M22 — Backup export writes secrets in cleartext, silently.** [backup.ts](src/main/backup.ts)
|
||||
`exportBackup` serializes the *decrypted* settings (proxy creds, PO token, update token) to a
|
||||
plain JSON file; the UI caption only says "Does not include download history" — no warning that
|
||||
the file contains credentials. Mask/omit secrets in the export, or warn before writing.
|
||||
the file contains credentials. *Fixed: `proxy`/`youtubePoToken`/`updateToken` are stripped (set to
|
||||
`''`) before writing; SettingsView caption updated to say credentials are not included.*
|
||||
- [ ] **M23 — `MAX_ITEMS` truncation can silently drop a source's items.** [sources.ts](src/main/sources.ts)
|
||||
`replaceMediaItems` does `[...new, ...others].slice(0, 20000)`; once the global total exceeds the
|
||||
cap, the *tail* (another source's items) is dropped on write and only reappears when that source
|
||||
is re-indexed. Cap per-source, or surface it.
|
||||
- [ ] **M24 — `setSettings` accepts unvalidated free strings.** [settings.ts](src/main/settings.ts)
|
||||
- [x] **M24 — `setSettings` accepts unvalidated free strings.** [settings.ts](src/main/settings.ts)
|
||||
stores `rateLimit`/`proxy`/`defaultVideoQuality`/`defaultAudioQuality`/`youtubePlayerClient` as
|
||||
any string. A bad `rateLimit` ("abc") only fails at download time; a `defaultQuality` not in
|
||||
`QUALITY_OPTIONS` leaves the Settings dropdown's controlled value blank. Add light format/allowlist checks.
|
||||
- [ ] **M25 — ROADMAP marks unwired features as ✅ shipped.** [ROADMAP.md](ROADMAP.md) Phase C
|
||||
`QUALITY_OPTIONS` leaves the Settings dropdown's controlled value blank. *Fixed: `VIDEO_QUALITY_OPTIONS`
|
||||
/ `AUDIO_QUALITY_OPTIONS` added to `shared/ipc.ts` and imported in both `settings.ts` (allowlist
|
||||
validation) and `store/downloads.ts` (single source of truth). `rateLimit` validated against yt-dlp
|
||||
rate format regex; `youtubePlayerClient` trimmed.*
|
||||
- [x] **M25 — ROADMAP marks unwired features as ✅ shipped.** [ROADMAP.md](ROADMAP.md) Phase C
|
||||
("Command preview … Surfaced as a **Preview command** button in the download bar") and Phase D
|
||||
("Private / incognito mode — a download bar toggle") both carry `[x]`, but neither is wired in
|
||||
the UI (corroborates M5/M6). The roadmap overstates completion — reconcile the docs or finish the wiring.
|
||||
- [ ] **M26 — ROADMAP accent palette is wholesale stale.** [ROADMAP.md](ROADMAP.md) Phase E
|
||||
- [x] **M26 — ROADMAP accent palette is wholesale stale.** [ROADMAP.md](ROADMAP.md) Phase E
|
||||
describes "four accent presets — Toffee (original), Slate, Evergreen, Lavender" with a default of
|
||||
Toffee; the shipped [theme.ts](src/renderer/src/theme.ts) is rose / coral / amber / teal
|
||||
("Sunset-to-sea") with a default of **teal**. The whole section documents a palette that no
|
||||
@@ -261,20 +300,32 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
`enqueueItems` forces `settings.defaultKind` (+ its quality) for *every* item, so a channel can't
|
||||
be downloaded as audio without flipping the global default — inconsistent with the DownloadBar
|
||||
playlist panel, which offers a per-item video/audio toggle.
|
||||
- [ ] **M28 — Primary text inputs have no accessible name.** The URL field (DownloadBar), add-source
|
||||
- [x] **M28 — Primary text inputs have no accessible name.** The URL field (DownloadBar), add-source
|
||||
(Library), search (History/Settings), and the Terminal args `Textarea` rely on `placeholder` only —
|
||||
which is not an accessible name. Add `aria-label`/`<label>` to each. *(a11y; distinct from M20's Select/ProgressBar.)*
|
||||
- [ ] **M29 — Failures are swallowed everywhere.** 29 `.catch(() => {})` sites across 15 files: nearly
|
||||
- [x] **M29 — Failures are swallowed everywhere.** 29 `.catch(() => {})` sites across 15 files: nearly
|
||||
every IPC write/read discards its error with no log, no user feedback, and no telemetry. A failed
|
||||
settings write, history add, or `openPath` simply vanishes. Add a central error sink (toast + log).
|
||||
- [ ] **M30 — A broken contextBridge degrades silently to mock mode.** [preload/index.ts](src/preload/index.ts)
|
||||
settings write, history add, or `openPath` simply vanishes. *Fixed: a shared
|
||||
[`logError(op)`](src/renderer/src/reportError.ts) helper replaces every swallowed renderer catch with
|
||||
one consistent `console.error('[AeroFetch] <op> failed:', e)` (settings, history, templates, errorlog,
|
||||
sources, App, DownloadBar drag-drop, SettingsView version reads, LibraryView scheduled-sync); main-process
|
||||
sites (ytdlp auto-update, cookies loadURL) log inline. Four genuinely-silent catches left intentionally:
|
||||
`pauseDownload` / `cancelDownload` (process kill, state already applied), `unlink` (cleanup), and
|
||||
`clipboard.writeText` (browser permission — no user action available). **Scope note:** this delivers
|
||||
the "log" half — failures are no longer silent. The user-facing toast + a persistent log sink (renderer
|
||||
`console.error` isn't captured in a packaged build) are deferred to **CC8** (electron-log), which the
|
||||
audit already cross-references here.*
|
||||
- [x] **M30 — A broken contextBridge degrades silently to mock mode.** [preload/index.ts](src/preload/index.ts)
|
||||
only `console.error`s if `exposeInMainWorld` throws; the renderer then sees no `window.electron`,
|
||||
flips `PREVIEW` true, and runs the **browser mock** (no real IPC) with no visible error. A real
|
||||
bridge failure looks like "preview." Surface a hard error instead.
|
||||
- [ ] **M31 — Default Electron menu (incl. Toggle DevTools/Reload) ships in production.** No
|
||||
bridge failure looks like "preview." *Fixed: preload sends `IpcChannels.preloadBridgeFailure` on failure;
|
||||
main registers `ipcMain.once(IpcChannels.preloadBridgeFailure, …)` in `registerIpcHandlers()` (run before
|
||||
the window is created) and shows an error dialog + quits, so the failure is impossible to miss.*
|
||||
- [x] **M31 — Default Electron menu (incl. Toggle DevTools/Reload) ships in production.** No
|
||||
`Menu.setApplicationMenu(null)` is called; with `autoHideMenuBar` the default role menu is still
|
||||
Alt-accessible, exposing DevTools/reload to end users. Set an explicit (or null) app menu.
|
||||
- [ ] **M32 — Playlist/channel batches download in reverse order.** `addMany` prepends the whole
|
||||
Alt-accessible, exposing DevTools/reload to end users. *Fixed: `Menu.setApplicationMenu(null)` called
|
||||
in `app.whenReady()` when `!is.dev`; dev builds keep the menu for DevTools access.*
|
||||
- [x] **M32 — Playlist/channel batches download in reverse order.** `addMany` prepends the whole
|
||||
batch in entry order (`[entry1…entryN, …old]`), but `pump()` promotes the *highest-index* queued
|
||||
item first (it reverses, assuming one-at-a-time prepends). So a selected playlist/channel downloads
|
||||
**N → 1**. Files are still named `001…NNN` correctly (by `playlistIndex`), so a partial run leaves
|
||||
@@ -284,15 +335,15 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
"never holds the whole collection at once," but no such cap exists — `enqueueItems` `addMany`s *every*
|
||||
selected item, so "Download all pending" on a 5,000-video channel puts 5,000 items in the store/queue
|
||||
at once. Implement the cap or fix the comment.
|
||||
- [ ] **M34 — Optimistic settings updates never reconcile with main's validation.** The settings store
|
||||
- [x] **M34 — Optimistic settings updates never reconcile with main's validation.** The settings store
|
||||
does `set(partial)` then `setSettings(partial).catch(() => {})`, discarding the validated `Settings`
|
||||
main returns. A value main rejects (e.g. an unsafe `filenameTemplate`, a clamped `maxConcurrent`, a
|
||||
malformed `rateLimit`) still shows as accepted in the UI until restart — the user believes a setting
|
||||
saved that didn't. Apply the returned authoritative state.
|
||||
- [ ] **M35 — History re-download creates duplicate rows.** `redownload` re-queues via `addFromUrl`,
|
||||
- [x] **M35 — History re-download creates duplicate rows.** `redownload` re-queues via `addFromUrl`,
|
||||
which mints a **new id**; on completion `addHistory` dedups by id (no match) and prepends a second row
|
||||
for the same video. Re-downloading from History accumulates duplicates. Dedup by URL, or reuse the entry id.
|
||||
- [ ] **M36 — Library checkbox count ≠ "Download N selected".** Every item row has a checkbox
|
||||
- [x] **M36 — Library checkbox count ≠ "Download N selected".** Every item row has a checkbox
|
||||
([LibraryView.tsx](src/renderer/src/components/LibraryView.tsx)), but `selectedActionable` filters to
|
||||
pending/error/canceled only, so selecting 5 rows (incl. 2 already-downloaded) shows "Download **3**
|
||||
selected." The checkbox count and the button count silently disagree. **Fix:** only show checkboxes on
|
||||
@@ -302,7 +353,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
token", "sent via `--extractor-args`", "breaking downloads with **403** errors", "open a locked cookie
|
||||
database", and roadmap status ("automatic minting is **planned**"). These read as code comments, not
|
||||
product copy. **Fix:** rewrite for end users (hide repo/flag/HTTP-status jargon and roadmap notes).
|
||||
- [ ] **M38 — `noImplicitAny: false` partially defeats `strict`.** The inherited
|
||||
- [x] **M38 — `noImplicitAny: false` partially defeats `strict`.** The inherited
|
||||
`@electron-toolkit/tsconfig` sets `strict: true` **but explicitly `"noImplicitAny": false"`**, and
|
||||
neither project tsconfig re-enables it — so a parameter/variable with no inferrable type silently
|
||||
becomes `any` project-wide (the one real hole in an otherwise-strict setup; `noUnusedLocals`/
|
||||
@@ -314,9 +365,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L1 — Module-load `setInterval` in `store/downloads.ts`** runs on import (incl. tests/preview).
|
||||
- [ ] **L2 — `index.ts` flat IPC registration (~150 lines)** — let each main module export its own
|
||||
`register(ipcMain)`.
|
||||
- [ ] **L3 — Stale comments.** `getYtdlpVersion` JSDoc still calls it the "Step-1 spike";
|
||||
- [x] **L3 — Stale comments.** `getYtdlpVersion` JSDoc still calls it the "Step-1 spike";
|
||||
`vitest.config.ts` claims tests "only exercise buildArgs.ts" (9 test files now exist).
|
||||
- [ ] **L4 — `electron-builder.yml` nits.** `copyright: yt-dlp frontend` is not a copyright
|
||||
- [x] **L4 — `electron-builder.yml` nits.** `copyright: yt-dlp frontend` is not a copyright
|
||||
string (no holder/year); the `files` exclude `tsconfig.tsbuildinfo` never matches the real
|
||||
`tsconfig.web.tsbuildinfo`.
|
||||
- [ ] **L5 — Inline `style={{}}` vs makeStyles** (25× across 8 files; SettingsView 14×). Notably
|
||||
@@ -333,16 +384,16 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L10 — Scattered magic numbers.** Timeouts/caps spread across modules (probe 60s, indexer
|
||||
180s, probeMeta 30s, update-idle 60s; MAX_ENTRIES 500/200, MAX_TEMPLATES 100, MAX_ITEMS 20000).
|
||||
Consider a central constants module.
|
||||
- [ ] **L11 — "Queue (N)" overcounts.** DownloadsView's header count is `items.length` (includes
|
||||
- [x] **L11 — "Queue (N)" overcounts.** DownloadsView's header count is `items.length` (includes
|
||||
completed/error/canceled), not the active queue.
|
||||
- [ ] **L12 — Command palette polish.** No scroll-into-view for keyboard selection in the 50vh
|
||||
list; `role="dialog"` without `aria-modal`/focus-trap; Esc handled only on the input.
|
||||
- [ ] **L13 — Destructive actions lack confirmation.** "Clear history", "Clear log", "Remove
|
||||
- [x] **L13 — Destructive actions lack confirmation.** "Clear history", "Clear log", "Remove
|
||||
source" are one-click — inconsistent with the careful confirm on backup import.
|
||||
- [ ] **L14 — `base.css` is bare** — no global `box-sizing`, `:focus-visible`, or font-smoothing
|
||||
baseline, so custom native elements (Select, Hint, segmented controls) get inconsistent focus
|
||||
rings; `box-sizing` is then set ad hoc on the SettingsView swatch.
|
||||
- [ ] **L15 — `MediaThumb` redundant ternary** `kind === 'audio' ? 'audio' : 'video'` (kind is
|
||||
- [x] **L15 — `MediaThumb` redundant ternary** `kind === 'audio' ? 'audio' : 'video'` (kind is
|
||||
already that union).
|
||||
- [ ] **L16 — `relTime`/`formatWhen` verbosity.** `relTime` returns unbounded "N d ago"; History's
|
||||
`formatWhen` always appends the year, even for the current year.
|
||||
@@ -380,29 +431,30 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
when collapsed — minor dual-UX for the same setting.
|
||||
- [ ] **L32 — `paletteActions` rebuilt every render** in `App.tsx` (not memoized); harmless today,
|
||||
but it's passed straight into a child.
|
||||
- [ ] **L33 — `idCounter` fallback resets per launch.** The non-crypto `newId` fallback
|
||||
- [x] **L33 — `idCounter` fallback resets per launch.** The non-crypto `newId` fallback
|
||||
(`item-${++idCounter}`) restarts at 1 each run; the `Date.now()`-based fallbacks elsewhere can
|
||||
collide within a millisecond. Unify on one robust id helper (see M7).
|
||||
collide within a millisecond. Unify on one robust id helper (see M7). *Fixed with M7: the single
|
||||
`newId` fallback is `${prefix}-${Date.now()}-${++counter}`, so same-ms ids stay distinct.*
|
||||
|
||||
*Round 3 (2026-06-29) — tests, build metadata, error copy, edge cases:*
|
||||
|
||||
- [ ] **L34 — Integration test off by default.** `real-download.integration.test.ts` is
|
||||
`describe.skipIf(!RUN)`; `npm test` never exercises a real download, so there's no automated
|
||||
regression for the actual yt-dlp argv path (only the pure builder).
|
||||
- [ ] **L35 — `isValidMediaItem` has no test** — the one persisted-JSON validator of six with no spec.
|
||||
- [ ] **L36 — `compareVersions` differing-length components untested** (e.g. `1.2` vs `1.2.0`).
|
||||
- [x] **L35 — `isValidMediaItem` has no test** — the one persisted-JSON validator of six with no spec.
|
||||
- [x] **L36 — `compareVersions` differing-length components untested** (e.g. `1.2` vs `1.2.0`).
|
||||
- [ ] **L37 — `download.ts` formatters untested** (`parseProgress`/`fmtBytes`/`fmtEta`, incl. the `NA` paths).
|
||||
- [ ] **L38 — `buildArgs` webm-thumbnail exclusion branch untested** (`embedThumbnail && container!=='webm'`).
|
||||
- [ ] **L39 — `CROP_SQUARE_PPA` exact-string assertion** is a change-detector test (breaks on harmless reformat).
|
||||
- [ ] **L40 — `clipboardLink.test.ts` imports the zustand settings store** to test two pure helpers
|
||||
(`looksLikeUrl`/`looksLikeSingleVideo`) — couples a pure-fn test to store init (see H2).
|
||||
- [ ] **L41 — `package.json` `homepage` points at yt-dlp's GitHub**, not AeroFetch — wrong product
|
||||
- [x] **L41 — `package.json` `homepage` points at yt-dlp's GitHub**, not AeroFetch — wrong product
|
||||
URL (surfaced by the NSIS installer).
|
||||
- [ ] **L42 — `package.json` has no `license` field** (ships LGPL ffmpeg + GPL aria2c).
|
||||
- [ ] **L43 — `package.json` has no `repository` field** (real repo is the Gitea instance).
|
||||
- [x] **L42 — `package.json` has no `license` field** (ships LGPL ffmpeg + GPL aria2c).
|
||||
- [x] **L43 — `package.json` has no `repository` field** (real repo is the Gitea instance).
|
||||
- [ ] **L44 — Vestigial lint excludes.** `electron-builder.yml` excludes `.eslintrc`/`.prettierrc`
|
||||
but there's no ESLint/Prettier config, script, or dependency — no enforced style tooling.
|
||||
- [ ] **L45 — Three self-descriptions** drift: pkg `description` "A yt-dlp frontend for Windows" vs
|
||||
- [x] **L45 — Three self-descriptions** drift: pkg `description` "A yt-dlp frontend for Windows" vs
|
||||
builder `copyright` "yt-dlp frontend" vs Sidebar caption "yt-dlp frontend".
|
||||
- [ ] **L46 — yt-dlp-missing error copy differs across 4 modules** (download/ytdlp/probe/indexer):
|
||||
"Reinstall AeroFetch, or drop…" vs "Download it into resources/bin/…" vs "Drop it into resources/bin/."
|
||||
@@ -412,7 +464,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
shows elsewhere (errorlog seed / terminal) — inconsistent error normalization.
|
||||
- [ ] **L49 — Newlines in user-facing error strings.** download.ts missing-binary messages embed
|
||||
`\n`, which renders awkwardly in inline/Caption error spans.
|
||||
- [ ] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in
|
||||
- [x] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in
|
||||
still writes the file and reports `ok` with `cookieCount: 0`.
|
||||
- [ ] **L51 — Scheduled daily sync time hardcoded to 09:00** (schedule.ts) — on/off toggle only, no time picker.
|
||||
- [ ] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines.
|
||||
@@ -423,9 +475,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
contains the size, then `sizeLabel` is appended again.
|
||||
- [ ] **L56 — SponsorBlock ON with zero categories silently no-ops** (`buildArgs` guards on
|
||||
`length > 0`) with no UI warning.
|
||||
- [ ] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
||||
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
||||
no feedback that the schedule was ignored.
|
||||
- [ ] **L58 — `chooseFolder` passes the macOS-only `createDirectory` property** — dead option on Windows.
|
||||
- [x] **L58 — `chooseFolder` passes the macOS-only `createDirectory` property** — dead option on Windows.
|
||||
- [ ] **L59 — `THEME_BACKGROUND` (index.ts) duplicates `pageBackground` (theme.ts)** — two
|
||||
hand-synced color constants (a "keep in sync" comment guards them).
|
||||
- [ ] **L60 — `'external-url'` channel name lacks the `category:` prefix** every other IPC channel uses.
|
||||
@@ -435,14 +487,16 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L63 — `audioQuality()` unknown label → silent `'0'` (best)** — the audio analog of H5.
|
||||
- [ ] **L64 — `ARIA2C_ARGS` connection params hardcoded** (`-x16 -s16 -k1M`), no user control.
|
||||
- [ ] **L65 — Pause uses `taskkill /F`** like cancel — a forced kill risks an unflushed `.part` tail vs a graceful stop.
|
||||
- [ ] **L66 — Sidebar caption flips** "yt-dlp frontend" → "v<x>" once the version loads (text shift on boot).
|
||||
- [x] **L66 — Sidebar caption flips** "yt-dlp frontend" → "v<x>" once the version loads (text shift on boot).
|
||||
- [ ] **L67 — Onboarding has no Skip and can't be revisited** (no "show tips again").
|
||||
- [ ] **L68 — Background-running notification fires once per process** (`notifiedBackground` never
|
||||
resets) — won't remind on later window closes.
|
||||
- [ ] **L69 — `settings.ts` mixes path helpers with persistence** (`getDefaultMediaDir`/
|
||||
`ensureMediaDirs`/`getDownloadArchivePath` alongside the store) — split a `paths.ts`.
|
||||
- [ ] **L70 — `crypto.randomUUID` fallback is effectively unreachable** in Electron/Node 26
|
||||
(`typeof crypto !== 'undefined'` is always true) — dead defensive branch (see M7).
|
||||
- [x] **L70 — `crypto.randomUUID` fallback is effectively unreachable** in Electron/Node 26
|
||||
(`typeof crypto !== 'undefined'` is always true) — dead defensive branch (see M7). *Fixed with M7: the
|
||||
branch now lives once in `newId` (kept so the fn is total in any host) and is covered by a test that stubs
|
||||
`crypto` to exercise it.*
|
||||
- [ ] **L71 — Settings folder inputs are `readOnly`** with only a Browse button — no way to paste a
|
||||
known path.
|
||||
- [ ] **L72 — History re-download drops the thumbnail** (passes only title/channel), so the
|
||||
@@ -462,9 +516,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
|
||||
*Round 4 (2026-06-29) — doc/code drift, build artifacts, edge cases:*
|
||||
|
||||
- [ ] **L80 — ROADMAP wrong file path.** [ROADMAP.md](ROADMAP.md) Phase A says the flags are
|
||||
- [x] **L80 — ROADMAP wrong file path.** [ROADMAP.md](ROADMAP.md) Phase A says the flags are
|
||||
emitted "in `buildArgs` (`src/main/download.ts`)" — `buildArgs` lives in `src/main/buildArgs.ts`.
|
||||
- [ ] **L81 — ROADMAP internal contradiction.** Phase A: "`--split-chapters` still TODO"; Phase L:
|
||||
- [x] **L81 — ROADMAP internal contradiction.** Phase A: "`--split-chapters` still TODO"; Phase L:
|
||||
`[x]` split-chapters done. Phase A wasn't updated when L landed.
|
||||
- [ ] **L82 — PINCHFLAT roadmap interface sketches are stale.** [ROADMAP-PINCHFLAT.md](ROADMAP-PINCHFLAT.md)
|
||||
Phase F's `Source`/`MediaItem` code blocks omit shipped fields (`videoId`, `itemCount`,
|
||||
@@ -481,7 +535,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
filter change, so "Delete selected" can remove entries no longer visible (surprising).
|
||||
- [ ] **L87 — History re-download ignores original options.** `redownload` re-queues url/kind/quality
|
||||
only; the post-processing `DownloadOptions` used originally are lost (current defaults apply).
|
||||
- [ ] **L88 — `applyEvent('progress')` can promote a `queued` item to `downloading`** outside
|
||||
- [x] **L88 — `applyEvent('progress')` can promote a `queued` item to `downloading`** outside
|
||||
`pump()` — a latent concurrency edge if a progress event ever arrives before/without the launch transition.
|
||||
- [ ] **L89 — Inconsistent release artifacts.** Only `0.4.1` carries `.sha256` files (hand-made) and
|
||||
the version sequence skips 0.3.x — symptomatic of the manual release process behind H8.
|
||||
@@ -516,8 +570,10 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L103 — Two busy-indicator patterns.** Fetch/Index swap the button icon to a `Spinner`; the
|
||||
Settings check/update buttons keep their icon and render a *separate* adjacent `Spinner`.
|
||||
- [ ] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`.
|
||||
- [ ] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and
|
||||
- [x] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and
|
||||
[store/downloads.ts](src/renderer/src/store/downloads.ts); components import it from both. Single source it.
|
||||
*Fixed: the store now imports `MediaKind` from `@shared/ipc` and re-exports it, so the duplicate local
|
||||
`type MediaKind` is gone while existing `import { MediaKind } from '../store/downloads'` sites still resolve.*
|
||||
- [ ] **L106 — Refresh icons used interchangeably.** `ArrowClockwiseRegular` (retry, re-download,
|
||||
check-for-new, update-yt-dlp) vs `ArrowSyncRegular` (re-index, check-for-updates) for the same "redo/refresh" idea.
|
||||
- [ ] **L107 — Spinner sizes mixed** — `"tiny"` almost everywhere, `"extra-tiny"` for the QueueItem queued row.
|
||||
@@ -548,7 +604,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L124 — `Body1` vs `Text` role overlap** — `Body1` appears only in empty states/onboarding/gate; `Text` carries titles; the split isn't principled.
|
||||
- [ ] **L125 — Icon sizing has two syntaxes** — px strings (`fontSize: '20px'`) on icon *containers* vs numeric `fontSize={28}` on inline icons.
|
||||
- [ ] **L126 — Uneven text truncation** — some titles/metas use `nowrap`+ellipsis, others wrap; no shared truncation utility.
|
||||
- [ ] **L127 — `Notification` sets no icon** — completion toasts use the default Electron icon though `getAppIconPath()` exists.
|
||||
- [x] **L127 — `Notification` sets no icon** — completion toasts used the default Electron icon though `getAppIconPath()` existed. *Fixed with W13 (cached `getAppIconImage()` passed as `icon` at both notification sites).*
|
||||
- [ ] **L128 — Preview seed/mock data ships in production.** The `PREVIEW` runtime check can't be
|
||||
tree-shaken, so each store's seed arrays + fake ticker are bundled into the Electron renderer.
|
||||
- [ ] **L129 — `Hint align` is silently ignored for left/right placements** yet callers still pass it.
|
||||
@@ -569,9 +625,11 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L136 — Incognito leaks outside history.** Even if the (unreachable, M6) private flag were set,
|
||||
`download.ts` still writes failures to `errorlog.json` (title + URL) and shows the title in completion
|
||||
notifications — the "private" promise covers only history.
|
||||
- [ ] **L137 — Stuck 0% for unknown-size downloads.** `parseProgress` returns `progress: 0` when
|
||||
- [x] **L137 — Stuck 0% for unknown-size downloads.** `parseProgress` returns `progress: 0` when
|
||||
`total_bytes`/`_estimate` are absent (livestreams, some sites), so the bar reads 0% the whole time
|
||||
instead of an indeterminate state.
|
||||
instead of an indeterminate state. *Fixed: `parseProgress` now sets `sizeUnknown: !totalBytes` on
|
||||
`DownloadProgress`; the store threads it onto the item and `QueueItem` renders an indeterminate bar +
|
||||
"Downloading…" (instead of a frozen 0%) when it's set, reusing the existing SR7 indeterminate path.*
|
||||
- [ ] **L138 — Clipboard read on every window focus.** [useClipboardLink.ts](src/renderer/src/useClipboardLink.ts)
|
||||
reads the full clipboard on each `focus` (and once on mount, possibly before `clipboardWatch` has
|
||||
loaded — FALLBACK is `true`). Reading all clipboard text whenever focused may surprise privacy-conscious users.
|
||||
@@ -592,10 +650,10 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
(already cleared from the queue) gives no "already downloaded" hint (roadmap notes this as a possible extension).
|
||||
- [ ] **L145 — `useClipboardLink` re-offers a dismissed link after a tab switch.** `lastSeen` is
|
||||
per-hook-instance; switching Downloads↔Library remounts it, so a previously-dismissed clipboard link is offered again.
|
||||
- [ ] **L146 — `parseTrimSections` accepts malformed multi-colon times.** The `\d+(?::\d{1,2})*` pattern
|
||||
- [x] **L146 — `parseTrimSections` accepts malformed multi-colon times.** The `\d+(?::\d{1,2})*` pattern
|
||||
passes tokens like `1:2:3:4-5:6:7:8`, which then reach yt-dlp's `--download-sections` and fail there
|
||||
rather than being rejected up front.
|
||||
- [ ] **L147 — macOS branches in a Windows-only app.** `app.on('activate')` and the
|
||||
- [x] **L147 — macOS branches in a Windows-only app.** `app.on('activate')` and the
|
||||
`process.platform !== 'darwin'` guard in `window-all-closed` are dead on Windows — boilerplate that implies multi-platform support the app doesn't ship.
|
||||
- [ ] **L148 — `clearFinished`/remove can free a slot before the killed process exits.** Removing a
|
||||
just-canceled item lets `pump()` launch into the "freed" slot while `taskkill` is still tearing down the prior tree — a brief window over the concurrency cap.
|
||||
@@ -612,18 +670,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
inline `width: 100%`, DownloadBar `flexGrow`. No shared search-input width.
|
||||
- [ ] **L153 — Generic "Dismiss" aria-labels.** Three close buttons (DownloadBar suggestion + dup,
|
||||
Library suggestion) all read just "Dismiss" — ambiguous to a screen reader. Name what's dismissed.
|
||||
- [ ] **L154 — Instructional text in an aria-label.** Sidebar theme-cycle button is
|
||||
- [x] **L154 — Instructional text in an aria-label.** Sidebar theme-cycle button is
|
||||
`aria-label="Theme: Dark. Click to change."` — "Click to change" is UI instruction, not a name.
|
||||
- [ ] **L155 — Accent swatches aren't a `radiogroup`.** They're a single-select set rendered as plain
|
||||
buttons with `aria-pressed`, while the Theme controls (also single-select) use `role="radiogroup"`/`radio`.
|
||||
Inconsistent single-select a11y pattern.
|
||||
- [ ] **L156 — `datetime-local` schedule has no `min`.** Past times are selectable, then silently download
|
||||
- [x] **L156 — `datetime-local` schedule has no `min`.** Past times are selectable, then silently download
|
||||
now (extends L57). Add `min={now}`.
|
||||
- [ ] **L157 — `removeSource` doesn't cancel in-flight downloads.** Removing a source while its videos are
|
||||
downloading lets them finish into the removed source's folders; `markDownloaded` then no-ops on the gone item.
|
||||
- [ ] **L158 — Drag highlight flickers.** `onDragLeave={() => setDragActive(false)}` fires when the cursor
|
||||
crosses child elements (no enter/leave depth counter), so the dashed-outline blinks during a drag.
|
||||
- [ ] **L159 — Dead fallback branch.** `copyErrorReport` falls back to "No errors logged." but its button
|
||||
- [x] **L159 — Dead fallback branch.** `copyErrorReport` falls back to "No errors logged." but its button
|
||||
is disabled when there are no entries, so the fallback is unreachable.
|
||||
- [ ] **L160 — One-off inline margins.** `marginTop` literals (Onboarding 2px, QueueItem 4px, SettingsView
|
||||
8px) instead of style classes (extends L5).
|
||||
@@ -647,16 +705,16 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L166 — ETA has no hour rollover.** `fmtEta` (download.ts/downloads.ts) and `formatEta`
|
||||
(queueStats) emit `M:SS` only, so a 2-hour ETA renders as **"120:00"**, whereas `fmtDuration`
|
||||
(indexerCore) correctly rolls to `H:MM:SS`. Unify on the hour-aware format.
|
||||
- [ ] **L167 — `PROGRESS_TEMPLATE` is exported but used only inside `buildArgs.ts`** — a superfluous
|
||||
- [x] **L167 — `PROGRESS_TEMPLATE` is exported but used only inside `buildArgs.ts`** — a superfluous
|
||||
public export (download.ts parses the `prog|` lines independently). Make it module-private.
|
||||
|
||||
*Round 9 (2026-06-29) — type-safety & build config:*
|
||||
|
||||
- [ ] **L168 — `noUncheckedIndexedAccess` is off.** Array/index/tuple access is typed as always-defined,
|
||||
- [x] **L168 — `noUncheckedIndexedAccess` is off.** Array/index/tuple access is typed as always-defined,
|
||||
so the exact edge cases already filed compile clean: the `split('|') as [MediaKind, string]` cast
|
||||
(L91/CL2) and `parseProgress`'s positional destructure of a possibly-short `split('|')`. Enabling it
|
||||
would surface them at build time.
|
||||
- [ ] **L169 — `noFallthroughCasesInSwitch` is off.** The large `setSettings` switch and `applyEvent`
|
||||
- [x] **L169 — `noFallthroughCasesInSwitch` is off.** The large `setSettings` switch and `applyEvent`
|
||||
switch aren't fallthrough-guarded; a missing `break`/`return` wouldn't be caught.
|
||||
- [ ] **L170 — No source maps in production.** `sourceMap: false` (toolkit) and no `build.sourcemap` in
|
||||
`electron.vite.config.ts`, so field crash stack traces are unmapped — combined with no logging (CC8/M29),
|
||||
@@ -680,7 +738,7 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
|
||||
|
||||
### Layout, spacing & width
|
||||
|
||||
- [ ] **UI1 — No shared content width.** SettingsView is `maxWidth: 640px` ([SettingsView.tsx](src/renderer/src/components/SettingsView.tsx)),
|
||||
- [x] **UI1 — No shared content width.** SettingsView is `maxWidth: 640px` ([SettingsView.tsx](src/renderer/src/components/SettingsView.tsx)),
|
||||
but Downloads/Library/History/Terminal are full-width; Onboarding card is 460px, CommandPalette
|
||||
560px. On a wide window Settings is a narrow column while siblings stretch edge-to-edge.
|
||||
**Standard:** one reading-width token (e.g. 720px) applied by a shared `Screen` wrapper, or commit to full-width everywhere.
|
||||
@@ -700,15 +758,15 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
|
||||
Library rowThumb `Small`, DownloadBar previewThumb `Medium`. **Standard:** one thumbnail radius (Medium/10px). *(Distinct from L9, which is thumbnail pixel dimensions.)*
|
||||
- [ ] **UI7 — Card radius (XLarge vs Large) has no stated rule.** Top-level cards use XLarge (16),
|
||||
list rows Large (12), but it's applied by feel. **Standard:** document surface tiers — page-card XLarge, list-item Large, control Medium — and apply uniformly.
|
||||
- [ ] **UI8 — Circular shapes mix token and literal.** `borderRadiusCircular` (Library pill/watchBadge)
|
||||
- [x] **UI8 — Circular shapes mix token and literal.** `borderRadiusCircular` (Library pill/watchBadge)
|
||||
vs literal `'50%'` (SettingsView swatch, History emptyBadge). **Standard:** always the token.
|
||||
|
||||
### Typography
|
||||
|
||||
- [ ] **UI9 — Screen titles are inconsistent.** Most screens use `<Subtitle2>` (Downloads "Queue",
|
||||
- [x] **UI9 — Screen titles are inconsistent.** Most screens use `<Subtitle2>` (Downloads "Queue",
|
||||
Library, Terminal, Settings cards); Onboarding uses `<Title2>`; **History has no title at all**
|
||||
(just a count). **Standard:** one page-title style on every screen.
|
||||
- [ ] **UI10 — Only Library has a screen subtitle/description.** Others jump straight to content.
|
||||
- [x] **UI10 — Only Library has a screen subtitle/description.** Others jump straight to content.
|
||||
**Standard:** a consistent header block (title + optional one-line description).
|
||||
- [ ] **UI11 — Semantic type ramp vs ad-hoc px.** Sidebar `brandName`/`navItem` and Library
|
||||
`watchBadge`/`pill` set raw `fontSizeBaseXXX`; `sectionIcon`/`mark` use literal `'20px'`/`'26px'`.
|
||||
@@ -723,17 +781,17 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
|
||||
|
||||
### Buttons & controls
|
||||
|
||||
- [ ] **UI14 — Two hand-rolled segmented controls.** DownloadBar kind toggle (`segment`, padding
|
||||
- [x] **UI14 — Two hand-rolled segmented controls.** DownloadBar kind toggle (`segment`, padding
|
||||
`7px 16px`) and Sidebar theme toggle (`themeSeg`, padding `7px 4px`) are separate implementations
|
||||
with different padding/markup. **Standard:** one shared `SegmentedControl`.
|
||||
- [ ] **UI15 — Raw `<button>`s alongside Fluent `<Button>`.** Sidebar navItem/iconBtn/themeSeg,
|
||||
- [x] **UI15 — Raw `<button>`s alongside Fluent `<Button>`.** Sidebar navItem/iconBtn/themeSeg,
|
||||
DownloadBar segment/plKindBtn, CommandPalette item, and Library `groupHead` (a `role="button"` div)
|
||||
are bespoke. **Standard:** wrap recurring patterns so hover/focus/disabled are uniform (ties to L5/M15).
|
||||
- [ ] **UI16 — Button size hierarchy isn't applied uniformly.** DownloadBar `large`; Library "Index"
|
||||
`large` but its toolbar `small`; History all `small`; Settings mostly default. **Standard:** size-by-role (screen primary = large; secondary = medium; row actions = small).
|
||||
- [ ] **UI17 — `appearance="secondary"` used in only two spots** (Library "Check for new", Terminal
|
||||
"Stop") while every other non-primary button is `subtle`. **Standard:** pick subtle *or* secondary as the standard non-primary appearance.
|
||||
- [ ] **UI18 — Two status-chip systems.** QueueItem uses Fluent `Badge`; LibraryView uses custom
|
||||
- [x] **UI18 — Two status-chip systems.** QueueItem uses Fluent `Badge`; LibraryView uses custom
|
||||
color `pill` spans for the same item-status concept (and labels differ — see M8). **Standard:** one shared status-chip component + label map.
|
||||
- [ ] **UI19 — Suggestion-banner CSS duplicated.** Identical `suggestion`/`suggestionText` styles in
|
||||
DownloadBar and LibraryView. **Standard:** a shared `LinkSuggestion` component (also kills drift).
|
||||
@@ -773,13 +831,13 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
|
||||
- [ ] **UI27 — Command palette uses JS hover, not CSS.** Items highlight via `onMouseEnter` setting
|
||||
`itemActive` (no `:hover`), unlike every other list, and expose no `aria-selected`/active-descendant,
|
||||
so the highlight is invisible to screen readers. **Standard:** CSS `:hover` + listbox/option semantics.
|
||||
- [ ] **UI28 — Command palette input has no focus ring.** The search `<input>` sets `outline: 'none'`
|
||||
- [x] **UI28 — Command palette input has no focus ring.** The search `<input>` sets `outline: 'none'`
|
||||
with no replacement. **Standard:** a visible focus ring (Fluent stroke token). *(a11y)*
|
||||
- [ ] **UI29 — Fragmented focus treatment.** Fluent ring (Fluent controls) vs custom border (Select)
|
||||
- [x] **UI29 — Fragmented focus treatment.** Fluent ring (Fluent controls) vs custom border (Select)
|
||||
vs removed (palette input) vs UA default (Sidebar buttons, segments, and Library `cardHead`/`groupHead`
|
||||
which are `role="button" tabIndex=0` with **no `:focus-visible`** → invisible keyboard focus).
|
||||
**Standard:** one focus-ring style on all interactive elements, custom and native. *(a11y; extends L14)*
|
||||
- [ ] **UI30 — Hand-rolled radiogroups aren't arrow-navigable.** DownloadBar kind and Sidebar theme
|
||||
- [x] **UI30 — Hand-rolled radiogroups aren't arrow-navigable.** DownloadBar kind and Sidebar theme
|
||||
use `role="radiogroup"`/`radio` but implement click only — no ←/→ roving focus a radiogroup implies.
|
||||
**Standard:** roving-tabindex arrow keys, or Fluent's RadioGroup. *(a11y)*
|
||||
- [ ] **UI31 — `Select` can't be disabled.** The native-`<select>` wrapper exposes no `disabled` prop,
|
||||
@@ -825,7 +883,7 @@ root cause is already filed.
|
||||
nothing signposts that. "Index" reads like "Download" but only catalogs — you must then expand,
|
||||
select, and Download. **Fix:** rename to "Add channel/playlist," and after indexing surface a clear
|
||||
"Download N videos" next step; detect channel URLs in the Downloads bar and suggest the Library.
|
||||
- [ ] **UX4 — Destructive actions have no confirmation and no undo** (L13). "Clear history,"
|
||||
- [x] **UX4 — Destructive actions have no confirmation and no undo** (L13). "Clear history,"
|
||||
"Clear log," "Remove source" (deletes an entire indexed channel + items), and "Delete selected"
|
||||
are all one click. A user exploring can wipe data irreversibly. **Fix:** confirm destructive/bulk
|
||||
deletes (or offer an undo toast).
|
||||
@@ -898,34 +956,34 @@ DPI handled by Chromium). The deviations:
|
||||
|
||||
### Window behavior & resizing
|
||||
|
||||
- [ ] **W1 — No minimum window size.** `createWindow` sets `width/height` but no `minWidth`/`minHeight`
|
||||
- [x] **W1 — No minimum window size.** `createWindow` sets `width/height` but no `minWidth`/`minHeight`
|
||||
([index.ts](src/main/index.ts)), so the window can be dragged down to a few pixels and the layout
|
||||
(212px sidebar + content) breaks. **Standard:** set a sensible min (e.g. 640×480).
|
||||
- [ ] **W2 — Window placement isn't persisted** (extends UX19): size, position, **maximized state**, and
|
||||
**which monitor** are all forgotten — it reopens 920×700 on the primary display every launch.
|
||||
**Standard:** persist & restore window placement per Windows app convention (e.g. `electron-window-state`).
|
||||
- [ ] **W3 — Title bar doesn't follow the in-app theme.** `nativeTheme.themeSource` is deliberately never
|
||||
- [x] **W3 — Title bar doesn't follow the in-app theme.** `nativeTheme.themeSource` is deliberately never
|
||||
set (index.ts:97), so the OS-drawn caption follows the *OS* theme. Choosing an explicit in-app **dark**
|
||||
theme on a **light** OS leaves a light title bar on a dark app (and vice-versa). **Standard:** set
|
||||
`nativeTheme.themeSource` to the resolved mode, or use `titleBarOverlay` with themed colors.
|
||||
|
||||
### Dialogs & file pickers
|
||||
|
||||
- [ ] **W4 — Text fields have no Cut/Copy/Paste context menu.** Electron adds no default editing menu and
|
||||
- [x] **W4 — Text fields have no Cut/Copy/Paste context menu.** Electron adds no default editing menu and
|
||||
the app registers no `context-menu` handler, so right-clicking any input/textarea (URL, search, terminal
|
||||
args, template fields) shows nothing — a basic Windows text-editing affordance is missing. **Standard:**
|
||||
wire a standard editing context menu (and spellcheck suggestions for textareas).
|
||||
- [ ] **W5 — File pickers don't open at the current value.** `chooseFolder` sets no `defaultPath`, so the
|
||||
- [x] **W5 — File pickers don't open at the current value.** `chooseFolder` sets no `defaultPath`, so the
|
||||
folder dialog opens at a default location instead of the currently-configured Video/Audio folder.
|
||||
**Standard:** seed the picker with the current path. *(Native dialogs themselves are correct — good.)*
|
||||
- [ ] **W6 — Cookie sign-in is a separate taskbar window.** `openCookieLoginWindow` creates a
|
||||
- [x] **W6 — Cookie sign-in is a separate taskbar window.** `openCookieLoginWindow` creates a
|
||||
`BrowserWindow` with no `parent`/`modal` ([cookies.ts](src/main/cookies.ts)), so it appears as a second
|
||||
AeroFetch taskbar button rather than a child/modal dialog. **Standard:** `parent: mainWindow` (+ `modal`
|
||||
if appropriate) so it groups under the app.
|
||||
|
||||
### Keyboard, context menus & focus
|
||||
|
||||
- [ ] **W7 — Lists have no keyboard navigation.** The queue, history, and library lists can't be arrowed
|
||||
- [x] **W7 — Lists have no keyboard navigation.** The queue, history, and library lists can't be arrowed
|
||||
through, **Delete** doesn't remove the focused/selected item, and there's no **Ctrl+A** select-all —
|
||||
all standard Windows list behaviors. **Standard:** roving focus + Delete/Ctrl+A on list surfaces. *(a11y; see also UI30)*
|
||||
- [ ] **W8 — No standard accelerators.** No **Ctrl+,** (Settings), **F1** (help), or **F5** (refresh);
|
||||
@@ -947,9 +1005,16 @@ DPI handled by Chromium). The deviations:
|
||||
- [ ] **W12 — Fallback tray icon is single-resolution.** The embedded fallback is a 32×32 PNG, so on
|
||||
150%/200% DPI the tray glyph is blurry when the real multi-size `.ico` is absent. **Standard:** a
|
||||
multi-size fallback (or rely only on the `.ico`).
|
||||
- [ ] **W13 — Notifications set no icon.** `new Notification({title, body})` omits `icon`; on the **portable**
|
||||
build (no installed AUMID shortcut) Windows toasts may show a generic icon. **Standard:** pass the app icon. *(refines L127)*
|
||||
- [ ] **W14 — App/notification icon is a placeholder** (M3-orig, deferred) — a designed asset is still wanted pre-v1.0.
|
||||
- [x] **W13 — Notifications set no icon.** `new Notification({title, body})` omitted `icon`; on the **portable**
|
||||
build (no installed AUMID shortcut) Windows toasts showed a generic icon. *Fixed: both notification sites
|
||||
([download.ts](src/main/download.ts) completion/failure toast + [index.ts](src/main/index.ts) background-running
|
||||
toast) now pass `icon: getAppIconImage()` — a new cached `NativeImage` helper in [binaries.ts](src/main/binaries.ts)
|
||||
that loads the app `.ico` once and falls back to an empty image (OS default) if it's missing. (refines L127)*
|
||||
- [x] **W14 — App/notification icon is a placeholder** (M3-orig) — *Fixed: redesigned
|
||||
[icon.svg](build/icon.svg) into a proper mark (teal brand square with a top-lit gradient + soft sheen and a
|
||||
bold rounded download glyph over a landing shelf), legible down to 16px; regenerated the multi-size
|
||||
`build/icon.ico` (256/128/64/48/32/16) via ImageMagick. The notification-icon **wiring** (passing the asset
|
||||
to `new Notification`) is also done — see W13/L127.*
|
||||
|
||||
### High DPI, multiple monitors, touch
|
||||
|
||||
@@ -962,7 +1027,7 @@ DPI handled by Chromium). The deviations:
|
||||
|
||||
### Dark mode & accessibility (Windows-specific)
|
||||
|
||||
- [ ] **W17 — No `aria-live` for status changes.** Narrator doesn't announce download progress,
|
||||
- [x] **W17 — No `aria-live` for status changes.** Narrator doesn't announce download progress,
|
||||
completion, or errors — there are no live regions. **Standard:** polite live regions for queue/status updates.
|
||||
- [ ] **W18 — Custom colors unverified under High Contrast.** The status pills, segmented controls, accent
|
||||
swatches, and thumbnail tints use explicit background colors that Windows forced-colors mode may not
|
||||
@@ -975,8 +1040,8 @@ DPI handled by Chromium). The deviations:
|
||||
never shows "3 downloading" or similar. **Standard:** reflect activity in the title/tooltip.
|
||||
- [ ] **(OK)** Per-user no-admin install, NSIS uninstaller, `aerofetch://` registration, AppUserModelID,
|
||||
tray tooltip + left-click restore + right-click menu, taskbar progress, and jump list all follow
|
||||
Windows conventions. **(Gap)** unsigned binary → SmartScreen prompt (deferred, SIGNING.md), and the
|
||||
default Electron menu with DevTools is still reachable (M31).
|
||||
Windows conventions. **(Non-goal)** unsigned binary → SmartScreen prompt — accepted; no certificate will
|
||||
be purchased (the build stays signing-ready via env vars if that ever changes — see [SIGNING.md](docs/SIGNING.md)).
|
||||
|
||||
---
|
||||
|
||||
@@ -993,7 +1058,7 @@ split — but that style is unenforced and several "do the same thing two ways"
|
||||
(bare). Keys say `videoDir`/`audioDir` while the UI says "folder." Preload names diverge from main
|
||||
(L92); `MediaKind` is declared twice (L105). **Standard:** booleans as `is`/`has`/`should`/`<verb>`
|
||||
consistently; one term ("folder") across keys + UI; align preload↔main names; single-source shared types in `@shared`.
|
||||
- [ ] **CC2 — Coding style is consistent but unenforced.** No ESLint/Prettier config, script, or dep
|
||||
- [x] **CC2 — Coding style is consistent but unenforced.** No ESLint/Prettier config, script, or dep
|
||||
(L44), so the (good) house style drifts only by discipline; `??` vs `||` is occasionally misused for
|
||||
null checks. **Standard:** add Prettier + typescript-eslint with `lint`/`format` scripts in CI; codify the existing style.
|
||||
- [ ] **CC3 — Different patterns for the same problem.** JSON persistence (M1), status chips (UI18),
|
||||
@@ -1079,7 +1144,7 @@ filed. Several listed categories are **N/A** to this stack and worth recording s
|
||||
|
||||
**Dead / unreachable code (the real list):**
|
||||
|
||||
- [ ] **Safe to remove now — `getDefaultFolder`** (M2): channel + preload method + main handler + mock,
|
||||
- [x] **Safe to remove now — `getDefaultFolder`** (M2): channel + preload method + main handler + mock,
|
||||
**zero callers**. Pure deletion, no behavior change.
|
||||
- [ ] **Decide "wire or remove" — command preview** (M5): channel + preload `previewCommand` + main
|
||||
`previewCommand()` + `formatCommandLine`/`quoteForDisplay` (buildArgs, **unit-tested**) +
|
||||
@@ -1094,7 +1159,7 @@ filed. Several listed categories are **N/A** to this stack and worth recording s
|
||||
|
||||
**New cleanliness findings:**
|
||||
|
||||
- [ ] **CL1 — Magic-string protocol markers duplicated across the emit/parse boundary.** `'prog|'` and
|
||||
- [x] **CL1 — Magic-string protocol markers duplicated across the emit/parse boundary.** `'prog|'` and
|
||||
`'path|'` are hard-coded in `buildArgs.ts` (`PROGRESS_TEMPLATE` / `--print after_move:path|…`) and again
|
||||
in `download.ts` (`line.startsWith('prog|')` / `'path|'`). Change one and the other breaks silently.
|
||||
**Fix:** export shared marker constants from one module.
|
||||
@@ -1104,7 +1169,7 @@ filed. Several listed categories are **N/A** to this stack and worth recording s
|
||||
`child` event handlers. **Fix:** extract the stdout-parse + close/error wiring (pairs with CC3's spawn-stream helper).
|
||||
- [ ] **CL4 — Deep nesting: `updater.downloadAppUpdate`** — Promise → `net.request` → `response` → `data`
|
||||
with nested conditionals/teardown is the hardest-to-follow block. **Fix:** extract a `streamToFile` helper.
|
||||
- [ ] **CL5 — Superfluous exports.** `PROGRESS_TEMPLATE` (L167) and `getManagedBinDir` are `export`ed but
|
||||
- [x] **CL5 — Superfluous exports.** `PROGRESS_TEMPLATE` (L167) and `getManagedBinDir` are `export`ed but
|
||||
used only within their own module. **Fix:** make them module-private.
|
||||
- [ ] **CL6 — Redundant wrappers** (minor): `MediaThumb`'s `kind === 'audio' ? 'audio' : 'video'` (L15),
|
||||
`clearDir` = `update({[t]:''})`, and the store `openFile`/`showInFolder` thin wrappers around `window.api`.
|
||||
@@ -1186,7 +1251,7 @@ earlier passes are listed so this is complete; and the categories that came back
|
||||
|
||||
### New bugs
|
||||
|
||||
- [ ] **B1 — No stall/idle timeout on downloads (resource leak + stuck slot).** `spawn(ytdlp, …, {
|
||||
- [x] **B1 — No stall/idle timeout on downloads (resource leak + stuck slot).** `spawn(ytdlp, …, {
|
||||
windowsHide: true })` ([download.ts](src/main/download.ts):306) sets **no `timeout`**, and `buildArgs`
|
||||
emits no `--socket-timeout`. A hung connection means yt-dlp never exits → the `active` map entry and the
|
||||
renderer's `downloading` item persist **forever**, permanently consuming a concurrency slot with no
|
||||
@@ -1196,20 +1261,20 @@ earlier passes are listed so this is complete; and the categories that came back
|
||||
sequential probes (each up to the 180 s `probeFlat` timeout); the UI shows "indexing…" with the Index
|
||||
button disabled and **no cancel**. A large channel locks the add-source flow for minutes with no abort.
|
||||
**Fix:** thread an `AbortSignal`/cancel token and a Cancel button.
|
||||
- [ ] **B3 — `extractSha256` ignores the filename (wrong-hash risk).** It returns the **first** 64-hex
|
||||
- [x] **B3 — `extractSha256` ignores the filename (wrong-hash risk).** It returns the **first** 64-hex
|
||||
token in the file ([updater.ts](src/main/updater.ts)); a combined multi-file checksum uploaded as
|
||||
`<asset>.sha256` would verify the installer against the wrong line's hash. Safe only by the one-hash-
|
||||
per-asset convention. **Fix:** match the hash on the asset's filename line.
|
||||
- [ ] **B4 — `probeMeta` assumes one line per `--print` field.** It does `stdout.split('\n')` → `[title,
|
||||
- [x] **B4 — `probeMeta` assumes one line per `--print` field.** It does `stdout.split('\n')` → `[title,
|
||||
uploader, duration]` ([download.ts](src/main/download.ts)); a title containing a newline shifts channel
|
||||
and duration by a line. **Fix:** use a single `--print` with an unlikely delimiter, or `-J`.
|
||||
- [ ] **B5 — Missing status guard on the `meta` event.** Between `cancel()` and the child's `close`,
|
||||
- [x] **B5 — Missing status guard on the `meta` event.** Between `cancel()` and the child's `close`,
|
||||
`active.has(id)` is briefly true, so `probeMeta` can `send` a `meta` event for a just-canceled item;
|
||||
`applyEvent`'s `meta` case (unlike `progress`/`done`/`error`) has **no canceled guard**, so it updates a
|
||||
canceled item's title. Harmless today, but the asymmetry is a latent bug. **Fix:** guard `meta` like the others.
|
||||
- [ ] **B6 — App-update download has no cancel.** Once `downloadAppUpdate` starts there's no user abort —
|
||||
only completion or the idle timeout stops it; the progress UI offers no Cancel. **Fix:** expose cancel (abort the request).
|
||||
- [ ] **B7 — Cookie-login promise can never resolve if the window is destroyed without `close`.**
|
||||
- [x] **B7 — Cookie-login promise can never resolve if the window is destroyed without `close`.**
|
||||
`openCookieLoginWindow` resolves only via the `close`→`exportAndResolve` path; a `destroy()` (or a
|
||||
`closed` without `close`) would leave `pendingResolvers` pending forever. Not triggered by current code,
|
||||
but a latent never-resolve. **Fix:** also resolve/reject on `closed`.
|
||||
@@ -1245,15 +1310,15 @@ A fresh lens: what happens when the *environment* fails (disk full, permission d
|
||||
killed mid-write, huge inputs) rather than when the logic is wrong. These are robustness findings, not
|
||||
cosmetics. `R` IDs.
|
||||
|
||||
- [ ] **R1 — JSON stores are not written atomically (corruption on crash/power-loss).** The hand-rolled
|
||||
- [x] **R1 — JSON stores are not written atomically (corruption on crash/power-loss).** The hand-rolled
|
||||
stores (history/errorlog/templates/sources/media-items) persist via `writeFileSync(file, JSON.stringify…)`
|
||||
— not write-temp-then-rename. A crash/power-cut mid-write truncates the file; the next read hits a parse
|
||||
error and returns `[]`. `electron-store` (settings) *is* atomic, so durability is inconsistent across the
|
||||
app. **Fix:** an atomic write (temp + rename, or `write-file-atomic`) in the shared `jsonStore` (SIMP1).
|
||||
- [ ] **R2 — A corrupt/invalid store silently loses all data.** `readJsonArray`/`listHistory` etc. catch a
|
||||
- [x] **R2 — A corrupt/invalid store silently loses all data.** `readJsonArray`/`listHistory` etc. catch a
|
||||
parse error and return `[]` with no warning, backup, or recovery — one bad byte wipes the user's history/
|
||||
sources/templates from their perspective. **Fix:** on parse failure, back up the bad file (`.corrupt`) and surface a notice.
|
||||
- [ ] **R3 — O(n) full-file rewrite per download completion (≈O(n²) per channel), synchronously on the
|
||||
- [x] **R3 — O(n) full-file rewrite per download completion (≈O(n²) per channel), synchronously on the
|
||||
main thread.** `setMediaItemDownloaded` ([sources.ts](src/main/sources.ts):126) reads + parses the entire
|
||||
`media-items.json` (up to `MAX_ITEMS` = 20,000) and rewrites the whole file on **every** completion, and
|
||||
`addHistory` rewrites all of `history.json` likewise. Downloading a 1,000-video channel ⇒ ~1,000 full
|
||||
@@ -1263,7 +1328,7 @@ cosmetics. `R` IDs.
|
||||
deletes the partial; canceled items vanish from the UI while `.part`/fragment files accumulate in the
|
||||
output folder. (App crash mid-download likewise — the queue isn't persisted to resume, M4.) **Fix:**
|
||||
clean up the partial on cancel, or surface/offer-resume for orphans.
|
||||
- [ ] **R5 — Settings write failure is unhandled.** `setSettings` calls `store.set(...)` with no try/catch;
|
||||
- [x] **R5 — Settings write failure is unhandled.** `setSettings` calls `store.set(...)` with no try/catch;
|
||||
on disk-full/read-only `electron-store` throws → the IPC rejects → the renderer's `setSettings().catch(()
|
||||
=> {})` swallows it while the optimistic UI keeps the change (the disk-full form of M34). **Fix:** catch and report.
|
||||
- [ ] **R6 — Silent data loss on any store write failure.** A completed download that can't be recorded
|
||||
@@ -1288,13 +1353,13 @@ the end so the picture is complete. **Verified-clean first:** card/label/button
|
||||
|
||||
### Poor / risky defaults (first-run impressions)
|
||||
|
||||
- [ ] **SR1 — Theme defaults to `'light'`, not `'system'`.** A dark-mode Windows user gets a bright white
|
||||
- [x] **SR1 — Theme defaults to `'light'`, not `'system'`.** A dark-mode Windows user gets a bright white
|
||||
app on first launch despite the app fully supporting "follow system." Single most-noticeable first-run
|
||||
miss. **Fix:** default `theme: 'system'`.
|
||||
- [ ] **SR2 — `ytdlpChannel` defaults to `'nightly'`.** The app ships auto-pulling yt-dlp's daily,
|
||||
- [x] **SR2 — `ytdlpChannel` defaults to `'nightly'`.** The app ships auto-pulling yt-dlp's daily,
|
||||
less-tested build (auto-update also on by default); one nightly regression breaks downloads for everyone.
|
||||
Defensible for chasing YouTube changes, but a risky default for a commercial release. **Fix:** consider `stable` as the shipped default.
|
||||
- [ ] **SR3 — `autoDownloadNew` defaults to `true`.** The moment a user marks a channel "watched," new
|
||||
- [x] **SR3 — `autoDownloadNew` defaults to `true`.** The moment a user marks a channel "watched," new
|
||||
uploads auto-download with no per-event consent — can quietly fill a disk. **Fix:** default off (let watching be "notify," downloading be opt-in).
|
||||
- [ ] **SR4 — `clipboardWatch` defaults to `true`.** The app reads the clipboard on every window focus from
|
||||
first launch (R7/L138). A privacy-conscious default would be off or first-run-prompted. **Fix:** default off, or ask on first run.
|
||||
@@ -1304,10 +1369,10 @@ the end so the picture is complete. **Verified-clean first:** card/label/button
|
||||
|
||||
### Stuck states & progress glitches
|
||||
|
||||
- [ ] **SR6 — "Resolving…" can stick forever.** A queued item's channel shows the `'Resolving…'`
|
||||
- [x] **SR6 — "Resolving…" can stick forever.** A queued item's channel shows the `'Resolving…'`
|
||||
placeholder until a `meta` event arrives; if metadata never resolves (error before meta, or no channel
|
||||
in the probe), the row reads "Resolving… • 1080p • Video" permanently — looks hung. **Fix:** clear the placeholder on error / when meta fails.
|
||||
- [ ] **SR7 — Progress bar visibly resets mid-download.** A video+audio merge downloads as two streams, so
|
||||
- [x] **SR7 — Progress bar visibly resets mid-download.** A video+audio merge downloads as two streams, so
|
||||
the bar fills 0→100%, then resets to 0→100% again before muxing — looks like a glitch/restart to the
|
||||
user. **Fix:** weight the two phases (e.g. 0–50% / 50–100%) or show a "merging…" state.
|
||||
- [ ] **SR8 — Window title never reflects state** (L108/W19): always the static "AeroFetch," so the taskbar
|
||||
@@ -1323,16 +1388,16 @@ the end so the picture is complete. **Verified-clean first:** card/label/button
|
||||
|
||||
### Also reduces perceived quality (already filed — listed for completeness)
|
||||
|
||||
- Placeholder (non-designed) app + notification icon (W14/L127); onboarding has no folder picker / feels
|
||||
thin (UX5/UX22); no boot loading state + one-frame theme flash (UX26); destructive actions have no
|
||||
confirmation (UX4/L13); refresh-icon mismatch ArrowClockwise vs ArrowSync (L106) and Regular/Filled brand
|
||||
icon (UI13); only the sidebar animates, everything else snaps (UI26); stuck 0% bar for unknown-size
|
||||
downloads (L137); separator glyph/spacing inconsistency `•`/`·` (L164); the command palette is
|
||||
undiscoverable (UX21); SmartScreen prompt on the unsigned build (SIGNING.md).
|
||||
- Onboarding has no folder picker / feels thin (UX5/UX22); no boot loading state + one-frame theme flash
|
||||
(UX26); refresh-icon mismatch ArrowClockwise vs ArrowSync (L106) and Regular/Filled brand icon (UI13); only
|
||||
the sidebar animates, everything else snaps (UI26); separator glyph/spacing inconsistency `•`/`·` (L164);
|
||||
the command palette is undiscoverable (UX21). *(The unsigned-build SmartScreen prompt is accepted as a
|
||||
non-goal — no certificate.)*
|
||||
|
||||
**Ship-blockers (highest perceived-quality impact):** SR1 (light-on-dark first launch), SR7 (progress
|
||||
visibly restarting), SR6 ("Resolving…" stuck), W14 (placeholder icon), and the unsigned-build SmartScreen
|
||||
prompt. None are hard fixes; together they're most of the "this feels finished" gap.
|
||||
visibly restarting), SR6 ("Resolving…" stuck), and W14 (placeholder icon) — **all now resolved.** The
|
||||
unsigned-build SmartScreen prompt is accepted as a non-goal (no certificate). Together these were most of
|
||||
the "this feels finished" gap.
|
||||
|
||||
---
|
||||
|
||||
@@ -1343,11 +1408,11 @@ acceptable first:** memoized `QueueItem`s don't re-render when their item is unc
|
||||
`map` preserves references for untouched items); the queue + large library lists are virtualized;
|
||||
`electron-store` is constructed lazily. The issues:
|
||||
|
||||
- [ ] **PERF1 — `getSettings()` is heavy and on every hot path.** It runs per download spawn (twice — in
|
||||
- [x] **PERF1 — `getSettings()` is heavy and on every hot path.** It runs per download spawn (twice — in
|
||||
`buildCommand` and the `maxConcurrent` check), per completion (`notify`), and on the system-theme bridge;
|
||||
each call re-reads the whole store and, for users who've configured a proxy/PO-token/update-token, runs
|
||||
up to 3 `safeStorage.decryptString` (DPAPI) calls. **Fix:** cache the decrypted settings, invalidate on write.
|
||||
- [ ] **PERF2 — `templates.json` is read+parsed on every download.** `resolveExtraArgs` evaluates
|
||||
- [x] **PERF2 — `templates.json` is read+parsed on every download.** `resolveExtraArgs` evaluates
|
||||
`templates: listTemplates()` eagerly as an argument ([download.ts](src/main/download.ts):182) *before*
|
||||
`selectExtraArgs` checks `customCommandEnabled`, so the file is read even when custom commands are off.
|
||||
**Fix:** pass a lazy getter, or short-circuit on the gate first.
|
||||
@@ -1363,7 +1428,7 @@ acceptable first:** memoized `QueueItem`s don't re-render when their item is unc
|
||||
re-measures/cache churn). **Fix:** `useCallback`/hoist them.
|
||||
- [ ] **PERF6 — Per-thumbnail store subscriptions.** Each `MediaThumb` calls `useResolvedDark()` (two
|
||||
store subscriptions) (L162); a long list creates N×2 subscriptions. **Fix:** resolve once, pass as a prop.
|
||||
- [ ] **PERF7 — (ref R3) O(n²) media-items rewrites** dominate the main-process cost when downloading a
|
||||
- [x] **PERF7 — (ref R3) O(n²) media-items rewrites** dominate the main-process cost when downloading a
|
||||
channel; closed by the cached/atomic `jsonStore` (SIMP1) — the single highest-value perf fix.
|
||||
- [ ] **PERF8 — No code-splitting.** All views (incl. the rarely-used Terminal/Settings) load up front in
|
||||
one renderer bundle alongside Fluent UI + React; software rendering (W15) compounds first-paint cost.
|
||||
@@ -1402,5 +1467,7 @@ Security & correctness audit (2026-06-23):
|
||||
- PO-token WebView auto-minting — only the `--extractor-args` plumbing shipped.
|
||||
- Taskbar overlay badge (needs a drawn `NativeImage`).
|
||||
- i18n — deliberately deferred (note: UI strings are hard-coded throughout).
|
||||
- Code signing — build is signing-ready; needs a purchased cert.
|
||||
- Code signing — **non-goal**: no certificate will be purchased, so builds ship unsigned and the SmartScreen
|
||||
prompt is accepted. The build remains signing-ready via `CSC_LINK`/`CSC_KEY_PASSWORD` env vars if this ever
|
||||
changes — see [SIGNING.md](docs/SIGNING.md).
|
||||
- Live smoke-test of OS-level wiring (tray/jump list/scheduled sync/`aerofetch://`).
|
||||
|
||||
Reference in New Issue
Block a user