diff --git a/modules/fx-rates/assets/js/app.js b/modules/fx-rates/assets/js/app.js index 6328e79e..bd23a681 100644 --- a/modules/fx-rates/assets/js/app.js +++ b/modules/fx-rates/assets/js/app.js @@ -41,7 +41,17 @@ if (!raw) { return 'n/a'; } - const parsed = new Date(raw.replace(' ', 'T')); + + let normalized = raw; + if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + normalized = `${raw}T00:00:00Z`; + } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)) { + normalized = `${raw.replace(' ', 'T')}Z`; + } else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(raw)) { + normalized = `${raw}Z`; + } + + const parsed = new Date(normalized); if (Number.isNaN(parsed.getTime())) { return raw; } @@ -148,6 +158,82 @@ .map((code) => ({ from: base, to: code })); } + function normalizedTimestamp(value) { + const raw = String(value || '').trim(); + if (!raw) { + return ''; + } + + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)) { + return `${raw.replace(' ', 'T')}Z`; + } + + if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(raw)) { + return `${raw}Z`; + } + + return raw; + } + + function buildHistoryMatrix(historyEntries) { + const columns = []; + const rowsByKey = new Map(); + + historyEntries.forEach((entry) => { + const pair = entry && entry.pair ? entry.pair : null; + const columnKey = pair ? `${String(pair.from || '').toUpperCase()}/${String(pair.to || '').toUpperCase()}` : ''; + if (!columnKey) { + return; + } + if (!columns.includes(columnKey)) { + columns.push(columnKey); + } + + (Array.isArray(entry.rows) ? entry.rows : []).forEach((row) => { + const fetchId = Number(row && row.fetch_id); + const fetchedAt = normalizedTimestamp(row && row.fetched_at); + const rowKey = fetchId > 0 ? `fetch:${fetchId}` : `time:${fetchedAt}`; + if (!rowKey || rowKey === 'time:') { + return; + } + + if (!rowsByKey.has(rowKey)) { + rowsByKey.set(rowKey, { + key: rowKey, + fetch_id: fetchId > 0 ? fetchId : null, + fetched_at: row && row.fetched_at ? String(row.fetched_at) : '', + provider: row && row.provider ? String(row.provider) : '', + values: {}, + }); + } + + const bucket = rowsByKey.get(rowKey); + if (!bucket) { + return; + } + + if (!bucket.fetched_at && row && row.fetched_at) { + bucket.fetched_at = String(row.fetched_at); + } + if (!bucket.provider && row && row.provider) { + bucket.provider = String(row.provider); + } + bucket.values[columnKey] = Number.isFinite(Number(row && row.rate)) ? Number(row.rate) : null; + }); + }); + + const rows = Array.from(rowsByKey.values()).sort((left, right) => { + const leftTs = new Date(normalizedTimestamp(left.fetched_at) || 0).getTime(); + const rightTs = new Date(normalizedTimestamp(right.fetched_at) || 0).getTime(); + return rightTs - leftTs; + }); + + return { + columns, + rows, + }; + } + async function loadOverviewData() { const [statuses, recentFetches] = await Promise.all([ request('status'), @@ -353,6 +439,7 @@ const latest = state.statuses[0] || null; const base = String(settings.default_base_currency || 'EUR').toUpperCase(); const preferred = Array.isArray(settings.preferred_currencies) ? settings.preferred_currencies : []; + const historyMatrix = buildHistoryMatrix(state.history); return `
@@ -371,7 +458,7 @@

Letzter Abruf

-
${escapeHtml(latest ? fmtDate(latest.fetched_at_display || latest.fetched_at) : 'Noch keiner')}
+
${escapeHtml(latest ? fmtDate(latest.fetched_at) : 'Noch keiner')}

${escapeHtml(latest ? `${latest.base_currency} · ${latest.provider} · ${latest.trigger_source_label || latest.trigger_source}` : 'Es wurden noch keine Kurse gespeichert.')}

@@ -432,26 +519,21 @@ - - + ${historyMatrix.columns.map((column) => ``).join('')} - ${state.history.length === 0 || state.history.every((entry) => !Array.isArray(entry.rows) || entry.rows.length === 0) - ? '' - : state.history.map((entry) => { - const pair = `${entry.pair.from}/${entry.pair.to}`; - return (Array.isArray(entry.rows) ? entry.rows.slice(0, 4) : []).map((row, index) => ` - - - - - - - `).join(''); - }).join('')} + ${historyMatrix.rows.length === 0 + ? `` + : historyMatrix.rows.map((row) => ` + + + + ${historyMatrix.columns.map((column) => ``).join('')} + + `).join('')}
Paar ZeitKurs Quelle${escapeHtml(column)}
Noch keine Verlaufsdaten vorhanden.
${index === 0 ? escapeHtml(pair) : ''}${escapeHtml(fmtDate(row.fetched_at_display || row.fetched_at))}${escapeHtml(fmtNumber(row.rate, 8))}${escapeHtml(row.provider || 'n/a')}
Noch keine Verlaufsdaten vorhanden.
${escapeHtml(fmtDate(row.fetched_at))}${escapeHtml(row.provider || 'n/a')}${escapeHtml(fmtNumber(row.values[column], 8))}
@@ -475,7 +557,7 @@ ? 'Noch keine Abrufe gespeichert.' : state.recentFetches.map((fetch) => ` - ${escapeHtml(fmtDate(fetch.fetched_at_display || fetch.fetched_at))} + ${escapeHtml(fmtDate(fetch.fetched_at))} ${escapeHtml(fetch.base_currency || 'n/a')} ${escapeHtml(fetch.provider || 'n/a')} ${escapeHtml(fetch.trigger_source_label || fetch.trigger_source || 'n/a')}