mining checker
This commit is contained in:
@@ -4,830 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||
|
||||
use App\ConfigLoader;
|
||||
use App\AccountGate;
|
||||
use App\KeycloakAuth;
|
||||
use MiningChecker\LegacyModuleStore;
|
||||
use MiningChecker\MiningCheckerUserScope;
|
||||
use ModulesCore\ModuleHttp;
|
||||
|
||||
session_start();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
|
||||
ModuleHttp::requireDesktopAccess($projectRoot, true);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($auth->isAuthenticated()) {
|
||||
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
|
||||
$accountCheck = $accountGate->checkUsername((string) ($currentUser['username'] ?? ''));
|
||||
|
||||
if (!($accountCheck['allowed'] ?? false)) {
|
||||
$auth->logout();
|
||||
respond(['error' => (string) ($accountCheck['message'] ?? 'Dieses Konto ist noch nicht freigeschaltet.')], 403);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$auth->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<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $project
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $project
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $offer
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $project
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
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<string, mixed> $project
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
function upgradeSchema(): array
|
||||
{
|
||||
return [
|
||||
'message' => 'Keine Schema-Upgrades fuer JSON-Speicher erforderlich.',
|
||||
'after' => schemaStatus(),
|
||||
'upgraded' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
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<int, array<string, mixed>> $rows
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
function findById(array $rows, int $id): ?array
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
if ((int) ($row['id'] ?? 0) === $id) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $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;
|
||||
}
|
||||
require $projectRoot . '/modules/mining-checker/api/index.php';
|
||||
|
||||
Reference in New Issue
Block a user