feat(ops): VersionWatcher — toast prompt when a new deploy is live
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ă: <shortSha> · 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
<FeatureFlagProvider flagDefinitions={DEFAULT_FLAGS}>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<VersionWatcher />
|
||||
</AuthProvider>
|
||||
</FeatureFlagProvider>
|
||||
</StorageProvider>
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [latest, setLatest] = useState<VersionPayload | null>(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 (
|
||||
<div
|
||||
role="status"
|
||||
className="fixed bottom-4 right-4 z-[60] flex items-center gap-2 rounded-lg border bg-background/95 px-3 py-2 shadow-lg backdrop-blur animate-in fade-in slide-in-from-bottom-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 text-primary" />
|
||||
<div className="flex flex-col text-xs">
|
||||
<span className="font-medium">Versiune nouă disponibilă</span>
|
||||
<span className="text-muted-foreground">
|
||||
<code className="font-mono">{latest.commitShort}</code> · apasă pentru reîncărcare
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="ml-2 rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Reîncarcă
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDismissed(true)}
|
||||
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label="Închide"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user