asdasd
This commit is contained in:
@@ -41,7 +41,17 @@
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return 'n/a';
|
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())) {
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
@@ -148,6 +158,82 @@
|
|||||||
.map((code) => ({ from: base, to: code }));
|
.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() {
|
async function loadOverviewData() {
|
||||||
const [statuses, recentFetches] = await Promise.all([
|
const [statuses, recentFetches] = await Promise.all([
|
||||||
request('status'),
|
request('status'),
|
||||||
@@ -353,6 +439,7 @@
|
|||||||
const latest = state.statuses[0] || null;
|
const latest = state.statuses[0] || null;
|
||||||
const base = String(settings.default_base_currency || 'EUR').toUpperCase();
|
const base = String(settings.default_base_currency || 'EUR').toUpperCase();
|
||||||
const preferred = Array.isArray(settings.preferred_currencies) ? settings.preferred_currencies : [];
|
const preferred = Array.isArray(settings.preferred_currencies) ? settings.preferred_currencies : [];
|
||||||
|
const historyMatrix = buildHistoryMatrix(state.history);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="fx-stack">
|
<div class="fx-stack">
|
||||||
@@ -371,7 +458,7 @@
|
|||||||
<section class="fx-cards">
|
<section class="fx-cards">
|
||||||
<article class="fx-card">
|
<article class="fx-card">
|
||||||
<p class="fx-kicker">Letzter Abruf</p>
|
<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>
|
<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>
|
||||||
<article class="fx-card">
|
<article class="fx-card">
|
||||||
@@ -432,26 +519,21 @@
|
|||||||
<table class="fx-table">
|
<table class="fx-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Paar</th>
|
|
||||||
<th>Zeit</th>
|
<th>Zeit</th>
|
||||||
<th>Kurs</th>
|
|
||||||
<th>Quelle</th>
|
<th>Quelle</th>
|
||||||
|
${historyMatrix.columns.map((column) => `<th>${escapeHtml(column)}</th>`).join('')}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${state.history.length === 0 || state.history.every((entry) => !Array.isArray(entry.rows) || entry.rows.length === 0)
|
${historyMatrix.rows.length === 0
|
||||||
? '<tr><td colspan="4">Noch keine Verlaufsdaten vorhanden.</td></tr>'
|
? `<tr><td colspan="${String(2 + historyMatrix.columns.length)}">Noch keine Verlaufsdaten vorhanden.</td></tr>`
|
||||||
: state.history.map((entry) => {
|
: historyMatrix.rows.map((row) => `
|
||||||
const pair = `${entry.pair.from}/${entry.pair.to}`;
|
<tr>
|
||||||
return (Array.isArray(entry.rows) ? entry.rows.slice(0, 4) : []).map((row, index) => `
|
<td>${escapeHtml(fmtDate(row.fetched_at))}</td>
|
||||||
<tr>
|
<td>${escapeHtml(row.provider || 'n/a')}</td>
|
||||||
<td>${index === 0 ? escapeHtml(pair) : ''}</td>
|
${historyMatrix.columns.map((column) => `<td>${escapeHtml(fmtNumber(row.values[column], 8))}</td>`).join('')}
|
||||||
<td>${escapeHtml(fmtDate(row.fetched_at_display || row.fetched_at))}</td>
|
</tr>
|
||||||
<td>${escapeHtml(fmtNumber(row.rate, 8))}</td>
|
`).join('')}
|
||||||
<td>${escapeHtml(row.provider || 'n/a')}</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
}).join('')}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -475,7 +557,7 @@
|
|||||||
? '<tr><td colspan="4">Noch keine Abrufe gespeichert.</td></tr>'
|
? '<tr><td colspan="4">Noch keine Abrufe gespeichert.</td></tr>'
|
||||||
: state.recentFetches.map((fetch) => `
|
: state.recentFetches.map((fetch) => `
|
||||||
<tr>
|
<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.base_currency || 'n/a')}</td>
|
||||||
<td>${escapeHtml(fetch.provider || 'n/a')}</td>
|
<td>${escapeHtml(fetch.provider || 'n/a')}</td>
|
||||||
<td>${escapeHtml(fetch.trigger_source_label || fetch.trigger_source || 'n/a')}</td>
|
<td>${escapeHtml(fetch.trigger_source_label || fetch.trigger_source || 'n/a')}</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user