Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 46 additions & 24 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Painel Pessoal · Portugal</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>

Check warning on line 7 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <link> element

Check warning on line 7 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <link> element
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>

Check warning on line 8 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <link> element

Check warning on line 8 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <link> element
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>

Check warning on line 9 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <script> element

Check warning on line 9 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

SRI "integrity" attribute is required on <script> element
<style>
:root {
--bg: #07090E;
Expand Down Expand Up @@ -3258,12 +3258,12 @@
</div>
<div class="hdr-right">
<div class="pills" id="pills"></div>
<button class="export-btn" id="exportOpenBtn" onclick="openExportModal()" aria-label="Exportar dados do painel" aria-haspopup="dialog" aria-controls="exportModal" aria-expanded="false">

Check warning on line 3261 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute

Check warning on line 3261 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute
<span class="icon" aria-hidden="true">📥</span>
<span>Exportar</span>
</button>
<div style="position:relative">
<button class="notif-btn" id="notifBtn" onclick="toggleNotifications()" aria-label="Abrir centro de notificações" aria-haspopup="dialog" aria-controls="notifPanel" aria-expanded="false">

Check warning on line 3266 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute

Check warning on line 3266 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute
<span class="notif-icon" aria-hidden="true">🔔</span>
<span class="notif-badge" id="notifBadge">0</span>
</button>
Expand All @@ -3273,10 +3273,10 @@
<div class="notif-header">
<div class="notif-title">Notificações</div>
<div class="notif-actions">
<button class="notif-action-btn" onclick="markAllAsRead()">Marcar lidas</button>

Check warning on line 3276 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)

Check warning on line 3276 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute

Check warning on line 3276 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)

Check warning on line 3276 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute
<button class="notif-action-btn" onclick="clearAllNotifications()">Limpar</button>

Check warning on line 3277 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)

Check warning on line 3277 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute

Check warning on line 3277 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)

Check warning on line 3277 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

<button> is missing recommended "type" attribute
</div>
<button class="notif-close" type="button" onclick="setNotificationsOpen(false)" aria-label="Fechar notificações">✕</button>

Check warning on line 3279 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)

Check warning on line 3279 in index.html

View workflow job for this annotation

GitHub Actions / Validate HTML

aria-hidden cannot be used on focusable elements (hidden by ancestor element)
</div>

<div class="notif-tabs">
Expand Down Expand Up @@ -4160,7 +4160,7 @@
<script src="src/cards/seis.js"></script>
<script src="src/cards/dams.js"></script>
<script>
/* global CFG,REF_DATE,EURIBOR_FALLBACK,PSI20_CONST,EU_INDICES,INFLATION_FALLBACK,IPMA_CITIES,IPMA_WIND,wxIPMA,CACHE_TTL,PFX,lsGet,lsSet,IS_LOCAL_DEV,localProxyUrl,fetchViaLocalProxy,fetchFirstOk,fetchJsonFirstOk,fetchTextFirstOk,fetchWithCORS,fetchStooqMulti,fmtAgeShort,getCacheMeta,refreshAllCardStatuses,applyStickyScrollCards,$,ld,er,chg,fmtP,DAY_NAMES,loadElec,loadNews,loadSeis,loadDams,notifyNonCriticalError */

Check warning on line 4163 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'localProxyUrl' is defined but never used

Check warning on line 4163 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'localProxyUrl' is defined but never used
/* ── Relógios das Praças Financeiras ── */
const MARKETS=[
{city:"Lisboa", flag:"🇵🇹",tz:"Europe/Lisbon", exch:"Euronext",oH:8, oM:0, cH:16,cM:30,days:[1,2,3,4,5]},
Expand Down Expand Up @@ -4218,7 +4218,7 @@
/* Dev proxy + fetch/cache helpers movidos para src/fetch.js e src/cache.js */

/* ── Oil tabs ── */
function switchOil(type,btn){

Check warning on line 4221 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'switchOil' is defined but never used

Check warning on line 4221 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'switchOil' is defined but never used
document.querySelectorAll(".oil-tab").forEach(b=>b.classList.remove("on"));
btn.classList.add("on");
$("oil-brent").style.display=type==="brent"?"block":"none";
Expand Down Expand Up @@ -4327,7 +4327,7 @@
btn.classList.toggle("on", p.deltaMode === "pct");
btn.title = p.deltaMode === "pct" ? "A mostrar Δ em %" : "A mostrar Δ em €";
}
function toggleFuelDeltaMode(){

Check warning on line 4330 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'toggleFuelDeltaMode' is defined but never used

Check warning on line 4330 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'toggleFuelDeltaMode' is defined but never used
const p = getFuelPrefs();
setFuelPrefs({ deltaMode: p.deltaMode === "pct" ? "abs" : "pct" });
}
Expand Down Expand Up @@ -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("<!DOCTYPE html>"),
);
if (!csv || csv.length < 20 || csv.includes("<!DOCTYPE html>")) {
throw new Error("CSV inválido ou página de erro recebida");
}
Expand All @@ -4747,16 +4753,14 @@
lsSet(cacheKey, sliced);
return sliced;
}
} catch (e) {
console.warn("[fetchStooqDailySeries] failed", e);
}
} catch {}

throw new Error("Stooq histórico indisponível");
}


/* ── Feriados ── */
function switchWxHol(tab,btn){

Check warning on line 4763 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'switchWxHol' is defined but never used

Check warning on line 4763 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'switchWxHol' is defined but never used
document.querySelectorAll("#wxhol-tab-wx,#wxhol-tab-res,#wxhol-tab-hol")
.forEach(b=>b.classList.remove("on"));
btn.classList.add("on");
Expand Down Expand Up @@ -5125,8 +5129,18 @@
/* Dados estaticos EMMI como fallback (ja centralizados em src/config.js) */

/* Estado global — preenchido pela API ou pelo fallback */
var EURIBOR_DATA=null;

Check warning on line 5132 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

Unexpected var, use let or const instead

Check warning on line 5132 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

Unexpected var, use let or const instead

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{
Expand Down Expand Up @@ -5299,7 +5313,7 @@
stroke="var(--b)" stroke-width="1"/>`).join("")}
${yticks.map(v=>`<text x="${PAD.l-3}" y="${sy(v)+4}" text-anchor="end"
font-size="7" fill="var(--t3)">${v.toFixed(1)}</text>`).join("")}
${histRev.filter((_,i)=>i%2===0).map((row,_,arr)=>{

Check warning on line 5316 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'arr' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 5316 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'arr' is defined but never used. Allowed unused args must match /^_/u
const origIdx=histRev.indexOf(row);
return`<text x="${sx(origIdx)}" y="${H-2}" text-anchor="middle"
font-size="7" fill="var(--t3)">${row[0]}</text>`;
Expand Down Expand Up @@ -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...");
Expand Down Expand Up @@ -5502,7 +5521,7 @@
try {
const j2 = await fetchJsonFirstOk("https://open.er-api.com/v6/latest/EUR", {}, 8000);
data = { rates: j2.rates, date: "" };
} catch (e2) {

Check warning on line 5524 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'e2' is defined but never used. Allowed unused caught errors must match /^_/u

Check warning on line 5524 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'e2' is defined but never used. Allowed unused caught errors must match /^_/u
throw new Error("Câmbio indisponível");
}
}
Expand Down Expand Up @@ -5577,11 +5596,11 @@
try { localStorage.setItem(PFX + FX_TREND_PREF_KEY, JSON.stringify(next)); } catch {}
return next;
}
function setFXTrendDays(days) {

Check warning on line 5599 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'setFXTrendDays' is defined but never used

Check warning on line 5599 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'setFXTrendDays' is defined but never used
setFXTrendPref({ days });
loadFXTrend(false).catch(() => {});
}
function setFXTrendCode(code) {

Check warning on line 5603 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'setFXTrendCode' is defined but never used

Check warning on line 5603 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'setFXTrendCode' is defined but never used
setFXTrendPref({ code });
loadFXTrend(false).catch(() => {});
}
Expand Down Expand Up @@ -5709,7 +5728,7 @@
const geoIndex = dimGeo.category.index; // {"PT": 0, "EA20": 1}

/* Número de períodos e geos */
const nTime = Object.keys(timeIndex).length;

Check warning on line 5731 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'nTime' is assigned a value but never used

Check warning on line 5731 in index.html

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'nTime' is assigned a value but never used
const nGeo = Object.keys(geoIndex).length;

/* Obter índice de PT e EA20 */
Expand Down Expand Up @@ -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("<!DOCTYPE html>"),
);
const rows=csv.trim().split("\n");
if(rows.length<2) continue;
/* CSV: Symbol,Date,Time,Open,High,Low,Close,Volume */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 11 additions & 5 deletions src/cards/elec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down Expand Up @@ -674,4 +681,3 @@
window.loadElecTrend = loadElecTrend;
window.loadElec = loadElec;
})();

30 changes: 19 additions & 11 deletions src/cards/news.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -102,26 +102,34 @@
}

if (!arts.length) throw new Error("Sem artigos");
el.innerHTML = `<div class="news-wrap">${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 `<div class="news-wrap">${arts
.map(
(a) => `
<div class="ni" onclick="window.open('${a.link.replace(/'/g, "\\'")}','_blank')">
<div class="ni-meta"><span class="ni-src">${(a.source || "—").slice(0, 24)}</span>
<span class="ni-tag ${isPT ? "tpt" : "twld"}">${isPT ? "PT" : "WORLD"}</span>
<span class="ni-time">${agoRSS(a.pubDate)}</span></div>
<div class="ni-title">${a.title}</div>
</div>`,
)
.join("")}</div>
<div class="src">✦ Google News RSS · ${new Date().toLocaleTimeString("pt-PT", {
)
.join("")}</div>
<div class="src">✦ Google News RSS · ${stale ? "cache" : new Date().toLocaleTimeString("pt-PT", {
hour: "2-digit",
minute: "2-digit",
})} · Clica para ler</div>`;
} catch (e) {
el.innerHTML = er(e.message);
}
}

window.loadNews = loadNews;
})();

25 changes: 20 additions & 5 deletions src/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};

Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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("<!DOCTYPE html>");
},
);
if (!r.ok) return {};
const csv = await r.text();
const lines = csv.trim().split("\n");
Expand Down Expand Up @@ -185,7 +199,7 @@
}
return results;
} catch (e) {
console.warn("[fetchStooqMulti] failed", e);
if (isFetchDebugEnabled()) console.warn("[fetchStooqMulti] failed", e);
return {};
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -257,4 +273,3 @@
window.fetchWithCORS = fetchWithCORS;
window.fetchStooqMulti = fetchStooqMulti;
})();

Loading