From 382940112fea9fda0d734ee1386ffdfd9dff6f92 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Mon, 18 May 2026 23:16:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(ops):=20VersionWatcher=20=E2=80=94=20toast?= =?UTF-8?q?=20prompt=20when=20a=20new=20deploy=20is=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client-side polling component mounted in providers.tsx. At mount, captures the initial commit from /api/version. Every 60s, re-checks. If commit differs from the captured one → renders a dismissible toast in the bottom-right offering a hard reload. Useful because Next.js bundles cache per commit hash → after a deploy users would otherwise keep running the old client until they manually refresh. Now they get a discoverable nudge. Banner UX: - "Versiune nouă disponibilă: · apasă pentru reîncărcare" - [Reîncarcă] button (window.location.reload) - [X] dismiss for current page life - Tailwind animate-in fade slide-from-bottom Polling interval 60s is fine for our deploy frequency; cheap (one GET per minute, ~150 bytes). Cache-busted with cache: "no-store". Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/providers.tsx | 2 + src/core/version/version-watcher.tsx | 76 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/core/version/version-watcher.tsx diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 975b8c4..fab14ea 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -5,6 +5,7 @@ import { I18nProvider } from '@/core/i18n'; import { StorageProvider } from '@/core/storage'; import { FeatureFlagProvider } from '@/core/feature-flags'; import { AuthProvider } from '@/core/auth'; +import { VersionWatcher } from '@/core/version/version-watcher'; import { DEFAULT_FLAGS } from '@/config/flags'; // Ensure module registry is populated @@ -22,6 +23,7 @@ export function Providers({ children }: ProvidersProps) { {children} + diff --git a/src/core/version/version-watcher.tsx b/src/core/version/version-watcher.tsx new file mode 100644 index 0000000..964dbd2 --- /dev/null +++ b/src/core/version/version-watcher.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { RefreshCw, X } from "lucide-react"; + +const POLL_INTERVAL_MS = 60_000; +const ENDPOINT = "/api/version"; + +type VersionPayload = { commit: string; commitShort: string; buildTime: string }; + +export function VersionWatcher() { + const initialRef = useRef(null); + const [latest, setLatest] = useState(null); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + let cancelled = false; + + const fetchVersion = async () => { + try { + const res = await fetch(ENDPOINT, { cache: "no-store" }); + if (!res.ok || cancelled) return; + const data = (await res.json()) as VersionPayload; + if (!data?.commit) return; + if (initialRef.current === null) { + initialRef.current = data.commit; + return; + } + if (data.commit !== initialRef.current) { + setLatest(data); + } + } catch { + // network noise — ignore + } + }; + + fetchVersion(); + const interval = setInterval(fetchVersion, POLL_INTERVAL_MS); + return () => { + cancelled = true; + clearInterval(interval); + }; + }, []); + + if (!latest || dismissed) return null; + + return ( +
+ +
+ Versiune nouă disponibilă + + {latest.commitShort} · apasă pentru reîncărcare + +
+ + +
+ ); +}