From c6642c0ef5f5b4581fe77c4d41626ae8618d04f7 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 9 May 2026 01:47:22 +0200 Subject: [PATCH] adasd --- modules/mining-checker/assets/css/app.css | 31 +++++ modules/mining-checker/assets/js/app.js | 94 +++++++++------ modules/mining-checker/bootstrap.php | 140 ++++++++++++++++++++++ modules/mining-checker/module.json | 14 +++ partials/landingpages/modules/setup.php | 63 +++++++++- 5 files changed, 302 insertions(+), 40 deletions(-) diff --git a/modules/mining-checker/assets/css/app.css b/modules/mining-checker/assets/css/app.css index 01127b1..d5b58b3 100644 --- a/modules/mining-checker/assets/css/app.css +++ b/modules/mining-checker/assets/css/app.css @@ -136,6 +136,37 @@ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); } +#mining-checker-app .mc-asset-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 14px; +} + +#mining-checker-app .mc-asset-card { + gap: 8px; +} + +#mining-checker-app .mc-asset-balance { + font-size: 1.15rem; + font-weight: 700; + color: var(--mc-text); +} + +#mining-checker-app .mc-asset-list { + display: grid; + gap: 10px; + min-width: 240px; +} + +#mining-checker-app .mc-asset-row { + display: grid; + gap: 2px; +} + +#mining-checker-app .mc-asset-row strong { + color: var(--mc-text); +} + #mining-checker-app .mc-main-grid { grid-template-columns: minmax(300px, 380px) minmax(0, 1fr); } diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index c165605..2a0b4eb 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -2134,9 +2134,7 @@ } if (activeTab === 'measurements') { - return h('div', { className: 'mc-main-grid' }, [ - h('div', { className: 'mc-stack' }, [ - ]), + return h('div', { className: 'mc-stack' }, [ panel('Mining-History', 'Die letzten 10 Mining-Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'thead' }, h('tr', null, [ @@ -2165,8 +2163,8 @@ } if (activeTab === 'upload') { - return h('div', { className: 'mc-main-grid' }, [ - h('div', { className: 'mc-stack' }, [ + return h('div', { className: 'mc-stack' }, [ + h('div', { className: 'mc-two-col' }, [ renderSharedOcrPanel(), panel('Mining manuell erfassen', 'Direkte Eingabe eines einzelnen Mining-Messpunkts mit serverseitiger Validierung.', h('form', { className: 'mc-form', @@ -2186,28 +2184,6 @@ disabled: saving, }, saving ? 'Speichert …' : 'Messpunkt speichern'), ])), - panel('Mining-Import per Copy & Paste', 'Mehrere historische Mining-Messpunkte auf einmal einfuegen. Doppelte Eintraege werden ignoriert.', h('form', { - className: 'mc-form', - onSubmit: submitMeasurementImport, - }, [ - h('div', { className: 'mc-inline-row' }, [ - h('button', { - key: 'help', - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: () => setImportHelpOpen(true), - }, 'Import-Hilfe'), - ]), - displayField('Format', 'DD.MM.YYYY HH:MM | Coins | Kurs | Waehrung | Notiz'), - textareaField('Importdaten', importForm.rows_text, (value) => setImportForm({ ...importForm, rows_text: value })), - selectField('Standard-Waehrung', importForm.default_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setImportForm({ ...importForm, default_currency: value })), - selectField('Import-Quelle', importForm.source, ['manual', 'seed_import'], (value) => setImportForm({ ...importForm, source: value })), - h('button', { - type: 'submit', - className: 'mc-button mc-button--secondary', - disabled: saving, - }, saving ? 'Importiert …' : 'Import ausfuehren'), - ])), importHelpOpen ? h('div', { className: 'mc-modal-backdrop', onClick: () => setImportHelpOpen(false) }, [ h('div', { key: 'modal', @@ -2246,16 +2222,56 @@ ]), ]) : null, ]), + panel('Mining-Import per Copy & Paste', 'Mehrere historische Mining-Messpunkte auf einmal einfuegen. Doppelte Eintraege werden ignoriert.', h('form', { + className: 'mc-form', + onSubmit: submitMeasurementImport, + }, [ + h('div', { className: 'mc-inline-row' }, [ + h('button', { + key: 'help', + type: 'button', + className: 'mc-button mc-button--ghost', + onClick: () => setImportHelpOpen(true), + }, 'Import-Hilfe'), + ]), + displayField('Format', 'DD.MM.YYYY HH:MM | Coins | Kurs | Waehrung | Notiz'), + textareaField('Importdaten', importForm.rows_text, (value) => setImportForm({ ...importForm, rows_text: value })), + selectField('Standard-Waehrung', importForm.default_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setImportForm({ ...importForm, default_currency: value })), + selectField('Import-Quelle', importForm.source, ['manual', 'seed_import'], (value) => setImportForm({ ...importForm, source: value })), + h('button', { + type: 'submit', + className: 'mc-button mc-button--secondary', + disabled: saving, + }, saving ? 'Importiert …' : 'Import ausfuehren'), + ])), ]); } if (activeTab === 'wallet') { - return h('div', { className: 'mc-main-grid' }, [ - h('div', { className: 'mc-stack' }, [ - panel('Wallet-Historie', `Erkannte Wallet-Snapshots. Fuer Reinvestitionen ist aktuell ${currentSettings.crypto_currency || 'DOGE'} entscheidend.`, h('div', { className: 'mc-table-shell' }, [ + const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null; + const latestWalletAssets = latestWalletSnapshot && latestWalletSnapshot.balances_json && typeof latestWalletSnapshot.balances_json === 'object' + ? Object.entries(latestWalletSnapshot.balances_json) + : []; + + return h('div', { className: 'mc-stack' }, [ + panel('Wallet-Bestaende', 'Der letzte Wallet-Snapshot zeigt alle erkannten Assets separat.', latestWalletAssets.length + ? h('div', { className: 'mc-asset-grid' }, latestWalletAssets.map(([code, asset]) => { + const balance = asset && typeof asset === 'object' ? asset.balance : asset; + const priceAmount = asset && typeof asset === 'object' ? asset.price_amount : null; + const priceCurrency = asset && typeof asset === 'object' ? asset.price_currency : null; + return h('div', { key: code, className: 'mc-display-field mc-asset-card' }, [ + h('div', { key: 'code', className: 'mc-field-label' }, code), + h('div', { key: 'balance', className: 'mc-asset-balance' }, `${fmtNumber(balance, 8)} ${code}`), + h('div', { key: 'price', className: 'mc-text' }, priceAmount !== null && priceAmount !== undefined + ? `1 ${code} = ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim() + : 'Kein Screenshot-Kurs erkannt'), + ]); + })) + : h('div', { className: 'mc-empty' }, 'Noch keine Wallet-Assets erkannt.')), + panel('Wallet-Historie', 'Erkannte Wallet-Snapshots mit allen aus dem Screenshot gelesenen Assets.', h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'wallet-table', className: 'mc-table' }, [ h('thead', { key: 'thead' }, h('tr', null, [ - 'Zeit', 'Mining-Waehrung', 'Bestand', 'Gesamtwert', 'Quelle', 'Assets' + 'Zeit', 'Mining-Waehrung', 'Mining-Bestand', 'Quelle', 'Assets' ].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'tbody' }, currentWalletSnapshots.length @@ -2263,20 +2279,24 @@ h('td', { key: 'measured' }, fmtDate(row.measured_at)), h('td', { key: 'currency' }, row.wallet_currency || currentSettings.crypto_currency || 'DOGE'), h('td', { key: 'balance' }, row.wallet_balance !== null && row.wallet_balance !== undefined ? `${fmtNumber(row.wallet_balance, 8)} ${row.wallet_currency || ''}`.trim() : 'n/a'), - h('td', { key: 'value' }, row.total_value_amount !== null && row.total_value_amount !== undefined ? `${fmtNumber(row.total_value_amount, 4)} ${row.total_value_currency || ''}`.trim() : 'n/a'), h('td', { key: 'source' }, row.source || 'manual'), - h('td', { key: 'assets' }, Object.entries(row.balances_json || {}).slice(0, 6).map(([code, asset]) => { + h('td', { key: 'assets' }, h('div', { className: 'mc-asset-list' }, Object.entries(row.balances_json || {}).map(([code, asset]) => { const balance = asset && typeof asset === 'object' ? asset.balance : asset; const priceAmount = asset && typeof asset === 'object' ? asset.price_amount : null; const priceCurrency = asset && typeof asset === 'object' ? asset.price_currency : null; - return `${fmtNumber(balance, 8)} ${code}${priceAmount ? ` @ ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim() : ''}`; - }).join(' · ') || 'n/a'), + return h('div', { key: code, className: 'mc-asset-row' }, [ + h('strong', { key: 'code' }, code), + h('span', { key: 'balance' }, `${fmtNumber(balance, 8)} ${code}`), + priceAmount !== null && priceAmount !== undefined + ? h('span', { key: 'price', className: 'mc-text' }, `1 ${code} = ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim()) + : null, + ]); + }))), ])) - : [h('tr', { key: 'empty' }, h('td', { colSpan: 6 }, 'Noch keine Wallet-Snapshots gespeichert.'))] + : [h('tr', { key: 'empty' }, h('td', { colSpan: 5 }, 'Noch keine Wallet-Snapshots gespeichert.'))] ), ]), ])), - ]), ]); } diff --git a/modules/mining-checker/bootstrap.php b/modules/mining-checker/bootstrap.php index a6d4744..4e5dc8c 100644 --- a/modules/mining-checker/bootstrap.php +++ b/modules/mining-checker/bootstrap.php @@ -2,7 +2,9 @@ declare(strict_types=1); use Modules\MiningChecker\Infrastructure\ConnectionFactory; +use Modules\MiningChecker\Infrastructure\MiningRepository; use Modules\MiningChecker\Infrastructure\ModuleConfig; +use Modules\MiningChecker\Infrastructure\SchemaManager; spl_autoload_register(static function (string $class): void { $prefix = 'Modules\\MiningChecker\\'; @@ -19,6 +21,144 @@ spl_autoload_register(static function (string $class): void { }); $mm = isset($modules) && $modules instanceof App\ModuleManager ? $modules : modules(); +$moduleName = 'mining-checker'; + +$mm->registerFunction($moduleName, 'runtime_settings', static function (): array { + $moduleBasePath = __DIR__; + $config = ModuleConfig::load($moduleBasePath); + $projectKey = $config->defaultProjectKey(); + $user = auth_user() ?? []; + $ownerSub = trim((string) ($user['sub'] ?? '')) !== '' ? trim((string) ($user['sub'] ?? '')) : 'local'; + + $pdo = ConnectionFactory::make($config); + $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); + + $settings = $repository->getSettings($projectKey) ?? []; + $displayTimezone = trim((string) ($settings['display_timezone'] ?? 'Europe/Berlin')); + if ($displayTimezone === '') { + $displayTimezone = 'Europe/Berlin'; + } + + $baselineMeasuredAt = trim((string) ($settings['baseline_measured_at'] ?? '')); + if ($baselineMeasuredAt !== '') { + try { + $baselineMeasuredAt = (new DateTimeImmutable($baselineMeasuredAt, new DateTimeZone('UTC'))) + ->setTimezone(new DateTimeZone($displayTimezone)) + ->format('Y-m-d\TH:i'); + } catch (\Throwable) { + $baselineMeasuredAt = ''; + } + } + + $preferredCurrencies = $settings['preferred_currencies'] ?? ['DOGE', 'USD', 'EUR']; + if (is_string($preferredCurrencies)) { + $preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: []; + } + $preferredCurrencies = array_values(array_filter(array_map( + static fn (mixed $value): string => strtoupper(trim((string) $value)), + is_array($preferredCurrencies) ? $preferredCurrencies : [] + ), static fn (string $value): bool => $value !== '')); + + return [ + 'baseline_measured_at' => $baselineMeasuredAt, + 'baseline_coins_total' => isset($settings['baseline_coins_total']) ? (string) $settings['baseline_coins_total'] : '', + 'daily_cost_amount' => isset($settings['daily_cost_amount']) ? (string) $settings['daily_cost_amount'] : '', + 'daily_cost_currency' => strtoupper(trim((string) ($settings['daily_cost_currency'] ?? 'EUR'))) ?: 'EUR', + 'report_currency' => strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR', + 'crypto_currency' => strtoupper(trim((string) ($settings['crypto_currency'] ?? 'DOGE'))) ?: 'DOGE', + 'display_timezone' => $displayTimezone, + 'preferred_currencies' => $preferredCurrencies, + ]; +}); + +$mm->registerFunction($moduleName, 'save_runtime_settings', static function (array $payload): array { + $moduleBasePath = __DIR__; + $config = ModuleConfig::load($moduleBasePath); + $projectKey = $config->defaultProjectKey(); + $user = auth_user() ?? []; + $ownerSub = trim((string) ($user['sub'] ?? '')) !== '' ? trim((string) ($user['sub'] ?? '')) : 'local'; + + $pdo = ConnectionFactory::make($config); + $schema = new SchemaManager($pdo, $config->tablePrefix(), $moduleBasePath); + $schema->ensureSchema(); + + $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); + $repository->ensureProject($projectKey, strtoupper(str_replace('-', ' ', $projectKey))); + + $existing = $repository->getSettings($projectKey) ?? []; + $displayTimezone = trim((string) ($payload['display_timezone'] ?? ($existing['display_timezone'] ?? 'Europe/Berlin'))); + if ($displayTimezone === '') { + $displayTimezone = 'Europe/Berlin'; + } + + try { + $displayTz = new DateTimeZone($displayTimezone); + } catch (\Throwable) { + throw new RuntimeException('Ungueltige Anzeige-Zeitzone.'); + } + + $baselineInput = trim((string) ($payload['baseline_measured_at'] ?? ($existing['baseline_measured_at'] ?? ''))); + if ($baselineInput === '') { + throw new RuntimeException('Baseline Zeitpunkt fehlt.'); + } + + try { + $baselineUtc = (new DateTimeImmutable($baselineInput, $displayTz)) + ->setTimezone(new DateTimeZone('UTC')) + ->format('Y-m-d H:i:s'); + } catch (\Throwable) { + throw new RuntimeException('Baseline Zeitpunkt ist ungueltig.'); + } + + $baselineCoins = trim((string) ($payload['baseline_coins_total'] ?? ($existing['baseline_coins_total'] ?? ''))); + $dailyCostAmount = trim((string) ($payload['daily_cost_amount'] ?? ($existing['daily_cost_amount'] ?? ''))); + if ($baselineCoins === '' || !is_numeric($baselineCoins)) { + throw new RuntimeException('Baseline Coins muessen numerisch sein.'); + } + if ($dailyCostAmount === '' || !is_numeric($dailyCostAmount)) { + throw new RuntimeException('Taegliche Kosten muessen numerisch sein.'); + } + + $preferredCurrencies = $payload['preferred_currencies'] ?? ($existing['preferred_currencies'] ?? ['DOGE', 'USD', 'EUR']); + if (is_string($preferredCurrencies)) { + $preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: []; + } + $preferredCurrencies = array_values(array_unique(array_filter(array_map( + static fn (mixed $value): string => strtoupper(trim((string) $value)), + is_array($preferredCurrencies) ? $preferredCurrencies : [] + ), static fn (string $value): bool => $value !== ''))); + + $settings = [ + 'baseline_measured_at' => $baselineUtc, + 'baseline_coins_total' => (float) $baselineCoins, + 'daily_cost_amount' => (float) $dailyCostAmount, + 'daily_cost_currency' => strtoupper(trim((string) ($payload['daily_cost_currency'] ?? ($existing['daily_cost_currency'] ?? 'EUR')))) ?: 'EUR', + 'report_currency' => strtoupper(trim((string) ($payload['report_currency'] ?? ($existing['report_currency'] ?? 'EUR')))) ?: 'EUR', + 'crypto_currency' => strtoupper(trim((string) ($payload['crypto_currency'] ?? ($existing['crypto_currency'] ?? 'DOGE')))) ?: 'DOGE', + 'display_timezone' => $displayTimezone, + 'fx_max_age_hours' => isset($existing['fx_max_age_hours']) && is_numeric((string) $existing['fx_max_age_hours']) + ? (int) $existing['fx_max_age_hours'] + : 3, + 'module_theme_mode' => in_array((string) ($existing['module_theme_mode'] ?? 'inherit'), ['inherit', 'custom'], true) + ? (string) $existing['module_theme_mode'] + : 'inherit', + 'module_theme_accent' => in_array((string) ($existing['module_theme_accent'] ?? 'teal'), ['teal', 'logo', 'pink', 'cyan', 'orange', 'green'], true) + ? (string) $existing['module_theme_accent'] + : 'teal', + 'preferred_currencies' => $preferredCurrencies, + ]; + + $repository->saveSettings($projectKey, $settings); + + if (modules()->isEnabled('fx-rates') && modules()->hasFunction('fx-rates', 'save_runtime_settings')) { + module_fn('fx-rates', 'save_runtime_settings', [ + 'preferred_currencies' => $preferredCurrencies, + ]); + } + + return module_fn($moduleName, 'runtime_settings'); +}); + $mm->registerFunction('mining-checker', 'sql_import_target', static function (): array { $moduleBasePath = __DIR__; $config = ModuleConfig::load($moduleBasePath); diff --git a/modules/mining-checker/module.json b/modules/mining-checker/module.json index 23792ad..9f63417 100644 --- a/modules/mining-checker/module.json +++ b/modules/mining-checker/module.json @@ -1,7 +1,21 @@ { "name": "Mining-Checker", + "title": "Mining-Checker", + "version": "0.3.0", "description": "Erfassung, OCR-Auswertung und Analyse von DOGE-Mining-Messwerten als eingebettetes Modul.", "enabled_by_default": true, + "setup": { + "fields": [ + { "name": "baseline_measured_at", "label": "Baseline Zeitpunkt", "type": "datetime-local", "required": false, "help": "Referenzzeitpunkt fuer die erste Baseline-Messung." }, + { "name": "baseline_coins_total", "label": "Baseline Coins", "type": "number", "required": false, "help": "Coin-Bestand zum Baseline-Zeitpunkt." }, + { "name": "daily_cost_amount", "label": "Taegliche Kosten", "type": "number", "required": false, "help": "Taegliche Kosten fuer die Break-even-Berechnung." }, + { "name": "daily_cost_currency", "label": "Kostenwaehrung", "type": "text", "required": false, "help": "FIAT-Waehrung der taeglichen Kosten, z.B. EUR." }, + { "name": "report_currency", "label": "Standard-Berichtswaehrung", "type": "text", "required": false, "help": "Zielwaehrung fuer Kennzahlen und Berichte, z.B. EUR." }, + { "name": "crypto_currency", "label": "Standard-Krypto-Waehrung", "type": "text", "required": false, "help": "Aktuell geminte Hauptwaehrung, z.B. DOGE oder BTC." }, + { "name": "display_timezone", "label": "Anzeige-Zeitzone", "type": "text", "required": false, "help": "Zeitzone fuer lokale Datums- und Uhrzeitanzeige, z.B. Europe/Berlin." }, + { "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "textarea", "required": false, "help": "Mehrere Codes mit Komma oder Zeilenumbruch trennen, z.B. DOGE, USD, EUR." } + ] + }, "auth": { "required": true, "users": [], diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php index 0bdc65a..242a23d 100644 --- a/partials/landingpages/modules/setup.php +++ b/partials/landingpages/modules/setup.php @@ -48,6 +48,17 @@ foreach ($fields as $field) { } $isFxRatesSetup = $moduleName === 'fx-rates'; $current = modules()->settings($moduleName); +$runtimeSettingsEnabled = modules()->hasFunction($moduleName, 'runtime_settings'); +if ($runtimeSettingsEnabled) { + try { + $runtimeSettings = module_fn($moduleName, 'runtime_settings'); + if (is_array($runtimeSettings)) { + $current = array_replace_recursive($current, $runtimeSettings); + } + } catch (\Throwable $e) { + $error = $e->getMessage(); + } +} $intervalTaskStatuses = []; $cronTaskDefinitions = modules()->cronTasks($moduleName); $cronTaskStatuses = []; @@ -405,15 +416,23 @@ $renderField = function (array $field) use (&$current, $getNested, $driverOption $type = (string)($field['type'] ?? 'text'); $required = !empty($field['required']); $help = (string)($field['help'] ?? $field['description'] ?? ''); + $options = is_array($field['options'] ?? null) ? $field['options'] : []; $postKey = str_replace('.', '_', $name); $value = ''; if ($name === 'kea_auto_init') { $value = !empty($current[$name]) ? '1' : '0'; } elseif (str_contains($name, '.')) { - $value = (string)($getNested($current, $name) ?? ''); + $value = $getNested($current, $name); } else { - $value = (string)($current[$name] ?? ''); + $value = $current[$name] ?? ''; + } + if (is_array($value)) { + $value = $type === 'multiselect' + ? array_values(array_map('strval', $value)) + : implode(', ', array_values(array_map('strval', $value))); + } else { + $value = (string) $value; } ?>