diff --git a/modules/boersenchecker/bootstrap.php b/modules/boersenchecker/bootstrap.php index 1a4a607..1779560 100644 --- a/modules/boersenchecker/bootstrap.php +++ b/modules/boersenchecker/bootstrap.php @@ -291,6 +291,16 @@ $mm->registerFunction($moduleName, 'bavest_request', static function ( $method = in_array($method, ['GET', 'POST'], true) ? $method : 'POST'; if ($apiKey === '') { + module_debug_push('boersenchecker', [ + 'label' => 'Bavest Request', + 'type' => 'api:error', + 'request' => [ + 'method' => $method, + 'path' => $path, + 'payload' => $payload, + ], + 'message' => 'Bavest-API-Key fehlt. Bitte im Modul-Setup hinterlegen.', + ]); return [ 'ok' => false, 'message' => 'Bavest-API-Key fehlt. Bitte im Modul-Setup hinterlegen.', @@ -366,6 +376,21 @@ $mm->registerFunction($moduleName, 'bavest_request', static function ( } if (!is_string($responseBody) || $responseBody === '') { + module_debug_push('boersenchecker', [ + 'label' => 'Bavest Request', + 'type' => 'api:error', + 'request' => [ + 'method' => $method, + 'url' => $url, + 'payload' => $payload, + ], + 'response' => [ + 'http_code' => $httpCode, + 'curl_error' => $curlError, + 'body' => null, + ], + 'message' => 'Bavest Anfrage fehlgeschlagen.', + ]); return [ 'ok' => false, 'message' => 'Bavest Anfrage fehlgeschlagen.' @@ -376,6 +401,20 @@ $mm->registerFunction($moduleName, 'bavest_request', static function ( $decoded = json_decode($responseBody, true); if (!is_array($decoded)) { + module_debug_push('boersenchecker', [ + 'label' => 'Bavest Request', + 'type' => 'api:error', + 'request' => [ + 'method' => $method, + 'url' => $url, + 'payload' => $payload, + ], + 'response' => [ + 'http_code' => $httpCode, + 'body_preview' => substr($responseBody, 0, 4000), + ], + 'message' => 'Bavest Antwort ist kein gueltiges JSON.', + ]); return [ 'ok' => false, 'message' => 'Bavest Antwort ist kein gueltiges JSON.', @@ -385,6 +424,20 @@ $mm->registerFunction($moduleName, 'bavest_request', static function ( foreach (['error', 'message', 'detail'] as $errorKey) { if (isset($decoded[$errorKey]) && is_string($decoded[$errorKey]) && trim($decoded[$errorKey]) !== '') { + module_debug_push('boersenchecker', [ + 'label' => 'Bavest Request', + 'type' => 'api:error', + 'request' => [ + 'method' => $method, + 'url' => $url, + 'payload' => $payload, + ], + 'response' => [ + 'http_code' => $httpCode, + 'body' => $decoded, + ], + 'message' => trim((string) $decoded[$errorKey]), + ]); return [ 'ok' => false, 'message' => trim((string) $decoded[$errorKey]), @@ -393,6 +446,20 @@ $mm->registerFunction($moduleName, 'bavest_request', static function ( } } + module_debug_push('boersenchecker', [ + 'label' => 'Bavest Request', + 'type' => 'api:response', + 'request' => [ + 'method' => $method, + 'url' => $url, + 'payload' => $payload, + ], + 'response' => [ + 'http_code' => $httpCode, + 'body' => $decoded, + ], + ]); + return [ 'ok' => true, 'data' => $decoded, @@ -769,3 +836,77 @@ $mm->registerFunction($moduleName, 'bavest_fetch_chart_series', static function 'source' => 'bavest:timeseries/history', ]; }); + +$mm->registerFunction($moduleName, 'store_market_quote', static function ( + int $instrumentId, + float $price, + string $currency, + string $quotedAt, + string $source +): array { + $pdo = module_fn('boersenchecker', 'pdo'); + $quoteTable = module_fn('boersenchecker', 'table', 'quotes'); + + $quotedAt = trim($quotedAt); + $currency = strtoupper(trim($currency)) ?: 'EUR'; + $source = trim($source) !== '' ? trim($source) : 'bavest:quote'; + + $checkStmt = $pdo->prepare( + 'SELECT id + FROM ' . $quoteTable . ' + WHERE instrument_id = :instrument_id + AND price = :price + AND currency = :currency + AND quoted_at = :quoted_at + AND source = :source + LIMIT 1' + ); + $checkStmt->execute([ + 'instrument_id' => $instrumentId, + 'price' => $price, + 'currency' => $currency, + 'quoted_at' => $quotedAt, + 'source' => $source, + ]); + $existingId = (int) $checkStmt->fetchColumn(); + if ($existingId > 0) { + module_debug_push('boersenchecker', [ + 'label' => 'Quote Store', + 'type' => 'quote:reuse', + 'instrument_id' => $instrumentId, + 'price' => $price, + 'currency' => $currency, + 'quoted_at' => $quotedAt, + 'source' => $source, + 'message' => 'Identischer Snapshot bereits vorhanden.', + ]); + return ['ok' => true, 'inserted' => false, 'id' => $existingId]; + } + + $insertStmt = $pdo->prepare( + 'INSERT INTO ' . $quoteTable . ' (instrument_id, price, currency, quoted_at, source) + VALUES (:instrument_id, :price, :currency, :quoted_at, :source)' + ); + $insertStmt->execute([ + 'instrument_id' => $instrumentId, + 'price' => $price, + 'currency' => $currency, + 'quoted_at' => $quotedAt, + 'source' => $source, + ]); + + $insertedId = (int) $pdo->lastInsertId(); + module_debug_push('boersenchecker', [ + 'label' => 'Quote Store', + 'type' => 'quote:insert', + 'instrument_id' => $instrumentId, + 'price' => $price, + 'currency' => $currency, + 'quoted_at' => $quotedAt, + 'source' => $source, + 'inserted_id' => $insertedId, + 'message' => 'Neuer Snapshot gespeichert.', + ]); + + return ['ok' => true, 'inserted' => true, 'id' => $insertedId]; +}); diff --git a/modules/boersenchecker/src/Support/DashboardPage.php b/modules/boersenchecker/src/Support/DashboardPage.php index daec9b6..1e245a5 100644 --- a/modules/boersenchecker/src/Support/DashboardPage.php +++ b/modules/boersenchecker/src/Support/DashboardPage.php @@ -427,14 +427,19 @@ final class DashboardPage throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); } - $this->storeQuote( + $storeResult = \module_fn( + 'boersenchecker', + 'store_market_quote', $instrumentId, (float) $apiResult['price'], $quoteCurrency, (string) $apiResult['fetched_at'], (string) $apiResult['source'] ); - return 'Bavest-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.'; + if (!empty($storeResult['inserted'])) { + return 'Bavest-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.'; + } + return 'Vorhandener Bavest-Snapshot fuer ' . (string) $row['instrument_name'] . ' wiederverwendet.'; } private function refreshMarketDataAll(): string @@ -499,14 +504,20 @@ final class DashboardPage continue; } - $this->storeQuote( + $storeResult = \module_fn( + 'boersenchecker', + 'store_market_quote', $instrumentId, (float) $apiResult['price'], $quoteCurrency, (string) $apiResult['fetched_at'], (string) $apiResult['source'] ); - $fetched++; + if (!empty($storeResult['inserted'])) { + $fetched++; + } else { + $reused++; + } } } diff --git a/modules/boersenchecker/src/Support/HomePage.php b/modules/boersenchecker/src/Support/HomePage.php index 5a0e2b3..c761e1d 100644 --- a/modules/boersenchecker/src/Support/HomePage.php +++ b/modules/boersenchecker/src/Support/HomePage.php @@ -157,25 +157,26 @@ final class HomePage if ($bulkCandidates !== []) { $bulkResult = \module_fn('boersenchecker', 'bavest_fetch_bulk_quotes', $bulkCandidates); $quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : []; - $stmtInsert = $this->pdo->prepare( - 'INSERT INTO ' . $this->quoteTable . ' (instrument_id, price, currency, quoted_at, source) - VALUES (:instrument_id, :price, :currency, :quoted_at, :source)' - ); - foreach ($bulkCandidates as $row) { $instrumentId = (int) ($row['id'] ?? 0); $quote = $quotes[$instrumentId] ?? null; if (!is_array($quote) || !is_numeric($quote['price'] ?? null)) { continue; } - $stmtInsert->execute([ - 'instrument_id' => $instrumentId, - 'price' => (float) $quote['price'], - 'currency' => strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, - 'quoted_at' => (string) ($quote['fetched_at'] ?? date('Y-m-d H:i:s')), - 'source' => (string) ($quote['source'] ?? 'bavest:quote'), - ]); - $updated++; + $storeResult = \module_fn( + 'boersenchecker', + 'store_market_quote', + $instrumentId, + (float) $quote['price'], + strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, + (string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')), + (string) ($quote['source'] ?? 'bavest:quote') + ); + if (!empty($storeResult['inserted'])) { + $updated++; + } else { + $reused++; + } } } diff --git a/modules/boersenchecker/src/Support/InstrumentPage.php b/modules/boersenchecker/src/Support/InstrumentPage.php index 0bfeff1..3e3c384 100644 --- a/modules/boersenchecker/src/Support/InstrumentPage.php +++ b/modules/boersenchecker/src/Support/InstrumentPage.php @@ -224,19 +224,19 @@ final class InstrumentPage throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); } - $stmtInsert = $this->pdo->prepare( - 'INSERT INTO ' . $this->quoteTable . ' (instrument_id, price, currency, quoted_at, source) - VALUES (:instrument_id, :price, :currency, :quoted_at, :source)' + $storeResult = \module_fn( + 'boersenchecker', + 'store_market_quote', + $instrumentId, + (float) $apiResult['price'], + strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, + (string) $apiResult['fetched_at'], + (string) $apiResult['source'] ); - $stmtInsert->execute([ - 'instrument_id' => $instrumentId, - 'price' => (float) $apiResult['price'], - 'currency' => strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, - 'quoted_at' => (string) $apiResult['fetched_at'], - 'source' => (string) $apiResult['source'], - ]); - return 'Bavest-Kurs gespeichert.'; + return !empty($storeResult['inserted']) + ? 'Bavest-Kurs gespeichert.' + : 'Vorhandener Bavest-Snapshot wiederverwendet.'; } private function searchSymbol(): string diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 672379d..00f31d5 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1098,6 +1098,90 @@ a { flex-wrap: wrap; } +.module-debug { + border: 1px solid var(--line); + border-radius: 22px; + background: var(--surface); + box-shadow: 0 12px 30px rgba(1, 22, 32, 0.08); +} + +.module-debug-details { + padding: 0; +} + +.module-debug-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + list-style: none; + font-weight: 700; +} + +.module-debug-summary::-webkit-details-marker { + display: none; +} + +.module-debug-meta, +.module-debug-entry-time { + color: var(--muted); + font-weight: 500; + font-size: 0.92rem; +} + +.module-debug-body { + display: grid; + gap: 12px; + padding: 0 20px 20px; +} + +.module-debug-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.module-debug-empty { + padding: 14px 16px; + border: 1px dashed var(--line); + border-radius: 16px; + color: var(--muted); +} + +.module-debug-entry { + border: 1px solid var(--line); + border-radius: 16px; + background: color-mix(in srgb, var(--surface) 92%, var(--brand-accent-2) 8%); +} + +.module-debug-entry summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + cursor: pointer; + list-style: none; +} + +.module-debug-entry summary::-webkit-details-marker { + display: none; +} + +.module-debug-pre { + margin: 0; + padding: 0 14px 14px; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.86rem; + line-height: 1.5; +} + .module-box-head p { margin: 6px 0 0; color: var(--muted); diff --git a/src/App/functions.php b/src/App/functions.php index ea2d822..3490ac3 100644 --- a/src/App/functions.php +++ b/src/App/functions.php @@ -276,6 +276,46 @@ function module_design(string $module): array return $cache[$module]; } +function module_debug_entries(string $module): array +{ + if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { + return []; + } + + app()->session()->start(); + $entries = $_SESSION['module_debug'][$module] ?? []; + return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; +} + +function module_debug_push(string $module, array $entry): void +{ + if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { + return; + } + + app()->session()->start(); + if (!isset($_SESSION['module_debug']) || !is_array($_SESSION['module_debug'])) { + $_SESSION['module_debug'] = []; + } + if (!isset($_SESSION['module_debug'][$module]) || !is_array($_SESSION['module_debug'][$module])) { + $_SESSION['module_debug'][$module] = []; + } + + $entry['at'] = $entry['at'] ?? date('Y-m-d H:i:s'); + array_unshift($_SESSION['module_debug'][$module], $entry); + $_SESSION['module_debug'][$module] = array_slice($_SESSION['module_debug'][$module], 0, 25); +} + +function module_debug_clear(string $module): void +{ + if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { + return; + } + + app()->session()->start(); + unset($_SESSION['module_debug'][$module]); +} + function module_shell_header(string $module, array $options = []): string { $design = module_design($module); @@ -374,7 +414,53 @@ function module_shell_header(string $module, array $options = []): string function module_shell_footer(): string { - return ''; + $html = ''; + $module = current_module_name(); + if (is_string($module) && $module !== '') { + if ((string) ($_GET['module_debug_clear'] ?? '') === '1') { + module_debug_clear($module); + } + + $entries = module_debug_entries($module); + $currentPath = app()->request()->path(); + $clearHref = $currentPath . '?module_debug_clear=1'; + + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= 'Debug'; + $html .= '' . e((string) count($entries)) . ' Eintraege'; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= '
Standard-Debugbereich des Moduls. Zeigt die letzten Request-/Response-Daten der aktuellen Sitzung.
'; + $html .= 'Debug leeren'; + $html .= '
'; + + if ($entries === []) { + $html .= '
Noch keine Debug-Daten vorhanden.
'; + } else { + foreach ($entries as $index => $entry) { + $label = trim((string) ($entry['label'] ?? ('Eintrag ' . ($index + 1)))); + $at = trim((string) ($entry['at'] ?? '')); + $payload = json_encode($entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($payload)) { + $payload = '{}'; + } + + $html .= '
'; + $html .= '' . e($label) . '' . ($at !== '' ? '' . e($at) . '' : '') . ''; + $html .= '
' . e($payload) . '
'; + $html .= '
'; + } + } + + $html .= '
'; + $html .= '
'; + $html .= '
'; + } + + return $html . ''; } /**