← Tous les manuels · Portail

MANUAL — content-dashboard

Manuel d'instructions de cette app. A LIRE avant toute intervention, et a METTRE A JOUR apres chaque modification (ajouter une ligne au CHANGELOG en bas).
Derniere MAJ : 2026-09-27 · Statut : en ligne (v1)

1. Identite

  • - Role : dashboard perso de creation de contenu pour la chaine YouTube @mattparaguay. Trois usages :
  • 1. Idees & production : un board (colonnes par statut idée → validé → scripté → tourné → publié) pour capturer des idees de video et suivre leur avancement jusqu'au tournage et la publication.

    2. Resultats YouTube : la liste des videos publiees avec leurs stats (vues, likes/commentaires si dispo, date, duree), triable, pour voir ce qui marche. Rafraichi par scraping yt-dlp.

    3. Analyse concurrentielle : par concurrent (Nomad Capitalist pour l'instant), leurs ~80 videos recentes avec vues + likes, tri par defaut sur les vues (leurs cartons), triable par colonne. Sert a voir ce qui performe chez eux.

    4. Recherche de miniatures : une barre de recherche par thematique qui affiche une galerie de miniatures YouTube (a la ViewStats / MrBeast), triable par vues, clic pour agrandir (lightbox maxres), titre = lien vers la video. Sert a s'inspirer des miniatures qui performent sur un sujet. Recherche live via yt-dlp ytsearch (pas de cle).

    6. Scripts : l'atelier d'ecriture central. Un seul endroit ou Matt ecrit ses scripts, quelle que soit l'origine : une idee de son board (bouton "✍️ Ecrire le script" dans la fiche idee) OU une video d'un autre createur a copier/adapter (bouton ✍ au survol d'une carte Shorts). Liste des scripts a gauche, editeur plein largeur avec sauvegarde automatique a droite, et un volet "source" qui affiche la video a copier (embed) ou les notes/structure/pilier de l'idee pendant qu'il ecrit. Statuts brouillon -> a tourner -> tourne -> publie. Ecrire un script depuis une idee la fait passer en "scripte" sur le board.

    5. Strategie : un onglet de reference avec DEUX strategies separees par un switch en haut de l'onglet : Matt (perso) par defaut (positionnement, methode Clouet, croyance, 4 piliers, 8 regles facecam, grille hebdo, roadmap, repris de strategie-instagram-perso/) et Monica (MOA) (strategie de contenu de l'agente MOA Monica Ayala : positionnement "la locale de confiance", croyance, filtre anti-dispersion, 5 piliers, hooks/CTA, tunnel, semaine type validee 2 YouTube + 5 shorts, scoreboard, source /root/workspace/MOA-Agent-Pack/). La vue Monica a un bouton de telechargement du PDF complet (strategie + plan d'action).

  • - URL publique : https://contenu.panelbay.com (mot de passe requis)
  • - Entite : Perso (Matt), chaine perso uniquement.
  • 2. Execution / infra

  • - Dossier : /root/workspace/apps/content-dashboard
  • - Stack : Python 3 / FastAPI + uvicorn, SQLite (stdlib), front statique (HTML/JS inline dans public/index.html).
  • - Service systemd : content-dashboard.service (/etc/systemd/system/, EnvironmentFile .env, Restart=always).
  • - Port : 8349 (bind 127.0.0.1).
  • - Reverse proxy (nginx) : /etc/nginx/sites-enabled/contenu.panelbay.com.conf (HTTPS Let's Encrypt).
  • - DNS : contenu.panelbay.com via Cloudflare (DNS-only), cree par le skill expose-webapp.
  • - Redemarrer : systemctl restart content-dashboard
  • - Logs : journalctl -u content-dashboard -n 50
  • 3. Authentification

  • - Login unique par mot de passe. Page /login, mot de passe = CONTENT_DASH_PASSWORD (.env), cookie de session HttpOnly signe HMAC (CONTENT_DASH_SECRET), valable 30 jours. Deconnexion : /logout.
  • - Changer le mot de passe : editer CONTENT_DASH_PASSWORD dans .env puis systemctl restart content-dashboard.
  • 4. Donnees

  • - Base : SQLite data/content.db (WAL). Tables principales :
  • - Seed initial (seed.py) : 40 idees importees de strategie-instagram-perso/idees-facecam-quotidienne.md (30 idees facecam) + plan-videos.md (Value Assets + reels), et les 43 videos de l'inventaire /root/aios/data/paraguay_videos_validated.json (channel_handle=mattparaguay). seed.py est idempotent (idees seedees seulement si table vide ; videos upsertees sans ecraser les vues scrapees).
  • 5. Scraping YouTube (le point sensible)

  • - Script : scrape_youtube.py. Channel id UCjmtRl_IpqjHkqrk67WICvQ (@mattparaguay).
  • - Pas de cle YouTube Data API dans ce stack, donc tout passe par yt-dlp (standard AIOS, cf lib/media_ingest.py). Trois passes :
  • 1. flat : yt-dlp -J --flat-playlist sur la page /videos. Recupere TOUTES les videos avec id, titre, vues, duree. C'est la colonne vertebrale : les vues sont la metrique fiable qui repond a "qu'est-ce qui marche".

    2. rss : le flux feeds/videos.xml (non gate) donne la date de publication + titre des ~15 videos les plus recentes (la passe flat n'a pas la date).

    3. ryd : API Return YouTube Dislike (returnyoutubedislikeapi.com/votes?videoId=, sans cle, sans auth) pour le vrai nombre de likes de chaque video. On ignore volontairement ses champs dislikes/rating : YouTube a cache les dislikes publics en dec 2021, donc ce sont des ESTIMATIONS extrapolees, pas des vrais chiffres (Matt a demande likes seulement, 2026-09-10).

    4. enrich : yt-dlp -J --no-playlist par video, best-effort pour les commentaires. Souvent bloque par YouTube ("Sign in to confirm you're not a bot") sans cookies : le nombre de commentaires reste donc souvent vide, c'est attendu et gere proprement (pas d'erreur, champ -). Pour les commentaires de facon fiable il faudrait une cle YouTube Data API (gratuite) ou des cookies YouTube.

  • - Regle titres : la passe flat n'ecrase JAMAIS un titre existant (COALESCE(title,?)), pour garder les titres FR de l'inventaire (YouTube sert parfois des titres traduits EN).
  • - Auto-refresh : timer systemd content-dashboard-scrape.timer lance scrape_youtube.py tous les jours a 06:30 (heure systeme = Asuncion). Bouton "Rafraichir depuis YouTube" dans l'UI (POST /api/refresh, lance le scrape en thread de fond, l'UI poll /api/bootstrap toutes les 4s).
  • - Lancer a la main : cd /root/workspace/apps/content-dashboard && python3 scrape_youtube.py (options --flat-only, --enrich-limit N).
  • - Concurrents : scrape_competitors.py reutilise les memes helpers (flat_pass/fetch_rss_map/ryd_likes de scrape_youtube.py) mais ecrit dans competitor_videos. Plafonne a 80 videos recentes par chaine (--limit) car ces chaines sont enormes, et ne prend les likes que pour le top 40 par vues (--like-top) pour rester rapide. Lance aussi par le timer quotidien (2e ExecStart de content-dashboard-scrape.service). Bouton "Rafraichir les concurrents" dans l'UI (POST /api/competitors/refresh). Dates : flat_pass(approximate_date=True) (extractor arg youtubetab:approximate_date) donne une date APPROXIMATIVE (basee sur "il y a X semaines") pour TOUTES leurs videos, et le RSS fournit la date EXACTE des ~15 plus recentes ; on garde l'exacte quand dispo, sinon l'approximative. Tri par defaut sur les VUES (leurs cartons).
  • 5bis. Analytics YouTube Studio (API officielle, depuis 2026-09-12)

  • - Script : import_analytics.py. Deux pulls via l'API YouTube Analytics (youtubeanalytics.googleapis.com/v2/reports, ids=channel==MINE) : (1) metriques par video (dimension=video, rapport "top videos" plafonne aux 200 plus vues) -> video_analytics ; (2) serie journaliere (dimension=day, ~400 j) -> channel_daily.
  • - Auth : reutilise l'OAuth Google de l'AIOS via lib.google_workspace.load_credentials() (memes creds que Gmail/Drive/Calendar). Scopes ajoutes le 2026-09-12 dans /root/aios/lib/google_workspace.py : youtube.readonly, yt-analytics.readonly, youtube.force-ssl. Pas de cle YouTube dediee. Chaine = "Matt Atma" (@mattparaguay), id UCjmtRl_IpqjHkqrk67WICvQ.
  • - Lancer a la main : cd /root/workspace/apps/content-dashboard && python3 import_analytics.py (--days N pour la fenetre channel_daily).
  • - Auto : 3e ExecStart de content-dashboard-scrape.service (timer quotidien 06:30), apres yt-dlp + concurrents.
  • - Expose dans /api/bootstrap : chaque video recoit avg_view_pct, minutes_watched, avg_view_duration_s, subs_net (join sur video_analytics) ; bloc channel_analytics (lifetime + fenetres 28j/90j). Colonnes UI "Retention / Watch / Abonnes" dans l'onglet Resultats + tuiles analytics.
  • 6. API / endpoints

  • - Pages : GET / (protege, sert public/index.html), GET /login, POST /api/login, GET /logout, GET /healthz.
  • - Telechargements : GET /downloads/strategie-monica.pdf (protege par le meme cookie de session) sert /root/workspace/MOA-Agent-Pack/strategie-monica.pdf (constante MONICA_PDF dans server.py). Le PDF est rebuild par bash /root/workspace/MOA-Agent-Pack/build-strategie-pdf.sh (pandoc -> HTML -> wkhtmltopdf, concatene strategie-contenu-monica.md + plan-action-monica.md) ; a relancer apres toute modif de ces deux fichiers.
  • - Board : GET /api/bootstrap (tout ce que le SPA affiche : idees, videos, stats, enums, meta), POST /api/ideas, PATCH /api/ideas/{id}, POST /api/ideas/{id}/archive.
  • - Videos : PATCH /api/videos/{video_id} (tague pilier/categorie/notes a la main), POST /api/refresh.
  • - Concurrents : GET /api/competitors (concurrents + leurs videos + stats, charge a l'ouverture de l'onglet), POST /api/competitors/refresh.
  • - Recherche miniatures : GET /api/thumbnails?q=<thematique>&n=<24|36|48> (recherche live YouTube via scrape_youtube.youtube_search, yt-dlp ytsearch). Renvoie id, titre, chaine, vues, thumbnail, url. Rien n'est stocke en base (recherche a la demande).
  • 7. Pieges connus

  • - Likes : remontent via l'API Return YouTube Dislike (passe ryd, cf §5). Commentaires souvent vides : bot-gating YouTube. Ne pas "reparer" en boucle : c'est une limite d'API, pas un bug. La solution durable pour les commentaires (et une garantie sur les likes) = cle YouTube Data API dans .env + adapter scrape_youtube.py.
  • - Dislikes / ratio like-dislike : impossible en reel. YouTube a supprime les dislikes publics (dec 2021). Les seules valeurs dispo (RYD) sont des estimations extrapolees, non fiables surtout sur petites/recentes videos. Ne pas les afficher comme KPI sans les etiqueter "estime".
  • - Vues arrondies pour les grosses videos : la passe flat renvoie "143K" -> 143000 (arrondi cote YouTube). C'est la valeur LIVE (plus a jour que l'inventaire exact de juillet), on la garde.
  • - guardrail_check.py bloque DELETE FROM en Bash : pour reset la base, supprimer le fichier data/content.db (+ -wal/-shm) puis python3 db.py && python3 seed.py.
  • - Impressions & CTR de miniature : impossibles via l'API. L'API YouTube Analytics ne les expose PAS (metriques impressions/impressionClickThroughRate rejetees, Studio-only). Seule metrique de qualite dispo = averageViewPercentage (retention). Ne pas promettre le CTR miniature en automatique ; il faut aller le lire dans Studio a la main, ou passer par l'export CSV de Studio.
  • - Retention (avg_view_pct) peut depasser 100% : normal pour les Shorts / videos courtes ou les gens rejouent/bouclent. Ce n'est pas un bug, ne pas plafonner a 100.
  • - video_analytics couvre 200 videos, videos en suit 48 : les deux tables sont independantes (join sur video_id). Les videos hors top-200 par vues (queue tres faible) n'ont pas d'analytics ; c'est le plafond de l'API, pas un bug.
  • - L'em-dash est banni de tout output (regle AIOS).
  • 8. Shorts (boite a idees de contenu court, alimentee depuis l'iPhone)

  • - Onglet Shorts : cartes (apercu, titre, chaine, vues/likes, 2 categories, note editable, statut a trier -> retenu -> tourne -> publie, bouton reanalyser, retirer). Filtres statut + categorie + type, tri (recents/vues/categorie/type), recherche.
  • - Apercu integre (click-to-play) : quand une carte a un lien embeddable (YouTube/TikTok/Instagram), un overlay ▶ sur la vignette charge un <iframe> d'embed a la demande (helper shEmbed() dans index.html : /embed/ YouTube, /embed/v2/ TikTok, /p/<code>/embed Instagram ; TikTok/Instagram = format haut .embed.tall). Best-effort : un shortlink vm.tiktok.com sans id video n'a pas d'apercu, fallback lien. Pas de cle, pas de token.
  • - Trois categories editables par short (demande Matt 2026-09-23) : pillar (categorie de CONTENU/sujet : Immobilier, Residence, Passeport, Fiscalite, Make money (general), Plan B, Vie sur place), ctype (TYPE de video : facecam, react, sketch, POV, visite, interview, vlog, autre) et account (COMPTE cible : Matt FR, Matt ES, MOA, RP). Les trois listes sont extensibles depuis l'UI (+ Nouvelle… -> prompt -> POST /api/shorts/categories {kind,value}, kind ∈ pillar/ctype/account -> CATEGORY_KEYS), stockees en JSON dans meta (cles short_pillars / short_types / short_accounts), seedees par db.init_db() depuis db.SHORT_PILLARS / SHORT_TYPES / SHORT_ACCOUNTS. Affichees en chips colores (.b-acc or = compte, .b-pill bleu = contenu, .b-type vert-lime = type). Barre : filtres statut + categorie + type + compte, tri par chacun.
  • - Table shorts (url, platform youtube/tiktok/instagram/autre/idee, title, channel, views, likes, thumbnail, note, status, pillar, ctype, account, source shortcut/manuel, enriched, archived). Colonnes pillar/ctype/account ajoutees par migration idempotente dans init_db() (ALTER TABLE si absentes).
  • - Bulk edit : case a cocher .shpick sur chaque carte -> barre #shBulk (apparait des qu'1 selection) avec selects Compte/Categorie/Type/Statut + Appliquer -> POST /api/shorts/bulk {ids,fields} (met a jour uniquement les champs remplis, sur tous les ids). Bouton "Tout selectionner" (bascule sur les shorts affiches/filtres). Selection = Set shSel cote client.
  • - Auto-categorisation : classifieur DETERMINISTE par mots-cles classify_short(text) (server.py, CLASSIFY_PILLARS/CLASSIFY_TYPES, FR/ES/EN, 1er hit gagne, plus specifique d'abord) devine pillar+ctype. Applique a l'arrivee : dans _enrich_short (URL shorts, sur title+channel+note+url) et _insert_short (idees sans lien, sur title+note). Ne remplit QUE les champs vides (COALESCE(NULLIF(col,''),?)), n'ecrase jamais un tag manuel. Bouton "Auto-categoriser" -> POST /api/shorts/autocat (repasse sur tous les shorts non tagues). Limite : les reels Instagram n'ont ni titre ni texte -> non classifies (a taguer via bulk edit). Pas d'IA/CLI, pur keyword.
  • - Ingest depuis le telephone : POST /api/shorts/ingest avec le jeton SHORTS_INGEST_TOKEN (dans le .env de l'app) en ?token= ou en-tete Bearer. Accepte JSON {url} / {text} / {note}, un formulaire, ou un corps texte brut : la 1re URL trouvee = le short, le reste = la note ; sans URL = idee texte. Meme URL renvoyee = pas de doublon, la note est ajoutee. Fiche utilisateur : RACCOURCI-SHORTS.md (raccourci de partage iOS "Short AIOS", meme mecanique que voice-inbox).
  • - Enrichissement (scrape_youtube.probe_short, thread de fond) : YouTube = oEmbed (titre, chaine, miniature) + RYD (vues, likes) ; TikTok = oEmbed (titre, auteur, miniature ; pas de vues, yt-dlp bloque par IP) ; Instagram = lien seul (connexion requise) ; autre = yt-dlp best-effort. Jamais bloquant.
  • - API session : GET /api/shorts, POST /api/shorts (ajout manuel), PATCH /api/shorts/{id} (note/statut/titre), POST /api/shorts/{id}/archive, POST /api/shorts/{id}/reprobe.
  • - Recettes perso : un short de recette de cuisine se tague avec la CATEGORIE de contenu pillar='Recette' (bulk edit + filtre). L'AIOS les traite hors-app (lit content.db WHERE pillar='Recette', extrait via yt-dlp, ecrit dans /root/workspace/Recettes/recipes.json, PDF pour l'assistante). Voir workspace/Recettes/README.md.
  • 8bis. Analyse + notation automatique des shorts (depuis 2026-09-27)

  • - But (demande Matt) : chaque reel envoye a l'AIOS arrive dans la bibliotheque avec (1) son extrait/hook transcrit, (2) une note de perf de la video, (3) une note du hook. Rien a la main.
  • - Worker : analyze_short.py. Par short non encore analyse (hook vide, analysis_tries < 3) : telecharge l'audio du reel (yt-dlp -x mp3), le transcrit (lib/stt_client.transcribe, cle STT lue dans /root/aios/.env), extrait le hook (1re phrase dite), le classe (classify_hook : Contradiction/Question/Chiffre/Secret/Histoire/Resultat/Probleme/Opinion/Affirmation...), et calcule 2 notes.
  • - Colonnes ajoutees a shorts (migration idempotente init_db) : hook, transcript, hook_type, perf_score (0-100), hook_score (0-100), analysis_tries, analyzed_at.
  • - perf_score (score_perf) : auto-suffisant faute de baseline par createur short par short = magnitude de vues (log) + taux de likes + taux de commentaires (plafonds 55/35/10, reglables). NULL si pas de vues : les reels Instagram n'exposent pas les vues (login wall), donc perf reste vide pour eux (le hook + hook_score, eux, marchent partout). Pour des vues IG il faudrait passer par Apify (voie B shortform-engine).
  • - hook_score (score_hook) : deterministe, base = famille d'accroche (curiosite), + bonus chiffre/mots a tension/adresse au spectateur/brievete, - malus amorce molle ("en gros", "du coup") / phrase trop longue. Reglable ; upgradable vers un juge LLM plus tard.
  • - Declencheurs : (1) a l'ingest, _enrich_short appelle analyze_short.analyze_one dans le meme thread de fond (apres avoir les vues/likes) ; (2) bouton "🎯 Analyser + noter" dans la barre Shorts -> POST /api/shorts/analyze (worker detache --limit 25, le front re-poll 10x/15s) ; (3) cron quotidien 06:17 analyze_short.py --limit 40 (log data/analyze_short.log), avant le scrape de 06:30.
  • - UI (public/index.html) : sur chaque carte short, une ligne hook en italique + deux pastilles de note colorees (rouge->vert par valeur, helper scoreBadge) Perf et Hook (pastille "—" grisee si pas encore note). Tri Note perf / Note hook ajoute a #shSort. Pas de vues IG = pastille Perf "—", c'est attendu.
  • - Piege : la table shorts n'a pas de colonne comments (ne pas la SELECT) ; score_perf recoit 0 pour les commentaires. Reels IG geles/supprimes = download qui echoue, analysis_tries bumpe, abandon apres 3 passes (comme classify_ai).
  • - Outil hors-app lie : /root/workspace/analyse-reels-marvin/ (carte des performances interactive + hooks-idees.md + build_tools.py reutilisable) genere a partir du corpus data/mason-ndiaye-coach/ ; meme grille de classification de hook.
  • CHANGELOG

  • - 2026-09-27 : analyse + notation automatique des shorts (demande Matt : "un extrait de toutes les futures videos + une note de perf sur la video + une note sur le hook"). Nouveau worker analyze_short.py (transcription yt-dlp+whisper -> hook -> classification -> perf_score + hook_score), 7 colonnes ajoutees a shorts (migration idempotente), declenche a l'ingest (_enrich_short) + bouton "🎯 Analyser + noter" (POST /api/shorts/analyze) + cron quotidien 06:17. UI : hook en italique + pastilles de note Perf/Hook colorees sur chaque carte, tri par note perf/hook. Limite : reels Instagram sans vues -> perf reste vide (hook + note hook OK partout). Fichiers : db.py (colonnes), server.py (trigger + endpoint), analyze_short.py (nouveau), public/index.html (badges + sort + bouton), crontab. Restart content-dashboard. Teste : 5/5 reels IG transcrits, hooks classes + notes stockees, service actif, JS OK.
  • - 2026-09-26 (3) : filtres d'exclusion avancés sur l'onglet Shorts (demande Matt). Bouton "⊘ Exclure" (badge du nombre d'exclusions actives, #shExclBtn) qui ouvre un panneau #shExclPanel : 4 rangées de chips toggle (Catégorie/pillar, Type/ctype, Compte/account, Statut), une par valeur existante. Cliquer une valeur la met en rouge barré = les shorts qui la portent sont CACHÉS. Cumulatif et combinable avec les filtres inclusifs existants (statut/catégorie/type/compte/recherche). Usage type : exclure "Recette" de la vue contenu. État shExcl={pillar,ctype,account,status} (Sets), renderExclPanel(), ajout dans shPass (masque si une valeur du short est exclue), nettoyage des exclusions obsolètes + refresh dans loadShorts, bouton "Réinitialiser les exclusions". Frontend only (index.html lu à la demande, pas de restart). Fichiers : public/index.html (CSS .exclpanel/.chip-ex, HTML panneau, JS).
  • - 2026-09-26 (2) : recettes = CATEGORIE, plus de bouton (demande Matt : "mets une categorie recette plutot qu'un bouton, ca permet le bulk edit et de garder la categorie a long terme"). Retrait du bouton 🍳 (.sh-btn.recipe + handler + endpoint POST /api/shorts/{id}/recipe + constantes RECIPES_DIR/QUEUE + file _queue.jsonl). A la place : valeur Recette ajoutee a la dimension de contenu pillar (defaut db.SHORT_PILLARS + injectee dans le meta short_pillars existant). Matt tague les recettes via le select pillar ou le bulk edit ; le tag persiste et se filtre. L'AIOS traite hors-app en lisant content.db (shorts WHERE pillar='Recette'), dedoublonnage via le short_id stocke dans recipes.json. La colonne shorts.recipe reste en base (inutilisee, inoffensive). Short 40 (recette de test) reclasse pillar='Recette', archived=0. Restart content-dashboard.
  • - 2026-09-26 : bouton "🍳 Recette" sur les cartes Shorts (demande Matt : sortir les videos de recettes perso du flux contenu). Une video de recette n'est pas du contenu a produire ; le bouton la fait quitter le board (recipe=1 + archived=1) et l'envoie dans une file d'attente que l'AIOS traite hors-app. Colonne shorts.recipe (migration idempotente init_db). Endpoint POST /api/shorts/{id}/recipe : marque le short + append une ligne JSON dans /root/workspace/Recettes/_queue.jsonl ({short_id,url,title,channel,platform,note,added_at,status}). UI : 3e bouton rond orange 🍳 dans .sh-actions (.sh-btn.recipe, confirm avant), a cote de ✍/✓/✕. Le traitement (extraction recette depuis description+transcript+metadata via yt-dlp, ecriture de la fiche, PDF pour l'assistante) vit hors de l'app dans /root/workspace/Recettes/ (source unique recipes.json -> scripts/build.py regenere INDEX.md + Recettes.pdf ; scripts/fetch_recipe.py = helper yt-dlp). Objectif : ne PAS stocker les videos, juste en extraire les recettes puis purger. Fichiers : db.py (colonne recipe), server.py (endpoint + RECIPES_DIR/QUEUE), public/index.html (bouton + CSS + handler). Restart content-dashboard. Teste : migration OK, service actif, build PDF OK.
  • - 2026-09-25 : onglet Scripts (atelier d'ecriture central) (demande Matt : "un espace pour ecrire mes scripts", en evitant de multiplier les ecrans). Un seul onglet qui fait converger les DEUX sources de contenu : les idees du board ET les videos d'autres createurs a copier (Shorts). Table scripts (title, body, status brouillon/a tourner/tourne/publie, origin idea|short|vierge, idea_id, short_id, snapshot source_url/source_title/source_thumbnail/source_platform, position, archived). Endpoints : GET /api/scripts (liste non archivee, tri publie en bas puis updated_at desc), POST /api/scripts (cree OU renvoie le script existant deja lie a la meme idee/short = pas de doublon ; si origin=idea, passe l'idee en "scripte" quand elle est en idee/valide), PATCH /api/scripts/{id} (title/body/status), POST /api/scripts/{id}/archive. UI : onglet 2 colonnes (liste gauche + editeur plein largeur droite avec sauvegarde auto debounce 700ms), volet source (embed video via shEmbed reutilise, ou notes/structure/pilier de l'idee live depuis STATE.ideas). Boutons d'entree : "✍️ Ecrire le script" dans la fiche idee (openIdea) et ✍ dans .sh-actions des cartes Shorts, tous deux via createScriptFrom() -> POST -> selectTab('scripts') -> openScript(). Refactor : selectTab(name) extrait du handler d'onglet (ajout de 'scripts' a la liste hidden). Fichiers : db.py (SCRIPT_STATUSES + table), server.py (4 endpoints + SCRIPT_FIELDS), public/index.html (tab, section, CSS .sc-*, module JS scripts, boutons). python3 db.py (cree la table) + restart content-dashboard. Teste : create/patch/list/archive OK (SQL direct), service actif, endpoints proteges (401 sans cookie). Limite : editeur en texte brut (pas de markdown rendu), autosave silencieux (indicateur "Enregistre").
  • - 2026-09-23 : auto-detection IA du type de short (vision) (demande Matt : "que ca se remplisse tout seul"). Nouveau worker autonome classify_ai.py : pour chaque short dont le TYPE (ctype) est vide, il recupere UNE image (miniature si dispo = YouTube/TikTok ; sinon frame yt-dlp+ffmpeg ; sinon, pour Instagram, screenshot Playwright de la page /embed) et l'envoie a Gemini vision (gemini-flash-latest, cle GEMINI_TTS_API_KEY lue directement dans /root/aios/.env) qui renvoie type + pilier choisis STRICTEMENT dans les listes editables de Matt (ou null si pas sur). Remplit uniquement les champs vides (jamais par-dessus un tag manuel). Colonne shorts.ai_tries (migration idempotente) : compte les echecs, on arrete de reessayer un short apres 3 passes (reels IG geles/supprimes). Deux declencheurs : (1) **cron */15** */15 * * * * cd .../content-dashboard && PATH=... python3 classify_ai.py --limit 25 (la biblio se remplit seule, log data/classify_ai.log) ; (2) bouton "✨ Detecter par IA" dans la barre Shorts -> POST /api/shorts/ai-classify (lance le worker detache via subprocess, le front re-poll 6x/15s pour voir les tags arriver). Pieges resolus : gemini-flash-latest = modele "thinking" (gemini-3.x) qui consomme tout le budget en pensees et renvoie un contenu vide (finishReason MAX_TOKENS) -> mettre generationConfig.thinkingConfig.thinkingBudget=0 + maxOutputTokens=512 ; le modele ecrit parfois la cle pilier/categorie au lieu de pillar (fallback dans le parsing). Test 2026-09-23 : 10/10 reels IG tagues (facecam/react/autre + pilier). Fichiers : classify_ai.py (nouveau), db.py (colonne ai_tries), server.py (endpoint + imports subprocess/sys), public/index.html (bouton). Restart content-dashboard (endpoint + import). Limite connue : type des shorts = du best-effort visuel, a corriger a la main via le bulk edit quand l'image manque ou que l'IA se trompe. MAJ meme jour : la selection du worker (et le compteur pending de l'endpoint) reprend un short des qu'il manque le type OU la categorie (pas seulement le type), pour que les DEUX dimensions se remplissent dans le temps ; l'IA laisse la categorie vide quand elle n'est pas sure du sujet (a taguer a la main).
  • - 2026-09-23 : carte short : boutons au survol (demande Matt, "meme principe que le module cinema"). Retrait de la barre de statut (<div class="foot"> : select statut a trier/retenu/tourne/publie + boutons reprobe ↻ et archive ✕). Remplace par deux boutons ronds .sh-actions visibles au survol de la carte (.shortcard:hover, opacite .55 en tactile) : ✓ vert = Valider (toggle statut retenu ↔ à trier, la carte prend la classe .is-valid = bord vert quand validee) et ✕ rouge = Supprimer (archive avec confirm). La fonction reprobe (recharger titre/vues) n'est plus accessible depuis la carte. Frontend only (index.html), pas de restart.
  • - 2026-09-23 : bulk edit + auto-categorisation des shorts (demande Matt). Bulk : cases .shpick, barre #shBulk, POST /api/shorts/bulk {ids,fields} (account/pillar/ctype/status), bouton Tout selectionner. Auto-cat : classify_short() keyword FR/ES/EN, applique dans _enrich_short + _insert_short (remplit uniquement les champs vides), bouton + POST /api/shorts/autocat. Reels IG sans texte non classifies (attendu). server.py + index.html. Restart content-dashboard.
  • - 2026-09-23 : embed Instagram recadre sur le media (demande Matt : "juste une image sans la bordure instagram"). IG ne fournit AUCUNE miniature propre sans token (og:image absent = login wall, /media/?size=l = 404). Solution : garder l'iframe /embed mais le recadrer en CSS (.embed.ig aspect-ratio 1/1 + iframe position absolue top:-54px height calc(100%+320px)) pour ne montrer que la zone media, en masquant l'en-tete profil et le bandeau likes/commentaires. shEmbed renvoie kind (yt/tt/ig), classe via embedCls(). Valeurs de crop best-effort (a ajuster si l'en-tete depasse ou si le bas est coupe). Frontend only.
  • - 2026-09-23 : frame par defaut : au chargement, une carte short sans miniature recuperable mais avec lien embeddable (surtout Instagram) affiche directement le <iframe> d'embed (une frame visible au lieu du rectangle vide). Les cartes avec miniature la gardent (leger) + ▶ click-to-play. Idee sans lien = placeholder. Frontend only.
  • - 2026-09-23 : filtre chaine (account) transforme en pilules toggle colorees (.chip-tg, barre #shChanBar, etat shChan, couleurs CHAN_COLORS + fallback) au-dessus de la grille Shorts, a la place du menu deroulant compte (le select compte reste SUR la carte pour l'assignation). Filtre par clic. HTML relu a la demande, pas de restart. Demande Matt (maquette pilules).
  • - 2026-09-23 : retrait du compteur de statuts (#shCount) de la barre Shorts (demande Matt, cosmetique). HTML relu a la demande, pas de restart.
  • - 2026-09-23 : 3e dimension account (Matt FR / Matt ES / MOA / RP) sur les shorts, meme mecanique editable que pillar/ctype (colonne migree, meta short_accounts, CATEGORY_KEYS, filtre + tri + chip or .b-acc). SHORT_FIELDS etendu, /api/shorts renvoie accounts, endpoint categories accepte kind=account.
  • - 2026-09-23 : onglet Shorts enrichi (demande Matt) : (1) apercu integre click-to-play (iframe embed YouTube/TikTok/Instagram a la demande via shEmbed(), overlay ▶ sur la vignette, plus de vignette vide inerte) ; (2) deux categories editables par short : pillar (categorie de contenu) + ctype (type de video), listes extensibles depuis l'UI (POST /api/shorts/categories, stockees dans meta short_pillars/short_types, defauts db.SHORT_PILLARS/SHORT_TYPES), chips colores b-pill/b-type ; (3) filtres categorie + type dans la barre + tri par categorie/type. Migration idempotente (ALTER TABLE shorts ADD pillar/ctype dans init_db). Fichiers : db.py, server.py (SHORT_FIELDS + endpoint categories + pillars/ctypes dans /api/shorts), public/index.html. Restart content-dashboard.
  • - 2026-09-17 : fix titres affiches en anglais. yt-dlp renvoyait le titre AUTO-TRADUIT par YouTube selon la locale du serveur (anglais), pas le titre original francais des videos de Matt (ex. "BOLIVIA VLOG" au lieu de "Je suis alle dans le pays le plus sous-estime..."). Correctif dans scrape_youtube.py : (1) flat_pass(lang=...) + enrich_one(lang="fr") ajoutent l'extractor-arg youtube:lang=fr / youtubetab:lang=fr (force le titre d'origine ; laisse None pour les concurrents), run() appelle flat_pass(lang="fr") ; (2) rss_pass rend le titre RSS/Atom AUTORITAIRE (title=COALESCE(?,title)) car le flux Atom porte toujours le titre original, jamais la traduction. Correction ponctuelle des 6 titres deja stockes en anglais. Note : le flat lang=fr et le RSS peuvent donner deux libelles FR legerement differents (A/B test de titre YouTube) ; le RSS gagne au prochain scrape. Aucun restart (service lit content.db a la demande).
  • - 2026-09-13 : onglet Shorts (boite a idees de contenu court) + endpoint ingest par jeton pour le raccourci iPhone de partage (RACCOURCI-SHORTS.md), enrichissement YouTube/TikTok best-effort.
  • - 2026-09-12 : strategie de Monica (agente MOA) ajoutee a cote de celle de Matt, sans les melanger. Onglet Strategie = switch Matt (perso) / Monica (MOA) (STRAT_VIEW, fonctions stratMattHTML() / stratMonicaHTML() dans index.html, Matt reste la vue par defaut). Nouvelle route protegee GET /downloads/strategie-monica.pdf (FileResponse, MONICA_PDF) + bouton PDF dans la vue Monica. Le PDF (10 pages) vit dans /root/workspace/MOA-Agent-Pack/strategie-monica.pdf, genere par build-strategie-pdf.sh du meme dossier. Demande Matt : garder la strat de Monica dans son dashboard + en fichier telechargeable.
  • - 2026-09-12 : fix : page blanche apres login. Un const PILLAR_COLORS={} vide (stub laisse en tete de script, ligne 205) entrait en collision avec le vrai const PILLAR_COLORS={...} plus bas -> erreur JS fatale "Identifier 'PILLAR_COLORS' has already been declared" qui empechait tout le SPA de demarrer (back-end OK, bootstrap 200, mais front mort). Stub supprime. Lecon : pas de double const de meme nom dans le meme scope ; index.html est servi a la demande, pas besoin de restart.
  • - 2026-09-12 : pilier/categorie affiches en texte colore (plus de bulle/chip). Une couleur par valeur, maps PILLAR_COLORS / CAT_COLORS en tete de script. Logique : PILIERS = sujet (Plan B bleu, Fiscalite vert, Immobilier terracotta, Vie sur place violet, N/A gris) ; CATEGORIES = intention (Credibilite or, Education cyan, Valeur vert-lime, Transformationnel rose). Style .tag-txt (texte gras colore), .tag-empty (italique "a classer"). Le crayon d'edition reste. Anciennes classes chip .b-cat2/.b-todo supprimees.
  • - 2026-09-12 : categories remplies sur les 48 videos + tags pilier/categorie affiches en fixe (fin des listes deroulantes). Classement manuel des categories (db.CATEGORIES : credibilite/education/valeur/transformationnel) d'apres titres + transcripts (/root/aios/data/transcripts/, data/paraguay_kb/) pour les cas ambigus. Reference des definitions : strategie-instagram-perso/STRATEGIE.md (section "Les 4 categories"). Distribution : education 18, valeur 14, transformationnel 9, credibilite 7. UI onglet Resultats : les colonnes Pilier et Categorie n'affichent plus un <select> mais un tag fixe (chip bleu = pilier, chip vert = categorie, chip pointille "a classer" si vide) ; un crayon en fin de ligne bascule la ligne en mode edition (les 2 dropdowns reapparaissent), une coche verte referme. Etat d'edition = Set editRows cote client, PATCH /api/videos/{id} inchange. Une nouvelle video scrapee sans categorie s'affiche "a classer" (a reclasser a la main via le crayon, ou a la prochaine passe de classement).
  • - 2026-09-12 : fusion des piliers Investissement + Revenus locatifs en un seul "Immobilier" (choix Matt) : les deux etaient des rungs du meme parcours (acheter puis louer) et se chevauchaient ; la distinction MOA Real Estate (achat) vs MOA Properties (rendement Airbnb) se fait desormais au CTA de chaque video, pas au niveau du pilier. db.PILLARS passe a 4 piliers + N/A ; 7 videos et 5 idees reclassees en Immobilier ; onglet Strategie mis a jour (4 piliers).
  • - 2026-09-12 : classement des 48 videos scrapees par pilier (Plan B 20, Vie sur place 15, Investissement 6, Fiscalite 3, Revenus locatifs 1). Nouvelle valeur de pilier "N/A" (ajoutee a db.PILLARS) pour les videos anterieures a la strategie qui ne rentrent dans aucun pilier (BOLIVIA VLOG, Michel Barnier, apprendre une langue) ; "N/A" est exclu de l'agregat by_pillar ("quel pilier performe"). Reste modifiable a la main via le select pilier de l'onglet Resultats.
  • - 2026-09-12 : strategie complete + lien analyse par video. Onglet Strategie enrichi avec la strat integrale (methode Clouet, croyance maitresse, echelle de croyances, les 3 peurs, theorie des 3 drapeaux, roadmap phases 0-3) tiree de /root/workspace/strategie-instagram-perso/ (STRATEGIE.md + ROADMAP.md). Nouvelle modale openVideoAnalysis(video_id) (vues, retention, watch time, abonnes nets, likes/comments + lien Studio) ouvrable depuis chaque ligne de l'onglet Resultats ("Analyse complete") ET depuis une carte du board liee a une video publiee (published_video_id). Le board (idees->...->tourne->publie) reste la liste de videos + suivi de tournage ; lier la video publiee dans la modale d'idee fait apparaitre le lien d'analyse sur la carte.
  • - 2026-09-12 : branchement de l'API YouTube Analytics (vraies stats Studio). Nouveau import_analytics.py (OAuth Google AIOS, scopes youtube ajoutes), tables video_analytics (200 videos : retention, watch time, abonnes nets, likes/comments/shares API) + channel_daily (serie ~400 j). Expose dans /api/bootstrap (par video + bloc channel_analytics lifetime/28j/90j), colonnes UI Retention/Watch/Abonnes + tuiles analytics dans l'onglet Resultats. Ajoute au timer quotidien (3e ExecStart). Limite connue : impressions/CTR miniature non exposes par l'API (Studio-only).
  • - 2026-09-11 : onglet "Recherche de miniatures" (galerie de miniatures YouTube par thematique, a la ViewStats), recherche live yt-dlp ytsearch via /api/thumbnails, tri par vues, lightbox maxres. + dates concurrents en approximate_date (toutes datees). + toutes les dates UI au format europeen JJ/MM/AAAA.
  • - 2026-09-11 : colonne "Vues 30j" (gain de vues sur 30 jours glissants) dans Resultats YouTube, calculee cote serveur depuis stats_history (views_deltas() : latest - plus ancien snapshot dans la fenetre). Tant que 30 jours d'historique ne sont pas accumules, montre le gain depuis le debut du tracking + note explicite (tracked_days). Grosses videos = delta grossier (vues arrondies au millier par YouTube), videos recentes = exact.
  • - 2026-09-11 : onglet Resultats YouTube trie par defaut sur les plus recentes (tri par colonne conserve). Nouvel onglet Analyse concurrentielle : scraper scrape_competitors.py (tables competitors/competitor_videos), Nomad Capitalist seede (80 videos recentes + likes top 40). Ajoute au timer quotidien.
  • - 2026-09-10 : ajout passe ryd (Return YouTube Dislike API) pour le vrai nombre de likes de chaque video, sans cle. Dislikes volontairement exclus (estimation non fiable depuis dec 2021).
  • - 2026-09-10 : creation v1. App FastAPI+SQLite, board idees/production (40 idees seedees), stats YouTube via yt-dlp (47 videos), onglet strategie. Service systemd + timer scrape quotidien 06:30. Expose sur contenu.panelbay.com. Ajout au portail (groupe Pilotage).