diff --git a/client/src/components/ai-chat.tsx b/client/src/components/ai-chat.tsx index c1c9d25..33aec8e 100644 --- a/client/src/components/ai-chat.tsx +++ b/client/src/components/ai-chat.tsx @@ -38,6 +38,9 @@ export default function AiChat() { // Fetch available models from OpenRouter const { data: modelsData, isLoading: modelsLoading } = useQuery<{ data: OpenRouterModel[] }>({ queryKey: ['/api/openrouter/models'], + // Liste servie par un appel sortant vers OpenRouter : elle ne bouge pas d'une + // heure à l'autre, inutile de la redemander à chaque retour sur l'onglet. + staleTime: 60 * 60 * 1000, }); const availableModels = modelsData?.data || []; diff --git a/client/src/components/ongoing-reels.tsx b/client/src/components/ongoing-reels.tsx index 774baa8..8545732 100644 --- a/client/src/components/ongoing-reels.tsx +++ b/client/src/components/ongoing-reels.tsx @@ -24,7 +24,10 @@ interface OngoingReelsProps { export default function OngoingReels({ compact = false }: OngoingReelsProps) { const { data: ongoingPosts = [] } = useQuery({ queryKey: ["/api/reels/ongoing"], - refetchInterval: 3000, // Poll every 3 seconds + // Cette route ne renvoie que les générations en cours : tant qu'elle est + // vide il n'y a pas de barre de progression à animer. On garde une veille + // lente, qui suffit à détecter une génération lancée depuis un autre écran. + refetchInterval: (query) => ((query.state.data?.length ?? 0) > 0 ? 3000 : 30000), }); // Nothing to show — render nothing (not even a card) diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx index b88f4cd..729e568 100644 --- a/client/src/components/sidebar.tsx +++ b/client/src/components/sidebar.tsx @@ -1,7 +1,7 @@ import { Home, PlusCircle, Calendar, Images, Users, Bot, Clock, Settings, Database, UserCog, LogOut, ChevronLeft, ChevronRight, ChevronDown, Wand2, BarChart3, Video, Music, Shield } from "lucide-react"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { apiRequest, queryClient } from "@/lib/queryClient"; +import { apiRequest, AUTH_MUTATION, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -52,6 +52,7 @@ export default function Sidebar({ onLinkClick }: SidebarProps = {}) { }; const logoutMutation = useMutation({ + mutationKey: [AUTH_MUTATION, "logout"], mutationFn: async () => { const res = await apiRequest("POST", "/api/auth/logout", {}); return await res.json(); diff --git a/client/src/lib/queryClient.ts b/client/src/lib/queryClient.ts index aac81aa..bdece4e 100644 --- a/client/src/lib/queryClient.ts +++ b/client/src/lib/queryClient.ts @@ -1,4 +1,4 @@ -import { QueryClient, QueryFunction } from "@tanstack/react-query"; +import { MutationCache, QueryClient, QueryFunction } from "@tanstack/react-query"; // Les routes d'authentification elles-mêmes ne doivent jamais déclencher // de redirection automatique (ex: mauvais mot de passe sur /login). @@ -88,13 +88,42 @@ export const getQueryFn: (options: { return await res.json(); }; +/** + * Marque les mutations d'authentification, exclues de l'invalidation globale + * ci-dessous. À passer en `mutationKey` : `[AUTH_MUTATION, 'logout']`. + */ +export const AUTH_MUTATION = "auth"; + export const queryClient = new QueryClient({ + // Les compteurs du tableau de bord et la liste des posts dérivent de tout le + // reste : publier, supprimer un média, générer un texte… Aucune mutation ne + // peut raisonnablement se souvenir de les invalider, et aucune ne le faisait : + // ils restaient figés jusqu'au rechargement de la page. On les invalide donc + // ici, une bonne fois, après chaque mutation réussie. + mutationCache: new MutationCache({ + onSuccess: (_data, _variables, _context, mutation) => { + // Sauf pour l'authentification : au moment où la déconnexion réussit, la + // session est déjà détruite. Relancer des requêtes ici ne produirait que + // des 401 et une redirection brutale, avant même que le cache soit vidé. + if (mutation.options.mutationKey?.[0] === AUTH_MUTATION) return; + + queryClient.invalidateQueries({ queryKey: ["/api/stats"] }); + queryClient.invalidateQueries({ queryKey: ["/api/posts"] }); + }, + }), defaultOptions: { queries: { queryFn: getQueryFn({ on401: "throw" }), - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: Infinity, + // Le serveur agit de son côté — le scheduler publie toutes les minutes, le + // renouvellement des jetons tourne en tâche de fond — sans que le + // navigateur en soit averti. Avec `staleTime: Infinity`, un écran restait + // sur le cache de sa première visite pour toute la session : d'où la + // nécessité de recharger la page pour voir quoi que ce soit. Une + // péremption courte, plus un rafraîchissement au retour sur l'onglet, + // suffit à garder l'affichage juste sans marteler l'API. + staleTime: 30_000, + refetchOnWindowFocus: true, + refetchOnReconnect: true, retry: false, }, mutations: { diff --git a/client/src/pages/history.tsx b/client/src/pages/history.tsx index 4deed86..1f393e8 100644 --- a/client/src/pages/history.tsx +++ b/client/src/pages/history.tsx @@ -17,6 +17,15 @@ type ScheduledPostWithRelations = ScheduledPost & { page?: SocialPage; }; +/** Publication dont l'heure est passée mais que le scheduler n'a pas encore traitée. */ +function isAwaitingPublication(scheduledPost: ScheduledPostWithRelations): boolean { + return ( + !!scheduledPost.scheduledAt && + new Date(scheduledPost.scheduledAt) <= new Date() && + !scheduledPost.publishedAt + ); +} + export default function History() { const [sidebarOpen, setSidebarOpen] = useState(false); const [previewModalOpen, setPreviewModalOpen] = useState(false); @@ -25,7 +34,11 @@ export default function History() { const { data: scheduledPosts = [], isLoading } = useQuery({ queryKey: ['/api/scheduled-posts'], - refetchInterval: 3000, + // Le scheduler publie en tâche de fond : on ne suit à la seconde que ce qui + // est réellement en attente. Le reste du temps, une veille lente suffit — + // sans quoi la page entière était rechargée 20 fois par minute pour rien. + refetchInterval: (query) => + (query.state.data ?? []).some(isAwaitingPublication) ? 3000 : 30000, }); // Filter attempted posts (scheduled in the past) diff --git a/client/src/pages/mobile/history.tsx b/client/src/pages/mobile/history.tsx index 2798059..01c444e 100644 --- a/client/src/pages/mobile/history.tsx +++ b/client/src/pages/mobile/history.tsx @@ -19,13 +19,25 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +/** Post dont le statut peut changer d'un instant à l'autre. */ +function isChanging(post: any): boolean { + if (post?.generationStatus === 'pending' || post?.generationStatus === 'processing') { + return true; + } + return post?.status === 'scheduled' && !!post?.scheduledFor && new Date(post.scheduledFor) <= new Date(); +} + export default function MobileHistory() { const [searchTerm, setSearchTerm] = useState(""); // Fetch posts history with polling to see status updates const { data: posts } = useQuery({ queryKey: ['/api/posts'], - refetchInterval: 3000, // Poll every 3 seconds + // Un post planifié pour la semaine prochaine ne bougera pas d'ici une + // seconde : on ne suit de près que ce dont le statut peut basculer + // maintenant (génération en cours, ou heure de publication dépassée). + refetchInterval: (query) => + (query.state.data ?? []).some(isChanging) ? 3000 : 30000, }); const getStatusIcon = (status: string) => { diff --git a/client/src/pages/mobile/settings.tsx b/client/src/pages/mobile/settings.tsx index dbaa01e..0edfd1f 100644 --- a/client/src/pages/mobile/settings.tsx +++ b/client/src/pages/mobile/settings.tsx @@ -47,6 +47,9 @@ export default function SettingsMobile() { const { data: openrouterModels, isLoading: modelsLoading } = useQuery({ queryKey: ['/api/openrouter/models'], + // Liste servie par un appel sortant vers OpenRouter : elle ne bouge pas d'une + // heure à l'autre, inutile de la redemander à chaque retour sur l'onglet. + staleTime: 60 * 60 * 1000, }); useEffect(() => { diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index fbbcf25..2adba75 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -55,6 +55,9 @@ export default function Settings() { const { data: openrouterModels, isLoading: modelsLoading } = useQuery({ queryKey: ['/api/openrouter/models'], + // Liste servie par un appel sortant vers OpenRouter : elle ne bouge pas d'une + // heure à l'autre, inutile de la redemander à chaque retour sur l'onglet. + staleTime: 60 * 60 * 1000, }); const { data: ffmpegConfig } = useQuery({