diff --git a/index.html b/index.html index 3f6baf4..eeff1bd 100644 --- a/index.html +++ b/index.html @@ -4725,7 +4725,13 @@ const url = `https://stooq.com/q/d/l/?s=${encodeURIComponent(sym)}&i=d`; try { - const csv = await fetchTextFirstOk(url, {}, 11000); + const csv = await fetchTextFirstOk( + url, + {}, + 11000, + ["local", "allorigins"], + (text) => !!text && text.includes("Date,Open,High,Low,Close") && !text.includes(""), + ); if (!csv || csv.length < 20 || csv.includes("")) { throw new Error("CSV inválido ou página de erro recebida"); } @@ -4747,9 +4753,7 @@ lsSet(cacheKey, sliced); return sliced; } - } catch (e) { - console.warn("[fetchStooqDailySeries] failed", e); - } + } catch {} throw new Error("Stooq histórico indisponível"); } @@ -5127,6 +5131,16 @@ /* Estado global — preenchido pela API ou pelo fallback */ var EURIBOR_DATA=null; +function isBceEuriborLiveEnabled(){ + try { + const qs = new URLSearchParams(location.search || ""); + if (qs.has("liveEuribor")) return (qs.get("liveEuribor") || "1") !== "0"; + return localStorage.getItem(PFX + "live_euribor_bce") === "1"; + } catch { + return false; + } +} + /* ── Parser SDMX JSON (formato BCE) ──────────────────────────── */ function parseBCEseries(json){ try{ @@ -5461,6 +5475,11 @@ if(ref) ref.textContent=EURIBOR_FALLBACK.updated; renderEuribor(); + if(!isBceEuriborLiveEnabled()){ + console.log("📐 Euribor: fallback EMMI (BCE live desativado)"); + return; + } + /* 3. Tentar BCE em background e actualizar se responder */ try{ console.log("📐 Euribor: a actualizar do BCE em background..."); @@ -7724,17 +7743,16 @@ /* ── Fetch TTF via Stooq CSV (tg.f = EUR/MWh) ──────────── */ async function fetchTTFStooq(){ const url=`https://stooq.com/q/l/?s=tg.f&f=sd2t2ohlcv&h&e=csv`; - const proxies=[ - ...(IS_LOCAL_DEV?[localProxyUrl(url)]:[]), - "https://api.allorigins.win/raw?url="+encodeURIComponent(url), - "https://corsproxy.io/?url="+encodeURIComponent(url), - "https://api.allorigins.win/raw?url="+encodeURIComponent(url.replace("stooq.com","stooq.pl")), - ]; - for(const p of proxies){ + const candidates=[url, url.replace("stooq.com","stooq.pl")]; + for(const candidate of candidates){ try{ - const r=await fetch(p,{signal:AbortSignal.timeout(9000)}); - if(!r.ok) continue; - const csv=await r.text(); + const csv=await fetchTextFirstOk( + candidate, + {}, + 9000, + ["local", "allorigins"], + (text) => !!text && text.includes("Symbol,Date,Time") && !text.includes(""), + ); const rows=csv.trim().split("\n"); if(rows.length<2) continue; /* CSV: Symbol,Date,Time,Open,High,Low,Close,Volume */ @@ -9586,11 +9604,13 @@ let d=force?null:lsGet(ck); if(!d){ try { - const url = "https://opensky-network.org/api/states/all?lamin=36&lomin=-12&lamax=44&lomax=-5"; - const j = await fetchJsonFirstOk(url, {}, 14000); - if(j && j.states?.length){ - d={ts:Date.now(),states:j.states,live:true}; - lsSet(ck,d); + if (IS_LOCAL_DEV) { + const url = "https://opensky-network.org/api/states/all?lamin=36&lomin=-12&lamax=44&lomax=-5"; + const j = await fetchJsonFirstOk(url, {}, 14000, ["local"], (jj) => Array.isArray(jj?.states)); + if(j && j.states?.length){ + d={ts:Date.now(),states:j.states,live:true}; + lsSet(ck,d); + } } } catch(e) { console.warn("⚠️ OpenSky falhou:", e.message); @@ -10585,11 +10605,13 @@ if(!d){ try { - const url = "https://datahub.ren.pt/service/renewable/"; - const j = await fetchJsonFirstOk(url, {}, 10000); - if(j && (j.value || j.data || Array.isArray(j))){ - d={src:"ren", raw:j}; - lsSet(ck, d); + if (IS_LOCAL_DEV) { + const url = "https://datahub.ren.pt/service/renewable/"; + const j = await fetchJsonFirstOk(url, {}, 10000, ["local"]); + if(j && (j.value || j.data || Array.isArray(j))){ + d={src:"ren", raw:j}; + lsSet(ck, d); + } } } catch(e) { console.warn("⚠️ REN DataHub falhou:", e.message); diff --git a/src/cards/elec.js b/src/cards/elec.js index c448418..830f339 100644 --- a/src/cards/elec.js +++ b/src/cards/elec.js @@ -276,10 +276,17 @@ `?start_date=${d}T00:00&end_date=${d}T23:59&time_trunc=hour`, ), ); - const mixFetch = fetchREE( - `/en/datos/generacion/estructura-generacion` + - `?start_date=${todayStr}T00:00&end_date=${todayStr}T23:59&time_trunc=hour`, - ); + const mixFetch = (async () => { + const mixDates = [todayStr, yesterStr]; + for (const d of mixDates) { + const data = await fetchREE( + `/en/datos/generacion/estructura-generacion` + + `?start_date=${d}T00:00&end_date=${d}T23:59&time_trunc=hour`, + ); + if (data) return data; + } + return null; + })(); const [todayData, yesterData, tomorrowData, mixData] = await Promise.all([...fetches, mixFetch]); @@ -674,4 +681,3 @@ window.loadElecTrend = loadElecTrend; window.loadElec = loadElec; })(); - diff --git a/src/cards/news.js b/src/cards/news.js index a01a9f2..fd72814 100644 --- a/src/cards/news.js +++ b/src/cards/news.js @@ -31,7 +31,7 @@ async function fetchRSS(url) { const isGoogleNews = /(^|\/\/)news\.google\.com\//i.test(url); const sources = isGoogleNews - ? ["direct", "local", "allorigins", "corsproxy"] + ? ["local", "jina", "allorigins"] : ["direct", "local", "allorigins", "jina", "corsproxy"]; const candidates = (buildFetchPlan(url, sources) || []).map((x) => x.url); @@ -102,26 +102,34 @@ } if (!arts.length) throw new Error("Sem artigos"); - el.innerHTML = `
${arts - .map( - (a) => ` + el.innerHTML = renderNewsItems(arts, isPT); + } catch (e) { + const stale = lsGet(cKey); + if (Array.isArray(stale) && stale.length) { + el.innerHTML = renderNewsItems(stale.slice(0, 8), isPT, true); + return; + } + el.innerHTML = er(e.message); + } + } + + function renderNewsItems(arts, isPT, stale = false) { + return `
${arts + .map( + (a) => `
${(a.source || "—").slice(0, 24)} ${isPT ? "PT" : "WORLD"} ${agoRSS(a.pubDate)}
${a.title}
`, - ) - .join("")}
-
✦ Google News RSS · ${new Date().toLocaleTimeString("pt-PT", { + ) + .join("")}
+
✦ Google News RSS · ${stale ? "cache" : new Date().toLocaleTimeString("pt-PT", { hour: "2-digit", minute: "2-digit", })} · Clica para ler
`; - } catch (e) { - el.innerHTML = er(e.message); - } } window.loadNews = loadNews; })(); - diff --git a/src/fetch.js b/src/fetch.js index e3733cb..a602eff 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -42,8 +42,8 @@ function buildFetchPlan(url, sources) { const isCorsHost = (u) => { const h = new URL(u).hostname.toLowerCase(); - return h.includes("stooq") || h.includes("apambiente") || h.includes("ecb.europa.eu") || - h.includes("eurostat") || h.includes("opensky-network") || h.includes("aishub") || + return h.includes("stooq") || h.includes("news.google.com") || h.includes("apambiente") || h.includes("ecb.europa.eu") || + h.includes("eurostat") || h.includes("opensky-network") || h.includes("aishub") || h.includes("ren.pt") || h.includes("usgs.gov") || h.includes("ine.pt") || h.includes("coingecko"); }; @@ -89,6 +89,10 @@ return out; } + function isDefinitiveHttpError(status) { + return status === 400 || status === 404; + } + function withTimeoutOpts(options = {}, timeoutMs = 12000) { const hasSignal = !!options.signal; return { ...options, signal: hasSignal ? options.signal : AbortSignal.timeout(timeoutMs) }; @@ -122,6 +126,7 @@ if (!r.ok) { lastErr = new Error(`HTTP ${r.status}`); + if ((step.source === "direct" || step.source === "localproxy") && isDefinitiveHttpError(r.status)) break; continue; } @@ -151,7 +156,16 @@ const url = `https://stooq.com/q/l/?s=${symStr}&f=sd2t2ohlcv&h&e=csv&_=${Date.now()}`; try { - const r = await fetchWithCORS(url, { cache: "no-store" }, 10000); + const { r } = await fetchFirstOk( + url, + { cache: "no-store" }, + 10000, + ["local", "allorigins"], + async (res) => { + const text = await res.clone().text(); + return text.includes("Symbol,Date,Time") && !text.includes(""); + }, + ); if (!r.ok) return {}; const csv = await r.text(); const lines = csv.trim().split("\n"); @@ -185,7 +199,7 @@ } return results; } catch (e) { - console.warn("[fetchStooqMulti] failed", e); + if (isFetchDebugEnabled()) console.warn("[fetchStooqMulti] failed", e); return {}; } } @@ -201,6 +215,7 @@ if (!r.ok) { lastErr = new Error(`HTTP ${r.status}`); if (isFetchDebugEnabled()) console.debug(`[fetch-json] fail ${step.source} HTTP ${r.status}`, url); + if ((step.source === "direct" || step.source === "localproxy") && isDefinitiveHttpError(r.status)) break; continue; } const j = await r.json(); @@ -229,6 +244,7 @@ if (!r.ok) { lastErr = new Error(`HTTP ${r.status}`); if (isFetchDebugEnabled()) console.debug(`[fetch-text] fail ${step.source} HTTP ${r.status}`, url); + if ((step.source === "direct" || step.source === "localproxy") && isDefinitiveHttpError(r.status)) break; continue; } const t = await r.text(); @@ -257,4 +273,3 @@ window.fetchWithCORS = fetchWithCORS; window.fetchStooqMulti = fetchStooqMulti; })(); -