Fuseaux horaires :
- POST /api/recordings rejette en 400 les dates sans fuseau et normalise
tout en UTC à l'écriture (l'interprétation des dates naïves dans le TZ
du conteneur décalait les enregistrements de 1-2 h)
- Helper frontend unique postRecording() : les 2 points de création
(modal, guide EPG) envoient la même convention UTC
Contrôle d'accès :
- stop/delete/logs d'un enregistrement et delete d'un season pass
vérifient la propriété (userId ou admin), comme playlists_handler
- Suppression des replis 'dev_user_id' et 'admin' (401 sans session)
SQLite :
- PRAGMA foreign_keys/WAL/busy_timeout (les ON DELETE CASCADE déclarés
ne s'appliquaient pas : sessions et playlists orphelines)
- Migrations de schéma versionnées (schema_version) + index user_id,
start_time, season_passes(user_id)
Gestion disque :
- Refus explicite de capture sous MIN_FREE_DISK_MB (défaut 500 Mo)
- Rotation par quota d'octets (RECORDINGS_QUOTA_GB, opt-in) qui ne touche
jamais un enregistrement actif et supprime fichiers + ligne ensemble ;
l'ancienne rotation « 50 fichiers » pouvait effacer une capture en cours
- DELETE /api/recordings/<id> supprime aussi .mkv/.log/parties (SafePath)
Scheduler :
- Arrêt gracieux orchestré par server.dart : clôture des enregistrements
(fusion des parties, statut) avant killAll des sessions de streaming ;
l'ancien handler SIGTERM de FfmpegSessionManager faisait exit(0) direct
- Noms de fichiers uniques par fragment d'id (deux enregistrements du
même programme s'écrasaient mutuellement avec -y)
- Requête filtrée scheduled/recording au lieu de toute la table / 10 s
- Statut cancelled pour un scheduled arrêté (completed sans fichier
cassait la lecture)
Season passes :
- Playlist du propriétaire du pass résolue à chaque scan (l'injection
figée du 1er utilisateur rendait les passes muets après ajout de
playlist, et mélangeait les credentials en multi-utilisateurs)
- Correspondance exacte par défaut (match_mode, migration en 'contains'
pour l'existant), plafond de créations par scan, réalignement des
horaires déplacés dans l'EPG, déduplication tolérante ±2 min
- Redaction des erreurs de scan (ClientException contient l'URL amont)
API de suivi (polling conservé) :
- GET /api/recordings enrichi : progress_pct, file_size_bytes,
retry_count, is_active ; barre de progression + taille dans la liste
Validé : dart analyze (0 issue) et dart test (48/48) sur bin/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
- Nouveau bus de notification (recordingsRefreshBus) : la liste se recharge
immédiatement dès qu'un enregistrement est créé, planifié ou arrêté depuis
n'importe où dans l'app (guide EPG, modal, widget rapide)
- Polling en arrière-plan : 5 s quand un enregistrement est en cours ou
planifié, 20 s sinon, pour suivre les changements de statut sans clic
- Rafraîchissement silencieux (pas de spinner ni de vidage de liste) pour
éviter le clignotement ; les erreurs transitoires ne remplacent pas la liste
- Indicateur « Suivi auto » avec l'heure de dernière actualisation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Les journaux du serveur montrent la grille Live TV se faire couper en 429 :
"Rate limit exceeded", une trentaine de requêtes refusées d'affilée après
quelques écrans de défilement.
Deux causes cumulées. D'une part le passage du guide de Dio vers AuthedHttp a
fait perdre le cache de réponses qui existait auparavant : chaque
reconstruction de tuile repartait en requête. Un cache mémoire de dix minutes
le rétablit — le backend garde déjà ces réponses trente minutes, il n'y a
aucune raison d'aller les rechercher.
D'autre part le plafond de deux cents requêtes par minute et par adresse était
trop bas pour l'usage réel : une grille demande un logo et un guide par tuile,
et tous les clients partagent l'adresse du reverse proxy. Il passe à six
cents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le guide du fournisseur est figé sur beaucoup de revendeurs : sur le compte de
test, get_simple_data_table répond correctement mais le dernier programme
français date de deux jours, et le xmltv.php du panneau ne couvre aucune chaîne
française. Aucun correctif côté appel Xtream ne pouvait donc remplir la grille.
Nouveau service XmltvEpgService : téléchargement de dumps XMLTV publics,
décompression gzip détectée sur le nombre magique, parsing en flux via
XmlEventReader — un dump national fait 46 Mo décompressés, en charger l'arbre
DOM coûterait plusieurs centaines de mégaoctets dans le conteneur. Mesuré à
environ 1 s pour 973 chaînes. Index rafraîchi toutes les 6 h, programmes
terminés depuis plus de 6 h écartés à l'indexation.
L'appel sortant reste un dernier recours. L'ordre est inchangé —
get_simple_data_table, puis get_short_epg — et le dump n'est consulté que si
le panneau ne rend aucun programme couvrant l'instant présent : compter les
entrées ne suffisait pas, un guide périmé en renvoie des centaines. La source
se configure par EPG_XMLTV_URLS ; vider la variable supprime tout appel
sortant. La réponse porte un en-tête X-Epg-Source pour savoir qui a répondu.
Les identifiants sont rapprochés après normalisation : le dump écrit
France.2.fr là où le panneau annonce France2.fr. Repli sur le nom affiché de
la chaîne, et table stream_id vers identifiant EPG mise en cache 6 h.
Le client passe désormais par /api/epg au lieu d'appeler player_api.php en
direct. Il dupliquait le choix de l'action, la conversion des fuseaux et le
décodage base64 ; le backend le fait une fois, met en cache pour tous les
utilisateurs, et sait basculer sur le repli — bascule impossible depuis le
navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La détection lisait `webkitFullscreenElement` et `mozFullScreenElement` via un
cast `dynamic`. dart2js ne retombe pas sur la propriété JavaScript quand le
membre n'est pas déclaré sur la classe `Document` : il lève un
`NoSuchMethodError`, visible en console sous la forme
`method not found: 'gaR5'`. Le test d'état échouait donc à chaque appel, avant
même de pouvoir demander le plein écran.
Seule l'API non préfixée est désormais utilisée ; elle est supportée par tous
les navigateurs visés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plein écran — le bouton n'existait que dans les contrôles VOD, sous une icône
aspect_ratio peu parlante, et les contrôles Live TV n'en avaient aucun. Ajout
du bouton côté Live TV et passage aux icônes fullscreen / fullscreen_exit.
L'état suit désormais l'évènement fullscreenchange du document : sortir par la
touche Échap laissait l'icône désynchronisée.
Proxy live — la route /api/live/<id>.ts relayait la source sans aucune reprise.
Les panneaux Xtream ferment régulièrement la connexion en cours de route, et le
MediaSource du navigateur recevait alors un sourceEnded : la lecture s'arrêtait
net au bout de quelques dizaines de secondes. Le proxy rouvre maintenant la
source tant que le client écoute, avec quatre tentatives et une seconde
d'attente entre chaque, le temps que le panneau libère le slot de connexion.
Le compteur repart de zéro dès qu'une connexion a tenu plus d'une minute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
41 couleurs Material brutes traînaient encore dans l'interface, hors palette :
rouge, vert, orange et surtout un violet omniprésent sur les Season Pass, qui
n'appartient à aucune famille du thème.
Correspondances retenues :
- Colors.red / redAccent -> AppColors.live (#E5484D) pour les badges LIVE et
les indicateurs d'enregistrement, AppColors.error pour les messages d'erreur
- Colors.green / greenAccent -> AppColors.success
- Colors.orangeAccent -> AppColors.warning
- Colors.amber -> AppColors.ratingGold, pour aligner les étoiles de notation
du mobile sur celles du bureau
- Colors.purpleAccent -> famille secondary (terre cuite)
- fonds de boutons passés sur les variantes *Container avec leur couleur de
premier plan, le remplissage clair ne portant pas de texte lisible
Le titre du programme EPG dans la grille Live TV utilisait ratingGold, un jeton
réservé aux notes : il passe sur primary, l'accent ember du thème.
Les fonds de SnackBar vert/rouge deviennent surfaceContainerHigh, le succès et
l'échec restant portés par le pictogramme du message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EPG — les trois chemins (backend /api/epg, XtreamService.getShortEpg et
getShortEPG) n'utilisaient que get_epg et get_short_epg. Beaucoup de
panneaux Xtream ne connaissent pas get_epg : ils répondent 200 avec le
bloc d'authentification, sans epg_listings, ce qui produisait un guide
vide indiscernable d'une chaîne sans programme. get_short_epg y renvoie
epg_listings vide en permanence.
- backend : get_simple_data_table en premier, get_short_epg en repli
- client : bascule automatique et mémorisée vers le tableau complet dès
qu'un panneau est muet sur l'action légère, pour ne pas doubler les
requêtes sur chaque tuile de grille
- dates dérivées des *_timestamp epoch et émises en ISO-8601 UTC : les
champs texte sont dans le fuseau du panneau, sans indicateur de zone,
et étaient relus comme de l'heure locale — le guide était décalé et
les enregistrements planifiés depuis le guide l'étaient aussi
- cache backend : la clé incluait seulement le channelId, deux playlists
partageant un stream_id se servaient mutuellement leur guide
UI — les deux halos d'ambiance du dashboard étaient restés de l'ancien
thème : primary (#FFB68C) à 40 % posé sur le coin haut-gauche, donc pile
derrière la sidebar, et info (#A3B8C4, bleu-gris) à 35 % dans une palette
entièrement chaude. Ramenés à des braises ember à 8 % / 6 %.
- fond via AppColors.backgroundGradient
- onglet actif rempli en primaryContainer (surface) et non primary
(teinte claire réservée au texte/icônes)
- sidebar en niveau flottant : en niveau 1 son fond #181310 se confondait
avec le haut du dégradé #1A1310
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getShortEpg ended in `catch (e) { return []; }`, so a 401, a timeout, a
rate-limited panel and a genuinely empty guide all rendered the same
"No Info" label. That made the current problem undiagnosable from the
UI. Failures are now logged with the HTTP status, the Dio error type and
the response shape.
Two concrete causes are handled while we are here:
- Some Xtream panels answer player_api.php with a textual Content-Type.
Dio then hands back a raw String, and `response.data['epg_listings']`
threw, landing in the silent catch. The body is now parsed explicitly
when it arrives as a String.
- The channel grid mounts one EPG provider per visible tile, firing
~32 concurrent requests at player_api.php. Many panels rate-limit well
below that and fail the whole batch. A 3-slot semaphore serialises
them in small groups.
This does not by itself prove why the guide is empty in production; it
makes the reason visible in the browser console.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gradient Container in the playlist selection and admin screens sized
itself to its content rather than the viewport: under the loose body
constraints, SingleChildScrollView and Wrap both shrink-wrap, so the
gradient was painted only across a 436 px band (48 padding + 340 card +
48 padding) with the rest of the screen falling through to bare black.
BoxConstraints.expand() forces full coverage.
Also:
- both screens now use AppColors.backgroundGradient. The previous ramp
ran surfaceContainerLowest -> background -> baseLevel0, i.e. dark to
lighter to dark, which put a pale band across the middle
- the playlist card was a 3% cream translucent plate, so it took on
whatever sat behind it instead of resting on it; it is now an opaque
surface (surfaceContainerLow, surfaceContainerHigh on hover) with the
lift() shadow at rest and the ember focus halo on hover
- Scaffold backgroundColor pinned to baseLevel0 so route transitions
never flash pure black
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The security hardening in 60d3f42 moved /api/epg, /api/recordings and
/api/season-passes behind authMiddleware, but the Flutter client still
called them through bare package:http. Those requests carry no
Authorization header, and on web BrowserClient sets withCredentials to
false so the session cookie is not sent either — every call came back
401. Symptoms: empty TV guide, empty recordings list, empty season
passes.
Adds AuthedHttp, a thin wrapper that injects the same token ApiClient
and XtreamService already use (localStorage['auth_token']), and routes
the 13 affected calls through it.
subtitle_service is left on plain http: it fetches third-party subtitle
URLs, not our API, and must not leak the session token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the Projector Noir palette with "Warm Cinema" and rebuilds the
web players around a shared, event-driven engine.
Design
- app_colors: warm charcoal (#0B0908), cream (#F3E9DF), ember (#D9541F);
adds posterScrim/heroScrim gradients, lift()/emberFocus() shadows,
const cream-alpha tokens replacing Colors.whiteNN, and primaryFill,
a deeper ember reserved for text-bearing fills so white labels clear
the 4.5:1 AA threshold that #D9541F alone does not
- app_theme / mobile_theme: Fraunces (display serif) + Karla (body),
tighter radii, 20+ component themes
- glass_container: opaque layered surfaces replace the blurred glass
- themed_loading_screen restyled; players and boot splash follow
Performance
- drop every BackdropFilter: backdrop blur forced a full framebuffer
read per frame per card, the main source of scroll jank
- bundle Fraunces/Karla as assets so google_fonts stops fetching from
fonts.gstatic.com at startup
- bound image decode size on 12 sites: a 40-poster grid drops from
~240 MB to a few tens of MB of GPU memory
Players (new shared engine: web/xf-player-core.js)
- start on media events instead of polling the buffer every 100-200 ms
- hls.js/mpegts.js loaded on demand, so ~213 KB of mpegts is never
fetched for HLS streams
- liveSyncDurationCount 10 -> 2, startFragPrefetch on, testBandwidth
off, MPEG-TS initial stash 512 KB -> 64 KB
- adaptive escalation fast -> balanced -> safe after repeated stalls in
a 60 s window, applied live for HLS so there is no visible cut
Preserved from the recent streaming work on main: server-side quality
selection, stable player viewId, the turbo direct MPEG-TS path, vendored
library cache headers and the CI/Docker changes. Only the theme layer and
the player front-end were replaced; GlassCard stays in ui_components.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Full visual refonte replacing the generic neon-blue glass look:
- palette: warm film-black surface ladder, tungsten amber primary,
warm taupe secondary, verdigris teal tertiary; semantic and
category colors retuned to match (coral live, gold movies)
- typography: Syne (display/headlines) + Instrument Sans (body/UI)
replace Space Grotesk/Inter/Outfit across all screens
- glassmorphism tokens warmed (amber inner glow, warm-white borders)
- web player theme.css + index.html loader mirror the new tokens
- hardcoded Colors.blue* in streaming settings and recordings tabs
now use AppColors tokens
- DESIGN_SYSTEM.md updated to v3.0 Projector Noir
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontend CI job ran `flutter analyze` from the repo root, which also
analyzed the bin/ server package without its dependencies resolved
(shelf_router, sqlite3, bcrypt, test), producing hundreds of
uri_does_not_exist errors. bin/ is a standalone package covered by the
backend job, so it is now excluded from root analysis.
Also:
- Remove all unused fields/variables flagged as analyzer warnings
(api_client, cache_service, player_screen, subtitle_service,
live_tv_tab, mobile screens)
- Run `flutter analyze --no-fatal-infos` in CI: pre-existing deprecation
infos (withOpacity, dart:html) stay non-fatal while errors and warnings
still fail the build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security:
- Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login)
- Add authenticated /api/xtream-api gateway: Xtream credentials are injected
server-side and never sent to the frontend; /api/playlists no longer
returns passwords
- Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs)
- Add auth to recordings, EPG, season-passes and streaming routes
(HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg)
- Lock player postMessage to same-origin in both directions
- Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest)
- Fix rate limiter (client IP was never resolved), add login rate limit,
restrict CORS, add CSP Report-Only, block private-IP SSRF targets,
fix path traversal in recording log retrieval, chmod 777 -> 770
- Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256)
- Fix authMiddleware not populating 'user' context (getPlaylist ignored the
logged-in user; admin purge always returned 403)
Streaming:
- New FfmpegSessionManager: process registry, idle reaper (4 min live /
15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown,
fast-fail with stderr instead of 30 s timeout
- Quality selection (source/high/medium/low) for live and VOD; source mode
streams with -c:v copy (zero transcoding); selector wired into the player
- Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts
retry on the next tick instead of silently failing
- Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3)
- Fix recording log lookup (.mp4 vs .mkv mismatch)
Design:
- Replace hardcoded colors with AppColors tokens (12 files)
- web/theme.css syncs HTML players with the Flutter palette
- DPAD/keyboard navigation (arrow-key focus, player shortcuts)
- Tooltips on player icon buttons, Semantics on content cards
- Remove 7 dead widgets broken since the Stitch merge
Quality:
- bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording
conflicts) plus a quality-selector widget test
- GitHub Actions CI (analyze + test + build web)
- Archive stale status docs into docs/archive/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace deprecated GlassContainer(opacity:/hasBorder:) calls in
dashboard_screen.dart and player_screen.dart with .glass() constructor.
- Fix epg_grid_screen.dart missing provider import and Playlist type mismatch.
- Delete broken/unreferenced files: network_service.dart, epg_grid_screen.dart.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolved conflicts by keeping remote functional features (RecordingScheduler,
FFmpeg HLS transcoding, mobile player, channel cards) and restoring local
Stitch theme foundation (app_colors, app_theme, glass_container, mobile_theme).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the legacy Apple TV-style purple/cyan theme with the full
Google Stitch Cyber-Cinematic Glass design system across the
entire app (desktop + mobile).
- New Material 3 dark ColorScheme: background #121317, primary
#adc6ff, primaryContainer #4b8eff, surface containers, etc.
- Typography: Space Grotesk (headlines) + Inter (body/labels)
- 3-level glassmorphism via GlassContainer: base, glass, floating
- Primary gradient: #007AFF → #00C6FF with blue glow effects
- Removed old compatibility aliases (focusColor, border, textPrimary,
textSecondary); all code now uses semantic Stitch tokens
- Systematically replaced all Colors.white/black/red/grey and
GoogleFonts.roboto/outfit with Stitch equivalents
- All 36 lib/ files updated, zero compilation errors
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- tv_channel_grid.dart: guard screenWidth <= 0 / NaN / Infinite on first
Flutter Web layout pass (previously caused NaN ~/ 225.03 crash cascade).
Also use .clamp(1.0, infinity) on item width for belt-and-suspenders safety.
Fix invalid 'padding.vertical as double?' cast in TvHorizontalList by using
padding.resolve(TextDirection.ltr).top instead.
- playlist_api_service.dart: remove silent catch-and-return-empty in
getPlaylists(). Exceptions now propagate to the Riverpod FutureProvider
so the UI shows the real error state (with Retry button) instead of
a misleading 'No playlists available' when the API call actually failed
(e.g., 401 Unauthorized or network error).
Fixed playlist loading failure by ensuring the auth token is automatically
restored from localStorage when ApiClient initializes. Previously, the token
was stored but never restored, causing 401 Unauthorized errors on API calls.
Added _restoreTokenFromStorage() method to ApiClient constructor to load
and apply stored token immediately on app startup. This ensures all API
calls (including playlist fetching) have proper authentication.
Fixes: "I still don't have a playlist" issue where playlists were not loading
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fixed NaN error in TvChannelGrid width calculation by properly resolving
EdgeInsetsGeometry to concrete values before arithmetic operations.
EdgeInsetsGeometry.horizontal can return NaN/infinity, causing invalid
width calculations.
Changed from: padding.horizontal
To: padding.resolve(TextDirection.ltr).left + padding.resolve(TextDirection.ltr).right
This ensures we get concrete padding values before division operations.
Fixes: Unsupported operation: Result of truncating division is NaN: NaN ~/ 225.0357142857143
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fixed NaN errors in FFmpeg bitrate argument generation by ensuring integer
conversion happens before integer division. The double multiplication (1.5, 2.0)
was happening before the ~/ operator, causing NaN values. Now properly converts
to int first, then divides.
- maxRateArg: ((bitrateBps * 1.5).toInt() ~/ 1000)
- bufferSizeArg: ((bitrateBps * 2).toInt() ~/ 1000)
Fixes: Unsupported operation: Result of truncating division is NaN
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Dashboard now has visible Apple TV-style glows:
- Teal glow (top-left): 40% opacity (was 25%)
- Blue glow (bottom-right): 35% opacity (was 15%)
- Larger glow sizes (800x800, 700x700) for better coverage
- Improved gradient background for depth
Changes:
- dashboard_screen.dart: increase glow opacity and size
- Add multi-stop gradients for smooth falloff
Result: Premium Apple TV background with visible glows instead of pure black
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Enhance category card gradient with teal accent (AppColors.primary)
- Add premium ambient glows to dashboard (teal + blue)
- Replace flat black background with subtle gradient for depth
- Increase glow intensity and size for better visual impact
- Apple TV inspired premium dark theme with accent lighting
Changes:
- live_tv_tab.dart: category gradient now uses AppColors.primary
- dashboard_screen.dart: add dual-glow background with premium styling
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Unify color palette across login, dashboard, admin, and player screens
- Replace Colors.white/black with AppColors semantic colors
- Use AppColors.backgroundGradient instead of hardcoded gradients
- Apply primary teal accent (#00A0D2) consistently
- Fix text hierarchy with textPrimary/Secondary/Tertiary
- Improve visual consistency with Apple TV theme
Changes:
- login_screen.dart: gradient, icon, text colors
- dashboard_screen.dart: border, text, background colors
- admin_panel.dart: all Colors.white/white70 → AppColors
- player_screen.dart: background, overlay, text colors
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>