mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-22 00:48:06 +00:00
feat: persistance localStorage + stale-if-error serveur
Client — lib/localCache.ts (nouveau) :
- saveCache / loadCache : wrapper localStorage préfixé "forex_v1_"
- formatCacheDate : affiche l'âge du cache (< 1h, 2h, 3j…)
Client — CurrencyCard.tsx :
- Sauvegarde chaque réponse macro réussie dans localStorage (clé macro_{currency})
- Si l'API échoue → charge la dernière valeur connue depuis localStorage
- Badge 🗄 "cache Xh" visible dans le header de la carte quand données stale
Client — page.tsx :
- Même logique pour drivers, expectations, yields
- Badge cache affiché dans le header à côté de l'heure
- Footer : Bytez → Groq
Serveur — macro/route.ts :
- stale-if-error : si tous les indicateurs reviennent null (panne API)
et qu'un cache serveur précédent existe → le renvoie avec stale:true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ecb815619b
commit
3c6e3bb52a
@@ -178,7 +178,10 @@ export async function GET(req: NextRequest) {
|
||||
if (!series) return NextResponse.json({ error: "Unknown currency" }, { status: 400 });
|
||||
|
||||
const cached = _cache.get(currency);
|
||||
// Sert le cache pendant 24h sans re-fetcher
|
||||
if (cached && Date.now() - cached.ts < 86_400_000) return NextResponse.json(cached.data);
|
||||
// Garde une référence au cache précédent pour fallback stale-if-error
|
||||
const staleCache = cached ?? null;
|
||||
|
||||
const key = process.env.FRED_API_KEY;
|
||||
if (!key) return NextResponse.json({ error: "FRED_API_KEY missing" }, { status: 500 });
|
||||
@@ -265,6 +268,13 @@ export async function GET(req: NextRequest) {
|
||||
indicators.pmiMfg = toPmiIndicator(pmiMfgRaw);
|
||||
indicators.pmiServices = toPmiIndicator(pmiSvcRaw);
|
||||
|
||||
// Stale-if-error : si tous les indicateurs sont null (API en panne),
|
||||
// on renvoie le cache précédent plutôt que des tirets vides.
|
||||
const hasAnyValue = Object.values(indicators).some((v) => v !== null);
|
||||
if (!hasAnyValue && staleCache) {
|
||||
return NextResponse.json({ ...staleCache.data, stale: true });
|
||||
}
|
||||
|
||||
const data = { currency, indicators, fetchedAt: new Date().toISOString() };
|
||||
_cache.set(currency, { data, ts: Date.now() });
|
||||
return NextResponse.json(data);
|
||||
|
||||
+45
-8
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { RefreshCw, TrendingUp, AlertTriangle, Zap } from "lucide-react";
|
||||
import { RefreshCw, TrendingUp, AlertTriangle, Zap, Database } from "lucide-react";
|
||||
import { CURRENCIES, CURRENCY_META } from "@/lib/constants";
|
||||
import type { Currency, DriverData } from "@/lib/types";
|
||||
import { saveCache, loadCache, formatCacheDate } from "@/lib/localCache";
|
||||
import CurrencyCard from "@/components/CurrencyCard";
|
||||
import DriversBar from "@/components/DriversBar";
|
||||
|
||||
@@ -16,6 +17,8 @@ export default function Dashboard() {
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeDivergences, setActiveDivergences] = useState<{ currency: Currency; score: number }[]>([]);
|
||||
const [driversFromCache, setDriversFromCache] = useState(false);
|
||||
const [driversCacheAge, setDriversCacheAge] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -27,16 +30,44 @@ export default function Dashboard() {
|
||||
fetch("/api/fx").then((r) => r.json()),
|
||||
]);
|
||||
|
||||
if (driversRes.status === "fulfilled") {
|
||||
const driversData = driversRes.value;
|
||||
// Merge DXY from /api/fx into drivers
|
||||
// ── Drivers (marchés globaux) ──────────────────────────────────────────
|
||||
if (driversRes.status === "fulfilled" && !driversRes.value?.error) {
|
||||
const driversData = driversRes.value as DriverData;
|
||||
if (fxRes.status === "fulfilled" && fxRes.value?.dxy != null) {
|
||||
driversData.dxy = fxRes.value.dxy;
|
||||
}
|
||||
setDrivers(driversData);
|
||||
setDriversFromCache(false);
|
||||
setDriversCacheAge(null);
|
||||
saveCache("drivers", driversData);
|
||||
} else {
|
||||
// Fallback localStorage
|
||||
const cached = loadCache<DriverData>("drivers");
|
||||
if (cached) {
|
||||
setDrivers(cached.data);
|
||||
setDriversFromCache(true);
|
||||
setDriversCacheAge(formatCacheDate(cached.savedAt));
|
||||
}
|
||||
}
|
||||
if (expectRes.status === "fulfilled") setExpectations(expectRes.value);
|
||||
if (yieldsRes.status === "fulfilled") setYields(yieldsRes.value);
|
||||
|
||||
// ── Attentes de taux ──────────────────────────────────────────────────
|
||||
if (expectRes.status === "fulfilled" && !expectRes.value?.error) {
|
||||
setExpectations(expectRes.value);
|
||||
saveCache("expectations", expectRes.value);
|
||||
} else {
|
||||
const cached = loadCache<Record<string, unknown>>("expectations");
|
||||
if (cached) setExpectations(cached.data);
|
||||
}
|
||||
|
||||
// ── Rendements obligataires ───────────────────────────────────────────
|
||||
if (yieldsRes.status === "fulfilled" && !yieldsRes.value?.error) {
|
||||
setYields(yieldsRes.value);
|
||||
saveCache("yields", yieldsRes.value);
|
||||
} else {
|
||||
const cached = loadCache<typeof yields>("yields");
|
||||
if (cached && cached.data) setYields(cached.data);
|
||||
}
|
||||
|
||||
setLastRefresh(new Date());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -82,7 +113,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
{driversFromCache && driversCacheAge && (
|
||||
<span className="flex items-center gap-0.5 text-amber-500" title="Marchés affichés depuis le cache local — API indisponible">
|
||||
<Database size={11} />
|
||||
<span>cache {driversCacheAge}</span>
|
||||
</span>
|
||||
)}
|
||||
{lastRefresh.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
|
||||
@@ -140,7 +177,7 @@ export default function Dashboard() {
|
||||
Sources: FRED · ECB · BoE · BoC · CFTC · Frankfurter · OANDA · investinglive.com
|
||||
</p>
|
||||
<p>
|
||||
LLM: Bytez (Llama 3.1) · Données à titre informatif uniquement — pas de conseil financier
|
||||
LLM: Groq (Llama 3.1) · Données à titre informatif uniquement — pas de conseil financier
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user