mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 14:28:47 +02:00
Sync Status: per-source ingestion health page with nav warning badge
The n8n flows fail invisibly from the app's perspective (review UX-17, the top trust gap). /api/v1/sync-status derives last-received + cadence thresholds for BAC, salary, municipal, pensions and exchange rate from existing data — no n8n integration needed. New Sincronización page shows per-source freshness and a hint to check n8n; the sidebar item gets an amber dot when any source is stale. Verified live against the dev DB (correctly flags its stale BAC data at 43 days). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
118
frontend/src/pages/SyncStatus.tsx
Normal file
118
frontend/src/pages/SyncStatus.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { getSyncStatus, type SyncSource } from '@/lib/api';
|
||||
import { formatRelativeAge } from '@/lib/dates';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import ErrorState from '@/components/ErrorState';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
function StatusIcon({ status }: { status: SyncSource['status'] }) {
|
||||
if (status === 'ok')
|
||||
return <CheckCircle2 className="w-5 h-5 text-emerald-500" aria-hidden="true" />;
|
||||
if (status === 'warning')
|
||||
return <AlertTriangle className="w-5 h-5 text-amber-500" aria-hidden="true" />;
|
||||
return <HelpCircle className="w-5 h-5 text-muted-foreground" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
function statusLabel(s: SyncSource): string {
|
||||
if (s.status === 'never') return 'Nunca recibido';
|
||||
if (s.status === 'warning')
|
||||
return `Sin datos nuevos (esperado cada ~${s.warn_after_days} días)`;
|
||||
return 'Al día';
|
||||
}
|
||||
|
||||
export default function SyncStatus() {
|
||||
const query = useQuery({
|
||||
queryKey: ['sync-status'],
|
||||
queryFn: () => getSyncStatus().then((r) => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<RefreshCw className="w-6 h-6 text-primary" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Sincronización</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Estado de los flujos automáticos de datos (n8n y tipo de cambio)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => query.refetch()}
|
||||
disabled={query.isFetching}
|
||||
>
|
||||
<RefreshCw
|
||||
className={query.isFetching ? 'w-4 h-4 mr-2 animate-spin' : 'w-4 h-4 mr-2'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{query.isError ? (
|
||||
<ErrorState
|
||||
message="No se pudo consultar el estado de sincronización"
|
||||
onRetry={() => query.refetch()}
|
||||
/>
|
||||
) : query.isPending ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<Skeleton key={i} className="h-20 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{query.data.sources.map((s) => (
|
||||
<Card
|
||||
key={s.key}
|
||||
className={
|
||||
s.status !== 'ok' ? 'border-amber-500/40' : undefined
|
||||
}
|
||||
>
|
||||
<CardContent className="p-4 flex items-center gap-4">
|
||||
<StatusIcon status={s.status} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{s.label}</span>
|
||||
{s.status !== 'ok' && (
|
||||
<Badge variant="destructive">Atención</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{s.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className="text-sm font-medium">{statusLabel(s)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{s.last_received
|
||||
? `Último: ${formatRelativeAge(s.last_received)}`
|
||||
: '—'}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Si una fuente aparece atrasada, revisá el flujo correspondiente en
|
||||
n8n — un cambio de formato en los correos del banco es la causa más
|
||||
común.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user