La barre partagée avait perdu la marge basse des blocs qu'elle a remplacés :
sur /p/:provider/t/:theme la 1re carte collait aux boutons (0 px, l'ancien
panneau portait mb-8), et /t/:theme ne gardait que mt-6.
- les deux pages thèmes enveloppent la barre dans un div mb-8 ;
- /t/:theme : le conteneur de sections perd son mt-6 (compensé par mb-8),
plus aucun double espace.
Mesuré au navigateur (Playwright, desktop 1280 + mobile 390) : 32 px sous
la barre sur /p/:…, 74 px jusqu'à la 1re carte sur /t/:…, build ng OK,
test:search vert. Instance locale 4200 rebuildée.
Nouveau composant partage app-filter-toolbar, utilise tel quel par la page
de recherche ET les deux pages de themes (meme visuel, seul le cablage
differe : URL pour la recherche, signaux locaux pour les themes).
- Boutons : [Filtres] (pastilles de type, ligne Sources, selects periode /
duree / langue, filtres actifs) et [Tri] (Pertinence, Date de mise en
ligne, Vues, Note (likes), Duree).
- Page de recherche : le bouton oeil « filtres rapides » et le panneau de
selects disparaissent, tout est sous [Filtres] ; le tri a son bouton.
- Sources : boutons Toutes / Aucune. « Aucune » masque tout (la garde
« derniere source masquable » est retiree) et un bandeau vide dit de
reactiver au moins un fournisseur.
- /t/:theme : les 3 pastilles maison sont remplacees par la barre ; les
sources masquent des blocs entiers ; filtres et tri appliques sur les
blocs deja charges.
- /p/:provider/t/:theme : les 5 selects et les pastilles sont remplaces par
la barre ; signaux maison unifies sur le modele SearchFilters (defaut
relevance) ; les onglets Twitch lisent/ecrivent filters().type.
- Tri etendu : FilterSort + rating + duration. Les providers ne connaissent
que relevance/date/views (serveur inchange, repli relevance) : Note et
Duree sont reordonnes cote client via sortItems(), partage avec les pages
themes.
- Communs : sortItems / itemViews / itemRating / matchesLanguage /
formatCount extraits de filters.ts (3 copies d'heuristiques supprimees) ;
cles i18n fr+en (filters.sort.rating, filters.sort.duration,
filters.sortHint, filters.localHint, « Date de mise en ligne »).
Tests : test:search, test:filters, test:suggest, test:kind, test:section
verts + test:api 41/41, build ng OK.
Nouvelle liste dans la section Vous de la barre latérale, avec un bouton
horloge a cote du coeur sur toutes les cartes (accueil, themes, provider-
themes, recherche).
- db : les 4 fonctions de listes (like/unlike/isLiked/list) prennent un
paramètre `tag` — la table `tags` sert déjà de mot-clés génériques, donc
zéro migration.
- api : les 4 routes likes sont générées par registerVideoTagRoutes(prefix,
tagName) ; /api/user/watch-later = même fabrique, même enrichissement
yt-dlp, même middleware cookie-aware. Entrée OpenAPI ajoutée.
- front : LikesService prend un VideoTag, like-button un @Input() list
(icône horloge, ambre quand enregistré, libellés a11y dédiés) — un seul
composant pour les deux boutons, pas de copie.
- cartes de recherche : la grille partagée n'avait AUCUN bouton ; cœur +
horloge ajoutés, masqués sur les lives et cartes chaîne (coin bas-gauche
déjà occupé par le compteur de spectateurs / pas des vidéos).
- route /library/watch-later = LikedComponent via data.list (titre, icône,
placeholder et état vide pilotés par la route), clés nav.watchLater FR/EN.
Tests : npm run test:api 41/41 (nouveau test watch-later : ajout, statut,
indépendance vis-à-vis des likes, liste, retrait, 400, 401) + 21 autres
suites vertes. Build OK, instance locale 4200 rebuildée. Sondage Playwright
desktop+mobile : 12/14 (2 faux positifs du sondage : accordéon replié sur
une route hors /library, 3×401 console pendant la phase déconnectée).
Trois causes du « toujours les mêmes vidéos » (peu importe le fournisseur) :
- watch-short : queryIndex repartait à 0 a chaque loadFeed() -> toujours la
même requête (« shorts ») et donc les mêmes résultats du fournisseur.
Départ désormais aléatoire dans SHORTS_QUERIES (la rotation par page reste).
- watch-short : dedup() ne triait rien -> le 1er lot arrivait toujours dans
l'ordre du fournisseur. Le lot neuf est maintenant mélangé (Fisher-Yates),
comme le chemin « Pour toi » le fait déjà. Un seul point de passage, donc
catalogue, historique, repli trending et Twitch sont couverts.
- youtube-api : la rotation des top games Twitch démarrait à l'index 0 ->
mêmes clips à chaque session. Départ aléatoire, rotation conservée.
Le cache serveur (5 min / 30 min YT) n'est pas en cause.
Vérif instance locale : YouTube 3/3 premières vidéos distinctes sur 3
rechargements, Twitch 2 distinctes sur 3. tests verts.
- /watch : muted=true supprime — son actif dès le démarrage (le chargement
de l'embed a lieu dans un geste, donc l'autoplay sonore est autorisé).
- /shorts : muted piloté par le bouton Son du menu (défaut : son ON) — ce
bouton était jusqu'ici sans effet sur Twitch.
- /shorts : plus d'avance auto pour Twitch. L'embed clips est cross-origin et
n'expose AUCUNE API d'état : impossible de savoir si la vidéo est en pause,
donc on ne change jamais de vidéo tout seul (swipe / flèches / molette).
- Verifs instance locale : 4/4 (URLs + aucune avance après 75 s d'un clip de
49 s) + 6/6 (nudge) + 8/8 (chargement au premier geste).
- Methode « touch listener b4 loading the iframe » : l'URL de l'embed Twitch
n'est produite qu'apres un premier geste — navigator.userActivation.
hasBeenActive a l'arrivee (clic sur un lien = fenetre d'activation de 5 s),
sinon pointerdown / keydown / wheel. Le premier chargement de l'iframe
tombe donc dans un contexte d'activation utilisateur.
- Invite « Touchez pour charger la vidéo » sur /watch et /shorts pendant
l'attente (sinon spinner infini sur /watch, cadre noir sur /shorts).
- /shorts : pas d'avance auto tant que l'embed n'est pas charge — le flux
ne switchait plus d'une video a l'autre sans jamais jouer.
- Le nudge (recharge &r=1 au premier geste) est conserve pour les videos
montees sans geste (avance auto, arrivee avec activation deja consommee).
- Verifs instance locale : 8/8 (arrivee a froid : invite + montage au geste)
et 6/6 (arrivee avec activation : montage immediat + recharge unique).
- /watch + /shorts : le player Twitch refuse de demarrer sans geste
utilisateur (anti-bot) et l'iframe etant cross-origin on ne peut pas
cliquer dedans. On ecoute document:pointerdown (+ le keydown existant) et
on ajoute &r=1 a l'URL de l'embed de la video courante : l'iframe se
recharge dans le contexte d'activation utilisateur.
- Une seule fois par video (twitchNudgedFor) : pas de boucle de
rechargement, les videos suivantes sont re-armees automatiquement.
- Verifie sur l'instance de test : 6/6 (URL initiale sans r=1, r=1 apres
le geste, aucune recharge au 2e geste) sur /watch et /shorts.
- /watch : les embeds Twitch etaient en autoplay=false depuis le 1er commit
(clips clips.twitch.tv + channel + VOD player.twitch.tv) alors que tous les
autres fournisseurs de la page demarrent automatiquement en muet
(YouTube mute=1, Dailymotion mute=1, Odysee muted=1, PeerTube autoplay=1).
- Passe a autoplay=true&muted=true : le lecteur demarre tout seul, le son se
reactive via les commandes du player (meme convention que /shorts).
- /shorts : inchange, l'embed demandait deja autoplay=true&muted=true.
- app-player-menu : stopPropagation wheel/touchstart/touchend sur le host —
les handlers window:wheel et window:touch* de /shorts ne volaient plus les
gestes sur le panneau (impossible de scroller jusqu'en bas du menu, tap =
play/pause, swipe = changement de short)
- /shorts : ArrowUp/Down ignore quand le menu est ouvert
- /shorts : le FAB passe en haut a DROITE du cadre (la fleche retour mobile
z-20 passait par-dessus le FAB piege dans la scene z-10)
- Look/taille : FAB 40->36px, panneau 288->240px, items compacts,
max-height 70vh/480 -> 65vh/430 (plus de rognage par overflow:hidden),
pastille en degrade + relief, panneau rounded-xl + highlight interne
- PlayerMenuComponent : FAB circulaire translucide (haut-gauche) + panneau
deroulant, commun aux deux pages (position, style, open/close, clic exterieur)
- /shorts : consolidation de tous les boutons (fournisseurs, langue, actions)
dans le menu FAB ; suppression du rail a droite et de la rangee du haut
- /watch : actions (cinema, qualite, j'aime, playlist, transcription,
telechargement, partage, ouvrir) deplacees dans le menu FAB
- AddToPlaylistComponent : variante menuItem pour s'integrer au menu
- styles .pm-item/.pm-sep/.pm-section-label dans index.css
Dailymotion exige désormais un Player ID (fév. 2026) : player.html?video=...
sans Player ID répond HTTP 403 'Forbidden'. L'URL correcte est
geo.dailymotion.com/player/{PLAYER_ID}.html?video=...
- helper dailymotionPlayerId()/dailymotionEmbedBase() (config window.DAILYMOTION_PLAYER_ID, repli demo x8lr5)
- /watch et /shorts utilisent le nouvel endpoint avec Player ID
- config.local.example.js documente la config
L'iframe Shorts utilisait referrerpolicy=strict-origin-when-cross-origin
alors que /watch utilise 'origin'. C'est la seule difference restante entre
les deux pages pour Dailymotion (URL identique, allow_embed=true) et elle
fait repondre 'Forbidden' au player. On aligne sur 'origin'.
Revert du referrerPolicy helmet (no-op pour les iframes qui ont leur propre
attribut, risque de side-effects inutile sur les CDN de vignettes).
Boutons fournisseurs avec disque aux couleurs de la marque + glyphe SVG (play YT, bulle Twitch, initiale sinon) ; icone etincelles pour Pour toi. Selecteur de langues Profil/FR/EN/FR+EN/Toutes (persiste newtube.shorts.langFilter, invite compris) : chaque flux passe par effectiveShortsLangs (resolveShortsLangs pur + spec). Docs 9.9.
Chrome auto-hide au hover desktop (2,5 s). Capteur tactile mobile : swipe garanti (l'iframe mangeait les touchers), tap = play/pause YT ou 6 s d'acces direct au lecteur sinon. Fleche retour accueil mobile, cadre quasi plein ecran. Avance auto : handshake YT listening (les evenements player n'arrivaient jamais), onStateChange canonique, retry borne anti-prefetch. Docs 9.9.
Odysee demarre muet (pas de rectangle tap-to-unmute) et ne passe au son que sur clic explicite du bouton Son (le geste autorise l'autoplay sonore). Avance auto : playerState YT (fin -> suivant, pause -> minuteur desarme, reprise -> rearme) + minuteur generique sur duree connue ou estimee (clip Twitch 60 s, sinon plafond Short 90 s). Docs 9.9.
Son active par defaut (YT/OD/DM/PT avec son ; clips Twitch en muet force, sans API de volume). Avance auto en fin de lecture (infoDelivery YT + minuteur duree generique garde par index/videoId, ignore si onglet cache). Dailymotion : allow_embed demande cote serveur, mappe en embeddable, propage en raw.allowEmbed pour le garde isPlayable (fini les Forbidden). Rumble autoplay=1/2 selon muet + nudge playVideo YT.
- format: des que largeur/hauteur sont connues, l'orientation fait autorite (horizontale/carree => jamais short, meme <75 s). Repli duree reserve aux providers muets
- dimensions mappees: Dailymotion (width,height), PeerTube (aspectRatio), Odysee (value.video.width/height) => validation verticale reelle
- YouTube: langue via snippet.defaultAudioLanguage (filtre FR/EN)
- autoplay: demarrage muet, suppression du deblocage automatique au scroll/selection (reload d'embed hors geste => ecran noir); son via le bouton dedie
- PeerTube embed: muted=1 pour l'autoplay
- server/search-filters.mjs: miroir de la regle d'orientation + tests mis a jour
- autoplay: les Shorts demarrent muets (comme Watch) pour garantir l'autoplay des iframes; le son se debloque a la premiere interaction (unlockSound) et reste controlable
- proxy prod: ajoute les alias /proxy/twitch-api, /proxy/twitch-auth, /proxy/dm, /proxy/odysee (evite le fallback SPA -> parse JSON 'status 200 ok:false'); forwardJson accepte les deux prefixes. Corrige aussi la validation des clips Twitch morts
- Dailymotion: allow_embed demande dans les flux shorts/trending -> les videos non-embeddables ('Forbidden') sont exclues
- langues: mapping language (Dailymotion/PeerTube/Odysee) + shortsLangOk applique a tous les fournisseurs; en+fr toujours acceptes
- PeerTube shorts: durationMin/Max cote serveur (plus de videos longues au hasard)
- Twitch: validation helix/clips de nouveau fonctionnelle + autoplay muet
Source unique de classification (video-kind.ts) + politique de section centralisee (section-policy.ts). Les composants ne portent plus de seuils ni de regles de type maison.
- section-policy.ts: sectionAccepts / routeForVideo / shortsFeedAccepts /
isPlayable + classifyOnce (enrichissement _kind a l'ingestion)
- watch-short: separe classification (shortsFeedAccepts) et jouabilite
(isPlayable: allowEmbed DM, live/channel Twitch, Rumble 404); supprime le
seuil Rumble 90 s et l'import YOUTUBE_SHORT_MAX_SECONDS du composant
- home: filtre sectionAccepts(...,'home') (plus de shorts dans le trending),
routeForVideo, pagination bouclee (min 20, max 5 iterations)
- themes: _kind pre-calcule une fois, sectionAccepts, capacites live lues
depuis le registre providers (fin de LIVE_PROVIDERS codes en dur)
- video-kind: helpers isNativeVerticalProvider / hasKnownDuration
- routes: alias /shorts/:id et /live/:id pour routeForVideo
- tests: section-policy.spec.ts + video-kind.spec.ts etendus; npm run test:section
Bug : le @if (hasQuery()) englobant toute la section basse avait ete passe en
@if (hasQuery() && railsOpen()), donc replier le rail masquait aussi la
grille, le loader et le sentinel de scroll infini. Seul le panneau
' types + sources ' est desormais imbrique dans @if (railsOpen()).
Logos : les formes geometriques approximees sont remplacees par les six
marques officielles (traces et couleurs Simple Icons, CC0-1.0), stockees en
local pour eviter toute dependance CDN a l'execution. Dailymotion garde le
bleu de la marque (#0066DC) plutot que le #0A0A0A de Simple Icons, invisible
sur fond sombre.
L'etat actif/masque n'est plus porte par la couleur de l'icone (fixe par
marque) mais par le bouton : fond plein + logo couleur, ou fond transparent +
logo grayscale 40%.
- nouveau bouton oeil a droite de 'Filtres' pour replier le bloc
types + sources (railsOpen, persiste en localStorage) ; pastille d'accent
quand un filtre de type est actif alors que le rail est replie
- nouveau composant app-provider-icon : marques des 6 sources en trait,
currentColor pour que l'icone herite de l'etat actif/masque du bouton
- pastilles sources en icone seule (h-8 w-8), libelle et compte passes par
aria-label / title ; plus de point de couleur ni de nom visible
- suppression de PROVIDER_DOTS et providerDotStyle
Les pastilles fournisseurs portaient des couleurs de marque en dur ([ngStyle])
qui ignoraient le systeme de themes : rendu illisible en thème clair. Tout
l'en-tete rebati sur les classes que le CSS remappe (bg/text/border-slate-*).
- sources : point de couleur plein (active) / creux (masquee) a la place des
fonds de marque, nom complet + compte compact tabular-nums
- actions (Tout afficher / Toutes les sources) en style contour, plus de
bordure pointillee qui ressemblait a un placeholder
- pastilles de type : bg-slate-200 pour l'etat actif, border-transparent
pour eviter le saut de 2px au changement d'etat
- rails regroupes dans un panneau + filet, fade de droite (scroll-rail-fade)
- selects : appearance-none + chevron SVG, libelles visibles remplaces par
sr-only (5 lignes de hauteur en moins)
- bannieres notice/error refaites en neutres themables (illisibles en clair)
- anneau focus-visible en var(--accent) sur tous les controles
- suppression de getProviderColors et formatViews (morts)
- rail 1 : pills de type resserrees (h-6/text-xs), role=group coherent
- rail 2 : les pastilles fournisseur deviennent des boutons d'affichage
(couleur de marque = visible, gris + opacite = masque), avec le compte de
resultats par source, aria-pressed, hover/focus
- filtre d'affichage local (aucun refetch, aucun squelette) persiste en
localStorage ; impossible de masquer la derniere source visible
- panneau de filtres repliable par defaut, selects h-8 inline labels sr-only,
badge du nombre de filtres actifs sur le bouton
- nouveau computed scopedVideos : compteurs de types et tri inter-provider
alignes sur les sources visibles
- suppression d'activeProvidersInfo devenu inutilise
- video-card: etat de chargement de la miniature (shimmer + spinner),
fallback 'Apertu indisponible' avec retry, referrerpolicy no-referrer
- grid: eager sur les premieres cartes, bandeau de rafraichissement quand
la grille est deja peuplee
- search: garde MIN_QUERY_LENGTH=2 (le loading partait pour une requete que
request$ filtrait, grille figee sur des skeletons) + filet de securite 20s
- skeleton: ligne chaine pour coller a la hauteur reelle de la carte
Barre de recherche
- Panneau de filtres unifié (remplace le ProviderPicker) : sources, type
(vidéos/shorts/direct/chaînes), période (heure/jour/semaine/mois/année),
durée (<4 / 4-20 / >20 min) et tri. Pastilles + roving tabindex
(←/→ dans un groupe, ↑/↓ entre groupes), focus trap, Échap, Ctrl/⌘+Maj+F,
recherches récentes, « mémoriser par défaut », « Réinitialiser ».
- Pastilles des filtres actifs sous la barre (retrait en un clic) + compteur.
- Typeahead : un seul keydown (fin du double Enter), ↑/↓ bouclants,
Home/End, PageUp/PageDown, Entrée valide la ligne surlignée (sinon
recherche brute), Tab complète sans chercher, Échap ferme puis vide,
ligne « Rechercher <q> », défilement de l'option active, aria à jour.
- Focus : le panneau se referme dès que le focus quitte la barre
(focusout + relatedTarget + activeElement), pointerdown neutralisé sur les
lignes pour garder le focus dans l'input (plus de scintillement).
- Opérateurs en clair : `linux live:`, `tuto today: long:` deviennent des
filtres et sont retirés de la requête (autocomplétion après le `:`).
Filtres côté serveur
- Modèle partagé : src/app/search/filters.ts + server/search-filters.mjs.
- /api/search?type=&duration=&period=&sort= : InnerTube (upload_date, type,
duration, features), Data API v3 (type, videoDuration, publishedAfter),
yt-dlp --dateafter ; post-filtrage pour les providers sans filtre natif.
Un champ manquant ne fait jamais disparaître un résultat.
- Filtres dans l'URL (partageables) + adapters + SearchService (clé de cache).
- Correction : la recherche Shorts renvoyait 0 (ShortsLockupView/GridShelfView
non mappés, id/titre dans l'endpoint) -> mappage + shelf + vignette/vues.
Tests / docs
- 23 tests unitaires (clavier, focus, opérateurs, panneau de filtres),
server/tests/search-filters.test.mjs (npm run test:filters) + CI,
scénarios e2e des filtres, README/api/README/todo/MCP à jour.
Cinq correctifs signalés sur le mode mobile et le mode desktop.
1. La modale « @ » était confinée dans le header (desktop + mobile)
<header> portait `backdrop-blur-sm`, et `backdrop-filter` fait de l'élément
le containing block de ses descendants en position:fixed. Les overlays du
provider-picker (et du menu Ctrl+K) se résolvaient donc contre le header :
mesuré 1440x64 au lieu de 1440x900, dialogue réduit à 32px de haut pour 466px
de contenu, haut de la fenêtre inatteignable.
Le flou passe sur une couche enfant (`-z-10`) qui n'est pas un ancêtre des
overlays : `fixed` redevient relatif au viewport.
Desktop : dialogue 512x468 centré. Mobile : 358x686, entièrement visible.
2. La barre de thèmes disparaissait sous le header (desktop + mobile)
`--header-offset` vaut 56/64px, mais le header mesure 57/65px (bordure) et
118px quand la feuille de recherche mobile est ouverte. La barre était donc
déjà glissée dessous, et totalement invisible dans ce dernier cas
(hauteur visible mesurée : 0px).
HeaderComponent publie maintenant sa hauteur réelle dans `--header-measured`
via ResizeObserver ; `--header-offset` l'utilise quand elle existe et garde
la valeur statique en repli. Barre visible dans tous les cas, y compris
feuille de recherche ouverte.
3. Barre de thèmes : icônes seules sur mobile
Les libellés sont masqués sous md (`aria-label` conservé pour
l'accessibilité). 7 pastilles visibles au lieu de 3, sans scroll.
4. Avatar en pastille
`.avatar-circle` est renforcé : `rounded-full` dans le markup plus
`border-radius: 9999px !important`, `aspect-ratio: 1/1`, min-width/min-height
et `line-height: 1`, pour qu'aucun style de thème ne puisse le dégrader en
rectangle.
Vérifié : audit navigateur sur 17 routes x 320/390/768/1440 px — aucun
débordement horizontal, barre de thèmes visible partout ; tests du dépôt verts ;
build production OK.
Le push réussissait puis le script sortait en 22 :
curl: (22) The requested URL returned error: 404
Write-Error: ... code d'erreur: 22
Deux causes cumulées, visibles seulement une fois MAX_VERSIONS dépassé.
1. Accept trop restrictif
`docker build` via buildx produit un OCI image index
(application/vnd.oci.image.index.v1+json), pas un manifest v2 plat.
delete_by_tag demandait le digest avec
`Accept: application/vnd.docker.distribution.manifest.v2+json` : le registre
répond 404 — et non 406 — quand l'Accept ne couvre pas le type stocké.
Le HEAD échouait donc sur TOUS les tags, y compris celui venait d'être
poussé. On liste maintenant les quatre media types possibles.
2. La rétention pouvait tuer un déploiement réussi
Sous `set -euo pipefail`, le curl de delete_by_tag n'était protégé par
aucun `|| true` : son code de sortie 22 se propageait et tuait le script,
transformant un push réussi en échec. Le ménage des anciennes versions est
du best-effort et ne doit jamais faire échouer le push.
Symptôme d'origine : la rétention ne s'est jamais déclenchée tant que le
nombre de tags semver est resté <= MAX_VERSIONS (5). Au 7e tag, elle a
commencé à tourner et a fait échouer le script.
Vérifié : deploy-img.ps1 publie 1.0.20, `latest` aligné sur le digest de
l'image locale (sha256:25555d9ee4bf…), rétention purgée sans erreur.
Le header était en grid-cols-[auto_1fr_auto] avec une barre de recherche
non-wrappable (7 chips + input + 2 boutons, aucun min-w-0) : ~1000px de
largeur intrinsèque dans un viewport de 375px. Toutes les routes
débordaient horizontalement et les boutons Connexion/Inscription partaient
hors écran.
Socle
- --header-h / --header-offset / --app-vh centralisent la métrique du
header : supprime les top-16 codés en dur (channel-tabs était en top-0 et
glissait sous le header au scroll).
- Safe-area iOS, viewport-fit=cover, color-scheme/theme-color.
- dvh partout (100vh > hauteur visible sur iOS), overflow-x: clip au lieu de
hidden (hidden cassait silencieusement tous les sticky).
- Cible tactile 44px sur pointeur grossier, skip link, focus-ring clavier,
scroll-rail pour les tab strips.
Header
- Barre mobile (burger, logo, recherche, menu ⋮) + barre desktop inchangée.
- Le menu ⋮ expose ce qui disparaissait : bande des thèmes, préférences,
historique, favoris, téléchargements, thème, connexion/inscription.
- Recherche en feuille sous le header avec focus automatique.
- Échap / clic extérieur / navigation ferment les panneaux transitoires.
Drawer
- Partait à inset-y-0 : les 56 premiers px du menu étaient derrière le
header. Descend sous le header, bouton fermer intégré, verrouille le
scroll, se ferme à Échap.
Barre de recherche
- Chips masquées sous lg, remplacées par un bouton résumé qui ouvre le
sélecteur de fournisseurs. Suggestions en dvh, menu Ctrl+K plein écran
sur mobile.
Pages
- Shorts : cadre 9:16 contraint par dvh ET vw (il débordait en paysage),
Préc./Suiv. qui étaient hors du conteneur height-capped.
- Watch : barre d'actions qui wrappe, mode cinéma en opacity-0 group-hover
donc invisible au doigt, thumbnails fluides, playsinline sur les iframes.
- Historique, thèmes, chaîne, playlists, préférences, sessions : rails
swipables, min-w-0, truncate, en-têtes qui wrappent.
Vérifié : audit navigateur (CDP) sur 17 routes x 7 largeurs (320→1440),
zéro débordement horizontal ; 19 tests d'interaction mobile ; suites de
tests du dépôt vertes (api 40/40, transcript 27/27, search, suggest,
playlists, preferences, subscriptions, highlight, telemetry).
- Expand .env.example with full runtime, cache, Docker, and AI keys
- Add deployment secrets to .gitignore (docker-compose/.env,
docker/.registry.env)
- Remove VIMEO_ACCESS_TOKEN from compose files; add OAuth redirect URIs
- Load registry credentials from .registry.env in deploy-img.sh
- Fail fast if protected registry lacks auth during pre-checks
- Pass DOCKER_USERNAME/PASSWORD explicitly to WSL in deploy-img.ps1
- Remove inline collapse buttons from themes-nav.component.html
Pipe histHighlight + utilitaire pur (HTML source echappe, matchs
insensibles a la casse, multi-mots, chevauchements fusionnes) :
titres/queries/apercus des 3 onglets + lignes des transcripts
deplies, avec badge de comptage d occurrences. test:highlight.
- distinctUntilChanged() sur Subject<void> avalait toutes les frappes
apres la premiere : barre de recherche morte sur les 3 onglets.
- Filtre provider recherches : ids courts stockes (yt, dm) vs filtre
long (youtube) -> zero resultat. Match court+long cote serveur.
- LIKE echappes (%, _, \\) sur q des 3 onglets.
- Watch/transcripts : provider normalise (court+long), lang
insensible a la casse avec prefixe (fr -> fr, fr-ca).
- Ecriture watch canonique (nom long). Test test:history.
Le fallback &tlang= peut renvoyer la piste source en un seul bloc
de plusieurs Ko (throttle YouTube) : il passait devant les pistes
directes et son blob anglais etait mis en cache comme 'fr'.
tlang desormais etape 3 (apres directes signees), isBlobTranscript()
refuse les cues uniques > 2000 chars (fetch, yt-dlp et post-dedupe).
Tests 27/27.
Les URLs timedtext InnerTube repondent 200-vide (throttle YouTube) :
sonde rapide max 2 sans retry, puis dump yt-dlp -> URLs signees en
direct (max 4), puis --write-sub. 200-vide traite comme 429
(retryAfterSec), fmt= plus jamais duplique (signature), cookies
YT_COOKIES_FILE reutilises par le fetch direct. Tests 26/26.