mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 15: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:
@@ -11,12 +11,16 @@ import {
|
||||
TrendingUp,
|
||||
Wallet,
|
||||
Menu,
|
||||
RefreshCw,
|
||||
Sun,
|
||||
Moon,
|
||||
Eye,
|
||||
EyeOff,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getSyncStatus } from "@/lib/api";
|
||||
import { useTheme } from "@/contexts/theme-context";
|
||||
import { usePrivacy } from "@/contexts/privacy-context";
|
||||
import { useAuth } from "@/AuthContext";
|
||||
@@ -39,7 +43,10 @@ interface NavSection {
|
||||
const navSections: NavSection[] = [
|
||||
{
|
||||
label: "General",
|
||||
items: [{ to: "/asistente", icon: Sparkles, label: "Asistente" }],
|
||||
items: [
|
||||
{ to: "/asistente", icon: Sparkles, label: "Asistente" },
|
||||
{ to: "/sync", icon: RefreshCw, label: "Sincronización" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Finanzas",
|
||||
@@ -64,6 +71,14 @@ function SidebarNav({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const isActive = (to: string) =>
|
||||
pathname === to || pathname.startsWith(`${to}/`);
|
||||
|
||||
// Surface stalled ingestion sources as a dot on the Sincronización item.
|
||||
const syncQ = useQuery({
|
||||
queryKey: ['sync-status'],
|
||||
queryFn: () => getSyncStatus().then((r) => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
const syncWarnings = syncQ.data?.warnings ?? 0;
|
||||
|
||||
return (
|
||||
<nav className="flex flex-col gap-0.5 px-3">
|
||||
{navSections.map((section) => (
|
||||
@@ -85,6 +100,13 @@ function SidebarNav({ onNavigate }: { onNavigate?: () => void }) {
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{label}
|
||||
{to === "/sync" && syncWarnings > 0 && (
|
||||
<span
|
||||
className="ml-auto w-2 h-2 rounded-full bg-amber-500"
|
||||
title={`${syncWarnings} fuente(s) atrasada(s)`}
|
||||
aria-label={`${syncWarnings} fuentes de datos atrasadas`}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -129,10 +151,10 @@ export default function Layout() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={togglePrivacy} title="Toggle privacy mode" aria-label="Toggle privacy mode">
|
||||
<Button variant="ghost" size="icon" onClick={togglePrivacy} title="Toggle privacy mode" aria-label="Toggle privacy mode" aria-pressed={privacyMode}>
|
||||
{privacyMode ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={toggleTheme} title="Toggle theme" aria-label="Toggle theme">
|
||||
<Button variant="ghost" size="icon" onClick={toggleTheme} title="Toggle theme" aria-label="Toggle theme" aria-pressed={theme === "dark"}>
|
||||
{theme === "dark" ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user