feat: détection drift Sheets sur le dashboard (approche A)

- /api/dashboard/stats appelle Google Drive API pour récupérer modifiedTime du Sheets
- Compare avec createdAt de la dernière SyncRun → sheetDrift: boolean
- Dashboard affiche un bandeau amber cliquable si Sheets plus récent que la dernière sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:14:06 +02:00
parent 6f4affcd66
commit 1b07359415
2 changed files with 45 additions and 1 deletions
+18
View File
@@ -23,6 +23,8 @@ interface LastSync {
interface DashboardStats { interface DashboardStats {
products: ProductStats products: ProductStats
lastSync: LastSync | null lastSync: LastSync | null
sheetDrift: boolean
sheetModifiedAt: string | null
} }
function StatCard({ function StatCard({
@@ -134,6 +136,22 @@ export default function DashboardPage() {
<div className="p-6 flex flex-col gap-8 flex-1 overflow-y-auto"> <div className="p-6 flex flex-col gap-8 flex-1 overflow-y-auto">
{/* ── Alerte drift Sheets ───────────────────────────────── */}
{!statsLoading && stats?.sheetDrift && (
<Link href="/sync" className="flex items-center gap-3 bg-amber-950/40 border border-amber-600/50 rounded-xl px-4 py-3 hover:bg-amber-950/60 transition-colors group">
<span className="text-xl"></span>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-amber-300">Google Sheets modifié depuis la dernière sync</p>
<p className="text-xs text-amber-500 mt-0.5">
{stats.sheetModifiedAt
? `Dernière modification : ${formatDate(stats.sheetModifiedAt)}`
: 'Des changements sont peut-être en attente de synchronisation'}
</p>
</div>
<span className="text-xs text-amber-400 group-hover:text-amber-300 whitespace-nowrap">Lancer la sync </span>
</Link>
)}
{/* ── KPIs ─────────────────────────────────────────────── */} {/* ── KPIs ─────────────────────────────────────────────── */}
<section> <section>
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Vue d&apos;ensemble</h2> <h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Vue d&apos;ensemble</h2>
+27 -1
View File
@@ -43,18 +43,36 @@ async function fetchProductsLight(status: 'active' | 'draft'): Promise<LightProd
return all return all
} }
async function fetchSheetModifiedTime(): Promise<string | null> {
const apiKey = process.env.GOOGLE_API_KEY
const sheetId = process.env.GOOGLE_SHEETS_ID
if (!apiKey || !sheetId) return null
try {
const res = await fetch(
`https://www.googleapis.com/drive/v3/files/${sheetId}?fields=modifiedTime&key=${apiKey}`,
{ cache: 'no-store' },
)
if (!res.ok) return null
const data = await res.json()
return (data.modifiedTime as string) ?? null
} catch {
return null
}
}
export async function GET() { export async function GET() {
const session = await getServerSession(authOptions) const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }) if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
try { try {
const [activeProducts, draftProducts, lastSyncRun] = await Promise.all([ const [activeProducts, draftProducts, lastSyncRun, sheetModifiedTime] = await Promise.all([
fetchProductsLight('active'), fetchProductsLight('active'),
fetchProductsLight('draft'), fetchProductsLight('draft'),
prisma.syncRun.findFirst({ prisma.syncRun.findFirst({
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: { user: { select: { name: true } } }, include: { user: { select: { name: true } } },
}), }),
fetchSheetModifiedTime(),
]) ])
const allProducts = [...activeProducts, ...draftProducts] const allProducts = [...activeProducts, ...draftProducts]
@@ -72,6 +90,12 @@ export async function GET() {
} }
: null : null
// Drift : Sheets modifié après la dernière sync réussie
const sheetDrift =
sheetModifiedTime !== null &&
lastSyncRun !== null &&
new Date(sheetModifiedTime) > lastSyncRun.createdAt
return NextResponse.json({ return NextResponse.json({
products: { products: {
active: activeProducts.length, active: activeProducts.length,
@@ -80,6 +104,8 @@ export async function GET() {
withoutImages, withoutImages,
}, },
lastSync, lastSync,
sheetDrift,
sheetModifiedAt: sheetModifiedTime,
}) })
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue' const message = err instanceof Error ? err.message : 'Erreur inconnue'