ddfsdfdf
This commit is contained in:
@@ -22,6 +22,82 @@
|
|||||||
.map((item) => String(item || '').trim().toUpperCase())
|
.map((item) => String(item || '').trim().toUpperCase())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
const refreshMaxAgeMinutes = Math.max(1, Number(settings.refresh_max_age_minutes || 60));
|
||||||
|
|
||||||
|
const parseDateValue = (value) => {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = raw.replace(' ', 'T');
|
||||||
|
normalized = normalized.replace(/([+-]\d{2})$/, '$1:00');
|
||||||
|
const parsed = new Date(normalized);
|
||||||
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const latestFetchedAt = () => {
|
||||||
|
const latest = page.latest && typeof page.latest === 'object' ? page.latest : null;
|
||||||
|
const direct = parseDateValue(latest?.fetched_at);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentFetches = Array.isArray(page.recent_fetches) ? page.recent_fetches : [];
|
||||||
|
for (const entry of recentFetches) {
|
||||||
|
const parsed = parseDateValue(entry?.fetched_at);
|
||||||
|
if (parsed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindManualRefreshAction = () => {
|
||||||
|
const refreshLink = Array.from(document.querySelectorAll('a[href]')).find((link) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(link.href, window.location.origin);
|
||||||
|
return url.pathname === '/module/fx-rates' && url.searchParams.get('refresh') === '1';
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!refreshLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshLink.addEventListener('click', (event) => {
|
||||||
|
const url = new URL(refreshLink.href, window.location.origin);
|
||||||
|
if (url.searchParams.get('force') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastFetch = latestFetchedAt();
|
||||||
|
if (!lastFetch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ageMinutes = (Date.now() - lastFetch.getTime()) / 60000;
|
||||||
|
if (!Number.isFinite(ageMinutes) || ageMinutes >= refreshMaxAgeMinutes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`Der letzte gespeicherte Abruf ist juenger als ${refreshMaxAgeMinutes} Minuten. ` +
|
||||||
|
'Ein manueller Abruf wuerde die externe API trotzdem erneut aufrufen. Jetzt trotzdem abrufen?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
url.searchParams.set('force', '1');
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderSnapshot = (snapshot) => {
|
const renderSnapshot = (snapshot) => {
|
||||||
const rates = snapshot && snapshot.rates ? snapshot.rates : null;
|
const rates = snapshot && snapshot.rates ? snapshot.rates : null;
|
||||||
@@ -56,7 +132,7 @@
|
|||||||
}
|
}
|
||||||
const entries = Array.isArray(fetches) ? fetches : [];
|
const entries = Array.isArray(fetches) ? fetches : [];
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
nodes.fetchesBody.innerHTML = '<tr><td colspan="3">Noch keine Abrufe vorhanden.</td></tr>';
|
nodes.fetchesBody.innerHTML = '<tr><td colspan="4">Noch keine Abrufe vorhanden.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nodes.fetchesBody.innerHTML = entries.map((entry) => `
|
nodes.fetchesBody.innerHTML = entries.map((entry) => `
|
||||||
@@ -64,6 +140,7 @@
|
|||||||
<td>${entry?.fetched_at_display || entry?.fetched_at || ''}</td>
|
<td>${entry?.fetched_at_display || entry?.fetched_at || ''}</td>
|
||||||
<td>${entry?.base_currency || ''}</td>
|
<td>${entry?.base_currency || ''}</td>
|
||||||
<td>${entry?.provider || ''}</td>
|
<td>${entry?.provider || ''}</td>
|
||||||
|
<td>${entry?.trigger_source_label || entry?.trigger_source || ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
};
|
};
|
||||||
@@ -226,6 +303,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderFetches(page.recent_fetches || []);
|
renderFetches(page.recent_fetches || []);
|
||||||
|
bindManualRefreshAction();
|
||||||
|
|
||||||
loadLatest().catch(() => {});
|
loadLatest().catch(() => {});
|
||||||
loadHistory().catch(() => {
|
loadHistory().catch(() => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{ "name": "api_url", "label": "FX API URL", "type": "text", "required": false, "help": "Nur die Basis-URL eintragen, z.B. https://api.currencyapi.com oder https://currencyapi.net." },
|
{ "name": "api_url", "label": "FX API URL", "type": "text", "required": false, "help": "Nur die Basis-URL eintragen, z.B. https://api.currencyapi.com oder https://currencyapi.net." },
|
||||||
{ "name": "api_key", "label": "FX API Key", "type": "password", "required": false },
|
{ "name": "api_key", "label": "FX API Key", "type": "password", "required": false },
|
||||||
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
||||||
{ "name": "refresh_max_age_minutes", "label": "Max. Alter fuer API-Refresh (Min.)", "type": "number", "required": false, "help": "Refresh aktualisiert nur, wenn der letzte gespeicherte Abruf aelter ist." },
|
{ "name": "refresh_max_age_minutes", "label": "Max. Alter fuer API-Refresh (Min.)", "type": "number", "required": false, "help": "Blockiert neue API-Refresh-Aufrufe, solange der letzte gespeicherte Abruf juenger ist. Manuelle Abrufe koennen nach Hinweis trotzdem erzwungen werden; Cron ignoriert diesen Wert." },
|
||||||
{ "name": "default_base_currency", "label": "Standard-Basiswaehrung", "type": "text", "required": false, "help": "Wird fuer taegliche Abrufe und Snapshot-Abfragen verwendet." },
|
{ "name": "default_base_currency", "label": "Standard-Basiswaehrung", "type": "text", "required": false, "help": "Wird fuer taegliche Abrufe und Snapshot-Abfragen verwendet." },
|
||||||
{ "name": "display_base_currency", "label": "Anzeige-Basiswaehrung", "type": "select", "required": false, "help": "Basis fuer die Anzeige der zuletzt gespeicherten Kurse im Modul." },
|
{ "name": "display_base_currency", "label": "Anzeige-Basiswaehrung", "type": "select", "required": false, "help": "Basis fuer die Anzeige der zuletzt gespeicherten Kurse im Modul." },
|
||||||
{ "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "multiselect", "required": false, "help": "Auswahl aus dem synchronisierten Waehrungskatalog." },
|
{ "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "multiselect", "required": false, "help": "Auswahl aus dem synchronisierten Waehrungskatalog." },
|
||||||
|
|||||||
@@ -18,13 +18,31 @@ $error = trim((string) ($_GET['error'] ?? ''));
|
|||||||
|
|
||||||
if ((string) ($_GET['refresh'] ?? '') === '1') {
|
if ((string) ($_GET['refresh'] ?? '') === '1') {
|
||||||
try {
|
try {
|
||||||
$result = $service->refreshLatestRates(null, (string) ($settings['default_base_currency'] ?? ''));
|
$force = !empty($_GET['force']);
|
||||||
$params = [
|
if ($force) {
|
||||||
'notice' => sprintf(
|
$result = $service->refreshLatestRates(null, (string) ($settings['default_base_currency'] ?? ''), 'manual');
|
||||||
'Aktuelle Kurse gespeichert. %d Werte aktualisiert.',
|
} else {
|
||||||
(int) ($result['updated_count'] ?? 0)
|
$result = $service->autoRefreshLatestRates(
|
||||||
),
|
(string) ($settings['default_base_currency'] ?? ''),
|
||||||
];
|
null,
|
||||||
|
(int) ($settings['refresh_max_age_minutes'] ?? 60),
|
||||||
|
'manual'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = !empty($result['reused'])
|
||||||
|
? [
|
||||||
|
'notice' => sprintf(
|
||||||
|
'Kein neuer API-Abruf. Der letzte gespeicherte Snapshot ist juenger als %d Minuten. Fuer einen erzwungenen Abruf bitte bestaetigen.',
|
||||||
|
(int) ($settings['refresh_max_age_minutes'] ?? 60)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'notice' => sprintf(
|
||||||
|
'Aktuelle Kurse gespeichert. %d Werte aktualisiert.',
|
||||||
|
(int) ($result['updated_count'] ?? 0)
|
||||||
|
),
|
||||||
|
];
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$params = ['error' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Kurse konnten nicht aktualisiert werden.'];
|
$params = ['error' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Kurse konnten nicht aktualisiert werden.'];
|
||||||
}
|
}
|
||||||
@@ -143,17 +161,19 @@ $pageData = json_encode([
|
|||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Basis</th>
|
<th>Basis</th>
|
||||||
<th>Provider</th>
|
<th>Provider</th>
|
||||||
|
<th>Ausloeser</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody data-bind="fetches-body">
|
<tbody data-bind="fetches-body">
|
||||||
<?php if ($recentFetches === []): ?>
|
<?php if ($recentFetches === []): ?>
|
||||||
<tr><td colspan="3">Noch keine Abrufe vorhanden.</td></tr>
|
<tr><td colspan="4">Noch keine Abrufe vorhanden.</td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($recentFetches as $fetch): ?>
|
<?php foreach ($recentFetches as $fetch): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= e((string) ($fetch['fetched_at_display'] ?? $fetch['fetched_at'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['fetched_at_display'] ?? $fetch['fetched_at'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
|
||||||
|
<td><?= e((string) ($fetch['trigger_source_label'] ?? $fetch['trigger_source'] ?? '')) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ final class Router
|
|||||||
$maxAgeMinutes = is_numeric($input['max_age_minutes'] ?? null) ? (int) $input['max_age_minutes'] : null;
|
$maxAgeMinutes = is_numeric($input['max_age_minutes'] ?? null) ? (int) $input['max_age_minutes'] : null;
|
||||||
|
|
||||||
$result = $force
|
$result = $force
|
||||||
? $this->service->refreshLatestRates(null, $base)
|
? $this->service->refreshLatestRates(null, $base, 'api')
|
||||||
: $this->service->autoRefreshLatestRates($base, null, $maxAgeMinutes);
|
: $this->service->autoRefreshLatestRates($base, null, $maxAgeMinutes, 'api');
|
||||||
|
|
||||||
$this->respond(['data' => $result], 201);
|
$this->respond(['data' => $result], 201);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ final class FxRatesService
|
|||||||
return $amount * (float) $rate['rate'];
|
return $amount * (float) $rate['rate'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshLatestRates(?array $currencies = null, ?string $baseCurrency = null): array
|
public function refreshLatestRates(?array $currencies = null, ?string $baseCurrency = null, string $triggerSource = 'manual'): array
|
||||||
{
|
{
|
||||||
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
||||||
$payload = $this->fetchLatestPayload($requestedBase, null);
|
$payload = $this->fetchLatestPayload($requestedBase, null);
|
||||||
@@ -209,7 +209,8 @@ final class FxRatesService
|
|||||||
$this->provider(),
|
$this->provider(),
|
||||||
$rateDate,
|
$rateDate,
|
||||||
$rates,
|
$rates,
|
||||||
gmdate('Y-m-d H:i:s')
|
gmdate('Y-m-d H:i:s'),
|
||||||
|
$triggerSource
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -223,7 +224,7 @@ final class FxRatesService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureFreshLatestRates(float $maxAgeHours = 24.0, ?string $baseCurrency = null, ?array $currencies = null): array
|
public function ensureFreshLatestRates(float $maxAgeHours = 24.0, ?string $baseCurrency = null, ?array $currencies = null, string $triggerSource = 'manual'): array
|
||||||
{
|
{
|
||||||
$base = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
$base = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
||||||
$latest = $this->repository->getLatestFetch($base);
|
$latest = $this->repository->getLatestFetch($base);
|
||||||
@@ -242,16 +243,16 @@ final class FxRatesService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->refreshLatestRates($currencies, $base);
|
$result = $this->refreshLatestRates($currencies, $base, $triggerSource);
|
||||||
$result['reused'] = false;
|
$result['reused'] = false;
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function autoRefreshLatestRates(?string $baseCurrency = null, ?array $currencies = null, ?int $maxAgeMinutes = null): array
|
public function autoRefreshLatestRates(?string $baseCurrency = null, ?array $currencies = null, ?int $maxAgeMinutes = null, string $triggerSource = 'api'): array
|
||||||
{
|
{
|
||||||
$minutes = $maxAgeMinutes ?? $this->refreshMaxAgeMinutes();
|
$minutes = $maxAgeMinutes ?? $this->refreshMaxAgeMinutes();
|
||||||
$hours = max(1, $minutes) / 60;
|
$hours = max(1, $minutes) / 60;
|
||||||
return $this->ensureFreshLatestRates($hours, $baseCurrency, $currencies);
|
return $this->ensureFreshLatestRates($hours, $baseCurrency, $currencies, $triggerSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function history(string $fromCurrency, string $toCurrency, ?string $from = null, ?string $to = null, int $limit = 200): array
|
public function history(string $fromCurrency, string $toCurrency, ?string $from = null, ?string $to = null, int $limit = 200): array
|
||||||
@@ -302,7 +303,8 @@ final class FxRatesService
|
|||||||
|
|
||||||
public function runScheduledRefresh(array $context = []): array
|
public function runScheduledRefresh(array $context = []): array
|
||||||
{
|
{
|
||||||
$result = $this->refreshLatestRates(null, $this->defaultBaseCurrency());
|
$triggerSource = ($context['trigger'] ?? null) === 'manual_test' ? 'manual' : 'cron';
|
||||||
|
$result = $this->refreshLatestRates(null, $this->defaultBaseCurrency(), $triggerSource);
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'message' => 'Geplanter FX-Abruf ausgefuehrt: ' . (int) ($result['updated_count'] ?? 0) . ' Kurse gespeichert.',
|
'message' => 'Geplanter FX-Abruf ausgefuehrt: ' . (int) ($result['updated_count'] ?? 0) . ' Kurse gespeichert.',
|
||||||
@@ -751,6 +753,7 @@ final class FxRatesService
|
|||||||
|
|
||||||
$fetch['fetched_at_display'] = $this->formatDisplayTimestamp($fetch['fetched_at'] ?? null);
|
$fetch['fetched_at_display'] = $this->formatDisplayTimestamp($fetch['fetched_at'] ?? null);
|
||||||
$fetch['created_at_display'] = $this->formatDisplayTimestamp($fetch['created_at'] ?? null);
|
$fetch['created_at_display'] = $this->formatDisplayTimestamp($fetch['created_at'] ?? null);
|
||||||
|
$fetch['trigger_source_label'] = $this->triggerSourceLabel((string) ($fetch['trigger_source'] ?? 'manual'));
|
||||||
return $fetch;
|
return $fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,4 +965,13 @@ final class FxRatesService
|
|||||||
{
|
{
|
||||||
return $this->scheduleTimezone();
|
return $this->scheduleTimezone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function triggerSourceLabel(string $source): string
|
||||||
|
{
|
||||||
|
return match (strtolower(trim($source))) {
|
||||||
|
'cron' => 'Cron',
|
||||||
|
'api' => 'API',
|
||||||
|
default => 'Manuell',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ final class FxRatesRepository
|
|||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
provider VARCHAR(64) NOT NULL,
|
provider VARCHAR(64) NOT NULL,
|
||||||
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||||
base_currency VARCHAR(10) NOT NULL,
|
base_currency VARCHAR(10) NOT NULL,
|
||||||
rate_date DATE NOT NULL,
|
rate_date DATE NOT NULL,
|
||||||
fetched_at TIMESTAMP NOT NULL,
|
fetched_at TIMESTAMP NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)");
|
)");
|
||||||
|
$this->pdo->exec("ALTER TABLE {$fetchTable} ADD COLUMN IF NOT EXISTS trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
fetch_id INTEGER NOT NULL REFERENCES {$fetchTable}(id) ON DELETE CASCADE,
|
fetch_id INTEGER NOT NULL REFERENCES {$fetchTable}(id) ON DELETE CASCADE,
|
||||||
@@ -44,6 +46,7 @@ final class FxRatesRepository
|
|||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
provider VARCHAR(64) NOT NULL,
|
provider VARCHAR(64) NOT NULL,
|
||||||
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||||
base_currency VARCHAR(10) NOT NULL,
|
base_currency VARCHAR(10) NOT NULL,
|
||||||
rate_date DATE NOT NULL,
|
rate_date DATE NOT NULL,
|
||||||
fetched_at DATETIME NOT NULL,
|
fetched_at DATETIME NOT NULL,
|
||||||
@@ -51,6 +54,7 @@ final class FxRatesRepository
|
|||||||
KEY {$fetchTable}_base_fetch_idx (base_currency, fetched_at, id),
|
KEY {$fetchTable}_base_fetch_idx (base_currency, fetched_at, id),
|
||||||
KEY {$fetchTable}_rate_date_idx (rate_date)
|
KEY {$fetchTable}_rate_date_idx (rate_date)
|
||||||
)");
|
)");
|
||||||
|
$this->ensureColumn($fetchTable, 'trigger_source', "ALTER TABLE {$fetchTable} ADD COLUMN trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
fetch_id INTEGER NOT NULL,
|
fetch_id INTEGER NOT NULL,
|
||||||
@@ -63,11 +67,13 @@ final class FxRatesRepository
|
|||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
provider VARCHAR(64) NOT NULL,
|
provider VARCHAR(64) NOT NULL,
|
||||||
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||||
base_currency VARCHAR(10) NOT NULL,
|
base_currency VARCHAR(10) NOT NULL,
|
||||||
rate_date DATE NOT NULL,
|
rate_date DATE NOT NULL,
|
||||||
fetched_at DATETIME NOT NULL,
|
fetched_at DATETIME NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)");
|
)");
|
||||||
|
$this->ensureColumn($fetchTable, 'trigger_source', "ALTER TABLE {$fetchTable} ADD COLUMN trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
||||||
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
fetch_id INTEGER NOT NULL,
|
fetch_id INTEGER NOT NULL,
|
||||||
@@ -83,7 +89,7 @@ final class FxRatesRepository
|
|||||||
|
|
||||||
public function getLatestFetch(?string $baseCurrency = null): ?array
|
public function getLatestFetch(?string $baseCurrency = null): ?array
|
||||||
{
|
{
|
||||||
$sql = 'SELECT id, provider, base_currency, rate_date, fetched_at, created_at FROM ' . $this->table('fetches');
|
$sql = 'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at FROM ' . $this->table('fetches');
|
||||||
$params = [];
|
$params = [];
|
||||||
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
||||||
$sql .= ' WHERE base_currency = :base_currency';
|
$sql .= ' WHERE base_currency = :base_currency';
|
||||||
@@ -99,7 +105,7 @@ final class FxRatesRepository
|
|||||||
public function listLatestFetches(): array
|
public function listLatestFetches(): array
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->query(
|
$stmt = $this->pdo->query(
|
||||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||||
FROM ' . $this->table('fetches') . '
|
FROM ' . $this->table('fetches') . '
|
||||||
ORDER BY fetched_at DESC, id DESC'
|
ORDER BY fetched_at DESC, id DESC'
|
||||||
);
|
);
|
||||||
@@ -120,7 +126,7 @@ final class FxRatesRepository
|
|||||||
public function listRecentFetches(int $limit = 20): array
|
public function listRecentFetches(int $limit = 20): array
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||||
FROM ' . $this->table('fetches') . '
|
FROM ' . $this->table('fetches') . '
|
||||||
ORDER BY fetched_at DESC, id DESC
|
ORDER BY fetched_at DESC, id DESC
|
||||||
LIMIT :limit'
|
LIMIT :limit'
|
||||||
@@ -161,7 +167,7 @@ final class FxRatesRepository
|
|||||||
foreach (['<=', '>='] as $operator) {
|
foreach (['<=', '>='] as $operator) {
|
||||||
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||||
FROM ' . $this->table('fetches') . '
|
FROM ' . $this->table('fetches') . '
|
||||||
WHERE fetched_at ' . $operator . ' :target_at
|
WHERE fetched_at ' . $operator . ' :target_at
|
||||||
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
||||||
@@ -277,11 +283,12 @@ final class FxRatesRepository
|
|||||||
return array_map(fn (array $row): array => $this->normalizeRate($row), $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []);
|
return array_map(fn (array $row): array => $this->normalizeRate($row), $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveFetch(string $baseCurrency, string $provider, string $rateDate, array $rates, ?string $fetchedAt = null): array
|
public function saveFetch(string $baseCurrency, string $provider, string $rateDate, array $rates, ?string $fetchedAt = null, string $triggerSource = 'manual'): array
|
||||||
{
|
{
|
||||||
$baseCurrency = strtoupper(trim($baseCurrency));
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
||||||
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
|
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
|
||||||
$fetchedAt = trim((string) $fetchedAt) !== '' ? trim((string) $fetchedAt) : gmdate('Y-m-d H:i:s');
|
$fetchedAt = trim((string) $fetchedAt) !== '' ? trim((string) $fetchedAt) : gmdate('Y-m-d H:i:s');
|
||||||
|
$triggerSource = $this->normalizeTriggerSource($triggerSource);
|
||||||
$normalizedRates = [];
|
$normalizedRates = [];
|
||||||
foreach ($rates as $currencyCode => $rate) {
|
foreach ($rates as $currencyCode => $rate) {
|
||||||
$currencyCode = strtoupper(trim((string) $currencyCode));
|
$currencyCode = strtoupper(trim((string) $currencyCode));
|
||||||
@@ -301,14 +308,15 @@ final class FxRatesRepository
|
|||||||
if ($this->driver === 'pgsql') {
|
if ($this->driver === 'pgsql') {
|
||||||
$fetchStmt = $this->pdo->prepare(
|
$fetchStmt = $this->pdo->prepare(
|
||||||
'INSERT INTO ' . $this->table('fetches') . ' (
|
'INSERT INTO ' . $this->table('fetches') . ' (
|
||||||
provider, base_currency, rate_date, fetched_at
|
provider, trigger_source, base_currency, rate_date, fetched_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:provider, :base_currency, :rate_date, :fetched_at
|
:provider, :trigger_source, :base_currency, :rate_date, :fetched_at
|
||||||
)
|
)
|
||||||
RETURNING *'
|
RETURNING *'
|
||||||
);
|
);
|
||||||
$fetchStmt->execute([
|
$fetchStmt->execute([
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
|
'trigger_source' => $triggerSource,
|
||||||
'base_currency' => $baseCurrency,
|
'base_currency' => $baseCurrency,
|
||||||
'rate_date' => $rateDate,
|
'rate_date' => $rateDate,
|
||||||
'fetched_at' => $fetchedAt,
|
'fetched_at' => $fetchedAt,
|
||||||
@@ -317,13 +325,14 @@ final class FxRatesRepository
|
|||||||
} else {
|
} else {
|
||||||
$fetchStmt = $this->pdo->prepare(
|
$fetchStmt = $this->pdo->prepare(
|
||||||
'INSERT INTO ' . $this->table('fetches') . ' (
|
'INSERT INTO ' . $this->table('fetches') . ' (
|
||||||
provider, base_currency, rate_date, fetched_at
|
provider, trigger_source, base_currency, rate_date, fetched_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:provider, :base_currency, :rate_date, :fetched_at
|
:provider, :trigger_source, :base_currency, :rate_date, :fetched_at
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
$fetchStmt->execute([
|
$fetchStmt->execute([
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
|
'trigger_source' => $triggerSource,
|
||||||
'base_currency' => $baseCurrency,
|
'base_currency' => $baseCurrency,
|
||||||
'rate_date' => $rateDate,
|
'rate_date' => $rateDate,
|
||||||
'fetched_at' => $fetchedAt,
|
'fetched_at' => $fetchedAt,
|
||||||
@@ -379,7 +388,7 @@ final class FxRatesRepository
|
|||||||
private function getFetchById(int $fetchId): ?array
|
private function getFetchById(int $fetchId): ?array
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||||
FROM ' . $this->table('fetches') . '
|
FROM ' . $this->table('fetches') . '
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
LIMIT 1'
|
LIMIT 1'
|
||||||
@@ -393,10 +402,10 @@ final class FxRatesRepository
|
|||||||
{
|
{
|
||||||
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||||
FROM ' . $this->table('fetches') . '
|
FROM ' . $this->table('fetches') . '
|
||||||
WHERE base_currency = :base_currency
|
WHERE base_currency = :base_currency
|
||||||
AND fetched_at ' . $operator . ' :target_at
|
AND fetched_at ' . $operator . ' :target_at
|
||||||
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
||||||
LIMIT 1'
|
LIMIT 1'
|
||||||
);
|
);
|
||||||
@@ -456,6 +465,7 @@ final class FxRatesRepository
|
|||||||
return [
|
return [
|
||||||
'id' => isset($row['id']) ? (int) $row['id'] : null,
|
'id' => isset($row['id']) ? (int) $row['id'] : null,
|
||||||
'provider' => (string) ($row['provider'] ?? ''),
|
'provider' => (string) ($row['provider'] ?? ''),
|
||||||
|
'trigger_source' => (string) ($row['trigger_source'] ?? 'manual'),
|
||||||
'base_currency' => strtoupper((string) ($row['base_currency'] ?? '')),
|
'base_currency' => strtoupper((string) ($row['base_currency'] ?? '')),
|
||||||
'rate_date' => (string) ($row['rate_date'] ?? ''),
|
'rate_date' => (string) ($row['rate_date'] ?? ''),
|
||||||
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
||||||
@@ -463,6 +473,34 @@ final class FxRatesRepository
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureColumn(string $table, string $column, string $alterSql): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->query('SELECT * FROM ' . $table . ' LIMIT 1');
|
||||||
|
if ($stmt instanceof \PDOStatement) {
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
if (in_array(strtolower($column), array_map('strtolower', array_keys($row)), true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo->exec($alterSql);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeTriggerSource(string $source): string
|
||||||
|
{
|
||||||
|
$source = strtolower(trim($source));
|
||||||
|
return match ($source) {
|
||||||
|
'cron', 'manual', 'api' => $source,
|
||||||
|
default => 'manual',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeRate(array $row): array
|
private function normalizeRate(array $row): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1737,6 +1737,7 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
setStatusText(entry, 'Letzter Erfolg', status.last_success_at_local || status.state?.last_success_at || '-');
|
setStatusText(entry, 'Letzter Erfolg', status.last_success_at_local || status.state?.last_success_at || '-');
|
||||||
setStatusText(entry, 'Naechster Lauf lokal', status.enabled ? (status.next_due_at_local || '-') : '-');
|
setStatusText(entry, 'Naechster Lauf lokal', status.enabled ? (status.next_due_at_local || '-') : '-');
|
||||||
setStatusText(entry, 'Status', status.state?.last_status || '-');
|
setStatusText(entry, 'Status', status.state?.last_status || '-');
|
||||||
|
setStatusText(entry, 'Meldung', status.state?.last_message || '-');
|
||||||
updateEntrySummary(entry);
|
updateEntrySummary(entry);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user