Long-running instances accumulate history indefinitely. As the history
table grows, SQLite COUNT(*), paginated SELECT, and index scans degrade.
YTP_MAX_HISTORY (default: 0 = unlimited) caps the number of completed
download records kept. Enforcement is a hook in DataStore.put(): on
each insertion, the oldest excess entries are immediately evicted from
the in-memory OrderedDict and their deletes are enqueued to SQLite.
DataStore.load() also trims on startup in case the limit was lowered
between restarts. No background tasks, no DB reads needed for the check.
Every item_updated WebSocket event and every history/queue list response
was serializing the full ItemDTO including fields the UI never reads.
Add ItemDTO.serialize_for_list() which excludes: options,
template_chapter, temp_dir, total_bytes, total_bytes_estimate,
tmpfilename, archive_id. The global Encoder now uses serialize_for_list()
for all list/event contexts; the full serialize() remains available for
detail endpoints (GET /api/history/{id}).
Mark the excluded fields optional in the TypeScript StoreItem type.
Two startup cost reductions:
1. Folder enumeration is moved from /api/system/configuration to a new
GET /api/system/folders endpoint. The frontend fetches it only when
the user focuses a folder input field, and caches the result for the
session. On large directory trees this was a noticeable startup delay.
2. The queue array is removed from /api/system/configuration. The queue
is already loaded separately via /api/history/live on mount; including
it in the config response was a double-load that inflated the initial
payload for users with large queues.
yt-dlp fires progress hooks faster than the browser can usefully render
them. Previously every callback triggered a WebSocket emit → Vue
reactivity update → DOM re-render, causing unnecessary main-thread work
during active downloads.
Now status changes (e.g. downloading → finished) are always emitted
immediately, while progress-only updates are capped at one per 500ms
per download. A flush_pending() call at loop termination ensures the
final state is never dropped.