From 886a5b426f76d5c744db39a8646498d85ae565f0 Mon Sep 17 00:00:00 2001 From: caty21 Date: Sat, 27 Jun 2026 15:19:35 +0200 Subject: [PATCH] fix: force-dynamic on all API routes + PWA manifest & service worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add export const dynamic = "force-dynamic" to routes previously pre-rendered as static (rate-probabilities, news, fx, yields, calendar) → fixes empty data on Vercel where static pre-render got HTTP 403 - Add PWA: manifest.json, sw.js, icons, layout meta + apple-touch-icon Co-Authored-By: Claude Sonnet 4.6 --- app/api/calendar/route.ts | 2 ++ app/api/fx/route.ts | 2 ++ app/api/news/route.ts | 2 ++ app/api/rate-probabilities/route.ts | 2 ++ app/api/yields/route.ts | 2 ++ app/layout.tsx | 25 ++++++++++++++++++-- public/icons/icon-192.svg | 4 ++++ public/icons/icon-512.svg | 4 ++++ public/manifest.json | 24 +++++++++++++++++++ public/sw.js | 36 +++++++++++++++++++++++++++++ 10 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 public/icons/icon-192.svg create mode 100644 public/icons/icon-512.svg create mode 100644 public/manifest.json create mode 100644 public/sw.js diff --git a/app/api/calendar/route.ts b/app/api/calendar/route.ts index 98172f3..8c7fb26 100644 --- a/app/api/calendar/route.ts +++ b/app/api/calendar/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import rateDecisionsData from "@/data/rate_decisions.json"; import { fetchFFEvents, nextWeekAvailable } from "@/lib/forexfactory"; + +export const dynamic = "force-dynamic"; import type { FFEvent } from "@/lib/forexfactory"; import type { Currency } from "@/lib/types"; import { fetchAllCBPaths, extractMeetingEvents } from "@/lib/rateprobability"; diff --git a/app/api/fx/route.ts b/app/api/fx/route.ts index 89a53fb..c821e9a 100644 --- a/app/api/fx/route.ts +++ b/app/api/fx/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from "next/server"; +export const dynamic = "force-dynamic"; + const CURRENCIES = ["EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "SEK"]; const AV_BASE = "https://www.alphavantage.co/query"; diff --git a/app/api/news/route.ts b/app/api/news/route.ts index f4b0062..fa764d7 100644 --- a/app/api/news/route.ts +++ b/app/api/news/route.ts @@ -2,6 +2,8 @@ import { NextResponse } from "next/server"; import { fetchAllNews } from "@/lib/newsfeed"; import type { NewsItem } from "@/lib/newsfeed"; +export const dynamic = "force-dynamic"; + export type { NewsItem } from "@/lib/newsfeed"; let _cache: { data: NewsItem[]; ts: number } | null = null; diff --git a/app/api/rate-probabilities/route.ts b/app/api/rate-probabilities/route.ts index 583d540..71222dd 100644 --- a/app/api/rate-probabilities/route.ts +++ b/app/api/rate-probabilities/route.ts @@ -2,6 +2,8 @@ import { NextResponse } from "next/server"; import { fetchAllCBPaths } from "@/lib/rateprobability"; import type { RateProbData } from "@/lib/rateprobability"; +export const dynamic = "force-dynamic"; + export type { RateProbData, CBRatePath, RateProbMeeting } from "@/lib/rateprobability"; export interface RateProbabilitiesResponse { diff --git a/app/api/yields/route.ts b/app/api/yields/route.ts index c691c0b..21cac51 100644 --- a/app/api/yields/route.ts +++ b/app/api/yields/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { fetchTEBondYields } from "@/lib/tebonds"; +export const dynamic = "force-dynamic"; + // Variation % d'un pair FX vs clôture J-1 (Yahoo Finance, cache 5 min) // Valeur positive = devise X plus forte vs USD (ou USD plus fort si pair inversé) async function fxChangePct(symbol: string, invert = false): Promise { diff --git a/app/layout.tsx b/app/layout.tsx index d1a7cc9..efbf010 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,36 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "./globals.css"; +import Script from "next/script"; export const metadata: Metadata = { title: "Forex Macro Dashboard", description: "Tableau de bord macroéconomique Forex — 8 devises majeures", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "FX Dashboard", + }, +}; + +export const viewport: Viewport = { + themeColor: "#090e1a", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + + + + {children} + + ); } diff --git a/public/icons/icon-192.svg b/public/icons/icon-192.svg new file mode 100644 index 0000000..7ace25e --- /dev/null +++ b/public/icons/icon-192.svg @@ -0,0 +1,4 @@ + + + FX + diff --git a/public/icons/icon-512.svg b/public/icons/icon-512.svg new file mode 100644 index 0000000..fea5ade --- /dev/null +++ b/public/icons/icon-512.svg @@ -0,0 +1,4 @@ + + + FX + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..b0f3b23 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "Forex Macro Dashboard", + "short_name": "FX Dashboard", + "description": "Tableau de bord macroéconomique Forex — 8 devises majeures", + "start_url": "/", + "display": "standalone", + "background_color": "#090e1a", + "theme_color": "#090e1a", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon-192.svg", + "sizes": "192x192", + "type": "image/svg+xml", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-512.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..b845fe6 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,36 @@ +const CACHE = "fxdash-v1"; +const STATIC = ["/", "/manifest.json", "/icons/icon-192.svg", "/icons/icon-512.svg"]; + +self.addEventListener("install", (e) => { + e.waitUntil(caches.open(CACHE).then((c) => c.addAll(STATIC))); + self.skipWaiting(); +}); + +self.addEventListener("activate", (e) => { + e.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (e) => { + // Only cache GET requests for same-origin static assets; pass API calls through + const url = new URL(e.request.url); + if (e.request.method !== "GET") return; + if (url.pathname.startsWith("/api/")) return; // always fresh for API + + e.respondWith( + caches.match(e.request).then((cached) => { + const network = fetch(e.request).then((res) => { + if (res.ok && res.type === "basic") { + const clone = res.clone(); + caches.open(CACHE).then((c) => c.put(e.request, clone)); + } + return res; + }); + return cached || network; + }) + ); +});