7 次代码提交 fd2177a229 ... e4b76d1de7

作者 SHA1 备注 提交日期
  gpt-engineer-app[bot] e4b76d1de7 Corrigé auth flow YouTube RSS 1 月之前
  gpt-engineer-app[bot] b200b604d8 Changes 1 月之前
  gpt-engineer-app[bot] f3726dadec Use session token for YouTube RSS 1 月之前
  gpt-engineer-app[bot] 6db23f98c2 Changes 1 月之前
  gpt-engineer-app[bot] 0bdea240e0 Migrate purge rules 1 月之前
  gpt-engineer-app[bot] 74fe872a56 Changes 1 月之前
  gpt-engineer-app[bot] d3eb2b7c73 Updated plan file 1 月之前

+ 43 - 12
.lovable/plan.md

@@ -1,20 +1,51 @@
-# Plan terminé ✅
 
-## Modification de la purge des articles
 
-La migration a été appliquée avec succès le 2026-01-29.
+## Proteger les articles non lus des abonnements actifs
 
-### Nouvelle logique
+### Nouvelle regle de purge
 
-- **Supprimer** : Articles non vus dans les flux depuis 48h+ ET non épinglés
-- **Protéger** : Uniquement les articles épinglés
+| Statut | Abonnement actif | Action |
+|--------|-------------------|--------|
+| Epingle | Peu importe | Protege |
+| Non lu + Non epingle | Oui (au moins 1 abonne) | Protege |
+| Non lu + Non epingle | Non (aucun abonne) | Supprime |
+| Lu + Non epingle | Peu importe | Supprime |
 
-### Résultat du test
+### Impact estime (donnees actuelles)
 
-| Métrique | Valeur |
+| Metrique | Valeur |
 |----------|--------|
-| Articles éligibles à la suppression | 6 458 |
-| Plus ancien article | 2025-12-16 |
-| Plus récent article éligible | 2026-01-27 |
+| Articles eligibles (> 48h) | 103 |
+| Epingles (proteges) | 9 |
+| Non lus avec abonnement (proteges) | 4 |
+| Non lus sans abonnement (supprimes) | 0 |
+| Lus non epingles (supprimes) | 90 |
+| **Total supprime** | **~90** |
+
+### Details techniques
+
+**Migration SQL** - Mise a jour de `purge_old_articles` et `test_purge_articles` :
+
+La condition de protection passe de :
+```text
+NOT EXISTS (is_pinned = true)
+```
+A :
+```text
+NOT EXISTS (is_pinned = true)
+AND NOT (article non lu ET feed a au moins un abonne)
+```
+
+Concretement, un article est supprime si :
+1. `last_seen_at` depasse 48h
+2. Il n'est epingle par personne
+3. ET il est soit lu par au moins un utilisateur, soit son flux n'a aucun abonne actif
+
+La fonction `test_purge_articles` sera aussi mise a jour pour refleter cette logique.
+
+### Fichiers modifies
+
+- **Migration SQL** : nouvelle fonction `purge_old_articles` et `test_purge_articles`
+- **src/data/changelog.ts** : ajout d'une entree pour documenter le changement
+- **.lovable/plan.md** : mise a jour de la documentation
 
-La prochaine exécution du cron job (3h du matin) supprimera jusqu'à 1000 articles par batch.

+ 25 - 0
src/data/changelog.ts

@@ -8,6 +8,31 @@ export interface ChangelogEntry {
 }
 
 export const changelogData: ChangelogEntry[] = [
+  {
+    version: "1.13.1",
+    date: "2026-02-09",
+    category: "bugfix",
+    title: "Correction de l'authentification pour la détection YouTube",
+    description: "La détection automatique du flux RSS YouTube utilise désormais le token de session de l'utilisateur connecté.",
+    details: [
+      "Correction de l'erreur 401 lors de l'ajout d'un flux YouTube",
+      "Utilisation du JWT utilisateur au lieu de la clé anon pour l'appel à fetch-youtube-rss",
+    ]
+  },
+  {
+    version: "1.13.0",
+    date: "2026-02-09",
+    category: "improvement",
+    title: "Protection des articles non lus avec abonnement actif",
+    description: "Les articles non lus appartenant à un flux suivi sont désormais protégés de la purge automatique.",
+    details: [
+      "Les articles épinglés restent toujours protégés",
+      "Les articles non lus dont le flux a au moins un abonné sont protégés",
+      "Les articles non lus sans aucun abonné sont supprimés",
+      "Les articles lus non épinglés sont supprimés après 48h",
+      "Mise à jour des fonctions purge_old_articles et test_purge_articles"
+    ]
+  },
   {
     version: "1.12.0",
     date: "2026-01-29",

+ 9 - 1
src/utils/youtube.ts

@@ -96,11 +96,19 @@ Méthode alternative :
 // Function to automatically fetch YouTube channel RSS and name
 export const fetchYouTubeRSSUrl = async (url: string): Promise<{rssUrl: string, channelName: string} | null> => {
   try {
+    const { supabase } = await import('@/integrations/supabase/client');
+    const { data: { session } } = await supabase.auth.getSession();
+    
+    if (!session?.access_token) {
+      console.error('No active session for fetch-youtube-rss');
+      return null;
+    }
+
     const response = await fetch(`https://wftyukugedtojizgatwj.supabase.co/functions/v1/fetch-youtube-rss`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
-        'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndmdHl1a3VnZWR0b2ppemdhdHdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkzNjIxNTEsImV4cCI6MjA2NDkzODE1MX0.KflrS6WiGksws1nO8NDm5i_Dav4u2JDSuEYtEnmKCRE`
+        'Authorization': `Bearer ${session.access_token}`
       },
       body: JSON.stringify({ url })
     });

+ 98 - 0
supabase/migrations/20260209135254_2e552754-4888-4426-a9e8-7b225eacab90.sql

@@ -0,0 +1,98 @@
+
+CREATE OR REPLACE FUNCTION public.purge_old_articles()
+ RETURNS TABLE(deleted_count integer, admin_emails text[])
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+  v_deleted_count INTEGER;
+  v_admin_emails TEXT[];
+  v_cutoff_date TIMESTAMP WITH TIME ZONE;
+BEGIN
+  v_cutoff_date := NOW() - INTERVAL '48 hours';
+  
+  SELECT ARRAY_AGG(email) INTO v_admin_emails
+  FROM public.super_users;
+  
+  WITH articles_to_delete AS (
+    SELECT a.id
+    FROM public.articles a
+    WHERE a.last_seen_at < v_cutoff_date
+    -- Exclure les articles épinglés
+    AND NOT EXISTS (
+      SELECT 1 FROM public.user_articles ua
+      WHERE ua.article_id = a.id AND ua.is_pinned = true
+    )
+    -- Exclure les articles non lus dont le flux a au moins un abonné
+    AND NOT (
+      NOT EXISTS (
+        SELECT 1 FROM public.user_articles ua
+        WHERE ua.article_id = a.id AND ua.is_read = true
+      )
+      AND EXISTS (
+        SELECT 1 FROM public.user_feeds uf
+        WHERE uf.feed_id = a.feed_id AND uf.is_followed = true
+      )
+    )
+    LIMIT 1000
+  ),
+  deleted_user_articles AS (
+    DELETE FROM public.user_articles
+    WHERE article_id IN (SELECT id FROM articles_to_delete)
+    RETURNING article_id
+  ),
+  deleted AS (
+    DELETE FROM public.articles
+    WHERE id IN (SELECT id FROM articles_to_delete)
+    RETURNING id
+  )
+  SELECT COUNT(*)::INTEGER INTO v_deleted_count FROM deleted;
+  
+  RAISE NOTICE 'Purge automatique: % articles supprimés (non vus depuis 48h, non épinglés, non protégés par abonnement)', v_deleted_count;
+  
+  RETURN QUERY SELECT v_deleted_count, v_admin_emails;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION public.test_purge_articles()
+ RETURNS TABLE(articles_to_delete integer, oldest_article_date timestamp with time zone, newest_article_date timestamp with time zone, sample_titles text[])
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+  v_cutoff_date TIMESTAMP WITH TIME ZONE;
+BEGIN
+  v_cutoff_date := NOW() - INTERVAL '48 hours';
+  
+  RETURN QUERY
+  WITH eligible_articles AS (
+    SELECT a.id, a.last_seen_at, a.title
+    FROM public.articles a
+    WHERE a.last_seen_at < v_cutoff_date
+    AND NOT EXISTS (
+      SELECT 1 FROM public.user_articles ua
+      WHERE ua.article_id = a.id AND ua.is_pinned = true
+    )
+    AND NOT (
+      NOT EXISTS (
+        SELECT 1 FROM public.user_articles ua
+        WHERE ua.article_id = a.id AND ua.is_read = true
+      )
+      AND EXISTS (
+        SELECT 1 FROM public.user_feeds uf
+        WHERE uf.feed_id = a.feed_id AND uf.is_followed = true
+      )
+    )
+  ),
+  sample_articles AS (
+    SELECT title FROM eligible_articles ORDER BY last_seen_at DESC LIMIT 5
+  )
+  SELECT 
+    (SELECT COUNT(*)::INTEGER FROM eligible_articles),
+    (SELECT MIN(last_seen_at) FROM eligible_articles),
+    (SELECT MAX(last_seen_at) FROM eligible_articles),
+    (SELECT ARRAY_AGG(title) FROM sample_articles);
+END;
+$function$;