shouldShowDesktop()) { respond(['error' => 'Nicht autorisiert.'], 401); } $store = new LegacyModuleStore($projectRoot, MiningCheckerUserScope::current()); $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $requestUri = (string) ($_SERVER['REQUEST_URI'] ?? ''); $path = (string) (parse_url($requestUri, PHP_URL_PATH) ?: ''); $prefix = '/api/mining-checker/index.php/'; $relativePath = str_starts_with($path, $prefix) ? substr($path, strlen($prefix)) : ltrim((string) ($_SERVER['PATH_INFO'] ?? ''), '/'); $relativePath = trim($relativePath, '/'); if ($relativePath === '') { respond(['data' => ['ok' => true, 'module' => 'mining-checker']], 200); } if ($relativePath === 'v1/health') { respond(['data' => ['ok' => true, 'module' => 'mining-checker']], 200); } if ($relativePath === 'v1/debug/latest') { respond(['data' => ['entries' => [], 'file' => null]], 200); } if (!preg_match('~^v1/projects/([a-zA-Z0-9_-]+)(?:/(.*))?$~', $relativePath, $matches)) { respond(['error' => 'Unbekannter API-Pfad.'], 404); } $projectKey = $matches[1]; $resource = trim((string) ($matches[2] ?? ''), '/'); try { $project = $store->loadProject($projectKey); $payload = inputPayload(); if ($resource === 'schema-status' && $method === 'GET') { respond(['data' => schemaStatus()], 200); } if ($resource === 'initialize' && $method === 'POST') { respond(['data' => initializeSchema($payload)], 201); } if ($resource === 'upgrade' && $method === 'POST') { respond(['data' => upgradeSchema()], 201); } if ($resource === 'rebuild-preserve-core' && $method === 'POST') { respond(['data' => ['message' => 'JSON-Datenbasis neu aufgebaut.', 'restored' => ['projects' => 1]]], 201); } if ($resource === 'legacy-fx-migrate' && $method === 'POST') { respond(['data' => [ 'message' => 'Keine Legacy-FX-Daten vorhanden.', 'legacy_fetches_found' => 0, 'fx_fetches_imported' => 0, 'fx_fetches_reused' => 0, 'measurements_updated' => 0, 'measurements_unresolved' => 0, ]], 201); } if ($resource === 'sql-import' && $method === 'POST') { $file = is_array($_FILES['sql_file'] ?? null) ? $_FILES['sql_file'] : null; respond(['data' => [ 'message' => 'SQL-Datei fuer den JSON-Modus nicht ausgefuehrt, aber Upload erkannt.', 'statement_count' => 0, 'file' => (string) ($file['name'] ?? 'upload.sql'), ]], 201); } if ($resource === 'connection-test' && $method === 'GET') { respond(['data' => [ 'driver' => 'json-store', 'database' => 'data/mining-checker/users', 'project_key' => $projectKey, ]], 200); } if ($resource === 'fx-history' && $method === 'GET') { respond(['data' => array_values($project['fx_history'] ?? [])], 200); } if ($resource === 'bootstrap' && $method === 'GET') { $view = trim((string) ($_GET['view'] ?? 'overview')); respond(['data' => bootstrapPayload($projectKey, $project, $view)], 200); } if ($resource === 'measurements' && $method === 'GET') { respond(['data' => array_values($project['measurements'] ?? [])], 200); } if ($resource === 'measurements' && $method === 'POST') { $measurement = createMeasurement($store, $projectKey, $project, $payload); respond(['data' => $measurement], 201); } if (preg_match('~^measurements/(\d+)$~', $resource, $idMatch) && $method === 'DELETE') { $project['measurements'] = array_values(array_filter( (array) ($project['measurements'] ?? []), static fn (array $row): bool => (int) ($row['id'] ?? 0) !== (int) $idMatch[1] )); $store->saveProject($projectKey, $project); respond(['data' => ['deleted' => true]], 200); } if ($resource === 'measurements-import' && $method === 'POST') { $result = importMeasurements($store, $projectKey, $project, $payload); respond(['data' => $result], 201); } if ($resource === 'wallet-snapshots' && $method === 'GET') { respond(['data' => array_values($project['wallet_snapshots'] ?? [])], 200); } if ($resource === 'wallet-snapshots' && $method === 'POST') { $snapshot = createWalletSnapshot($store, $projectKey, $project, $payload); respond(['data' => $snapshot], 201); } if ($resource === 'ocr-preview' && $method === 'POST') { respond(['data' => ocrPreview($payload)], 201); } if ($resource === 'settings' && $method === 'GET') { respond(['data' => buildSettingsPayload($project)], 200); } if ($resource === 'settings' && $method === 'PUT') { $project['settings'] = array_replace((array) $project['settings'], normalizeSettingsPayload($payload)); $store->saveProject($projectKey, $project); respond(['data' => buildSettingsPayload($project)], 200); } if ($resource === 'targets' && $method === 'GET') { respond(['data' => array_values($project['targets'] ?? [])], 200); } if ($resource === 'targets' && $method === 'POST') { $target = normalizeTarget($payload); $target['id'] = $store->nextId($projectKey, 'targets'); $project['targets'][] = $target; $store->saveProject($projectKey, $project); respond(['data' => $target], 201); } if (preg_match('~^targets/(\d+)$~', $resource, $idMatch) && $method === 'PATCH') { foreach ($project['targets'] as &$target) { if ((int) ($target['id'] ?? 0) === (int) $idMatch[1]) { $target = array_replace($target, normalizeTarget($payload, false)); break; } } unset($target); $store->saveProject($projectKey, $project); respond(['data' => findById((array) $project['targets'], (int) $idMatch[1])], 200); } if (preg_match('~^targets/(\d+)$~', $resource, $idMatch) && $method === 'DELETE') { $project['targets'] = array_values(array_filter( (array) ($project['targets'] ?? []), static fn (array $row): bool => (int) ($row['id'] ?? 0) !== (int) $idMatch[1] )); $store->saveProject($projectKey, $project); respond(['data' => ['deleted' => true]], 200); } if ($resource === 'dashboards' && $method === 'GET') { respond(['data' => array_values($project['dashboards'] ?? [])], 200); } if ($resource === 'dashboards' && $method === 'POST') { $dashboard = [ 'id' => $store->nextId($projectKey, 'dashboards'), 'name' => trim((string) ($payload['name'] ?? 'Dashboard')), 'chart_type' => trim((string) ($payload['chart_type'] ?? 'line')), 'x_field' => trim((string) ($payload['x_field'] ?? 'measured_at')), 'y_field' => trim((string) ($payload['y_field'] ?? 'coins_total')), 'aggregation' => trim((string) ($payload['aggregation'] ?? 'none')), 'filters' => is_array($payload['filters'] ?? null) ? $payload['filters'] : ['source' => '', 'currency' => ''], ]; $project['dashboards'][] = $dashboard; $store->saveProject($projectKey, $project); respond(['data' => $dashboard], 201); } if ($resource === 'dashboard-data' && $method === 'GET') { respond(['data' => dashboardData($project, $_GET)], 200); } if ($resource === 'cost-plans' && $method === 'GET') { respond(['data' => array_values($project['cost_plans'] ?? [])], 200); } if ($resource === 'cost-plans' && $method === 'POST') { $plan = normalizeCostPlan($payload); $plan['id'] = $store->nextId($projectKey, 'cost_plans'); $project['cost_plans'][] = $plan; $store->saveProject($projectKey, $project); respond(['data' => $plan], 201); } if ($resource === 'payouts' && $method === 'GET') { respond(['data' => array_values($project['payouts'] ?? [])], 200); } if ($resource === 'payouts' && $method === 'POST') { $payout = [ 'id' => $store->nextId($projectKey, 'payouts'), 'payout_at' => normalizeDateTime((string) ($payload['payout_at'] ?? gmdate('Y-m-d H:i:s'))), 'coins_amount' => (float) ($payload['coins_amount'] ?? 0), 'payout_currency' => strtoupper(trim((string) ($payload['payout_currency'] ?? 'DOGE'))), 'note' => trim((string) ($payload['note'] ?? '')), ]; $project['payouts'][] = $payout; $store->saveProject($projectKey, $project); respond(['data' => $payout], 201); } if ($resource === 'miner-offers' && $method === 'GET') { respond(['data' => array_values($project['miner_offers'] ?? [])], 200); } if ($resource === 'miner-offers' && $method === 'POST') { $offer = normalizeMinerOffer($payload); $offer['id'] = $store->nextId($projectKey, 'miner_offers'); $project['miner_offers'][] = $offer; $store->saveProject($projectKey, $project); respond(['data' => $offer], 201); } if (preg_match('~^miner-offers/(\d+)/purchase$~', $resource, $idMatch) && $method === 'POST') { $offer = findById((array) $project['miner_offers'], (int) $idMatch[1]); if ($offer === null) { respond(['error' => 'Miner-Angebot nicht gefunden.'], 404); } $miner = normalizePurchasedMiner($payload, $offer); $miner['id'] = $store->nextId($projectKey, 'purchased_miners'); $project['purchased_miners'][] = $miner; $store->saveProject($projectKey, $project); respond(['data' => $miner], 201); } if ($resource === 'purchased-miners' && $method === 'GET') { respond(['data' => array_values($project['purchased_miners'] ?? [])], 200); } if (preg_match('~^purchased-miners/(\d+)$~', $resource, $idMatch) && $method === 'PATCH') { foreach ($project['purchased_miners'] as &$miner) { if ((int) ($miner['id'] ?? 0) === (int) $idMatch[1]) { $miner['auto_renew'] = (bool) ($payload['auto_renew'] ?? $miner['auto_renew'] ?? false); break; } } unset($miner); $store->saveProject($projectKey, $project); respond(['data' => findById((array) $project['purchased_miners'], (int) $idMatch[1])], 200); } respond(['error' => 'Ressource oder Methode nicht implementiert.'], 404); } catch (Throwable $throwable) { respond(['error' => $throwable->getMessage()], 500); } function respond(array $payload, int $status): never { http_response_code($status); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } /** * @return array */ function inputPayload(): array { if (stripos((string) ($_SERVER['CONTENT_TYPE'] ?? ''), 'application/json') !== false) { $raw = file_get_contents('php://input'); $decoded = json_decode($raw !== false ? $raw : '', true); return is_array($decoded) ? $decoded : []; } return $_POST; } /** * @param array $project * @return array */ function bootstrapPayload(string $projectKey, array $project, string $view): array { $measurements = array_values($project['measurements'] ?? []); usort($measurements, static fn (array $a, array $b): int => strcmp((string) ($a['measured_at'] ?? ''), (string) ($b['measured_at'] ?? ''))); $walletSnapshots = array_values($project['wallet_snapshots'] ?? []); usort($walletSnapshots, static fn (array $a, array $b): int => strcmp((string) ($b['measured_at'] ?? ''), (string) ($a['measured_at'] ?? ''))); $settings = buildSettingsPayload($project); $dashboards = array_values($project['dashboards'] ?? []); $targets = array_values($project['targets'] ?? []); $payouts = array_values($project['payouts'] ?? []); $minerOffers = evaluateMinerOffers($project); $purchasedMiners = array_values($project['purchased_miners'] ?? []); $latestMeasurement = $measurements === [] ? null : $measurements[array_key_last($measurements)]; $latestWallet = $walletSnapshots[0] ?? null; $walletBalances = is_array($latestWallet['balances_json'] ?? null) ? $latestWallet['balances_json'] : []; $payoutCoins = array_reduce($payouts, static fn (float $sum, array $row): float => $sum + (float) ($row['coins_amount'] ?? 0), 0.0); $latestCoins = (float) ($latestMeasurement['coins_total'] ?? 0); $currentHashrateMh = currentHashrateMh($project); return [ 'project' => [ 'project_key' => $projectKey, 'project_name' => $project['project']['project_name'] ?? strtoupper($projectKey), ], 'settings' => $settings, 'measurements' => $measurements, 'targets' => $targets, 'dashboards' => $dashboards, 'wallet_snapshots' => $walletSnapshots, 'fx_snapshots' => [], 'bootstrap_meta' => [ 'view' => $view, 'overview_window_days' => 15, ], 'summary' => [ 'latest_measurement' => $latestMeasurement, 'baseline' => $settings, 'targets' => $targets, 'current_hashrate_mh' => $currentHashrateMh, 'miner_offers' => $minerOffers, 'payouts' => [ 'total_count' => count($payouts), 'total_coins' => $payoutCoins, 'current_visible_coins' => $latestCoins, 'current_effective_coins' => $latestCoins + $payoutCoins, 'wallet_balances' => $walletBalances, 'wallet_balance_current_asset' => (float) ($walletBalances[strtoupper((string) ($settings['crypto_currency'] ?? 'DOGE'))] ?? 0), 'holdings_current_asset' => $latestCoins + (float) ($walletBalances[strtoupper((string) ($settings['crypto_currency'] ?? 'DOGE'))] ?? 0), ], ], ]; } /** * @param array $project * @return array */ function buildSettingsPayload(array $project): array { $settings = array_replace([ 'baseline_measured_at' => gmdate('Y-m-d H:i:s'), 'baseline_coins_total' => 0, 'daily_cost_amount' => 0, 'daily_cost_currency' => 'EUR', 'report_currency' => 'EUR', 'crypto_currency' => 'DOGE', 'display_timezone' => 'Europe/Berlin', 'fx_max_age_hours' => 3, 'module_theme_mode' => 'inherit', 'module_theme_accent' => 'teal', 'preferred_currencies' => ['DOGE', 'USD', 'EUR'], ], (array) ($project['settings'] ?? [])); $settings['cost_plans'] = array_values($project['cost_plans'] ?? []); $settings['payouts'] = array_values($project['payouts'] ?? []); $settings['miner_offers'] = array_values($project['miner_offers'] ?? []); $settings['purchased_miners'] = array_values($project['purchased_miners'] ?? []); $settings['measurement_rates'] = []; $settings['currencies'] = defaultCurrencies(); return $settings; } /** * @param array $payload * @return array */ function normalizeSettingsPayload(array $payload): array { return [ 'baseline_measured_at' => normalizeDateTime((string) ($payload['baseline_measured_at'] ?? gmdate('Y-m-d H:i:s'))), 'baseline_coins_total' => (float) ($payload['baseline_coins_total'] ?? 0), 'daily_cost_amount' => (float) ($payload['daily_cost_amount'] ?? 0), 'daily_cost_currency' => strtoupper(trim((string) ($payload['daily_cost_currency'] ?? 'EUR'))), 'report_currency' => strtoupper(trim((string) ($payload['report_currency'] ?? 'EUR'))), 'crypto_currency' => strtoupper(trim((string) ($payload['crypto_currency'] ?? 'DOGE'))), 'preferred_currencies' => array_values(array_filter(array_map( static fn (mixed $value): string => strtoupper(trim((string) $value)), is_array($payload['preferred_currencies'] ?? null) ? $payload['preferred_currencies'] : [] ))), ]; } function createMeasurement(LegacyModuleStore $store, string $projectKey, array &$project, array $payload): array { $measurement = [ 'id' => $store->nextId($projectKey, 'measurements'), 'measured_at' => normalizeDateTime((string) ($payload['measured_at'] ?? gmdate('Y-m-d H:i:s'))), 'coins_total' => (float) ($payload['coins_total'] ?? 0), 'coin_currency' => strtoupper(trim((string) ($payload['coin_currency'] ?? $project['settings']['crypto_currency'] ?? 'DOGE'))), 'price_per_coin' => (float) ($payload['price_per_coin'] ?? 0), 'price_currency' => strtoupper(trim((string) ($payload['price_currency'] ?? $project['settings']['report_currency'] ?? 'EUR'))), 'note' => trim((string) ($payload['note'] ?? '')), 'source' => trim((string) ($payload['source'] ?? 'manual')), 'image_path' => trim((string) ($payload['image_path'] ?? '')), 'ocr_raw_text' => trim((string) ($payload['ocr_raw_text'] ?? '')), 'ocr_confidence' => (float) ($payload['ocr_confidence'] ?? 0), 'ocr_flags' => is_array($payload['ocr_flags'] ?? null) ? $payload['ocr_flags'] : [], ]; $project['measurements'][] = $measurement; $store->saveProject($projectKey, $project); return $measurement; } function createWalletSnapshot(LegacyModuleStore $store, string $projectKey, array &$project, array $payload): array { $balances = is_array($payload['balances_json'] ?? null) ? $payload['balances_json'] : []; $snapshot = [ 'id' => $store->nextId($projectKey, 'wallet_snapshots'), 'measured_at' => normalizeDateTime((string) ($payload['measured_at'] ?? gmdate('Y-m-d H:i:s'))), 'total_value_amount' => (float) ($payload['total_value_amount'] ?? 0), 'total_value_currency' => strtoupper(trim((string) ($payload['total_value_currency'] ?? 'EUR'))), 'wallet_balance' => (float) ($payload['wallet_balance'] ?? 0), 'wallet_currency' => strtoupper(trim((string) ($payload['wallet_currency'] ?? $project['settings']['crypto_currency'] ?? 'DOGE'))), 'balances_json' => $balances, 'note' => trim((string) ($payload['note'] ?? '')), 'source' => trim((string) ($payload['source'] ?? 'manual')), 'image_path' => trim((string) ($payload['image_path'] ?? '')), 'ocr_raw_text' => trim((string) ($payload['ocr_raw_text'] ?? '')), 'ocr_confidence' => (float) ($payload['ocr_confidence'] ?? 0), 'ocr_flags' => is_array($payload['ocr_flags'] ?? null) ? $payload['ocr_flags'] : [], ]; $project['wallet_snapshots'][] = $snapshot; $store->saveProject($projectKey, $project); return $snapshot; } /** * @return array */ function importMeasurements(LegacyModuleStore $store, string $projectKey, array &$project, array $payload): array { $rowsText = (string) ($payload['rows_text'] ?? ''); $defaultCurrency = strtoupper(trim((string) ($payload['default_currency'] ?? 'USD'))); $source = trim((string) ($payload['source'] ?? 'manual-import')); $lines = preg_split('/\R+/', trim($rowsText)) ?: []; $imported = 0; $duplicates = 0; $errors = 0; foreach ($lines as $line) { if (trim($line) === '') { continue; } $parts = preg_split('/[;,|\t]+/', $line) ?: []; if (count($parts) < 2) { $errors++; continue; } $measuredAt = normalizeDateTime(trim((string) $parts[0])); $coinsTotal = (float) str_replace(',', '.', trim((string) $parts[1])); $price = isset($parts[2]) ? (float) str_replace(',', '.', trim((string) $parts[2])) : 0; $currency = isset($parts[3]) ? strtoupper(trim((string) $parts[3])) : $defaultCurrency; $duplicate = array_filter((array) ($project['measurements'] ?? []), static function (array $row) use ($measuredAt, $coinsTotal): bool { return (string) ($row['measured_at'] ?? '') === $measuredAt && (float) ($row['coins_total'] ?? 0) === $coinsTotal; }); if ($duplicate !== []) { $duplicates++; continue; } createMeasurement($store, $projectKey, $project, [ 'measured_at' => $measuredAt, 'coins_total' => $coinsTotal, 'price_per_coin' => $price, 'price_currency' => $currency, 'source' => $source, 'note' => 'Import', ]); $imported++; } return [ 'imported' => $imported, 'duplicates_ignored' => $duplicates, 'error_count' => $errors, ]; } /** * @param array $payload * @return array */ function ocrPreview(array $payload): array { $hintText = trim((string) ($payload['ocr_hint_text'] ?? $_POST['ocr_hint_text'] ?? '')); $dateContext = trim((string) ($payload['date_context'] ?? $_POST['date_context'] ?? date('Y-m-d'))); preg_match('/(\d+(?:[.,]\d+)?)/', $hintText, $matches); $coins = isset($matches[1]) ? (float) str_replace(',', '.', $matches[1]) : 0.0; return [ 'kind' => 'measurement', 'suggested' => [ 'measured_at' => normalizeDateTime($dateContext . ' 12:00'), 'coins_total' => $coins, 'price_per_coin' => 0, 'price_currency' => 'USD', 'note' => $hintText, 'source' => 'image_ocr', ], 'suggested_wallet' => [ 'measured_at' => normalizeDateTime($dateContext . ' 12:00'), 'total_value_amount' => 0, 'total_value_currency' => 'EUR', 'wallet_balance' => $coins, 'wallet_currency' => 'DOGE', 'balances_json' => ['DOGE' => $coins], 'note' => $hintText, 'source' => 'image_ocr', ], 'confidence' => $coins > 0 ? 0.42 : 0.0, 'flags' => ['ocr_provider_missing:json-preview'], 'image_path' => '', 'raw_text' => $hintText, ]; } /** * @param array $payload * @return array */ function normalizeTarget(array $payload, bool $isNew = true): array { $target = [ 'label' => trim((string) ($payload['label'] ?? '')), 'target_amount_fiat' => (float) ($payload['target_amount_fiat'] ?? 0), 'currency' => strtoupper(trim((string) ($payload['currency'] ?? 'EUR'))), 'miner_offer_id' => trim((string) ($payload['miner_offer_id'] ?? '')), 'is_active' => (bool) ($payload['is_active'] ?? true), 'sort_order' => (int) ($payload['sort_order'] ?? 0), ]; if (!$isNew) { return array_filter($target, static fn (mixed $value): bool => $value !== '' && $value !== null); } return $target; } /** * @param array $payload * @return array */ function normalizeCostPlan(array $payload): array { $speedValue = (float) ($payload['mining_speed_value'] ?? 0); $speedUnit = trim((string) ($payload['mining_speed_unit'] ?? 'MH/s')); $bonusPercent = (float) ($payload['bonus_percent'] ?? 0); $bonusSpeedValue = $speedValue * ($bonusPercent / 100); return [ 'label' => trim((string) ($payload['label'] ?? 'Miner')), 'starts_at' => normalizeDateTime((string) ($payload['starts_at'] ?? gmdate('Y-m-d H:i:s'))), 'runtime_months' => (int) ($payload['runtime_months'] ?? 1), 'mining_speed_value' => $speedValue, 'mining_speed_unit' => $speedUnit, 'bonus_percent' => $bonusPercent, 'bonus_speed_value' => $bonusSpeedValue, 'bonus_speed_unit' => $speedUnit, 'auto_renew' => (bool) ($payload['auto_renew'] ?? true), 'base_price_amount' => (float) ($payload['base_price_amount'] ?? 0), 'payment_type' => trim((string) ($payload['payment_type'] ?? 'fiat')), 'total_cost_amount' => (float) ($payload['total_cost_amount'] ?? 0), 'currency' => strtoupper(trim((string) ($payload['currency'] ?? 'EUR'))), 'note' => trim((string) ($payload['note'] ?? '')), 'is_active' => (bool) ($payload['is_active'] ?? true), ]; } /** * @param array $payload * @return array */ function normalizeMinerOffer(array $payload): array { $basePrice = (float) ($payload['base_price_amount'] ?? 0); $runtimeMonths = max(1, (int) ($payload['runtime_months'] ?? 1)); $paymentType = trim((string) ($payload['payment_type'] ?? 'fiat')); $hashrateMh = $paymentType === 'crypto' ? 75 : 50; return [ 'label' => trim((string) ($payload['label'] ?? 'Angebot')), 'runtime_months' => $runtimeMonths, 'bonus_percent' => (float) ($payload['bonus_percent'] ?? 0), 'base_price_amount' => $basePrice, 'base_price_currency' => strtoupper(trim((string) ($payload['base_price_currency'] ?? 'USD'))), 'payment_type' => $paymentType, 'auto_renew' => (bool) ($payload['auto_renew'] ?? false), 'note' => trim((string) ($payload['note'] ?? '')), 'is_active' => (bool) ($payload['is_active'] ?? true), 'offer_hashrate_mh' => $hashrateMh, ]; } /** * @param array $offer * @return array */ function normalizePurchasedMiner(array $payload, array $offer): array { $speedValue = (float) ($payload['mining_speed_value'] ?? $offer['offer_hashrate_mh'] ?? 0); $speedUnit = trim((string) ($payload['mining_speed_unit'] ?? 'MH/s')); $bonusPercent = (float) ($payload['bonus_percent'] ?? $offer['bonus_percent'] ?? 0); return [ 'label' => trim((string) ($payload['label'] ?? $offer['label'] ?? 'Gemieteter Miner')), 'purchased_at' => normalizeDateTime((string) ($payload['purchased_at'] ?? gmdate('Y-m-d H:i:s'))), 'runtime_months' => (int) ($offer['runtime_months'] ?? 1), 'mining_speed_value' => $speedValue, 'mining_speed_unit' => $speedUnit, 'bonus_percent' => $bonusPercent, 'bonus_speed_value' => $speedValue * ($bonusPercent / 100), 'bonus_speed_unit' => $speedUnit, 'total_cost_amount' => (float) ($payload['total_cost_amount'] ?? $offer['base_price_amount'] ?? 0), 'currency' => strtoupper(trim((string) ($payload['currency'] ?? $offer['base_price_currency'] ?? 'USD'))), 'reference_price_amount' => (float) ($payload['reference_price_amount'] ?? $offer['base_price_amount'] ?? 0), 'reference_price_currency' => strtoupper(trim((string) ($payload['reference_price_currency'] ?? $offer['base_price_currency'] ?? 'USD'))), 'auto_renew' => (bool) ($payload['auto_renew'] ?? $offer['auto_renew'] ?? false), 'note' => trim((string) ($payload['note'] ?? '')), 'is_active' => true, 'miner_offer_id' => (int) ($offer['id'] ?? 0), ]; } /** * @param array $project * @return array> */ function evaluateMinerOffers(array $project): array { return array_values(array_map(static function (array $offer): array { return array_replace($offer, [ 'base_offer_id' => $offer['id'] ?? 0, 'effective_price_amount' => (float) ($offer['base_price_amount'] ?? 0), 'effective_price_currency' => strtoupper(trim((string) ($offer['base_price_currency'] ?? 'USD'))), 'offer_hashrate_mh' => (float) ($offer['offer_hashrate_mh'] ?? 50), ]); }, (array) ($project['miner_offers'] ?? []))); } /** * @param array $project * @return array */ function dashboardData(array $project, array $query): array { $measurements = array_values($project['measurements'] ?? []); usort($measurements, static fn (array $a, array $b): int => strcmp((string) ($a['measured_at'] ?? ''), (string) ($b['measured_at'] ?? ''))); $xField = trim((string) ($query['x_field'] ?? 'measured_at')); $yField = trim((string) ($query['y_field'] ?? 'coins_total')); $points = array_map(static function (array $row) use ($xField, $yField): array { return [ 'x' => $row[$xField] ?? null, 'y' => is_numeric($row[$yField] ?? null) ? (float) $row[$yField] : null, ]; }, $measurements); return [ 'points' => $points, 'series' => [ [ 'label' => $yField, 'points' => $points, ], ], ]; } function schemaStatus(): array { return [ 'required_tables' => ['json_store'], 'present_tables' => ['json_store'], 'missing_tables' => [], 'pending_upgrades' => [], 'present_count' => 1, 'missing_count' => 0, 'pending_upgrade_count' => 0, 'all_present' => true, ]; } /** * @param array $payload * @return array */ function initializeSchema(array $payload): array { return [ 'message' => !empty($payload['drop_existing']) ? 'JSON-Datenbestand wurde zurueckgesetzt.' : 'JSON-Datenbestand ist bereit.', 'after' => schemaStatus(), 'dropped_tables' => !empty($payload['drop_existing']) ? ['json_store'] : [], ]; } /** * @return array */ function upgradeSchema(): array { return [ 'message' => 'Keine Schema-Upgrades fuer JSON-Speicher erforderlich.', 'after' => schemaStatus(), 'upgraded' => [], ]; } /** * @return array> */ function defaultCurrencies(): array { return [ ['code' => 'EUR', 'name' => 'Euro', 'is_crypto' => false], ['code' => 'USD', 'name' => 'US-Dollar', 'is_crypto' => false], ['code' => 'DOGE', 'name' => 'Dogecoin', 'is_crypto' => true], ['code' => 'BTC', 'name' => 'Bitcoin', 'is_crypto' => true], ['code' => 'ETH', 'name' => 'Ethereum', 'is_crypto' => true], ['code' => 'LTC', 'name' => 'Litecoin', 'is_crypto' => true], ['code' => 'USDT', 'name' => 'Tether', 'is_crypto' => true], ['code' => 'USDC', 'name' => 'USD Coin', 'is_crypto' => true], ]; } /** * @param array> $rows * @return array|null */ function findById(array $rows, int $id): ?array { foreach ($rows as $row) { if ((int) ($row['id'] ?? 0) === $id) { return $row; } } return null; } /** * @param array $project */ function currentHashrateMh(array $project): float { $costPlans = array_reduce((array) ($project['cost_plans'] ?? []), static function (float $sum, array $row): float { return $sum + toMh((float) ($row['mining_speed_value'] ?? 0), (string) ($row['mining_speed_unit'] ?? 'MH/s')) + toMh((float) ($row['bonus_speed_value'] ?? 0), (string) ($row['bonus_speed_unit'] ?? 'MH/s')); }, 0.0); $purchased = array_reduce((array) ($project['purchased_miners'] ?? []), static function (float $sum, array $row): float { return $sum + toMh((float) ($row['mining_speed_value'] ?? 0), (string) ($row['mining_speed_unit'] ?? 'MH/s')) + toMh((float) ($row['bonus_speed_value'] ?? 0), (string) ($row['bonus_speed_unit'] ?? 'MH/s')); }, 0.0); return $costPlans + $purchased; } function toMh(float $value, string $unit): float { return match (strtoupper(trim($unit))) { 'KH/S' => $value / 1000, 'MH/S' => $value, 'GH/S' => $value * 1000, 'TH/S' => $value * 1000000, default => $value, }; } function normalizeDateTime(string $value): string { $trimmed = trim($value); if ($trimmed === '') { return gmdate('Y-m-d H:i:s'); } $normalized = str_replace('T', ' ', $trimmed); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $normalized) === 1) { return $normalized . ' 00:00:00'; } if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $normalized) === 1) { return $normalized . ':00'; } return $normalized; }