From 25bf0981bd3f34a522763c3866f1ff1d782bdd5f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 06:22:21 +0000 Subject: [PATCH] =?UTF-8?q?perf(client):=20rafra=C3=AEchir=20les=20donn?= =?UTF-8?q?=C3=A9es=20sans=20recharger=20la=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `staleTime: Infinity` combiné à `refetchOnWindowFocus: false` empêchait toute revalidation : une donnée chargée à la première visite restait affichée pour toute la session. Or le serveur agit de son côté — le scheduler publie chaque minute, les jetons se renouvellent en tâche de fond — sans que le navigateur en soit averti. D'où la nécessité de recharger la page pour voir quoi que ce soit. Les compteurs du tableau de bord étaient le cas le plus visible : /api/stats et /api/posts n'étaient invalidés par aucune mutation de l'application, et ne pouvaient donc plus bouger du tout. - Péremption courte (30 s) et rafraîchissement au retour sur l'onglet ou sur reconnexion, en remplacement du cache éternel. - /api/stats et /api/posts, qui dérivent de tout le reste, sont invalidés depuis le MutationCache après chaque mutation réussie : aucune mutation ne peut raisonnablement se souvenir de le faire, et aucune ne le faisait. Les mutations d'authentification en sont exclues, la session étant déjà détruite quand la déconnexion aboutit. - Les trois sondages à 3 s (historique desktop et mobile, reels en cours) devenaient inutiles hors traitement en cours. Ils ne s'emballent plus que lorsqu'il y a effectivement quelque chose à suivre — une publication dont l'heure est passée, une génération en route — et retombent à 30 s sinon. La page Historique rechargeait tout l'historique 20 fois par minute. - La liste des modèles OpenRouter, servie par un appel sortant, garde une péremption d'une heure pour ne pas être redemandée à chaque retour d'onglet. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qns6Gcpd95bCHWUxySh4Ew --- client/src/components/ai-chat.tsx | 3 ++ client/src/components/ongoing-reels.tsx | 5 +++- client/src/components/sidebar.tsx | 3 +- client/src/lib/queryClient.ts | 37 ++++++++++++++++++++++--- client/src/pages/history.tsx | 15 +++++++++- client/src/pages/mobile/history.tsx | 14 +++++++++- client/src/pages/mobile/settings.tsx | 3 ++ client/src/pages/settings.tsx | 3 ++ 8 files changed, 75 insertions(+), 8 deletions(-) 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({