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

This commit is contained in:
2026-05-01 03:13:29 +02:00
parent 29e2724cd8
commit 5a154f896b
3 changed files with 63 additions and 119 deletions

View File

@@ -138,3 +138,26 @@
font-size: 0.95rem; font-size: 0.95rem;
text-align: right; text-align: right;
} }
.fx-history-date {
display: inline-flex;
align-items: center;
gap: 0.45rem;
}
.fx-info-button {
width: 1.4rem;
height: 1.4rem;
border: 1px solid #d0d7e2;
border-radius: 999px;
background: #fff;
color: #5b6573;
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
cursor: help;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
}

View File

@@ -7,10 +7,8 @@
const page = JSON.parse(root.dataset.page || '{}'); const page = JSON.parse(root.dataset.page || '{}');
const settings = page.settings || {}; const settings = page.settings || {};
const nodes = { const nodes = {
ratesBody: root.querySelector('[data-bind="rates-body"]'),
historyHead: root.querySelector('[data-bind="history-head"]'), historyHead: root.querySelector('[data-bind="history-head"]'),
historyBody: root.querySelector('[data-bind="history-body"]'), historyBody: root.querySelector('[data-bind="history-body"]'),
fetchesBody: root.querySelector('[data-bind="fetches-body"]'),
convertResult: root.querySelector('[data-bind="convert-result"]'), convertResult: root.querySelector('[data-bind="convert-result"]'),
convertFrom: root.querySelector('select[name="convert_from"]'), convertFrom: root.querySelector('select[name="convert_from"]'),
convertTo: root.querySelector('select[name="convert_to"]'), convertTo: root.querySelector('select[name="convert_to"]'),
@@ -99,51 +97,12 @@
}); });
}; };
const renderSnapshot = (snapshot) => { const escapeHtml = (value) => String(value || '')
const rates = snapshot && snapshot.rates ? snapshot.rates : null; .replaceAll('&', '&')
const entries = rates ? Object.entries(rates) : []; .replaceAll('<', '&lt;')
const visibleCurrencies = preferredCurrencies.length .replaceAll('>', '&gt;')
? new Set(preferredCurrencies) .replaceAll('"', '&quot;')
: null; .replaceAll("'", '&#39;');
if (!nodes.ratesBody) {
return;
}
const filteredEntries = visibleCurrencies
? entries.filter(([code]) => visibleCurrencies.has(String(code || '').trim().toUpperCase()))
: entries;
if (!filteredEntries.length) {
nodes.ratesBody.innerHTML = '<tr><td colspan="2">Noch keine Wechselkurse fuer die ausgewaehlten Waehrungen gespeichert.</td></tr>';
return;
}
nodes.ratesBody.innerHTML = filteredEntries.map(([code, rate]) => {
const formatted = typeof rate === 'number'
? rate.toLocaleString('de-DE', { maximumFractionDigits: 8 })
: 'n/a';
return `<tr><td>${code}</td><td>${formatted}</td></tr>`;
}).join('');
};
const renderFetches = (fetches) => {
if (!nodes.fetchesBody) {
return;
}
const entries = Array.isArray(fetches) ? fetches : [];
if (!entries.length) {
nodes.fetchesBody.innerHTML = '<tr><td colspan="4">Noch keine Abrufe vorhanden.</td></tr>';
return;
}
nodes.fetchesBody.innerHTML = entries.map((entry) => `
<tr>
<td>${entry?.fetched_at_display || entry?.fetched_at || ''}</td>
<td>${entry?.base_currency || ''}</td>
<td>${entry?.provider || ''}</td>
<td>${entry?.trigger_source_label || entry?.trigger_source || ''}</td>
</tr>
`).join('');
};
const renderHistory = (rows, currencies) => { const renderHistory = (rows, currencies) => {
if (!nodes.historyHead || !nodes.historyBody) { if (!nodes.historyHead || !nodes.historyBody) {
@@ -172,7 +131,19 @@
nodes.historyBody.innerHTML = entries.map((entry) => ` nodes.historyBody.innerHTML = entries.map((entry) => `
<tr> <tr>
<td>${entry.label}</td> <td>
<div class="fx-history-date">
<span>${entry.label}</span>
${entry.fetch ? `
<button
type="button"
class="fx-info-button"
title="${escapeHtml(`Basis: ${entry.fetch.base_currency || '-'} | Provider: ${entry.fetch.provider || '-'} | Ausloeser: ${entry.fetch.trigger_source_label || entry.fetch.trigger_source || '-'}`)}"
aria-label="${escapeHtml(`Abrufinfo fuer ${entry.label}`)}"
>i</button>
` : ''}
</div>
</td>
${series.map((currency) => { ${series.map((currency) => {
const value = entry.rates?.[currency]; const value = entry.rates?.[currency];
if (typeof value !== 'number' || !Number.isFinite(value)) { if (typeof value !== 'number' || !Number.isFinite(value)) {
@@ -201,21 +172,6 @@
return payload.data; return payload.data;
}; };
const loadLatest = async () => {
const base = String(
settings.display_base_currency || settings.default_base_currency || 'EUR'
).trim().toUpperCase();
const query = new URLSearchParams();
query.set('base', base);
if (preferredCurrencies.length) {
query.set('symbols', preferredCurrencies.join(','));
}
const data = await request(`/latest?${query.toString()}`);
renderSnapshot(data);
return data;
};
const loadHistory = async () => { const loadHistory = async () => {
const base = String( const base = String(
settings.display_base_currency || settings.default_base_currency || 'EUR' settings.display_base_currency || settings.default_base_currency || 'EUR'
@@ -236,7 +192,21 @@
return { currency, rows: Array.isArray(rows) ? rows : [] }; return { currency, rows: Array.isArray(rows) ? rows : [] };
})); }));
const recentFetches = Array.isArray(page.recent_fetches) ? page.recent_fetches : [];
const byDate = new Map(); const byDate = new Map();
recentFetches.forEach((fetch) => {
const key = String(fetch?.fetched_at || '').trim();
if (!key || byDate.has(key)) {
return;
}
byDate.set(key, {
sortKey: key,
label: fetch?.fetched_at_display || fetch?.fetched_at || key,
fetch,
rates: base !== '' ? { [base]: 1 } : {},
});
});
histories.forEach(({ currency, rows }) => { histories.forEach(({ currency, rows }) => {
rows.forEach((row) => { rows.forEach((row) => {
const key = String(row?.fetched_at || row?.rate_date || '').trim(); const key = String(row?.fetched_at || row?.rate_date || '').trim();
@@ -247,6 +217,7 @@
byDate.set(key, { byDate.set(key, {
sortKey: key, sortKey: key,
label: row?.fetched_at_display || row?.fetched_at || row?.rate_date || key, label: row?.fetched_at_display || row?.fetched_at || row?.rate_date || key,
fetch: recentFetches.find((fetch) => String(fetch?.fetched_at || '').trim() === key) || null,
rates: base !== '' ? { [base]: 1 } : {}, rates: base !== '' ? { [base]: 1 } : {},
}); });
} }
@@ -258,7 +229,8 @@
}); });
const mergedRows = Array.from(byDate.values()) const mergedRows = Array.from(byDate.values())
.sort((left, right) => String(left.sortKey).localeCompare(String(right.sortKey))); .sort((left, right) => String(right.sortKey).localeCompare(String(left.sortKey)))
.slice(0, 15);
renderHistory(mergedRows, selectedCurrencies); renderHistory(mergedRows, selectedCurrencies);
}; };
@@ -305,10 +277,8 @@
}); });
}); });
renderFetches(page.recent_fetches || []);
bindManualRefreshAction(); bindManualRefreshAction();
loadLatest().catch(() => {});
loadHistory().catch(() => { loadHistory().catch(() => {
renderHistory([], preferredCurrencies); renderHistory([], preferredCurrencies);
}); });

View File

@@ -51,7 +51,7 @@ if ((string) ($_GET['refresh'] ?? '') === '1') {
} }
$latest = $service->latestStatus(); $latest = $service->latestStatus();
$recentFetches = $service->recentFetches(12); $recentFetches = $service->recentFetches(15);
$pageData = json_encode([ $pageData = json_encode([
'settings' => $settings, 'settings' => $settings,
'latest' => $latest, 'latest' => $latest,
@@ -110,33 +110,14 @@ $pageData = json_encode([
<div class="fx-card"> <div class="fx-card">
<div class="fx-card-head"> <div class="fx-card-head">
<div> <div>
<h2>Letzte Kurse</h2> <h2>Kursverlauf</h2>
<p>Letzter gespeicherter Snapshot, umgerechnet auf <?= e((string) ($settings['display_base_currency'] ?? $settings['default_base_currency'] ?? 'EUR')) ?>.</p> <p>Neueste Abrufe zuerst. Verlauf der bevorzugten Waehrungen relativ zur Anzeige-Basiswaehrung.</p>
</div> </div>
<div class="fx-card-meta"> <div class="fx-card-meta">
<div><strong>Letzter Abruf:</strong> <?= e((string) ($latest['fetched_at_display'] ?? $latest['fetched_at'] ?? 'noch keiner')) ?></div>
<div><strong>Anzeige-Basis:</strong> <?= e((string) ($settings['display_base_currency'] ?? $settings['default_base_currency'] ?? '')) ?></div> <div><strong>Anzeige-Basis:</strong> <?= e((string) ($settings['display_base_currency'] ?? $settings['default_base_currency'] ?? '')) ?></div>
<div><strong>Snapshot-Basis:</strong> <?= e((string) ($latest['base_currency'] ?? '')) ?></div> <div><strong>Letzter Abruf:</strong> <?= e((string) ($latest['fetched_at_display'] ?? $latest['fetched_at'] ?? 'noch keiner')) ?></div>
</div> </div>
</div> </div>
<div class="fx-table-wrap">
<table class="fx-table">
<thead>
<tr>
<th>Waehrung</th>
<th>Kurs</th>
</tr>
</thead>
<tbody data-bind="rates-body">
<tr><td colspan="2">Noch keine Daten geladen.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="fx-card">
<h2>Kursverlauf</h2>
<p>Chronologischer Verlauf der bevorzugten Waehrungen relativ zur Anzeige-Basiswaehrung.</p>
<div class="fx-table-wrap"> <div class="fx-table-wrap">
<table class="fx-table"> <table class="fx-table">
<thead data-bind="history-head"> <thead data-bind="history-head">
@@ -151,36 +132,6 @@ $pageData = json_encode([
</table> </table>
</div> </div>
</div> </div>
<div class="fx-card">
<h2>Letzte Abrufe</h2>
<div class="fx-table-wrap">
<table class="fx-table">
<thead>
<tr>
<th>Datum</th>
<th>Basis</th>
<th>Provider</th>
<th>Ausloeser</th>
</tr>
</thead>
<tbody data-bind="fetches-body">
<?php if ($recentFetches === []): ?>
<tr><td colspan="4">Noch keine Abrufe vorhanden.</td></tr>
<?php else: ?>
<?php foreach ($recentFetches as $fetch): ?>
<tr>
<td><?= e((string) ($fetch['fetched_at_display'] ?? $fetch['fetched_at'] ?? '')) ?></td>
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
<td><?= e((string) ($fetch['trigger_source_label'] ?? $fetch['trigger_source'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div> </div>
</div> </div>
<?= module_shell_footer() ?> <?= module_shell_footer() ?>