fix(ui): message clair quand l'onglet est périmé après un déploiement

« Server Action "…" was not found on the server » n'est pas une panne du
snapshot : Next.js identifie chaque Server Action par un hash calculé à la
compilation, et un onglet ouvert avant un nouveau build continue d'envoyer les
anciens identifiants. La seule issue est de recharger la page.

- src/lib/stale-action.ts : détection de cette erreur + message utilisateur.
- Grille et Snapshots : au lieu d'afficher l'erreur technique brute, on propose
  « Recharger la page ». Les brouillons de la Grille sont persistés
  (localStorage), recharger ne perd rien.
- SuccessModal accepte `variant="error"` : icône d'alerte rouge au lieu de la
  coche verte, et surtout plus de fermeture automatique au bout de 3 s — un
  message d'échec doit rester lisible. Les erreurs s'affichaient jusqu'ici sous
  une coche de succès.
- SuccessModal accepte une action secondaire optionnelle, et joue son animation
  d'entrée en CSS au lieu d'un setState synchrone dans un effet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
Claude committed 2026-07-29 12:40:16 +00:00
1 parent a1a95fa290
commit b2149ad0a6
4 files changed
+149 -33

No files matched your search

@@ -7,6 +7,7 @@ import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera, ChevronDown } fro
import { useMemo, useState, useTransition } from "react";
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
import { SuccessModal } from "@/components/shared/success-modal";
import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
import {
DropdownMenu,
DropdownMenuContent,
@@ -37,7 +38,13 @@ export function FloatingSummaryBar() {
const [isPending, startTransition] = useTransition();
const [isSavingSnapshot, setIsSavingSnapshot] = useState(false);
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({
const [modal, setModal] = useState<{
isOpen: boolean;
title: string;
message: string;
variant?: "success" | "error";
action?: { label: string; onClick: () => void };
}>({
isOpen: false,
title: "",
message: ""
@@ -118,12 +125,26 @@ export function FloatingSummaryBar() {
} catch (err) {
console.error(err);
if (!labelOverride) {
const msg = err instanceof Error ? err.message : String(err);
setModal({
isOpen: true,
title: "Erreur Snapshot",
message: `Impossible de créer le snapshot : ${msg}`
});
// Onglet resté ouvert pendant un déploiement : les identifiants de
// Server Action ont changé côté serveur. Rien à réparer côté
// métier, il faut recharger le bundle client.
if (isStaleServerActionError(err)) {
setModal({
isOpen: true,
title: "Page à recharger",
message: STALE_ACTION_MESSAGE,
variant: "error",
action: { label: "Recharger la page", onClick: () => window.location.reload() },
});
} else {
const msg = err instanceof Error ? err.message : String(err);
setModal({
isOpen: true,
title: "Erreur Snapshot",
message: `Impossible de créer le snapshot : ${msg}`,
variant: "error",
});
}
}
} finally {
setIsSavingSnapshot(false);
@@ -327,6 +348,8 @@ export function FloatingSummaryBar() {
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
title={modal.title}
message={modal.message}
variant={modal.variant}
action={modal.action}
/>
</div>
);