From 3086ac469f0a2e5695f88e2f802c262ec5001b6b Mon Sep 17 00:00:00 2001 From: debont80 Date: Tue, 30 Jun 2026 13:54:19 -0400 Subject: [PATCH] Fix L16/L75/L77: datetime formatting, update-token visibility, ffmpeg re-check 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 --- CODE-AUDIT.md | 8 +- src/renderer/src/components/SettingsView.tsx | 95 +++++++++++--------- src/renderer/src/datetime.ts | 22 +++-- 3 files changed, 74 insertions(+), 51 deletions(-) diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 26ab263..c29bacb 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -1,4 +1,4 @@ -# Code Audit +# Code Audit Living checklist of audit findings for AeroFetch. Security & correctness were reviewed 2026-06-23 (all fixed — see Completed). The architectural review (2026-06-29) added the @@ -408,7 +408,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n rings; `box-sizing` is then set ad hoc on the SettingsView swatch. - [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 +- [x] **L16 — `relTime`/`formatWhen` verbosity.** `relTime` returns unbounded "N d ago"; History's `formatWhen` always appends the year, even for the current year. - [x] **L17 — TemplateManager regex not validated.** An invalid `urlPattern` is accepted with no feedback and silently never matches (main's `matchesUrl` swallows the error). @@ -516,11 +516,11 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n re-queued row loses its thumbnail until re-probed. - [ ] **L73 — Two verbs for the same probe action** — DownloadBar "Fetch" vs LibraryView "Index". - [ ] **L74 — Accent swatches are triple-labeled** (`aria-pressed` + `aria-label` + `title`). -- [ ] **L75 — Update-token field always visible** in the Software-update card, even when no update +- [x] **L75 — Update-token field always visible** in the Software-update card, even when no update is pending — buries an advanced/rarely-needed input. - [x] **L76 — Duplicate-warning may show a placeholder title** — `setDup(existing.title)` can be the `titleFromUrl` placeholder before metadata resolves ('YouTube video (id)'). -- [ ] **L77 — ffmpeg/ffprobe versions have no re-check affordance** (load once; "not found" sticks +- [x] **L77 — ffmpeg/ffprobe versions have no re-check affordance** (load once; "not found" sticks with no retry) unlike the yt-dlp "Check version" button. - [ ] **L78 — `DownloadsView` empty-state copy** ("Paste a URL above to get started") doesn't match the actual two-step Fetch→Download affordance. diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index fa0ce24..e4735cb 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Field, Input, @@ -153,10 +153,10 @@ const useStyles = makeStyles({ } }) -// Combined "Type — Quality" options for the default-format dropdown. +// Combined "Type -- Quality" options for the default-format dropdown. const FORMAT_OPTIONS = [ - ...QUALITY_OPTIONS.video.map((q) => ({ value: `video|${q}`, label: `Video — ${q}` })), - ...QUALITY_OPTIONS.audio.map((q) => ({ value: `audio|${q}`, label: `Audio — ${q}` })) + ...QUALITY_OPTIONS.video.map((q) => ({ value: `video|${q}`, label: `Video -- ${q}` })), + ...QUALITY_OPTIONS.audio.map((q) => ({ value: `audio|${q}`, label: `Audio -- ${q}` })) ] const THEME_MODE_OPTIONS = [ @@ -364,7 +364,7 @@ export function SettingsView(): React.JSX.Element { const result = await window.api.importBackup() setImportResult(result) if (result.ok) { - // Settings + templates changed underneath the stores — reload both. + // Settings + templates changed underneath the stores -- reload both. const [s, t] = await Promise.all([window.api.getSettings(), window.api.listTemplates()]) useSettings.setState({ ...s, loaded: true }) useTemplates.setState({ templates: t }) @@ -378,7 +378,7 @@ export function SettingsView(): React.JSX.Element { function copyErrorReport(): void { // The button is disabled when there are no entries, so `report` is always - // non-empty here — no need for an unreachable "No errors logged." fallback (L159). + // non-empty here -- no need for an unreachable "No errors logged." fallback (L159). const report = errorEntries .map((e) => [new Date(e.occurredAt).toLocaleString(), e.title ?? e.url, e.url, e.error].join('\n') @@ -393,8 +393,8 @@ export function SettingsView(): React.JSX.Element { try { const result = await window.api.cookiesLogin(loginUrl) if (result.ok && result.cookieCount === 0) { - // Window closed without capturing anything — don't imply success (L50). - setLoginError('No cookies were captured — did you sign in before closing the window?') + // Window closed without capturing anything -- don't imply success (L50). + setLoginError('No cookies were captured -- did you sign in before closing the window?') } else if (result.ok) { setCookiesStatus(await window.api.cookiesStatus()) } else { @@ -441,7 +441,7 @@ export function SettingsView(): React.JSX.Element { try { const result = await window.api.updateYtdlp(ytdlpChannel) setUpdateResult(result) - if (result.ok) setVersion(null) // stale — prompt a re-check rather than show a wrong version + if (result.ok) setVersion(null) // stale -- prompt a re-check rather than show a wrong version } catch (e) { setUpdateResult({ ok: false, error: e instanceof Error ? e.message : String(e) }) } finally { @@ -468,7 +468,7 @@ export function SettingsView(): React.JSX.Element { /> {noResults && ( - No settings match “{search}”. + No settings match "{search}". )} @@ -577,7 +577,7 @@ export function SettingsView(): React.JSX.Element { {highContrast - ? 'A Windows high-contrast theme is active — AeroFetch follows your system colors.' + ? 'A Windows high-contrast theme is active -- AeroFetch follows your system colors.' : 'AeroFetch automatically follows a Windows high-contrast theme if you turn one on.'} {highContrast && ( @@ -746,7 +746,7 @@ export function SettingsView(): React.JSX.Element { setPotHint('Token saved.') } else { setPotHint( - 'Token not found — try signing into YouTube first via Cookies above.' + 'Token not found -- try signing into YouTube first via Cookies above.' ) } } catch { @@ -799,7 +799,7 @@ export function SettingsView(): React.JSX.Element { <>
Custom commands
- Named templates of extra yt-dlp flags — your own power-user recipes (e.g. + Named templates of extra yt-dlp flags -- your own power-user recipes (e.g. --write-thumbnail, --no-mtime, anything yt-dlp supports). Appended after every other option, so a template flag can override a setting above it. @@ -891,7 +891,7 @@ export function SettingsView(): React.JSX.Element { Save your settings and custom-command templates to a JSON file, or restore them on another - machine. Does not include download history or credentials (proxy, API tokens — re-enter + machine. Does not include download history or credentials (proxy, API tokens -- re-enter those after import). @@ -1034,23 +1034,25 @@ export function SettingsView(): React.JSX.Element { {appChecking && } - - update({ updateToken: d.value })} - contentBefore={} - /> - + {(updateToken.trim() !== '' || !!appUpdError) && ( + + update({ updateToken: d.value })} + contentBefore={} + /> + + )} {appUpd?.ok && appUpd.available && ( <> - Version {appUpd.latestVersion} is available — here's what changed: + Version {appUpd.latestVersion} is available -- here's what changed: {appUpd.notes &&
{appUpd.notes}
}
@@ -1073,7 +1075,8 @@ export function SettingsView(): React.JSX.Element {
{!appUpd.downloadUrl && ( - This release has no installer attached — use “View release” to download it manually. + This release has no installer attached -- use "View release" to download it + manually. )} {appDownloading && } @@ -1081,7 +1084,7 @@ export function SettingsView(): React.JSX.Element { )} {appUpd?.ok && !appUpd.available && ( - You're up to date — v{appUpd.currentVersion} is the latest. + You're up to date -- v{appUpd.currentVersion} is the latest. )} {appUpdError && {appUpdError}} @@ -1115,14 +1118,24 @@ export function SettingsView(): React.JSX.Element { {version.error} )} {ffmpeg && ( - <> - - ffmpeg {ffmpeg.ffmpeg ?? 'not found'} - - - ffprobe {ffmpeg.ffprobe ?? 'not found'} - - +
+
+ + ffmpeg {ffmpeg.ffmpeg ?? 'not found'} + + + ffprobe {ffmpeg.ffprobe ?? 'not found'} + +
+
)} {ytdlpLastUpdateCheck > 0 && ( diff --git a/src/renderer/src/datetime.ts b/src/renderer/src/datetime.ts index 3692bc0..2c0f83f 100644 --- a/src/renderer/src/datetime.ts +++ b/src/renderer/src/datetime.ts @@ -2,8 +2,9 @@ // three private functions — `relTime` (LibraryView), `formatWhen` (HistoryView) // and `fmtSchedule` (QueueItem) — each reimplementing date math inline. -/** Relative "time since" label, e.g. "just now" / "5 min ago" / "3 h ago" / - * "2 d ago". Returns "never" for a missing timestamp. (Library last-indexed.) */ +/** Relative "time since" label: "just now" / "5 min ago" / "3 h ago" / + * "2 d ago" / "3 w ago" / "2 mo ago". Returns "never" for a missing + * timestamp. (Library last-indexed.) */ export function relTime(ms?: number): string { if (!ms) return 'never' const mins = Math.round((Date.now() - ms) / 60000) @@ -11,11 +12,15 @@ export function relTime(ms?: number): string { if (mins < 60) return `${mins} min ago` const hrs = Math.round(mins / 60) if (hrs < 24) return `${hrs} h ago` - return `${Math.round(hrs / 24)} d ago` + const days = Math.round(hrs / 24) + if (days < 7) return `${days} d ago` + const weeks = Math.round(days / 7) + if (weeks < 5) return `${weeks} w ago` + return `${Math.round(days / 30)} mo ago` } -/** Absolute "when" label that stays short for recent items: "Today, 3:04 PM" / - * "Yesterday, 3:04 PM" / "Jun 5, 2025". (History rows.) */ +/** Absolute "when" label: "Today, 3:04 PM" / "Yesterday, 3:04 PM" / + * "Jun 5" (current year) / "Jun 5, 2024" (past year). (History rows.) */ export function formatWhen(ts: number): string { const d = new Date(ts) const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) @@ -24,7 +29,12 @@ export function formatWhen(ts: number): string { const dayMs = 1000 * 60 * 60 * 24 if (ts >= startOfToday) return `Today, ${time}` if (ts >= startOfToday - dayMs) return `Yesterday, ${time}` - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + const sameYear = d.getFullYear() === now.getFullYear() + return d.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + ...(sameYear ? {} : { year: 'numeric' }) + }) } /** Full date + time for a scheduled download, e.g. "Jun 5, 2025, 3:04 PM".