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
+20
View File
@@ -9,6 +9,7 @@ import {
collectionOutputTemplate,
CROP_SQUARE_PPA,
ARIA2C_ARGS,
aria2cArgs,
DEST_MARKER,
type AccessOptions
} from '../src/main/buildArgs'
@@ -105,6 +106,25 @@ describe('network options', () => {
const argv = build(opts(), dlo(), NO_ACCESS)
expect(argv).not.toContain('--downloader-args')
})
it('threads a custom aria2c connection count into --downloader-args (L64)', () => {
const argv = build(opts(), dlo(), {
...NO_ACCESS,
aria2cPath: 'C:/fake/bin/aria2c.exe',
aria2cConnections: 4
})
expect(hasSeq(argv, '--downloader-args', 'aria2c:-x 4 -s 4 -k 1M')).toBe(true)
})
it('aria2cArgs clamps to aria2cs 116 ceiling and defaults to 16 (L64)', () => {
expect(aria2cArgs()).toBe('aria2c:-x 16 -s 16 -k 1M')
expect(aria2cArgs(8)).toBe('aria2c:-x 8 -s 8 -k 1M')
expect(aria2cArgs(0)).toBe('aria2c:-x 1 -s 1 -k 1M') // below floor → 1
expect(aria2cArgs(99)).toBe('aria2c:-x 16 -s 16 -k 1M') // above ceiling → 16
expect(aria2cArgs(7.6)).toBe('aria2c:-x 8 -s 8 -k 1M') // rounds
expect(aria2cArgs(NaN)).toBe('aria2c:-x 16 -s 16 -k 1M') // non-finite → default
expect(ARIA2C_ARGS).toBe(aria2cArgs()) // the exported default stays in sync
})
})
// --- Cookies: browser store vs. sign-in window's exported file --------------
+34 -1
View File
@@ -8,7 +8,40 @@ import {
isValidSource,
isValidMediaItem
} from '../src/main/validation'
import type { HistoryEntry, ErrorLogEntry, Source, MediaItem } from '@shared/ipc'
import {
isValidSyncTime,
type HistoryEntry,
type ErrorLogEntry,
type Source,
type MediaItem
} from '@shared/ipc'
// --- L51: daily-sync time (reaches `schtasks /ST`) ---------------------------
describe('isValidSyncTime', () => {
it('accepts well-formed 24h HH:MM times', () => {
expect(isValidSyncTime('00:00')).toBe(true)
expect(isValidSyncTime('09:00')).toBe(true)
expect(isValidSyncTime('19:30')).toBe(true)
expect(isValidSyncTime('23:59')).toBe(true)
})
it('rejects out-of-range, partial, and 12h forms', () => {
expect(isValidSyncTime('24:00')).toBe(false)
expect(isValidSyncTime('12:60')).toBe(false)
expect(isValidSyncTime('9:00')).toBe(false) // must be zero-padded
expect(isValidSyncTime('09:0')).toBe(false)
expect(isValidSyncTime('9am')).toBe(false)
expect(isValidSyncTime('')).toBe(false)
})
it('rejects non-strings and injection-shaped values', () => {
expect(isValidSyncTime(undefined)).toBe(false)
expect(isValidSyncTime(900)).toBe(false)
expect(isValidSyncTime('09:00 /TN evil')).toBe(false) // nothing may trail the time
expect(isValidSyncTime('09:00\n')).toBe(false)
})
})
// --- S4: filename template path-traversal -----------------------------------