Miner-Upgrade
All checks were successful
Deploy / deploy-staging (push) Successful in 7s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-09 00:58:48 +02:00
parent ee5a46254f
commit fc95898a9d
11 changed files with 976 additions and 131 deletions

View File

@@ -253,6 +253,15 @@ final class Router
Http::json(['data' => $this->savePayout($projectKey, Http::input())], 201);
}
if ($resource === 'wallet-snapshots' && $method === 'GET') {
Http::json(['data' => $this->walletSnapshots($projectKey)]);
}
if ($resource === 'wallet-snapshots' && $method === 'POST') {
$this->repository()->ensureProject($projectKey);
Http::json(['data' => $this->saveWalletSnapshot($projectKey, Http::input())], 201);
}
if ($resource === 'miner-offers' && $method === 'GET') {
Http::json(['data' => $this->minerOffers($projectKey)]);
}
@@ -329,6 +338,10 @@ final class Router
'purchased_miners' => [],
'measurement_rates' => [],
], ['project_key' => $projectKey]);
$walletSnapshots = $this->safeTimed('bootstrap.wallet_snapshots', fn () => $this->bootstrapWalletSnapshots($projectKey, $view), [], [
'project_key' => $projectKey,
'view' => $view,
]);
$measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings, $view), [], [
'project_key' => $projectKey,
'view' => $view,
@@ -369,6 +382,7 @@ final class Router
return [
'project' => $this->repository()->getProject($projectKey),
'settings' => $settings,
'wallet_snapshots' => $walletSnapshots,
'measurements' => $measurements,
'targets' => $targets,
'dashboards' => $dashboards,
@@ -483,6 +497,7 @@ final class Router
'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), []),
'wallet_snapshots' => $this->safeRead(fn () => $this->repository()->listWalletSnapshots($projectKey, 500), []),
'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), []),
@@ -587,6 +602,23 @@ final class Router
]);
}
foreach ($backup['wallet_snapshots'] as $snapshot) {
$this->repository()->saveWalletSnapshot($projectKey, [
'measured_at' => $snapshot['measured_at'],
'total_value_amount' => $snapshot['total_value_amount'] ?? null,
'total_value_currency' => $snapshot['total_value_currency'] ?? null,
'wallet_balance' => $snapshot['wallet_balance'] ?? null,
'wallet_currency' => $snapshot['wallet_currency'] ?? ($backup['settings']['crypto_currency'] ?? 'DOGE'),
'balances_json' => $snapshot['balances_json'] ?? [],
'note' => $snapshot['note'] ?? null,
'source' => $snapshot['source'] ?? 'manual',
'image_path' => $snapshot['image_path'] ?? null,
'ocr_raw_text' => $snapshot['ocr_raw_text'] ?? null,
'ocr_confidence' => $snapshot['ocr_confidence'] ?? null,
'ocr_flags' => $snapshot['ocr_flags'] ?? [],
]);
}
$minerOfferIdMap = [];
foreach ($backup['miner_offers'] as $offer) {
$savedOffer = $this->repository()->saveMinerOffer($projectKey, [
@@ -694,6 +726,7 @@ final class Router
'purchased_miners' => count($backup['purchased_miners']),
'cost_plans' => count($backup['cost_plans']),
'payouts' => count($backup['payouts']),
'wallet_snapshots' => count($backup['wallet_snapshots']),
'targets' => count($backup['targets']),
'dashboards' => count($backup['dashboards']),
'miner_offers' => count($backup['miner_offers']),
@@ -1191,7 +1224,7 @@ final class Router
private function bootstrapMeasurements(string $projectKey, array $settings, string $view): array
{
if (in_array($view, ['settings', 'dashboards'], true)) {
if (in_array($view, ['settings', 'dashboards', 'wallet'], true)) {
return [];
}
@@ -1212,6 +1245,15 @@ final class Router
return $this->analytics()->enrichMeasurements($rows, $settings);
}
private function bootstrapWalletSnapshots(string $projectKey, string $view): array
{
if (!in_array($view, ['wallet'], true)) {
return [];
}
return $this->repository()->listWalletSnapshots($projectKey, 50);
}
private function bootstrapTargets(string $projectKey, string $view): array
{
return in_array($view, ['overview', 'mining'], true) ? $this->targets($projectKey) : [];
@@ -1285,6 +1327,7 @@ final class Router
? $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $projectTimezone)
: $this->currentTimestamp(),
'coins_total' => $this->requiredDecimal($input['coins_total'] ?? null, 'coins_total'),
'coin_currency' => $this->measurementCoinCurrency($projectKey, $input),
'price_per_coin' => $this->optionalDecimal($input['price_per_coin'] ?? null),
'price_currency' => $this->optionalCurrency($input['price_currency'] ?? null),
'note' => $this->optionalString($input['note'] ?? null, 2000),
@@ -1330,7 +1373,7 @@ final class Router
}
try {
$payload = $this->parseImportLine($trimmed, $defaultCurrency, $defaultSource, $this->projectTimezone($projectKey));
$payload = $this->parseImportLine($projectKey, $trimmed, $defaultCurrency, $defaultSource, $this->projectTimezone($projectKey));
$this->syncCurrencyCatalogForMeasurement($payload);
$payload['fx_fetch_id'] = $this->resolveMeasurementFxFetchId($projectKey, $payload, false);
$result = $this->repository()->createMeasurementIfNotExists($projectKey, $payload);
@@ -1368,6 +1411,15 @@ final class Router
private function syncCurrencyCatalogForMeasurement(array $payload): void
{
$coinCurrency = strtoupper(trim((string) ($payload['coin_currency'] ?? '')));
if ($coinCurrency !== '' && $this->currencyCatalogEntry($coinCurrency) === null) {
throw new ApiException(
'Coin-Waehrung ist im fx-rates Katalog nicht vorhanden.',
422,
['currency' => $coinCurrency]
);
}
$priceCurrency = strtoupper(trim((string) ($payload['price_currency'] ?? '')));
if ($priceCurrency === '') {
return;
@@ -1382,7 +1434,7 @@ final class Router
}
}
private function parseImportLine(string $line, ?string $defaultCurrency, string $defaultSource, string $projectTimezone): array
private function parseImportLine(string $projectKey, string $line, ?string $defaultCurrency, string $defaultSource, string $projectTimezone): array
{
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 2) {
@@ -1402,6 +1454,7 @@ final class Router
return [
'measured_at' => $measuredAt,
'coins_total' => $coinsTotal,
'coin_currency' => $this->measurementCoinCurrency($projectKey),
'price_per_coin' => $pricePerCoin,
'price_currency' => $priceCurrency,
'note' => $note,
@@ -1629,6 +1682,36 @@ final class Router
return $this->repository()->savePayout($projectKey, $payload);
}
private function walletSnapshots(string $projectKey): array
{
return $this->repository()->listWalletSnapshots($projectKey, 100);
}
private function saveWalletSnapshot(string $projectKey, array $input): array
{
$balances = $input['balances_json'] ?? [];
if (!is_array($balances)) {
$balances = [];
}
$payload = [
'measured_at' => $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $this->projectTimezone($projectKey)),
'total_value_amount' => $this->optionalDecimal($input['total_value_amount'] ?? null),
'total_value_currency' => $this->optionalCurrency($input['total_value_currency'] ?? null),
'wallet_balance' => $this->optionalDecimal($input['wallet_balance'] ?? null),
'wallet_currency' => $this->requiredCurrency($input['wallet_currency'] ?? ($this->settings($projectKey)['crypto_currency'] ?? 'DOGE'), 'wallet_currency'),
'balances_json' => $balances,
'note' => $this->optionalString($input['note'] ?? null, 1000),
'source' => $this->enumValue($input['source'] ?? 'manual', ['manual', 'image_ocr', 'seed_import'], 'source'),
'image_path' => $this->optionalString($input['image_path'] ?? null, 255),
'ocr_raw_text' => $this->optionalString($input['ocr_raw_text'] ?? null, 20000),
'ocr_confidence' => $this->optionalDecimal($input['ocr_confidence'] ?? null),
'ocr_flags' => is_array($input['ocr_flags'] ?? null) ? $input['ocr_flags'] : [],
];
return $this->repository()->saveWalletSnapshot($projectKey, $payload);
}
private function saveMinerOffer(string $projectKey, array $input): array
{
$payload = [
@@ -1658,9 +1741,16 @@ final class Router
throw new ApiException('Miner-Angebot nicht gefunden.', 404);
}
$settings = $this->settings($projectKey);
$cryptoCurrency = $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
$isAutoRenew = array_key_exists('auto_renew', $input) ? !empty($input['auto_renew']) : !empty($offer['auto_renew']);
$purchaseCurrency = $this->optionalCurrency($input['currency'] ?? null) ?? (string) ($offer['effective_price_currency'] ?? $offer['price_currency'] ?? $offer['base_price_currency'] ?? '');
if (!$isAutoRenew) {
$purchaseCurrency = $cryptoCurrency;
}
$purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null);
if ($purchaseCost === null) {
if ($purchaseCost === null || !$isAutoRenew) {
$purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [
'price_currency' => $purchaseCurrency !== '' ? $purchaseCurrency : ($offer['price_currency'] ?? ''),
]));
@@ -1684,7 +1774,7 @@ final class Router
'usd_reference_amount' => $offer['usd_reference_amount'] ?? null,
'reference_price_amount' => $referencePriceAmount,
'reference_price_currency' => $referencePriceCurrency,
'auto_renew' => array_key_exists('auto_renew', $input) ? (!empty($input['auto_renew']) ? 1 : 0) : (!empty($offer['auto_renew']) ? 1 : 0),
'auto_renew' => $isAutoRenew ? 1 : 0,
'note' => $this->optionalString($input['note'] ?? ($offer['note'] ?? null), 1000),
'is_active' => 1,
]);
@@ -2171,6 +2261,17 @@ final class Router
return null;
}
private function measurementCoinCurrency(string $projectKey, array $input = []): string
{
$requested = $this->optionalCurrency($input['coin_currency'] ?? null);
if ($requested !== null) {
return $requested;
}
$settings = $this->settings($projectKey);
return $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
}
private function ensureMeasurementFxReferences(string $projectKey, array $rows, ?array $settings = null): array
{
$maxAgeHours = self::FX_FETCH_MAX_AGE_HOURS;