asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 24s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-22 01:56:03 +02:00
parent 7d4c3e4378
commit 64a1c5811d

View File

@@ -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 `
<div class="fx-stack">
@@ -371,7 +458,7 @@
<section class="fx-cards">
<article class="fx-card">
<p class="fx-kicker">Letzter Abruf</p>
<div class="fx-stat-value">${escapeHtml(latest ? fmtDate(latest.fetched_at_display || latest.fetched_at) : 'Noch keiner')}</div>
<div class="fx-stat-value">${escapeHtml(latest ? fmtDate(latest.fetched_at) : 'Noch keiner')}</div>
<p class="fx-copy">${escapeHtml(latest ? `${latest.base_currency} · ${latest.provider} · ${latest.trigger_source_label || latest.trigger_source}` : 'Es wurden noch keine Kurse gespeichert.')}</p>
</article>
<article class="fx-card">
@@ -432,26 +519,21 @@
<table class="fx-table">
<thead>
<tr>
<th>Paar</th>
<th>Zeit</th>
<th>Kurs</th>
<th>Quelle</th>
${historyMatrix.columns.map((column) => `<th>${escapeHtml(column)}</th>`).join('')}
</tr>
</thead>
<tbody>
${state.history.length === 0 || state.history.every((entry) => !Array.isArray(entry.rows) || entry.rows.length === 0)
? '<tr><td colspan="4">Noch keine Verlaufsdaten vorhanden.</td></tr>'
: 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) => `
${historyMatrix.rows.length === 0
? `<tr><td colspan="${String(2 + historyMatrix.columns.length)}">Noch keine Verlaufsdaten vorhanden.</td></tr>`
: historyMatrix.rows.map((row) => `
<tr>
<td>${index === 0 ? escapeHtml(pair) : ''}</td>
<td>${escapeHtml(fmtDate(row.fetched_at_display || row.fetched_at))}</td>
<td>${escapeHtml(fmtNumber(row.rate, 8))}</td>
<td>${escapeHtml(fmtDate(row.fetched_at))}</td>
<td>${escapeHtml(row.provider || 'n/a')}</td>
${historyMatrix.columns.map((column) => `<td>${escapeHtml(fmtNumber(row.values[column], 8))}</td>`).join('')}
</tr>
`).join('');
}).join('')}
`).join('')}
</tbody>
</table>
</div>
@@ -475,7 +557,7 @@
? '<tr><td colspan="4">Noch keine Abrufe gespeichert.</td></tr>'
: state.recentFetches.map((fetch) => `
<tr>
<td>${escapeHtml(fmtDate(fetch.fetched_at_display || fetch.fetched_at))}</td>
<td>${escapeHtml(fmtDate(fetch.fetched_at))}</td>
<td>${escapeHtml(fetch.base_currency || 'n/a')}</td>
<td>${escapeHtml(fetch.provider || 'n/a')}</td>
<td>${escapeHtml(fetch.trigger_source_label || fetch.trigger_source || 'n/a')}</td>