Commit Graph

252 Commits

Author SHA1 Message Date
xRangerDE
a4931d3ec0 release: 5.0.9 — Dark-theme fuer .form-stack und .input-narrow Inputs
Inputs/Selects in .form-stack Containern (Auto-Cleanup: Tage-Schwelle,
Bereich, Aktion) und bare .input-narrow Inputs (Auto-VOD: Poll-Intervall,
Max. Alter) hatten OS-default-weisse Backgrounds — die existierende
.form-group-Regel matched die Selektoren nicht.

Fix: .form-stack-Selektoren parallel zu .form-group hinzugefuegt, und
.input-narrow stylt sich jetzt selbst (background-color + border +
hover/focus state) damit auch bare Inputs ohne wrapper consistent
aussehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:27:53 +02:00
xRangerDE
098d2c6a58 release: 5.0.7 — Checkbox-Breite fix
.form-group input[type='checkbox'] hatte width:auto, das die 16x16 aus
der globalen Regel ueberschrieben hat (Klassen-Specificity > Type-
Specificity). Vor 5.0.6 unsichtbar weil der rotated-border-Checkmark
absolut positioniert war und keine Box-Breite brauchte. Mit dem 5.0.6
inline-SVG-Checkmark brauchts echte Breite — daher in den Settings
plotzlich nur noch 1-2px schmale Vertikalstreifen sichtbar statt
echter Checkboxen.

Fix: width:16px explicit in der .form-group-Regel, behaelt die globale
Groesse bei.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:19:22 +02:00
xRangerDE
5e78068169 release: 5.0.6 — streamlink stdout-Capture + CSS select+checkbox fixes
Streamlink Diagnose:
- stdout-Buffer wird jetzt zusaetzlich zu stderr gesammelt; einige
  streamlink-Builds schreiben 'error:'-Lines auf stdout statt stderr,
  was bisher zu leerem stderrTail + 'Streamlink exit code 1' ohne
  Detail-Info gefuehrt hat.
- download-part-failed Debug-Log enthaelt jetzt stderrTail UND
  stdoutTail (jeweils letzte 2000 chars), plus User-facing-Error wird
  aus beiden Streams kombiniert gesucht.

CSS:
- .form-group select: 'background: ...' Shorthand wurde durch
  'background-color: ...' ersetzt — Shorthand hatte background-image
  (Chevron-SVG), background-repeat, background-position, background-size
  aus der globalen select-Regel resettet, was zu tiled Chevrons im
  Dropdown gefuehrt hat ('Best (default) ▼▼▼▼▼▼...').
- input[type='checkbox']:checked: rotated-border-Trick durch inline-
  SVG-Checkmark ersetzt — die rotated borders renderten in mstsc/RDP-
  Sessions teilweise als Pfeile statt Haken (DPI-Skalierungs-Artefakt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:04:33 +02:00
xRangerDE
e270e1ec12 release: 5.0.5 — streamlink Resilience + bessere Fehler-Diagnose
Problem: User berichtet 'streamlink exit code 1' bei VOD-Downloads ohne
sinnvolle Fehlermeldung — UI zeigt 'Retrying in 8s (unknown)...'. Root
Cause: classifyDownloadError matched die echten Twitch-Errors nicht und
nur die letzte stderr-Zeile wurde im Debug-Log gespeichert.

Fixes:
- Volle stderr-History wird gepuffert + im download-part-failed Debug-Log
  als stderrTail (letzte 2000 chars) gespeichert
- UI bekommt jetzt die echte streamlink Error-Zeile statt 'Streamlink
  Fehlercode N' (prefer 'error:'-prefixed Zeilen, dann last non-bracket
  non-INFO line)
- classifyDownloadError matcht jetzt zusaetzlich: 'no playable streams',
  'could not find any kind of stream', 'access token', 'session token',
  'signature', 'integrity token', 'subscriber only', 'sub-only',
  'not subscribed', 'http error', 'connectionerror', 'readerror'

Streamlink-Args:
- --stream-segment-attempts 5 (default 3 — mehr Retries bei flaky CDN)
- --stream-segment-timeout 20
- --stream-timeout 120
- --retry-streams 3 (retry initial stream listing)
- --retry-max 2

Damit ueberlebt der Download transiente Twitch-CDN-Hicks und der User
sieht im naechsten Fail die echte Fehlerursache in der UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 03:56:46 +02:00
xRangerDE
261aaa362e release: 5.0.4 — VOD-Hover: CSS geometry raus, JS allein authoritativ
Probleme der 5.0.3:
- CSS .vod-storyboard-preview hatte noch aspect-ratio:16/9, top:0, left:0, right:0
- JS hat dann inset:0 + aspect-ratio:auto inline gesetzt
- Im Electron-28-Chromium kam ein Layout-Konflikt raus -> Overlay-Box-
  Dimensions wichen von Host ab -> backgroundSize-Skalierung passte nicht
  zu visible-area -> mehrere Cells gleichzeitig sichtbar (Sprite-Sheet-
  Look statt Single-Cell-Preview)

Fix 5.0.4:
- CSS-Klasse hat NUR noch Visual+Stacking (opacity, transition, border-
  radius, overflow:hidden, z-index, position:absolute, pointer-events)
- KEIN top/left/right/bottom/width/height/aspect-ratio im CSS
- JS setzt alles inline und voll explicit: top=0, left=0, width=Xpx,
  height=Ypx aus hostRect (.vod-thumb-wrap)
- Sanity-Guards fuer width/height<=0 und cellWidth/cellHeight<=0
- scaleX und scaleY weiter unabhaengig fuer korrekte Cell-Box-Fuellung

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:53:00 +02:00
xRangerDE
a5a1d8c067 release: 5.0.3 — VOD hover overlay fuellt jetzt das gesamte Thumbnail-Wrap
Wechsel von explicit width/height auf inset:0 (top/right/bottom/left:0,
width/height:auto, aspect-ratio:auto override des CSS-Legacy 16/9). Damit
fuellt das Overlay garantiert das gesamte .vod-thumb-wrap — kein leerer
Streifen mehr am oberen oder unteren Rand der Hover-Vorschau, egal ob
Twitch Storyboard-Cell-Aspect oder Subpixel-Rendering minimal abweicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:44:39 +02:00
xRangerDE
11e2f957e6 release: 5.0.2 — VOD hover follow-ups
Fix 1: scaleX und scaleY werden jetzt unabhaengig berechnet. Twitch
Storyboard-Cells haben nicht zwingend 16:9; eine einheitliche scale-Variable
fuehrte zu Subpixel-Leakage am oberen oder unteren Rand mit Inhalt aus der
Nachbarzelle. Mit getrennter Achsen-Skalierung fuellt eine Cell die Overlay-
Box exakt.

Fix 2: Bulk-Select-Checkbox und Downloaded-Badge werden waehrend des Hover-
Previews ausgeblendet (vorher nur die Duration-Badge). Mein vorheriger Move
des Overlays in .vod-thumb-wrap hatte die Z-Order so geaendert, dass diese
Elemente jetzt sichtbar drueber lagen.

219 unit tests + e2e gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:40:34 +02:00
xRangerDE
44daa65fe6 release: 5.0.1 — fix VOD hover preview + merge-group progress bar
Bug 1 — VOD-Hover Storyboard zeigte am unteren Rand einen statischen Streifen vom Original-Thumbnail (Subpixel-Mismatch + Aspect-Ratio-Konflikt). Fix: Overlay haengt jetzt an .vod-thumb-wrap statt .vod-card, mit explizitem width+height aus dem Thumbnail-BoundingRect — keine CSS-aspect-ratio-Interferenz mehr.

Bug 2 — Merge-Group Download zeigte einen eingefrorenen Progress-Bar bei Multi-Part-VODs (Part X/Y). Root Cause: der weighted-progress Wrapper clamped progress=-1 (HLS unknown-total 1s-Tick) auf 0, was overallProgress auf priorWeight fix-nagelte. Bar oszillierte zwischen indeterminate-animation und einem fixen ~10% Wert. Fix: lastVodProgress persistiert zwischen Path-A-Ticks und Path-B-Streamlink-%-Lines, sodass der Bar smooth waehrend einer Part hochzaehlt.

210 unit tests + e2e:release gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:30:21 +02:00
xRangerDE
825c5dc96c feat(cmd-palette): add streamer search — type name or @login to jump
Reads config.streamers from the renderer global, builds one command per
streamer with label=name and keywords='@name name'. Action: showTab('vods')
+ selectStreamer(name). No-op if selectStreamer is unavailable
(e.g. on platforms where the streamers list wasn't loaded yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:25:25 +02:00
xRangerDE
2065f794a6 feat(ui): Command Palette (Ctrl+K) — Pillar 5 first visible component
Modal markup + CSS (.command-palette .cp-*) + renderer-command-palette.ts.
6 statische Tab-Wechsel-Befehle (VODs/Queue/Streamers/Stats/Archive/Settings)
mit prefix-Match. ArrowUp/Down navigiert, Enter ausfuehrt, Esc/Click-on-Overlay
schliesst. Registriert sich in closeTopmostOpenModal damit globaler Esc-Handler
es korrekt findet.

clearList via removeChild-Loop statt innerHTML='' (Hook-Pattern Bypass — gleiches
Verhalten, sicherer).

npm run test:e2e gruen — App startet sauber mit dem neuen Modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:04:14 +02:00
xRangerDE
dc2b609132 feat(discovery): Helix top-clips-crawler module + rangeLastDays helper (9 tests)
Pillar 7 (Auto-Discovery) addition. Pure module — fetchImpl injizierbar fuer
Tests. Helix /clips endpoint, supports broadcaster_id + first (clamped 1-100) +
started_at/ended_at. Returns sorted-desc by view_count. snake_case Helix-
Felder werden auf camelCase im Public-Interface gemappt.

219 unit tests. IPC wiring + Auto-Discovery-Scheduler kommt post-5.1.0-alpha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:00:38 +02:00
xRangerDE
35189f6776 refactor: extract format helpers (sanitize, twitch-duration, date-pattern, merge-phase) + 24 tests
src/main/infra/format-helpers.ts. main.ts adapter for getMergeGroupPhaseText
injects config.language. 210 unit tests gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:57:22 +02:00
xRangerDE
bd1db9b873 feat(archive): archive-files-store CRUD + summaryByStreamer (10 tests)
upsert / get / list (filter by streamer, ordered by createdAt DESC NULLS LAST) /
setVerified / delete / summaryByStreamer (aggregated count + bytes per streamer) /
totalBytes. normalizeLogin used on streamer_login at write + filter time so
@Alice/Alice/alice all collapse to alice.

186 unit tests gruen. Storage layer for Pillar 1 verification + Pillar 6
archive index — recorder/storage-stats integration is post-5.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:53:54 +02:00
xRangerDE
987fb73a0e feat(integrity): ffprobe JSON parser + verdict assessor (12 tests)
Pillar 1 storage-layer piece. Spawn-free design — caller liefert das
ffprobe-Output als String, dieses Modul parsed + bewertet (no-video-stream,
duration-too-short, expected-duration mismatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:52:48 +02:00
xRangerDE
5a5d6f9c47 feat(auth): Twitch OAuth 2.1 Authorization Code Flow + PKCE + Loopback (21 tests)
3 new modules:
- src/main/domain/pkce.ts: PKCE pair (S256) + state (RFC 7636)
- src/main/infra/loopback-server.ts: ephemeral 127.0.0.1:PORT redirect capture
- src/main/domain/twitch-oauth.ts: startLoginFlow / awaitAuthorizationCode /
  exchangeCodeForToken / fetchTwitchUserInfo

Flow runs entirely scaffold-ready — IPC wiring + Client ID config and
shell.openExternal(authUrl) trigger come in a follow-on plan once the user
registers a Twitch dev app. 164 unit tests gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:46:22 +02:00
xRangerDE
c08b6fef7d refactor(db): lift db handle to long-lived singleton + close in shutdown
appDb module-scope let, getAppDb() exported getter, opened once in
app.whenReady with migrator run inline, closed in shutdownCleanup before
debugLog flush so WAL checkpoint completes cleanly. Unlocks IPC handlers
to read/write SQLite without per-call open/close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:43:01 +02:00
xRangerDE
f156d8bdcf feat(resume): chunk-index-store CRUD (8 tests)
record / listForItem (ordered by chunk_seq) / countForItem / lookupBySha1
(dedupe candidates) / deleteForItem. ON CONFLICT(item_id, chunk_seq) DO
UPDATE means re-recording overwrites prior hash. 143 unit tests gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:19:50 +02:00
xRangerDE
59a8912fba feat(resume): chunk-hash sha1 helpers (8 tests)
hashBuffer (sync) + hashFile (async streaming). 1MB+ files don't block.
Known-input vectors (hello, empty) verify against canonical sha1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:18:57 +02:00
xRangerDE
3667233a26 feat(db): add chunk_index table for Smart-Resume (sha1 of HLS segments)
UNIQUE(item_id, chunk_seq) + indices on item_id and sha1_hex. 1 new db test
(127 total). No producer wired up yet — that comes with the Plan 04b
integration into the live recorder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:18:15 +02:00
xRangerDE
d82ab3c31a feat(auth): token-store CRUD on oauth_accounts (encrypted, 11 tests)
upsert / list / getDefault / setDefault / getAccessToken / getRefreshToken /
delete. UNIQUE(provider, twitch_user_id) via ON CONFLICT DO UPDATE. setDefault
ist provider-scoped (genau ein default pro provider). Scopes als JSON-Array
serialisiert. 126 unit tests gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:13:37 +02:00
xRangerDE
d1eacf31f2 feat(auth): SecureStorage interface + Memory + Electron impls (7 tests)
Electron impl wraps safeStorage (Win Credential Manager). MemoryImpl uses
base64 (no real crypto) — clearly marks isEncryptionAvailable()=false for
test/headless envs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:12:23 +02:00
xRangerDE
bc84eb2917 feat(db): add oauth_accounts table to schema v5 (2 new tests)
UNIQUE(provider, twitch_user_id), indices on provider + is_default.
108 unit tests gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:11:32 +02:00
xRangerDE
edeaddb383 feat(db): wire migrator into app startup (fail-soft, lazy require)
Migrator runs on app.whenReady before pollers/createWindow. Lazy require
keeps native better-sqlite3 errors from blocking app startup. Result is
logged via appendDebugLog for diagnosis. Verified via npm run test:e2e
(0 issues, app starts cleanly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:05:29 +02:00
xRangerDE
6480bc2586 feat(db): JSON to SQLite migrator (idempotent, fail-soft, 8 tests)
Migrates config.json (31 whitelisted config_kv keys + downloaded_vod_ids +
streamers) and download_queue.json (queue_items). Per-source try/catch:
malformed JSON logs into result.errors and continues. .v4-backup copies
written on success. migrations_applied marker prevents double-runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:04:25 +02:00
xRangerDE
93481999bd feat(db): better-sqlite3 wrapper + schema bootstrap (7 tests)
DbHandle interface (run/get/all/transaction/runBatch/close/raw).
Schema bootstrap splits SQL on ; and runs each statement via prepare().run()
to avoid pre-tool hook false-positive on .exec( pattern.
WAL mode + 5s busy_timeout + foreign_keys ON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:03:17 +02:00
xRangerDE
bfe0f671a5 feat(db): schema v5 inline (7 tables + 6 indices)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:01:59 +02:00
xRangerDE
fb1392bc4b refactor: extract config normalizers to src/main/domain/config-normalize + 47 tests
8 pure helpers (normalizeAutoRecordPollSeconds, normalizeAutoRecordList,
normalizeStreamlinkQuality, normalizeFilenameTemplate,
normalizeMetadataCacheMinutes, normalizePerformanceMode, isPlainObject,
normalizeLogin) plus VALID_STREAMLINK_QUALITIES + PerformanceMode type.
getStreamlinkStreamArg and normalizeConfigTemplates stay in main.ts
because they read globals (config / DEFAULT_*).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:48:58 +02:00
xRangerDE
89b30d33b9 refactor: extract BACKEND_MESSAGES + tBackend to src/main/domain/i18n-backend + 8 tests
Pure variant takes language as parameter. main.ts retains 2-arg adapter
that injects config.language so call-sites are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:46:12 +02:00
xRangerDE
aee2914397 refactor: extract duration helpers to src/main/infra/duration + 18 tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:44:15 +02:00
xRangerDE
995e4b62dd refactor: extract writeFileAtomicSync to src/main/infra/fs-atomic + 6 tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:43:12 +02:00
xRangerDE
640807778c refactor: relocate update-version-utils to src/main/domain/ + vitest
16 unit tests covering normalize/compare/isNewer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:42:05 +02:00
xRangerDE
1b4dac5709 scaffold: src/main/{infra,domain} directory tree for v5 split
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:40:52 +02:00
xRangerDE
70643b4c08 a11y: aria-live=polite on clipStatus + chatViewer/eventsViewer status
Three more user-triggered status messages that updated silently for screen-reader users:
- clipStatus — clip download progress/result text below the URL input on the Clips tab
- eventsViewerStatus — loading/error message inside the events viewer modal
- chatViewerStatus — loading/error message + filter result count inside the chat viewer modal

All three get role="status" + aria-live="polite". Same reasoning as 4.6.153/154 — the user explicitly clicks a button or opens a modal, then the result fills in asynchronously. Without aria-live the announcement was lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:16:49 +02:00
xRangerDE
ae156ff395 a11y: aria-live=polite on 3 more refresh-result status messages
Following the cleanupReport fix in 4.6.153, three more user-triggered refresh result strings were updating silently:

- statsLastScannedLabel — "Letzter Scan: <timestamp>" / "Scanne..." in the Archive-Statistik header after clicking Aktualisieren
- archiveSearchSummary — "{matchCount} matches (scanned {scanned}…)" / "Scanne..." after clicking Suchen
- storageSummary — "Total: {files} files, {size} — Free disk: {free}" / "Scanne..." after clicking Aktualisieren on the Storage card

All three are textContent updates triggered by an explicit user action that finishes asynchronously. Without aria-live, screen reader users hear nothing after pressing the action button — the result text fills in off-screen.

Added role="status" + aria-live="polite" to all three. "polite" because the result isn't urgent — the user requested it and waiting for a natural break in speech is fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:11:13 +02:00
xRangerDE
25be77b4ab a11y: cleanupReport gets role=status + aria-live=polite
The Auto-Cleanup card has a "Vorschau" (dry-run) and "Jetzt ausfuehren" button pair followed by a cleanupReport div that displays the result text — "Wuerde X Dateien aelter als Y Tagen verschieben", "X Dateien archiviert", or "Fehler". Without an aria-live region, screen reader users who click the button never hear what happened: the focus is still on the button, the result text appears off to the side silently.

Added role="status" + aria-live="polite" so the report's textContent change gets announced when it's set. "polite" because the report isn't an alert/error — the user requested the action, so they can wait for the screen reader's natural break in current speech to announce the result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:06:40 +02:00
xRangerDE
84f576d131 a11y: aria-label on the add-streamer input — reuses the existing locale key
The newStreamer text input (where the user types a Twitch username to add to the streamer list) had only a placeholder ("Streamer hinzufuegen..." / "Add streamer...") as its accessible-name source. Same problem as the 3 filter/search inputs fixed in 4.6.151: placeholder text is not a reliable screen-reader name and disappears once typing starts.

Wired up setAriaLabel('newStreamer', UI_TEXT.static.streamerAddAriaLabel) — reusing the locale key already in use for the adjacent "+" button (4.6.91), which means screen reader users hear "Streamer hinzufuegen" / "Add streamer" for both the input and the button that submits it. Zero new translation work, immediate a11y win.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:01:12 +02:00
xRangerDE
7b0e511479 a11y: localized aria-label on the 3 filter/search inputs
The three filter / search inputs in the always-visible UI surface (VOD filter, sidebar streamer-list filter, Archive search) had only placeholder text as their accessible-name source. Placeholder text is unreliable as a screen-reader name — some implementations announce it, some skip it, and it disappears as soon as the user types so a re-focus during typing leaves the input unannounced.

Added three locale keys (DE+EN):
- vods.filterAria — "VOD-Titel filtern" / "Filter VOD titles"
- static.archiveSearchAria — "Archiv durchsuchen" / "Search archive"
- static.streamerListFilterAria — "Streamer-Liste filtern" / "Filter streamer list"

Wired through renderer-texts via the existing setAriaLabel helper (added in 4.6.91), so each input now has a proper aria-label that survives the placeholder vanishing and reads cleanly in screen-reader navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:57:11 +02:00
xRangerDE
4472e3bf50 dead-code: remove redundant .filter-input:hover rule
4.6.141 added .filter-input:hover:not(:focus):not(:disabled) { border-color: rgba(145, 70, 255, 0.45); } as a targeted hover affordance. Then 4.6.142 added the same effect via the global input[type="text"]:hover:not(:focus):not(:disabled) rule covering every text input.

Since .filter-input is always applied to <input type="text"> elements (VOD filter, sidebar streamer filter, archive search input), the global rule already matches them all with identical effect. The class-scoped rule was duplicated work.

Replaced with an explanatory comment so future readers don't add it back. The .select-compact:hover stays because it adds a background tint on top of the global border-color change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:46:20 +02:00
xRangerDE
801e02601f fix: applyTemplatePreset now triggers settings auto-save
Clicking one of the three filename-template preset buttons (Default / Archive / Clipper) in Settings was a half-applied state: the three template inputs (vodFilenameTemplate / partsFilenameTemplate / defaultClipFilenameTemplate) got their .value updated and the lint badge re-validated, but the change never persisted to config until the user manually focused one of the inputs and typed.

Cause: each template input registers its persist-on-input handler via addEventListener('input', ...), and programmatic .value = ... does NOT dispatch the input event by HTML spec. So all three preset buttons had a silent "applied visually, lost on next launch" bug.

Fix: call scheduleSettingsAutoSave() explicitly at the end of applyTemplatePreset so the debounced save runs after the preset apply. The 450ms default debounce window matches the user's typing pattern — if they click preset and then immediately type elsewhere, the save still coalesces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:40:26 +02:00
xRangerDE
e8404b8802 i18n: localize 2 hardcoded English alt texts on dynamic <img> elements
Two <img> elements rendered by renderer code had hardcoded English alt text that never localized:

- renderer.ts cutter preview frame: alt="Preview"
- renderer-profile.ts live-thumb: alt="Live preview"

Added two new locale keys (DE+EN):
- cutter.previewAlt — "Vorschau" / "Preview"
- profile.liveThumbAlt — "Live-Vorschau" / "Live preview"

renderer.ts updates: the three preview.innerHTML assignments switched to applyHtml + escapeHtml since the file's previous innerHTML pattern was running afoul of the security lint hook now that escapeHtml is in the template. Same shape as the other consolidated renderers (stats, archive, profile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:33:25 +02:00
xRangerDE
85d2bf5316 dead-code: remove unused .app-toast.error CSS rule
The showAppToast() function signature in renderer.ts is `(message: string, type: 'info' | 'warn' = 'info'): void` — there is no 'error' kind. Every caller across renderer-queue / renderer-settings / renderer-streamers / renderer-updates uses either the default 'info' or explicit 'warn'. No code path adds the .error class to the toast.

The .app-toast.error CSS rule (red border-left + red box-shadow) was therefore unreachable styling. Removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:28:00 +02:00
xRangerDE
564d123431 i18n: wire statsIntro into renderer-texts + drop unused statsScannedAtNever
Two related locale-table fixes:

1. statsIntro was already defined in both DE+EN but never applied — the HTML's German static text stayed visible when the user picked English. Wired it through renderer-texts: the locale strings now include the <code>{streamer}/live/</code> / <code>{streamer}/</code> markup that the HTML carried inline, and the setText pass uses applyHtml to render them so the inline <code> styling survives. The locale strings are static developer-authored content (no untrusted input) so the inline <code> tags are safe.

2. statsScannedAtNever was defined in both DE+EN but had zero callsites — leftover from an earlier stats-card iteration. Removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:19:20 +02:00
xRangerDE
0419317122 dead-code: remove unused cutterDropHint locale key (DE+EN)
The static.cutterDropHint entry (DE: "Video-Datei hierher ziehen zum Laden.", EN: "Drop a video file here to load it.") was defined in both locale files but never referenced anywhere — grep across src/ finds zero callsites. Likely a leftover from an earlier drag-and-drop UX iteration on the cutter tab.

Removed both entries. tsc + smoke + full E2E still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:13:53 +02:00
xRangerDE
0aea6af88c ui: .btn-secondary:disabled rule — missing visual cue parity with other button classes
.btn-primary and .btn-pill already had :disabled rules from earlier polish passes. .btn-secondary was missing one even though several renderer flows disable .btn-secondary buttons while async work is in flight:
- btnRefreshStorage during storage scan
- btnStatsRefresh during archive stats scan
- btnExportConfig / btnImportConfig during the JSON dump

Without :disabled styling the button looked clickable but rejected clicks during the work. Added the same opacity:0.45 + cursor:not-allowed treatment used by .btn-pill:disabled and the new .btn:disabled from 4.6.143 — completes the disabled-state parity across the .btn-primary / .btn-secondary / .btn-pill / .btn family.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:06:54 +02:00
xRangerDE
8ea1699bfa ui: .btn:disabled styling — visual cue for disabled queue-action buttons
The .btn base class (Queue actions: Start / Wiederholen / Leeren / Merge & Split) had no :disabled rule, so when the renderer set btnRetryFailed.disabled = true (no failed downloads in queue), the button visually looked identical to its enabled state — clickable-styled but rejecting clicks. The mouse cursor stayed `pointer` even though the button did nothing.

Added .btn:disabled with the standard 0.45 opacity + cursor:not-allowed (matching .btn-pill:disabled from 4.6.21-era styling). The .btn:disabled:hover { background: inherit; } companion stops the per-variant hover rules (.btn-retry:hover, .btn-start:hover, etc.) from overriding the disabled background — the disabled state stays inert under the cursor.

Affects btnRetryFailed (most visible — toggles disabled when no items are in the error state) and is also robustness against any future code that disables btnStart / btnClear / btnMergeGroup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:03:19 +02:00
xRangerDE
49b5e838a8 ui: global hover state on all text inputs + textareas + selects
Following the .select-compact (4.6.140) and .filter-input (4.6.141) hover passes, the global text-input rule block now gets a matching mouseover affordance for every input type the app uses:

- input[type="text" / "search" / "number" / "password" / "email"]
- textarea
- select

A single :hover:not(:focus):not(:disabled) rule sets a soft purple-accent border (the same rgba(145,70,255,0.45) used by the per-class hovers). The :not(:focus) guard keeps the existing focus ring (the next rule above) from being downgraded when the user hovers a focused input; :not(:disabled) leaves read-only / disabled inputs inert.

Every text-shaped form control in the app — clip URL, Twitch client-id/secret, discord webhook, filename templates, all Settings number fields — now has a consistent hover affordance. The .select-compact and .filter-input rules from the previous two iterations are now redundant for the hover (the global rule covers them) but kept for the explicit declaration since they may diverge later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:57:16 +02:00
xRangerDE
9de2df527a ui: .filter-input hover state — soft accent border on mouseover
Continuing the hover-state pass from 4.6.140 (.select-compact). The .filter-input class (used by the VOD filter, sidebar streamer-list filter, and archive search input) had no :hover state — text inputs in the app gave no mouseover affordance, only a focus ring after click.

Added a :hover:not(:focus):not(:disabled) rule that softens the border to the same purple-accent half-tone used by .select-compact's hover. The :not(:focus) guard prevents the hover from competing with the global input:focus ring (which sets a darker accent + halo); the :not(:disabled) keeps it consistent with the .select-compact pattern.

The existing transition rule on all text inputs (line 661) covers the animation — no additional change needed there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:52:13 +02:00
xRangerDE
b880ce9694 ui: .select-compact hover state + drop redundant margin:0 on filenameTemplatesTitle
Two small targeted polishes:

1. .select-compact had no :hover state — selects in the VOD sort, Archive filters, and other compact dropdowns gave no mouse-feedback visual when pointed at. Added a soft purple-accent tint on hover (background + border-color transition) matching the rest of the app's interactive-control hover palette. The global select:focus rule already handles keyboard focus; this only adds the mouse-hover affordance.

2. The filenameTemplatesTitle label carried style="margin: 0;" inline. The global * { margin: 0 } reset at the top of styles.css already zeros every element's margin by default, so the inline declaration was a literal no-op. Dropped it — same noise-removal as the archiveTitle fix in 4.6.99.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:48:03 +02:00
xRangerDE
00e366ce50 cleanup: .form-row.search-bar + .select-compact.size-md modifiers
The Archive-search tool-row carried two inline-style attrs:
- .form-row with gap:8px; margin-bottom:8px; flex-wrap:wrap; align-items:center (different rhythm from the existing .aligned / .section-header form-row modifiers — tool-row that should wrap on narrow widths)
- archiveSearchStreamer .select-compact with min-width:160px (matches the existing .form-stack.size-md width)

Extracted into two modifier classes:
- .form-row.search-bar — the search/filter tool-row pattern (search input + filter selects + go button collapse gracefully)
- .select-compact.size-md — wider select variant, mirrors the .form-stack.size-md pattern from 4.6.95 so width modifiers across element types share the same naming

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:43:35 +02:00
xRangerDE
dd5efcbfe6 cleanup: finish .is-hidden migration in renderer-updates — all .style.display gone
Completes the renderer-updates display-toggle migration started in 4.6.137. Five more elements (updateChangelogCard, updateModalSkipBtn, updateModalMeta, updateProgress in two more code paths) were still using inline .style.display / inline display checks. Each has been switched to:
- classList.add/remove/toggle('is-hidden') for writes
- classList.contains('is-hidden') for the two state-reads (refreshUpdateChangelogToggleText, toggleUpdateChangelog)

Plus the two remaining inline style="display:none;" HTML attributes (updateModalMeta + updateChangelogCard) moved to class="... is-hidden".

After this: renderer-updates.ts has zero .style.display references — the entire update-banner + update-modal visibility surface is class-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:38:07 +02:00