asdasd
This commit is contained in:
@@ -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) => `
|
||||
<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(row.provider || 'n/a')}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}).join('')}
|
||||
${historyMatrix.rows.length === 0
|
||||
? `<tr><td colspan="${String(2 + historyMatrix.columns.length)}">Noch keine Verlaufsdaten vorhanden.</td></tr>`
|
||||
: historyMatrix.rows.map((row) => `
|
||||
<tr>
|
||||
<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('')}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user