diff --git a/site/docs/CURRENCY_RATE_FRESHNESS_2026-10-25.md b/site/docs/CURRENCY_RATE_FRESHNESS_2026-10-25.md new file mode 100644 index 0000000..5df4847 --- /dev/null +++ b/site/docs/CURRENCY_RATE_FRESHNESS_2026-10-25.md @@ -0,0 +1,16 @@ +# Toolify365 FX rate freshness (2026-10-25) + +**Observed source risk:** `site/src/data/rates.json` fallback is dated 2026-09-03. `scripts/fetch-rates.ts` fetches live reference rates at prebuild and intentionally retains fallback on upstream network failure. The GitHub fallback date **does not prove** production currently serves stale rates; live builds may have refreshed successfully. + +## Fixed +- The 31-currency calculator now shows a prominent, translated warning when embedded reference rates are >=7 days old (or malformed/future-dated), rather than relying solely on a small ordinary disclaimer. +- All 17 locale JSON files have `note.staleWarning` with a date placeholder; no bank account details or payment data collected. +- Prebuild `node scripts/check-currency-rate-freshness.mjs` validates USD base, finite positive rates, date and all localized warning strings. Old fallback rates produce a **warning**, not a failed global website build. `npm run check:currency` provides an independent check. +- Calculator formulas/amount handling, SEO indexability, robots, sitemaps and conversion rates were not rewritten. No unverified "live" conversion claim is made. + +## Release checks +1. From `site/`: `npm run fetch:rates && npm run check:currency && npm run build`. +2. If the rate endpoint is unavailable and the fallback is older than 7 days, the UI must show the stale warning in all locales (test date 2026-09-03). +3. If the prebuild fetch succeeds with a current date, warning should not appear. The ordinary reference-rate date disclaimer remains. +4. Verify on real production `/en/tools/currency-converter` and `/ja/tools/currency-converter`; changes merged to GitHub do not prove live deployment. +5. Rates are indicative and bank/exchange spread differs; never make direct financial decisions on stale numbers. diff --git a/site/package.json b/site/package.json index 371dca5..57d0f1a 100644 --- a/site/package.json +++ b/site/package.json @@ -9,7 +9,7 @@ "lint": "next lint", "typecheck": "tsc --noEmit", "fetch:rates": "tsx scripts/fetch-rates.ts", - "prebuild": "tsx scripts/fetch-rates.ts && node scripts/check-pressure-reference.mjs", + "prebuild": "tsx scripts/fetch-rates.ts && node scripts/check-pressure-reference.mjs && node scripts/check-currency-rate-freshness.mjs", "sitemap": "tsx scripts/generate-sitemap.ts", "validate": "tsx scripts/validate.ts", "audit:seo": "tsx scripts/audit-seo.ts", @@ -20,7 +20,8 @@ "indexnow": "tsx scripts/indexnow.ts", "report:calculate": "node scripts/calculate-ranking.mjs", "postbuild": "node scripts/check-built-titles.mjs && node scripts/check-built-canonical.mjs && node scripts/check-built-descriptions.mjs", - "check:pressure": "node scripts/check-pressure-reference.mjs" + "check:pressure": "node scripts/check-pressure-reference.mjs", + "check:currency": "node scripts/check-currency-rate-freshness.mjs" }, "dependencies": { "@types/qrcode": "^1.5.6", diff --git a/site/scripts/check-currency-rate-freshness.mjs b/site/scripts/check-currency-rate-freshness.mjs new file mode 100644 index 0000000..dd2ed20 --- /dev/null +++ b/site/scripts/check-currency-rate-freshness.mjs @@ -0,0 +1,30 @@ +/** Verify that stale FX fallback cannot ship without visible localized disclosure. */ +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const rates = JSON.parse(readFileSync(join(root, "src/data/rates.json"), "utf8")); +const component = readFileSync(join(root, "src/tools/currency-converter/Component.tsx"), "utf8"); +const localeDir = join(root, "src/tools/currency-converter/messages"); +const names = readdirSync(localeDir).filter(x => x.endsWith(".json")); +const errors = []; +const updated = Date.parse(String(rates.updatedAt) + "T00:00:00Z"); +if (!Number.isFinite(updated) || !/^\d{4}-\d{2}-\d{2}$/.test(rates.updatedAt)) errors.push("Invalid rates date"); +if (rates.base !== "USD" || rates.rates?.USD !== 1) errors.push("Unexpected rate base"); +if (!rates.rates || Object.values(rates.rates).some(v=>typeof v !== "number" || !Number.isFinite(v) || v<=0)) errors.push("Invalid rate entries"); +if (!component.includes('role="alert"') || !component.includes('note.staleWarning') || !component.includes('ageMs >= 7 * 86400000')) errors.push("Missing client stale-rate alert"); +for (const file of names) { + try { + const msg=JSON.parse(readFileSync(join(localeDir,file),"utf8")); + const v=msg.note?.staleWarning; + if (typeof v!=="string" || !v.includes("{date}") || v.length<25) errors.push(file+": missing localized stale warning"); + } catch(e) { errors.push(file+": unreadable translation "+String(e)); } +} +if (errors.length) { + console.error("[currency-rates] FAIL: "+errors.join(" | ")); + process.exitCode=1; +} else { + const age = Math.floor((Date.now() - updated)/86400000); + if (age >= 7 || age < -1) console.warn("[currency-rates] WARNING: cached rate is "+age+" days old. Live page must display warning until refreshed."); + console.log("[currency-rates] PASS: validated "+names.length+" localized stale warnings and currency base"); +} diff --git a/site/src/tools/currency-converter/Component.tsx b/site/src/tools/currency-converter/Component.tsx index 9b05eb0..637f489 100644 --- a/site/src/tools/currency-converter/Component.tsx +++ b/site/src/tools/currency-converter/Component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import ratesData from "@/data/rates.json"; @@ -60,6 +60,14 @@ export default function CurrencyConverter() { const [amount, setAmount] = useState("100"); const [from, setFrom] = useState("USD"); const [to, setTo] = useState("JPY"); + // The bundled fallback file may be old if a rate API fetch failed during + // prebuild. Compute age only after hydration to avoid static-HTML mismatch. + const [rateIsStale, setRateIsStale] = useState(false); + useEffect(() => { + const updated = Date.parse(`${RATES_UPDATED_AT}T00:00:00Z`); + const ageMs = Date.now() - updated; + setRateIsStale(!Number.isFinite(updated) || ageMs >= 7 * 86400000 || ageMs < -86400000); + }, []); function handleSwap() { setFrom(to); @@ -104,6 +112,11 @@ export default function CurrencyConverter() { return (
+ {t("note.staleWarning", { date: RATES_UPDATED_AT })} +
+ )}