feat(audit): L51 + L64 — daily-sync time picker + aria2c connection tuning

Two small self-contained features from the audit's deferred list:

- L51: the Task Scheduler daily sync is no longer pinned to 09:00. New
  Settings.syncTime (24h HH:MM) with a native time input beside the Library
  "Daily sync" switch — persists on change, re-registers the task on blur
  (schtasks /Create /F overwrite is the time-change mechanism). The value
  reaches the schtasks /ST argv, so a shared isValidSyncTime guards it at
  both the settings write and in schedule.ts (unit-tested incl.
  injection-shaped input). Live-verified against the machine's real task.

- L64: aria2c connection count (-x/-s) is user-tunable. New
  Settings.aria2cConnections (1-16, aria2c's own ceiling) exposed as a
  SpinButton in Settings -> Network while the aria2c toggle is on.
  ARIA2C_ARGS became the pure aria2cArgs(n?) with a defensive clamp;
  threaded through AccessOptions. The -k 1M split floor stays fixed — a
  free-text field would bypass the custom-command consent gate.

typecheck + 309 tests + eslint + production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:57:20 -04:00
parent e19f988c72
commit 96800d5a17
12 changed files with 199 additions and 21 deletions
+27 -1
View File
@@ -237,6 +237,7 @@ export function LibraryView(): React.JSX.Element {
const syncing = useSources((s) => s.syncing)
const downloadItems = useDownloads((s) => s.items)
const autoDownloadNew = useSettings((s) => s.autoDownloadNew)
const syncTime = useSettings((s) => s.syncTime)
const updateSettings = useSettings((s) => s.update)
const [url, setUrl] = useState('')
@@ -294,11 +295,26 @@ export function LibraryView(): React.JSX.Element {
async function toggleScheduled(next: boolean): Promise<void> {
setScheduled(next) // optimistic
if (PREVIEW) return
const res = await window.api.setScheduledSync(next)
const res = await window.api.setScheduledSync(next, syncTime)
setScheduled(res.enabled)
if (res.error) setError(res.error)
}
// The native time input emits a complete 'HH:MM' or '' (cleared/partial) — only
// persist real times, so the picker can never store a malformed value (L51).
function onSyncTimeChange(value: string): void {
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(value)) return
updateSettings({ syncTime: value })
}
// Re-register the task with the new time once editing settles (blur), so typing
// through the segments doesn't spawn schtasks per keystroke (L51).
async function onSyncTimeCommit(): Promise<void> {
if (!scheduled || PREVIEW) return
const res = await window.api.setScheduledSync(true, useSettings.getState().syncTime)
if (res.error) setError(res.error)
}
// Reset the selection (and any batch note) whenever the expanded source changes.
useEffect(() => {
setSelected(new Set())
@@ -578,6 +594,16 @@ export function LibraryView(): React.JSX.Element {
onChange={(_, d) => toggleScheduled(d.checked)}
aria-label="Daily background sync"
/>
{scheduled && (
<Input
type="time"
size="small"
value={syncTime}
onChange={(_, d) => onSyncTimeChange(d.value)}
onBlur={onSyncTimeCommit}
aria-label="Daily sync time"
/>
)}
</span>
</div>
@@ -1,5 +1,14 @@
import { useState } from 'react'
import { Field, Input, Switch, Button, Card, Subtitle2, Spinner } from '@fluentui/react-components'
import {
Field,
Input,
Switch,
SpinButton,
Button,
Card,
Subtitle2,
Spinner
} from '@fluentui/react-components'
import { GlobeRegular } from '@fluentui/react-icons'
import { useSettings } from '../../store/settings'
import { useSettingsStyles } from './settingsStyles'
@@ -9,6 +18,7 @@ export function NetworkCard(): React.JSX.Element {
const proxy = useSettings((s) => s.proxy)
const rateLimit = useSettings((s) => s.rateLimit)
const useAria2c = useSettings((s) => s.useAria2c)
const aria2cConnections = useSettings((s) => s.aria2cConnections)
const youtubePlayerClient = useSettings((s) => s.youtubePlayerClient)
const youtubePoToken = useSettings((s) => s.youtubePoToken)
const update = useSettings((s) => s.update)
@@ -53,6 +63,25 @@ export function NetworkCard(): React.JSX.Element {
<Switch checked={useAria2c} onChange={(_, d) => update({ useAria2c: d.checked })} />
</Field>
{useAria2c && (
<Field
label="aria2c connections"
hint="Parallel connections per download (116). More can be faster on throttled connections; fewer is gentler on servers."
>
<SpinButton
value={aria2cConnections}
min={1}
max={16}
onChange={(_, d) => {
const v = d.value ?? Number(d.displayValue)
if (typeof v === 'number' && Number.isFinite(v)) {
update({ aria2cConnections: Math.min(16, Math.max(1, Math.round(v))) })
}
}}
/>
</Field>
)}
<Field
label="YouTube client (advanced)"
hint="Override how AeroFetch identifies itself to YouTube when downloads start failing. Try web_safari, tv, or mweb. Leave blank for the automatic default."