feat: banques centrales (vote+dot plot+previsions), calendrier elargi, fix InvestingLive, M3 auto

Central bank governance (nouvel onglet Banques centrales) :
- lib/centralBankGovernance.ts + app/api/central-bank-sources : scraping live du
  vote de la derniere reunion, dot plot Fed (SEP), et desormais les previsions
  macro (PIB + inflation) publiees par chaque BC elle-meme (Fed SEP, Eurosystem
  staff projections, BoJ Outlook Report PDF, SNB conditional forecast, BoC MPR,
  RBA SMP). GBP/NZD laisses honnetement vides quand aucune source chiffree
  fiable n'est accessible (RBNZ bloque par Cloudflare).
- components/CentralBankSourcesTab.tsx : nouvel onglet avec cards par banque.

Taux directeurs + Money Supply M3 :
- data/rate_decisions.json corrige (JPY, EUR, NZD etc. etaient perimes d'1-2
  decisions) et desormais auto-maintenu : .github/workflows/update-rate-decisions.yml
  (horaire) detecte les changements de taux via Trading Economics et fait
  glisser current -> prev sans perte de donnee.
- data/money-supply-m3.json (nouveau) + .github/workflows/fetch-money-supply.yml
  (hebdo) : M3 par devise (proxy M2 pour l'USD, la Fed ne publiant plus M3
  depuis 2006), affiche dans CurrencyCard.

Calendrier economique elargi :
- lib/calendar-countries.ts, lib/calendar-taxonomy.ts, lib/fxstreetCalendar.ts :
  couverture pays elargie + classification des evenements + source FXStreet.

Fix InvestingLive :
- lib/investinglive.ts : l'ancienne API WordPress (wp-json) renvoyait 404 depuis
  leur migration Nuxt.js -> reecrit vers api.investinglive.com/api/homepage/articles,
  + fix crash silencieux (Tldr pas toujours un tableau).

Divers :
- .vercel/ ajoute au .gitignore (ne doit jamais etre commite, cf. son propre README).
- scripts/ (lancement PWA, push env Vercel), captures d'ecran, cache InvestingLive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
caty21
2026-07-08 16:46:05 +02:00
parent 82a732fbdf
commit 0657b50c07
38 changed files with 3257 additions and 443 deletions
+42 -18
View File
@@ -52,8 +52,8 @@ function loadLS<T>(key: string, fb: T): T {
if (typeof window === "undefined") return fb;
try { const r = localStorage.getItem(key); return r ? JSON.parse(r) as T : fb; } catch { return fb; }
}
function saveLS(key: string, val: unknown) {
try { localStorage.setItem(key, JSON.stringify(val)); } catch {}
function saveLS(key: string, val: unknown): boolean {
try { localStorage.setItem(key, JSON.stringify(val)); return true; } catch { return false; }
}
const DEFAULT_SLOT = (symbol = "FX:EURUSD"): SlotState => ({
@@ -444,7 +444,9 @@ function ArchiveCard({ a, onDelete, onRestore }: {
)}
{/* Carte compacte */}
<div className="border border-slate-700/30 rounded-xl overflow-hidden bg-slate-900/30 hover:bg-slate-800/30 transition-colors">
{/* Pas de overflow-hidden ici : le menu déroulant "Restaurer" est en position
absolute et serait rogné par un ancêtre overflow-hidden (cf. bug signalé). */}
<div className="border border-slate-700/30 rounded-xl bg-slate-900/30 hover:bg-slate-800/30 transition-colors">
{/* Header */}
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-slate-700/20">
<span className="text-[10px] font-bold text-sky-400/80 font-mono">{a.slot.symbol}</span>
@@ -483,18 +485,24 @@ function ArchiveCard({ a, onDelete, onRestore }: {
className="text-[8px] text-slate-600 hover:text-sky-400 transition-colors ml-1 shrink-0"
> Voir</button>
<button
onClick={onDelete}
className="text-[8px] text-red-500/30 hover:text-red-400 transition-colors ml-1 shrink-0"
></button>
onClick={() => { if (window.confirm("Supprimer définitivement cette archive ?")) onDelete(); }}
title="Supprimer définitivement"
className="text-[8px] text-red-400/80 hover:text-red-400 transition-colors ml-1 shrink-0"
> Supprimer</button>
</div>
{/* Corps : texte + images */}
{a.slot.notes && (
<div className="px-4 py-3">
{/* Texte brut (sans tags HTML) */}
{/* Texte brut (sans tags HTML) — les tags de bloc sont convertis en
retours à la ligne avant extraction, sinon .textContent colle tous
les paragraphes/lignes de liste bout à bout sans séparation. */}
{(() => {
const doc = new DOMParser().parseFromString(a.slot.notes, "text/html");
const text = doc.body.textContent?.trim() ?? "";
const withBreaks = a.slot.notes
.replace(/<\/(p|div|li|h[1-6])>/gi, "\n")
.replace(/<br\s*\/?>/gi, "\n");
const doc = new DOMParser().parseFromString(withBreaks, "text/html");
const text = (doc.body.textContent ?? "").replace(/\n{3,}/g, "\n\n").trim();
const imgs = Array.from(doc.images);
return (
<>
@@ -564,6 +572,14 @@ export default function IdeesTab() {
DEFAULT_SLOT("FX:GBPUSD"),
]);
const [archives, setArchives] = useState<Archive[]>([]);
// Passe à true si une écriture localStorage échoue (ex: quota dépassé à cause
// des images en base64 dans les notes archivées) — sans ça l'échec est silencieux
// et une suppression/modification peut sembler ne "pas marcher" après rechargement.
const [saveError, setSaveError] = useState(false);
const persist = useCallback((key: string, val: unknown) => {
setSaveError(!saveLS(key, val));
}, []);
useEffect(() => {
setSlots(loadLS<[SlotState, SlotState]>(LS_SLOTS, [DEFAULT_SLOT("FX:EURUSD"), DEFAULT_SLOT("FX:GBPUSD")]));
@@ -574,30 +590,30 @@ export default function IdeesTab() {
setSlots(prev => {
const next: [SlotState, SlotState] = [prev[0], prev[1]];
next[idx] = s;
saveLS(LS_SLOTS, next);
persist(LS_SLOTS, next);
return next;
});
}, []);
}, [persist]);
const archiveSlot = useCallback((idx: 0 | 1) => {
const entry: Archive = { id: Date.now().toString(), savedAt: new Date().toISOString(), slot: slots[idx] };
const next = [entry, ...archives];
setArchives(next);
saveLS(LS_ARCHIVES, next);
persist(LS_ARCHIVES, next);
const reset = DEFAULT_SLOT(idx === 0 ? "FX:EURUSD" : "FX:GBPUSD");
setSlots(prev => {
const n: [SlotState, SlotState] = [prev[0], prev[1]];
n[idx] = reset;
saveLS(LS_SLOTS, n);
persist(LS_SLOTS, n);
return n;
});
}, [slots, archives]);
}, [slots, archives, persist]);
const deleteArchive = useCallback((id: string) => {
const next = archives.filter(a => a.id !== id);
setArchives(next);
saveLS(LS_ARCHIVES, next);
}, [archives]);
persist(LS_ARCHIVES, next);
}, [archives, persist]);
const restoreArchive = useCallback((id: string, slotIdx: 0 | 1) => {
const entry = archives.find(a => a.id === id);
@@ -605,10 +621,10 @@ export default function IdeesTab() {
setSlots(prev => {
const next = [...prev] as [SlotState, SlotState];
next[slotIdx] = { ...entry.slot };
saveLS(LS_SLOTS, next);
persist(LS_SLOTS, next);
return next;
});
}, [archives]);
}, [archives, persist]);
return (
<div className="space-y-4">
@@ -619,6 +635,14 @@ export default function IdeesTab() {
<div className="h-px flex-1 bg-purple-500/20" />
</div>
{saveError && (
<div className="text-[10px] text-red-300 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2">
Échec de sauvegarde locale le quota de stockage du navigateur est probablement dépassé
(les archives avec images occupent beaucoup de place). Supprime quelques archives pour libérer
de la place, sinon tes changements ne seront pas conservés au rechargement.
</div>
)}
<ResearchSlot slot={slots[0]} label="Recherche A" onChange={s => updateSlot(0, s)} onArchive={() => archiveSlot(0)} />
<ResearchSlot slot={slots[1]} label="Recherche B" onChange={s => updateSlot(1, s)} onArchive={() => archiveSlot(1)} />