From 7400caa687b0354f39c689a44d2ced142b3fdd45 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 11 Apr 2026 03:02:39 +0200 Subject: [PATCH] Importer --- modules/mining-checker/assets/js/app.js | 37 +++++ modules/mining-checker/src/Api/Router.php | 121 ++++++++++++--- .../src/Infrastructure/MiningRepository.php | 145 ++++++++++++++++++ 3 files changed, 282 insertions(+), 21 deletions(-) diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 35e3791..f714b27 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -1542,6 +1542,36 @@ } } + async function importOldData() { + if (!window.confirm('Alte Mining-Checker Daten ueber alle Tabellen sichern, Schema neu aufbauen und danach importieren?')) { + return; + } + + setSaving(true); + setError(''); + setMessage(''); + try { + const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/rebuild-preserve-core`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + const restored = result.restored && typeof result.restored === 'object' ? result.restored : {}; + const restoredParts = Object.keys(restored) + .filter((key) => Number(restored[key]) > 0) + .map((key) => `${key}: ${restored[key]}`); + setMessage( + `${result.message || 'Alte Daten wurden importiert.'} ` + + (restoredParts.length ? `Importiert: ${restoredParts.join(', ')}.` : 'Keine Altdaten gefunden.') + ); + await loadSchemaStatus(projectKey); + await loadBootstrap(projectKey); + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + } + async function testDatabaseConnection() { setSaving(true); setError(''); @@ -2829,6 +2859,13 @@ onClick: upgradeDatabaseSchema, disabled: saving, }, saving ? 'Upgradet …' : 'DB auf neueste Version upgraden'), + h('button', { + key: 'old-data-import', + type: 'button', + className: 'mc-button mc-button--secondary', + onClick: importOldData, + disabled: saving, + }, saving ? 'Importiert …' : 'Alte Daten importieren'), ]), panel('Datenbank-Test', 'Prueft, ob das Modul die Projekt-Datenbank erreichen und eine einfache Anfrage ausfuehren kann.', [ dbCheck diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index b02a858..c70a02c 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -388,13 +388,16 @@ final class Router 'project' => $this->repository()->getProject($projectKey), 'settings' => $this->safeRead(fn () => $this->repository()->getSettings($projectKey)), 'currencies' => $this->safeRead(fn () => $this->repository()->listCurrencies(), []), + 'currency_aliases' => $this->safeRead(fn () => $this->repository()->listCurrencyAliases(), []), 'cost_plans' => $this->safeRead(fn () => $this->repository()->listCostPlans($projectKey), []), - 'measurements' => $this->safeRead(fn () => $this->repository()->listMeasurements($projectKey, 5000), []), + 'measurements' => $this->safeRead(fn () => $this->repository()->listAllMeasurements($projectKey), []), + 'measurement_rates' => $this->safeRead(fn () => $this->repository()->listMeasurementRates($projectKey), []), 'payouts' => $this->safeRead(fn () => $this->repository()->listPayouts($projectKey), []), 'targets' => $this->safeRead(fn () => $this->repository()->listTargets($projectKey), []), 'dashboards' => $this->safeRead(fn () => $this->repository()->listDashboards($projectKey), []), 'miner_offers' => $this->safeRead(fn () => $this->repository()->listMinerOffers($projectKey), []), 'purchased_miners' => $this->safeRead(fn () => $this->repository()->listPurchasedMiners($projectKey), []), + 'fx_rates' => $this->safeRead(fn () => $this->repository()->listAllFxRates(), []), ]; $result = $this->schemaManager()->rebuildSchemaDirect(); @@ -415,10 +418,17 @@ final class Router 'name' => $currency['name'], 'symbol' => $currency['symbol'] ?? null, 'is_active' => !empty($currency['is_active']) ? 1 : 0, + 'is_crypto' => !empty($currency['is_crypto']) ? 1 : 0, 'sort_order' => (int) ($currency['sort_order'] ?? 0), ]); } + foreach ($backup['currency_aliases'] as $alias) { + if (!empty($alias['alias_code']) && !empty($alias['currency_code'])) { + $this->repository()->saveCurrencyAlias((string) $alias['alias_code'], (string) $alias['currency_code']); + } + } + if (is_array($backup['settings'])) { $this->repository()->saveSettings($projectKey, [ 'baseline_measured_at' => $backup['settings']['baseline_measured_at'], @@ -429,6 +439,8 @@ final class Router 'crypto_currency' => $backup['settings']['crypto_currency'] ?? 'DOGE', 'display_timezone' => $backup['settings']['display_timezone'] ?? 'Europe/Berlin', 'fx_max_age_hours' => $backup['settings']['fx_max_age_hours'] ?? 3, + 'module_theme_mode' => $backup['settings']['module_theme_mode'] ?? 'inherit', + 'module_theme_accent' => $backup['settings']['module_theme_accent'] ?? 'teal', 'preferred_currencies' => $backup['settings']['preferred_currencies'] ?? ['DOGE', 'USD', 'EUR'], ]); } @@ -450,6 +462,21 @@ final class Router ]); } + $measurementRatesByOldId = []; + foreach ($backup['measurement_rates'] as $rate) { + $oldMeasurementId = (int) ($rate['measurement_id'] ?? 0); + if ($oldMeasurementId > 0) { + $measurementRatesByOldId[$oldMeasurementId][] = [ + 'base_currency' => $rate['base_currency'] ?? null, + 'quote_currency' => $rate['quote_currency'] ?? null, + 'rate' => $rate['rate'] ?? null, + 'provider' => $rate['provider'] ?? 'derived', + ]; + } + } + + $measurementIdMap = []; + $restoredMeasurementRates = 0; foreach ($backup['measurements'] as $measurement) { $created = $this->repository()->createMeasurementIfNotExists($projectKey, [ 'measured_at' => $measurement['measured_at'], @@ -464,7 +491,17 @@ final class Router 'ocr_flags' => $measurement['ocr_flags'] ?? null, ]); if (is_array($created)) { - $this->captureMeasurementRates($projectKey, $created); + $oldMeasurementId = (int) ($measurement['id'] ?? 0); + $newMeasurementId = (int) ($created['id'] ?? 0); + if ($oldMeasurementId > 0 && $newMeasurementId > 0) { + $measurementIdMap[$oldMeasurementId] = $newMeasurementId; + } + if ($oldMeasurementId > 0 && isset($measurementRatesByOldId[$oldMeasurementId])) { + $this->repository()->replaceMeasurementRates($newMeasurementId, $projectKey, $measurementRatesByOldId[$oldMeasurementId]); + $restoredMeasurementRates += count($measurementRatesByOldId[$oldMeasurementId]); + } else { + $this->captureMeasurementRates($projectKey, $created); + } } } @@ -477,12 +514,36 @@ final class Router ]); } + $minerOfferIdMap = []; + foreach ($backup['miner_offers'] as $offer) { + $savedOffer = $this->repository()->saveMinerOffer($projectKey, [ + 'label' => $offer['label'], + 'runtime_months' => $offer['runtime_months'] ?? null, + 'mining_speed_value' => $offer['mining_speed_value'] ?? null, + 'mining_speed_unit' => $offer['mining_speed_unit'] ?? null, + 'bonus_speed_value' => $offer['bonus_speed_value'] ?? null, + 'bonus_speed_unit' => $offer['bonus_speed_unit'] ?? null, + 'base_price_amount' => $offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? $offer['price_amount'], + 'base_price_currency' => $offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ($offer['price_currency'] ?? null)), + 'payment_type' => $offer['payment_type'] ?? (!empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true) ? 'crypto' : 'fiat'), + 'auto_renew' => !empty($offer['auto_renew']) ? 1 : 0, + 'note' => $offer['note'] ?? null, + 'is_active' => !empty($offer['is_active']) ? 1 : 0, + ]); + $oldOfferId = (int) ($offer['id'] ?? 0); + $newOfferId = (int) ($savedOffer['id'] ?? 0); + if ($oldOfferId > 0 && $newOfferId > 0) { + $minerOfferIdMap[$oldOfferId] = $newOfferId; + } + } + foreach ($backup['targets'] as $target) { + $oldOfferId = (int) ($target['miner_offer_id'] ?? 0); $this->repository()->saveTarget($projectKey, [ 'label' => $target['label'], 'target_amount_fiat' => $target['target_amount_fiat'], 'currency' => $target['currency'], - 'miner_offer_id' => $target['miner_offer_id'] ?? null, + 'miner_offer_id' => $oldOfferId > 0 ? ($minerOfferIdMap[$oldOfferId] ?? null) : null, 'is_active' => !empty($target['is_active']) ? 1 : 0, 'sort_order' => (int) ($target['sort_order'] ?? 0), ]); @@ -500,26 +561,10 @@ final class Router ]); } - foreach ($backup['miner_offers'] as $offer) { - $this->repository()->saveMinerOffer($projectKey, [ - 'label' => $offer['label'], - 'runtime_months' => $offer['runtime_months'] ?? null, - 'mining_speed_value' => $offer['mining_speed_value'] ?? null, - 'mining_speed_unit' => $offer['mining_speed_unit'] ?? null, - 'bonus_speed_value' => $offer['bonus_speed_value'] ?? null, - 'bonus_speed_unit' => $offer['bonus_speed_unit'] ?? null, - 'base_price_amount' => $offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? $offer['price_amount'], - 'base_price_currency' => $offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ($offer['price_currency'] ?? null)), - 'payment_type' => $offer['payment_type'] ?? (!empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true) ? 'crypto' : 'fiat'), - 'auto_renew' => !empty($offer['auto_renew']) ? 1 : 0, - 'note' => $offer['note'] ?? null, - 'is_active' => !empty($offer['is_active']) ? 1 : 0, - ]); - } - foreach ($backup['purchased_miners'] as $miner) { + $oldOfferId = (int) ($miner['miner_offer_id'] ?? 0); $this->repository()->restorePurchasedMiner($projectKey, [ - 'miner_offer_id' => null, + 'miner_offer_id' => $oldOfferId > 0 ? ($minerOfferIdMap[$oldOfferId] ?? null) : null, 'purchased_at' => $miner['purchased_at'], 'label' => $miner['label'], 'runtime_months' => $miner['runtime_months'] ?? null, @@ -538,15 +583,49 @@ final class Router ]); } + $fxRatesByFetch = []; + foreach ($backup['fx_rates'] as $rate) { + $fetchId = (int) ($rate['fetch_id'] ?? 0); + $targetCurrency = strtoupper(trim((string) ($rate['target_currency'] ?? ''))); + if ($fetchId <= 0 || $targetCurrency === '' || !is_numeric($rate['rate'] ?? null)) { + continue; + } + if (!isset($fxRatesByFetch[$fetchId])) { + $fxRatesByFetch[$fetchId] = [ + 'base_currency' => $rate['base_currency'] ?? '', + 'provider' => $rate['provider'] ?? 'currencyapi', + 'rate_date' => $rate['rate_date'] ?? date('Y-m-d'), + 'fetched_at' => $rate['fetched_at'] ?? null, + 'rates' => [], + ]; + } + $fxRatesByFetch[$fetchId]['rates'][$targetCurrency] = (float) $rate['rate']; + } + + $restoredFxRates = 0; + foreach ($fxRatesByFetch as $fetch) { + $restored = $this->repository()->restoreFxFetch( + (string) $fetch['base_currency'], + (string) $fetch['provider'], + (string) $fetch['rate_date'], + is_string($fetch['fetched_at'] ?? null) ? $fetch['fetched_at'] : null, + $fetch['rates'] + ); + $restoredFxRates += count($restored['rates'] ?? []); + } + return array_merge($result, [ 'restored' => [ 'measurements' => count($backup['measurements']), + 'measurement_rates' => $restoredMeasurementRates, 'purchased_miners' => count($backup['purchased_miners']), 'cost_plans' => count($backup['cost_plans']), 'payouts' => count($backup['payouts']), 'targets' => count($backup['targets']), 'dashboards' => count($backup['dashboards']), 'miner_offers' => count($backup['miner_offers']), + 'currency_aliases' => count($backup['currency_aliases']), + 'fx_rates' => $restoredFxRates, ], ]); } diff --git a/modules/mining-checker/src/Infrastructure/MiningRepository.php b/modules/mining-checker/src/Infrastructure/MiningRepository.php index b7be03d..cf8fb57 100644 --- a/modules/mining-checker/src/Infrastructure/MiningRepository.php +++ b/modules/mining-checker/src/Infrastructure/MiningRepository.php @@ -410,6 +410,20 @@ final class MiningRepository return $this->normalizeRows($stmt->fetchAll() ?: []); } + public function listAllMeasurements(string $projectKey): array + { + $stmt = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('measurements') . ' + WHERE project_key = :project_key AND owner_sub = :owner_sub + ORDER BY measured_at ASC' + ); + $stmt->execute([ + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + ]); + return $this->normalizeRows($stmt->fetchAll() ?: []); + } + public function createMeasurement(string $projectKey, array $payload): array { $this->debug?->add('db.createMeasurement.start', [ @@ -979,6 +993,137 @@ final class MiningRepository return $this->normalizeRows($stmt->fetchAll() ?: []); } + public function listAllFxRates(): array + { + $stmt = $this->pdo->query( + 'SELECT + r.id, + r.fetch_id, + f.base_currency, + r.currency_code AS target_currency, + r.current_value AS rate, + f.rate_date, + f.provider, + f.fetched_at + FROM ' . $this->table('fx_rates') . ' r + INNER JOIN ' . $this->table('fx_fetches') . ' f ON f.id = r.fetch_id + ORDER BY f.fetched_at ASC, f.id ASC, r.currency_code ASC' + ); + return $this->normalizeRows($stmt->fetchAll() ?: []); + } + + public function restoreFxFetch(string $baseCurrency, string $provider, string $rateDate, ?string $fetchedAt, array $rates): array + { + $baseCurrency = strtoupper(trim($baseCurrency)); + $provider = trim($provider) !== '' ? trim($provider) : 'currencyapi'; + $fetchedAt = trim((string) $fetchedAt) !== '' ? (string) $fetchedAt : $this->currentUtcTimestamp(); + + if ($rates === []) { + return ['fetch' => null, 'rates' => []]; + } + + $currenciesToEnsure = [ + [ + 'code' => substr($baseCurrency, 0, 10), + 'name' => $baseCurrency, + 'symbol' => substr($baseCurrency, 0, 8), + 'is_active' => 1, + 'is_crypto' => $this->isCryptoCode($baseCurrency) ? 1 : 0, + 'sort_order' => 1000, + ], + ]; + foreach (array_keys($rates) as $currencyCode) { + $normalizedCurrencyCode = strtoupper(trim((string) $currencyCode)); + if ($normalizedCurrencyCode === '') { + continue; + } + $currenciesToEnsure[] = [ + 'code' => substr($normalizedCurrencyCode, 0, 10), + 'name' => $normalizedCurrencyCode, + 'symbol' => substr($normalizedCurrencyCode, 0, 8), + 'is_active' => 1, + 'is_crypto' => $this->isCryptoCode($normalizedCurrencyCode) ? 1 : 0, + 'sort_order' => 1000, + ]; + } + $this->saveCurrencies($currenciesToEnsure); + + if ($this->driver === 'pgsql') { + $fetchStmt = $this->pdo->prepare( + 'INSERT INTO ' . $this->table('fx_fetches') . ' ( + provider, base_currency, rate_date, fetched_at + ) VALUES ( + :provider, :base_currency, :rate_date, :fetched_at + ) + RETURNING *' + ); + $fetchStmt->execute([ + 'provider' => $provider, + 'base_currency' => $baseCurrency, + 'rate_date' => $rateDate, + 'fetched_at' => $fetchedAt, + ]); + $fetch = $this->normalizeRow($fetchStmt->fetch() ?: []); + } else { + $fetchStmt = $this->pdo->prepare( + 'INSERT INTO ' . $this->table('fx_fetches') . ' ( + provider, base_currency, rate_date, fetched_at + ) VALUES ( + :provider, :base_currency, :rate_date, :fetched_at + )' + ); + $fetchStmt->execute([ + 'provider' => $provider, + 'base_currency' => $baseCurrency, + 'rate_date' => $rateDate, + 'fetched_at' => $fetchedAt, + ]); + $fetchId = (int) $this->pdo->lastInsertId(); + $fetchLookup = $this->pdo->prepare('SELECT * FROM ' . $this->table('fx_fetches') . ' WHERE id = :id LIMIT 1'); + $fetchLookup->execute(['id' => $fetchId]); + $fetch = $this->normalizeRow($fetchLookup->fetch() ?: []); + } + + $placeholders = []; + $params = ['fetch_id' => $fetch['id']]; + $savedRates = []; + $index = 0; + foreach ($rates as $currencyCode => $rate) { + if (!is_numeric($rate)) { + continue; + } + $codeKey = 'currency_code_' . $index; + $valueKey = 'current_value_' . $index; + $targetCurrency = strtoupper(trim((string) $currencyCode)); + if ($targetCurrency === '') { + continue; + } + $placeholders[] = "(:fetch_id, :{$codeKey}, :{$valueKey})"; + $params[$codeKey] = $targetCurrency; + $params[$valueKey] = (float) $rate; + $savedRates[] = [ + 'fetch_id' => $fetch['id'], + 'base_currency' => $baseCurrency, + 'target_currency' => $targetCurrency, + 'rate' => (float) $rate, + 'rate_date' => $rateDate, + 'provider' => $provider, + 'fetched_at' => $fetch['fetched_at'] ?? $fetchedAt, + ]; + $index++; + } + + if ($placeholders !== []) { + $insert = $this->pdo->prepare('INSERT INTO ' . $this->table('fx_rates') . ' (fetch_id, currency_code, current_value) VALUES ' . implode(', ', $placeholders)); + $insert->execute($params); + } + + return [ + 'fetch' => $fetch, + 'rates' => $savedRates, + ]; + } + public function getLatestMeasurementRate(string $baseCurrency, string $targetCurrency): ?array { $stmt = $this->pdo->prepare(