asdsad
This commit is contained in:
@@ -6,105 +6,816 @@ require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
|||||||
|
|
||||||
use App\ConfigLoader;
|
use App\ConfigLoader;
|
||||||
use App\KeycloakAuth;
|
use App\KeycloakAuth;
|
||||||
use MiningChecker\MiningCheckerCalculator;
|
use MiningChecker\LegacyModuleStore;
|
||||||
use MiningChecker\MiningCheckerStore;
|
|
||||||
use MiningChecker\MiningCheckerUserScope;
|
use MiningChecker\MiningCheckerUserScope;
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
$projectRoot = dirname(__DIR__, 3);
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||||
$auth = new KeycloakAuth($keycloakConfig);
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
if (!$auth->shouldShowDesktop()) {
|
if (!$auth->shouldShowDesktop()) {
|
||||||
http_response_code(401);
|
respond(['error' => 'Nicht autorisiert.'], 401);
|
||||||
echo json_encode(['error' => 'Nicht autorisiert.'], JSON_UNESCAPED_UNICODE);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$store = new MiningCheckerStore($projectRoot, MiningCheckerUserScope::current());
|
$store = new LegacyModuleStore($projectRoot, MiningCheckerUserScope::current());
|
||||||
$calculator = new MiningCheckerCalculator();
|
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||||
$method = $_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 {
|
try {
|
||||||
if ($method === 'GET') {
|
$project = $store->loadProject($projectKey);
|
||||||
$state = $store->load();
|
$payload = inputPayload();
|
||||||
$state['metrics'] = $calculator->calculate((array) ($state['settings'] ?? []));
|
|
||||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
if ($resource === 'schema-status' && $method === 'GET') {
|
||||||
exit;
|
respond(['data' => schemaStatus()], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payloadRaw = file_get_contents('php://input');
|
if ($resource === 'initialize' && $method === 'POST') {
|
||||||
$payload = json_decode($payloadRaw !== false ? $payloadRaw : '', true);
|
respond(['data' => initializeSchema($payload)], 201);
|
||||||
$payload = is_array($payload) ? $payload : [];
|
|
||||||
|
|
||||||
if ($method === 'PUT') {
|
|
||||||
$state = $store->load();
|
|
||||||
$state['settings'] = normalizeSettings(array_merge((array) ($state['settings'] ?? []), $payload));
|
|
||||||
$store->save($state);
|
|
||||||
$state['metrics'] = $calculator->calculate((array) $state['settings']);
|
|
||||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($method === 'POST') {
|
if ($resource === 'upgrade' && $method === 'POST') {
|
||||||
$state = $store->load();
|
respond(['data' => upgradeSchema()], 201);
|
||||||
$settings = normalizeSettings(array_merge((array) ($state['settings'] ?? []), is_array($payload['settings'] ?? null) ? $payload['settings'] : []));
|
|
||||||
$metrics = $calculator->calculate($settings);
|
|
||||||
$note = trim((string) ($payload['note'] ?? ''));
|
|
||||||
$history = is_array($state['history'] ?? null) ? $state['history'] : [];
|
|
||||||
|
|
||||||
array_unshift($history, [
|
|
||||||
'id' => bin2hex(random_bytes(8)),
|
|
||||||
'created_at' => gmdate('c'),
|
|
||||||
'project_name' => (string) ($settings['project_name'] ?? ''),
|
|
||||||
'coin_symbol' => (string) ($settings['coin_symbol'] ?? ''),
|
|
||||||
'currency' => (string) ($settings['currency'] ?? 'EUR'),
|
|
||||||
'note' => $note,
|
|
||||||
'settings' => $settings,
|
|
||||||
'metrics' => $metrics,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$state['settings'] = $settings;
|
|
||||||
$state['history'] = array_slice($history, 0, 25);
|
|
||||||
$store->save($state);
|
|
||||||
|
|
||||||
$state['metrics'] = $metrics;
|
|
||||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
http_response_code(405);
|
if ($resource === 'rebuild-preserve-core' && $method === 'POST') {
|
||||||
echo json_encode(['error' => 'Methode nicht erlaubt.'], JSON_UNESCAPED_UNICODE);
|
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) {
|
} catch (Throwable $throwable) {
|
||||||
http_response_code(500);
|
respond(['error' => $throwable->getMessage()], 500);
|
||||||
echo json_encode(['error' => $throwable->getMessage()], JSON_UNESCAPED_UNICODE);
|
}
|
||||||
|
|
||||||
|
function respond(array $payload, int $status): never
|
||||||
|
{
|
||||||
|
http_response_code($status);
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $settings
|
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
function normalizeSettings(array $settings): 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<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 [
|
return [
|
||||||
'project_name' => trim((string) ($settings['project_name'] ?? 'DOGE Mining')),
|
'project' => [
|
||||||
'coin_symbol' => strtoupper(trim((string) ($settings['coin_symbol'] ?? 'DOGE'))),
|
'project_key' => $projectKey,
|
||||||
'algorithm' => trim((string) ($settings['algorithm'] ?? 'Scrypt')),
|
'project_name' => $project['project']['project_name'] ?? strtoupper($projectKey),
|
||||||
'currency' => strtoupper(trim((string) ($settings['currency'] ?? 'EUR'))),
|
],
|
||||||
'hashrate' => (float) ($settings['hashrate'] ?? 0),
|
'settings' => $settings,
|
||||||
'hashrate_unit' => trim((string) ($settings['hashrate_unit'] ?? 'MH/s')),
|
'measurements' => $measurements,
|
||||||
'network_hashrate' => (float) ($settings['network_hashrate'] ?? 0),
|
'targets' => $targets,
|
||||||
'network_hashrate_unit' => trim((string) ($settings['network_hashrate_unit'] ?? 'TH/s')),
|
'dashboards' => $dashboards,
|
||||||
'block_reward' => (float) ($settings['block_reward'] ?? 0),
|
'wallet_snapshots' => $walletSnapshots,
|
||||||
'block_time_seconds' => (float) ($settings['block_time_seconds'] ?? 60),
|
'fx_snapshots' => [],
|
||||||
'coin_price' => (float) ($settings['coin_price'] ?? 0),
|
'bootstrap_meta' => [
|
||||||
'power_watts' => (float) ($settings['power_watts'] ?? 0),
|
'view' => $view,
|
||||||
'electricity_price' => (float) ($settings['electricity_price'] ?? 0),
|
'overview_window_days' => 15,
|
||||||
'pool_fee_percent' => (float) ($settings['pool_fee_percent'] ?? 0),
|
],
|
||||||
'hardware_cost' => (float) ($settings['hardware_cost'] ?? 0),
|
'summary' => [
|
||||||
'notes' => trim((string) ($settings['notes'] ?? '')),
|
'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;
|
||||||
|
}
|
||||||
|
|||||||
52
public/api/module-auth/mining-checker/index.php
Normal file
52
public/api/module-auth/mining-checker/index.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 5) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
|
use App\ConfigLoader;
|
||||||
|
use App\KeycloakAuth;
|
||||||
|
use MiningChecker\LegacyModuleStore;
|
||||||
|
use MiningChecker\MiningCheckerUserScope;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__, 5);
|
||||||
|
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if (!$auth->shouldShowDesktop()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Nicht autorisiert.'], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$store = new LegacyModuleStore($projectRoot, MiningCheckerUserScope::current());
|
||||||
|
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||||
|
|
||||||
|
if ($method === 'GET') {
|
||||||
|
echo json_encode($store->loadAuth(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'PUT') {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$payload = json_decode($raw !== false ? $raw : '', true);
|
||||||
|
$payload = is_array($payload) ? $payload : [];
|
||||||
|
|
||||||
|
$users = preg_split('/[\s,;]+/', (string) ($payload['users'] ?? '')) ?: [];
|
||||||
|
$groups = preg_split('/[\s,;]+/', (string) ($payload['groups'] ?? '')) ?: [];
|
||||||
|
|
||||||
|
$store->saveAuth([
|
||||||
|
'required' => (bool) ($payload['required'] ?? true),
|
||||||
|
'users' => array_values(array_filter(array_map('trim', $users))),
|
||||||
|
'groups' => array_values(array_filter(array_map('trim', $groups))),
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode($store->loadAuth(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Methode nicht erlaubt.'], JSON_UNESCAPED_UNICODE);
|
||||||
@@ -10,8 +10,12 @@ use App\KeycloakAuth;
|
|||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
$projectRoot = dirname(__DIR__, 3);
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||||
$auth = new KeycloakAuth($keycloakConfig);
|
|
||||||
|
if (!$auth->shouldShowDesktop()) {
|
||||||
|
header('Location: /auth/login', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$assetVersion = static function (string $publicPath) use ($projectRoot): string {
|
$assetVersion = static function (string $publicPath) use ($projectRoot): string {
|
||||||
$filesystemPath = $projectRoot . '/public' . $publicPath;
|
$filesystemPath = $projectRoot . '/public' . $publicPath;
|
||||||
@@ -25,152 +29,85 @@ $assetVersion = static function (string $publicPath) use ($projectRoot): string
|
|||||||
return $mtime === false ? $publicPath : $publicPath . '?v=' . $mtime;
|
return $mtime === false ? $publicPath : $publicPath . '?v=' . $mtime;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!$auth->shouldShowDesktop()) {
|
$sections = [
|
||||||
header('Location: /auth/login', true, 302);
|
['key' => 'overview', 'label' => 'Übersicht'],
|
||||||
exit;
|
['key' => 'upload', 'label' => 'Upload'],
|
||||||
|
['key' => 'measurements', 'label' => 'Mining-History'],
|
||||||
|
['key' => 'wallet', 'label' => 'Wallet'],
|
||||||
|
['key' => 'mining', 'label' => 'Miner-Daten'],
|
||||||
|
['key' => 'dashboards', 'label' => 'Dashboards'],
|
||||||
|
];
|
||||||
|
$defaultProjectKey = getenv('MINING_CHECKER_DEFAULT_PROJECT_KEY') ?: 'doge-main';
|
||||||
|
$activeView = trim((string) ($_GET['view'] ?? 'overview'));
|
||||||
|
$sectionKeys = array_values(array_filter(array_map(
|
||||||
|
static fn (mixed $section): string => is_array($section) ? trim((string) ($section['key'] ?? '')) : '',
|
||||||
|
$sections
|
||||||
|
)));
|
||||||
|
|
||||||
|
if ($sectionKeys === []) {
|
||||||
|
$sectionKeys = ['overview'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!in_array($activeView, $sectionKeys, true)) {
|
||||||
|
$activeView = $sectionKeys[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sectionsJson = json_encode($sections, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
?><!DOCTYPE html>
|
?><!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Mining-Checker</title>
|
<title>Mining-Checker</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--surface: rgba(255, 255, 255, 0.92);
|
||||||
|
--line: rgba(148, 163, 184, 0.24);
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--brand-accent: #14b8a6;
|
||||||
|
--brand-accent-3: #0f766e;
|
||||||
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(45, 212, 191, 0.12), transparent 24%),
|
||||||
|
linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mining-module-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<link rel="stylesheet" href="<?= htmlspecialchars($assetVersion('/assets/apps/mining-checker/app.css'), ENT_QUOTES) ?>">
|
<link rel="stylesheet" href="<?= htmlspecialchars($assetVersion('/assets/apps/mining-checker/app.css'), ENT_QUOTES) ?>">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-module-debug-enabled="0">
|
||||||
<main class="mining-app" id="mining-checker-app">
|
<div class="mining-module-shell">
|
||||||
<section class="mining-hero">
|
<div
|
||||||
<div>
|
id="mining-checker-app"
|
||||||
<p class="mining-kicker">App Modul</p>
|
data-default-project-key="<?= htmlspecialchars($defaultProjectKey, ENT_QUOTES) ?>"
|
||||||
<h1>Mining-Checker</h1>
|
data-api-base="/api/mining-checker/index.php/v1"
|
||||||
<p class="mining-copy">Profitabilitaet, Stromkosten, ROI und Messhistorie fuer dein Mining-Setup.</p>
|
data-active-view="<?= htmlspecialchars($activeView, ENT_QUOTES) ?>"
|
||||||
</div>
|
data-sections-json="<?= htmlspecialchars(is_string($sectionsJson) ? $sectionsJson : '[]', ENT_QUOTES) ?>"
|
||||||
<div class="mining-hero-actions">
|
></div>
|
||||||
<button type="button" class="preset-button" data-preset="doge">DOGE Basis</button>
|
|
||||||
<button type="button" class="preset-button" data-preset="ltc">LTC Basis</button>
|
|
||||||
<button type="button" class="preset-button" data-preset="custom">Aktuelle Werte behalten</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mining-layout">
|
|
||||||
<form class="mining-panel mining-form" id="mining-settings-form">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h2>Setup</h2>
|
|
||||||
<p>Alle Werte werden pro Benutzer gespeichert.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||||
<label>
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||||
<span>Projektname</span>
|
|
||||||
<input name="project_name" type="text" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Coin Symbol</span>
|
|
||||||
<input name="coin_symbol" type="text" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Algorithmus</span>
|
|
||||||
<input name="algorithm" type="text" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Waehrung</span>
|
|
||||||
<input name="currency" type="text" maxlength="3" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Hashrate</span>
|
|
||||||
<input name="hashrate" type="number" min="0" step="0.01" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Hashrate Einheit</span>
|
|
||||||
<select name="hashrate_unit">
|
|
||||||
<option>KH/s</option>
|
|
||||||
<option selected>MH/s</option>
|
|
||||||
<option>GH/s</option>
|
|
||||||
<option>TH/s</option>
|
|
||||||
<option>PH/s</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Netzwerk Hashrate</span>
|
|
||||||
<input name="network_hashrate" type="number" min="0" step="0.01" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Netzwerk Einheit</span>
|
|
||||||
<select name="network_hashrate_unit">
|
|
||||||
<option>GH/s</option>
|
|
||||||
<option>TH/s</option>
|
|
||||||
<option selected>PH/s</option>
|
|
||||||
<option>EH/s</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Block Reward</span>
|
|
||||||
<input name="block_reward" type="number" min="0" step="0.00000001" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Blockzeit in Sekunden</span>
|
|
||||||
<input name="block_time_seconds" type="number" min="1" step="1" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Coin Preis</span>
|
|
||||||
<input name="coin_price" type="number" min="0" step="0.00000001" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Leistungsaufnahme in Watt</span>
|
|
||||||
<input name="power_watts" type="number" min="0" step="1" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Strompreis pro kWh</span>
|
|
||||||
<input name="electricity_price" type="number" min="0" step="0.0001" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Pool Fee in %</span>
|
|
||||||
<input name="pool_fee_percent" type="number" min="0" max="100" step="0.01" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Hardwarekosten</span>
|
|
||||||
<input name="hardware_cost" type="number" min="0" step="0.01" required>
|
|
||||||
</label>
|
|
||||||
<label class="form-span-2">
|
|
||||||
<span>Notizen</span>
|
|
||||||
<textarea name="notes" rows="4" placeholder="Optional: Modell, Pool, Standort, Temperatur, Firmware ..."></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="primary-button">Einstellungen speichern</button>
|
|
||||||
<button type="button" class="secondary-button" id="snapshot-button">Snapshot speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mining-sidebar">
|
|
||||||
<section class="mining-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h2>Kennzahlen</h2>
|
|
||||||
<p>Direkt aus den aktuellen Werten berechnet.</p>
|
|
||||||
</div>
|
|
||||||
<div class="stats-grid" id="stats-grid"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mining-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h2>Snapshot Verlauf</h2>
|
|
||||||
<p>Gespeicherte Stichtage pro Benutzer.</p>
|
|
||||||
</div>
|
|
||||||
<div class="history-list" id="history-list"></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<template id="stat-card-template">
|
|
||||||
<article class="stat-card">
|
|
||||||
<span class="stat-label"></span>
|
|
||||||
<strong class="stat-value"></strong>
|
|
||||||
<small class="stat-meta"></small>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="<?= htmlspecialchars($assetVersion('/assets/apps/mining-checker/app.js'), ENT_QUOTES) ?>"></script>
|
<script src="<?= htmlspecialchars($assetVersion('/assets/apps/mining-checker/app.js'), ENT_QUOTES) ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,258 +1,772 @@
|
|||||||
:root {
|
#mining-checker-app {
|
||||||
color-scheme: light;
|
--mc-surface: var(--surface);
|
||||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
--mc-surface-strong: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,252,252,0.92));
|
||||||
|
--mc-line: var(--line);
|
||||||
|
--mc-line-strong: color-mix(in srgb, var(--brand-accent) 28%, transparent);
|
||||||
|
--mc-text: var(--text);
|
||||||
|
--mc-text-muted: var(--muted);
|
||||||
|
--mc-accent: var(--brand-accent);
|
||||||
|
--mc-accent-strong: var(--brand-accent-3);
|
||||||
|
--mc-danger: #d92d20;
|
||||||
|
--mc-success: #14804a;
|
||||||
|
--mc-warning: #b54708;
|
||||||
|
min-height: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--mc-text);
|
||||||
|
font-family: inherit;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
#mining-checker-app,
|
||||||
|
#mining-checker-app * {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
#mining-checker-app .mc-grid-bg {
|
||||||
|
background: none;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
padding: 0;
|
||||||
background:
|
|
||||||
radial-gradient(circle at top left, rgba(253, 224, 71, 0.16), transparent 26%),
|
|
||||||
linear-gradient(180deg, #f7fafc 0%, #edf2f7 100%);
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mining-app {
|
#mining-checker-app .mc-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-hero,
|
|
||||||
.mining-panel {
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.88);
|
|
||||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-hero {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-kicker {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.16em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-hero h1,
|
|
||||||
.panel-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-copy,
|
|
||||||
.panel-header p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
color: #475569;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-hero-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-button,
|
|
||||||
.primary-button,
|
|
||||||
.secondary-button {
|
|
||||||
border: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 11px 16px;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-button,
|
|
||||||
.secondary-button {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-button {
|
|
||||||
background: linear-gradient(180deg, #facc15, #f59e0b);
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.9fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-form,
|
|
||||||
.mining-sidebar {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mining-panel {
|
|
||||||
padding: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid label,
|
#mining-checker-app .mc-panel,
|
||||||
.stat-card,
|
#mining-checker-app .mc-stat-card,
|
||||||
.history-item {
|
#mining-checker-app .mc-dashboard-card,
|
||||||
display: grid;
|
#mining-checker-app .mc-target-card,
|
||||||
gap: 8px;
|
#mining-checker-app .mc-alert,
|
||||||
|
#mining-checker-app .mc-empty,
|
||||||
|
#mining-checker-app .mc-table-shell,
|
||||||
|
#mining-checker-app .mc-display-field {
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--mc-surface);
|
||||||
|
/* box-shadow: 0 12px 30px rgba(1, 22, 32, 0.08); */
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid span {
|
#mining-checker-app .mc-panel,
|
||||||
font-size: 13px;
|
#mining-checker-app .mc-dashboard-card,
|
||||||
font-weight: 700;
|
#mining-checker-app .mc-alert,
|
||||||
color: #334155;
|
#mining-checker-app .mc-empty,
|
||||||
|
#mining-checker-app .mc-table-shell {
|
||||||
|
padding: 18px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid input,
|
#mining-checker-app .mc-hero-top,
|
||||||
.form-grid select,
|
#mining-checker-app .mc-inline-row,
|
||||||
.form-grid textarea {
|
#mining-checker-app .mc-flex-split,
|
||||||
width: 100%;
|
#mining-checker-app .mc-section-head {
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-span-2 {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
margin-top: 18px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
#mining-checker-app .mc-hero-top,
|
||||||
|
#mining-checker-app .mc-flex-split,
|
||||||
|
#mining-checker-app .mc-section-head {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero-copy,
|
||||||
|
#mining-checker-app .mc-hero-controls,
|
||||||
|
#mining-checker-app .mc-panel-body,
|
||||||
|
#mining-checker-app .mc-form,
|
||||||
|
#mining-checker-app .mc-field,
|
||||||
|
#mining-checker-app .mc-filter-grid,
|
||||||
|
#mining-checker-app .mc-chart,
|
||||||
|
#mining-checker-app .mc-target-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
#mining-checker-app .mc-filter-grid {
|
||||||
padding: 16px;
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
border-radius: 18px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label,
|
#mining-checker-app .mc-text,
|
||||||
.stat-meta {
|
#mining-checker-app p,
|
||||||
color: #64748b;
|
#mining-checker-app td,
|
||||||
font-size: 13px;
|
#mining-checker-app th,
|
||||||
|
#mining-checker-app label,
|
||||||
|
#mining-checker-app summary,
|
||||||
|
#mining-checker-app pre {
|
||||||
|
color: var(--mc-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
#mining-checker-app h1,
|
||||||
font-size: 22px;
|
#mining-checker-app h2,
|
||||||
line-height: 1.2;
|
#mining-checker-app h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list {
|
#mining-checker-app .mc-stats-grid,
|
||||||
|
#mining-checker-app .mc-target-grid,
|
||||||
|
#mining-checker-app .mc-overview-grid,
|
||||||
|
#mining-checker-app .mc-two-col,
|
||||||
|
#mining-checker-app .mc-main-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-empty {
|
#mining-checker-app .mc-stats-grid {
|
||||||
padding: 18px;
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
border-radius: 18px;
|
|
||||||
background: #f8fafc;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-item {
|
#mining-checker-app .mc-overview-grid {
|
||||||
padding: 16px;
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
border-radius: 18px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-head {
|
#mining-checker-app .mc-target-grid {
|
||||||
display: flex;
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-two-col {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stat-card,
|
||||||
|
#mining-checker-app .mc-target-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stat-card {
|
||||||
|
background: var(--mc-surface-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-kicker {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--mc-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: var(--mc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-badge {
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
gap: 12px;
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
border: 1px solid var(--mc-line-strong);
|
||||||
|
background: color-mix(in srgb, var(--mc-accent) 16%, transparent);
|
||||||
|
color: var(--mc-accent-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-title {
|
#mining-checker-app .mc-badge--warn {
|
||||||
|
background: color-mix(in srgb, var(--mc-warning) 12%, transparent);
|
||||||
|
color: var(--mc-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-badge--info {
|
||||||
|
background: color-mix(in srgb, var(--mc-accent) 16%, transparent);
|
||||||
|
color: var(--mc-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-badge--danger {
|
||||||
|
background: color-mix(in srgb, var(--mc-danger) 12%, transparent);
|
||||||
|
color: var(--mc-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-badge--success {
|
||||||
|
background: color-mix(in srgb, var(--mc-success) 12%, transparent);
|
||||||
|
color: var(--mc-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button,
|
||||||
|
#mining-checker-app button,
|
||||||
|
#mining-checker-app input,
|
||||||
|
#mining-checker-app select,
|
||||||
|
#mining-checker-app textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button--primary {
|
||||||
|
background: linear-gradient(135deg, var(--mc-accent), var(--mc-accent-strong));
|
||||||
|
color: #05121f;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-note {
|
#mining-checker-app .mc-button--secondary {
|
||||||
margin: 0;
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: #475569;
|
color: var(--mc-text);
|
||||||
line-height: 1.45;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-meta {
|
#mining-checker-app .mc-button--danger {
|
||||||
|
background: linear-gradient(135deg, rgba(251, 113, 133, 0.92), rgba(239, 68, 68, 0.92));
|
||||||
|
color: #fff7f7;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button--ghost {
|
||||||
|
background: color-mix(in srgb, var(--mc-accent) 14%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--mc-accent) 34%, transparent);
|
||||||
|
color: var(--mc-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-debug-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: #64748b;
|
gap: 12px;
|
||||||
font-size: 13px;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
#mining-checker-app .mc-debug-console {
|
||||||
|
border-color: rgba(125, 211, 252, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-debug-view-switch {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-debug-log {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-debug-text-console {
|
||||||
|
max-height: 520px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-debug-entry {
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: color-mix(in srgb, var(--brand-accent-2) 8%, var(--mc-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-field {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-field-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--mc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-input,
|
||||||
|
#mining-checker-app .mc-select,
|
||||||
|
#mining-checker-app .mc-textarea,
|
||||||
|
#mining-checker-app .mc-file {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 13px 15px;
|
||||||
|
color: var(--mc-text);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-select option {
|
||||||
|
color: #09111f;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-input::placeholder,
|
||||||
|
#mining-checker-app .mc-textarea::placeholder {
|
||||||
|
color: #6d7c90;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-input:focus,
|
||||||
|
#mining-checker-app .mc-select:focus,
|
||||||
|
#mining-checker-app .mc-textarea:focus,
|
||||||
|
#mining-checker-app .mc-file:focus {
|
||||||
|
border-color: rgba(125, 211, 252, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(125, 211, 252, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-inline-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-inline-fields > .mc-field {
|
||||||
|
flex: 1 1 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-alert--error {
|
||||||
|
border-color: rgba(251, 113, 133, 0.28);
|
||||||
|
background: rgba(127, 29, 29, 0.35);
|
||||||
|
color: #ffe4e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-alert--warning {
|
||||||
|
border-color: rgba(245, 158, 11, 0.28);
|
||||||
|
background: rgba(120, 53, 15, 0.34);
|
||||||
|
color: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-alert--success {
|
||||||
|
border-color: rgba(52, 211, 153, 0.28);
|
||||||
|
background: rgba(6, 78, 59, 0.34);
|
||||||
|
color: #d1fae5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table-shell {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table th,
|
||||||
|
#mining-checker-app .mc-table td {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table thead {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-empty {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app details {
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-mini-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-mini-card,
|
||||||
|
#mining-checker-app .mc-display-field {
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-code-block {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: "JetBrains Mono", "SFMono-Regular", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-chart svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 18px;
|
inset: 0;
|
||||||
bottom: 18px;
|
z-index: 80;
|
||||||
z-index: 30;
|
display: flex;
|
||||||
padding: 12px 16px;
|
align-items: center;
|
||||||
border-radius: 14px;
|
justify-content: center;
|
||||||
background: rgba(15, 23, 42, 0.92);
|
padding: 20px;
|
||||||
color: #fff;
|
background: rgba(2, 6, 23, 0.72);
|
||||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
#mining-checker-app .mc-modal {
|
||||||
.mining-layout {
|
width: min(720px, 100%);
|
||||||
grid-template-columns: 1fr;
|
max-height: min(80vh, 900px);
|
||||||
}
|
overflow: auto;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--mc-line);
|
||||||
|
border-radius: 28px;
|
||||||
|
background: rgba(9, 17, 31, 0.96);
|
||||||
|
box-shadow: 0 28px 60px rgba(0, 0, 0, 0.34);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
#mining-checker-app .mc-chart path,
|
||||||
.mining-app {
|
#mining-checker-app .mc-chart polyline,
|
||||||
padding: 14px;
|
#mining-checker-app .mc-chart line,
|
||||||
|
#mining-checker-app .mc-chart rect {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mining-hero {
|
@media (max-width: 960px) {
|
||||||
|
#mining-checker-app .mc-hero-top,
|
||||||
|
#mining-checker-app .mc-inline-row,
|
||||||
|
#mining-checker-app .mc-flex-split,
|
||||||
|
#mining-checker-app .mc-section-head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid,
|
#mining-checker-app .mc-main-grid {
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-shell {
|
||||||
|
width: min(100% - 10px, 1360px);
|
||||||
|
padding: 8px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stack {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero,
|
||||||
|
#mining-checker-app .mc-panel,
|
||||||
|
#mining-checker-app .mc-dashboard-card,
|
||||||
|
#mining-checker-app .mc-alert,
|
||||||
|
#mining-checker-app .mc-empty,
|
||||||
|
#mining-checker-app .mc-table-shell {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-title {
|
||||||
|
font-size: clamp(1.45rem, 8vw, 2.15rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero-copy {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero-copy p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-home-link {
|
||||||
|
justify-self: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-tabs {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px -4px 0;
|
||||||
|
padding: 0 4px 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button--tab {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table th,
|
||||||
|
#mining-checker-app .mc-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-modal {
|
||||||
|
max-height: min(92vh, 900px);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#mining-checker-app,
|
||||||
|
#mining-checker-app * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app {
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-grid-bg {
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-size: 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-shell {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stack {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero,
|
||||||
|
#mining-checker-app .mc-panel,
|
||||||
|
#mining-checker-app .mc-dashboard-card,
|
||||||
|
#mining-checker-app .mc-alert,
|
||||||
|
#mining-checker-app .mc-empty {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero {
|
||||||
|
border-radius: 0 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-panel,
|
||||||
|
#mining-checker-app .mc-dashboard-card,
|
||||||
|
#mining-checker-app .mc-alert,
|
||||||
|
#mining-checker-app .mc-empty,
|
||||||
|
#mining-checker-app .mc-table-shell {
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-title,
|
||||||
|
#mining-checker-app .mc-hero-copy p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-hero-top {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-kicker {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-tabs {
|
||||||
|
margin: 10px -6px 0;
|
||||||
|
padding: 0 6px 6px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-button:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stats-grid,
|
||||||
|
#mining-checker-app .mc-target-grid,
|
||||||
|
#mining-checker-app .mc-overview-grid,
|
||||||
|
#mining-checker-app .mc-two-col,
|
||||||
|
#mining-checker-app .mc-main-grid,
|
||||||
|
#mining-checker-app .mc-filter-grid,
|
||||||
|
#mining-checker-app .mc-mini-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stat-card,
|
||||||
|
#mining-checker-app .mc-target-card,
|
||||||
|
#mining-checker-app .mc-mini-card,
|
||||||
|
#mining-checker-app .mc-display-field {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-stat-value {
|
||||||
|
font-size: clamp(1.45rem, 8vw, 1.85rem);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app h3 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-text,
|
||||||
|
#mining-checker-app p,
|
||||||
|
#mining-checker-app td,
|
||||||
|
#mining-checker-app th,
|
||||||
|
#mining-checker-app label,
|
||||||
|
#mining-checker-app summary,
|
||||||
|
#mining-checker-app pre {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-input,
|
||||||
|
#mining-checker-app .mc-select,
|
||||||
|
#mining-checker-app .mc-textarea,
|
||||||
|
#mining-checker-app .mc-file {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-inline-fields,
|
||||||
|
#mining-checker-app .mc-debug-tools,
|
||||||
|
#mining-checker-app .mc-debug-view-switch {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-inline-fields > .mc-field {
|
||||||
|
flex-basis: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-badge {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table-shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table {
|
||||||
|
max-width: none;
|
||||||
|
min-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-table th,
|
||||||
|
#mining-checker-app .mc-table td {
|
||||||
|
padding: 9px 10px;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-chart svg {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-modal-backdrop {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mining-checker-app .mc-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(100vh - 16px);
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -114,7 +114,7 @@ if (payloadNode) {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="window-content window-content--embedded">
|
<div class="window-content window-content--embedded">
|
||||||
<iframe class="window-app-frame" src="${src}" title="${title}" loading="lazy"></iframe>
|
<iframe class="window-app-frame" src="${src}" title="${title}"></iframe>
|
||||||
<div class="window-embed-fallback">
|
<div class="window-embed-fallback">
|
||||||
<p><strong>${title}</strong> wird geladen.</p>
|
<p><strong>${title}</strong> wird geladen.</p>
|
||||||
<p>Falls die Einbettung leer bleibt, direkt oeffnen: <a href="${src}" target="_blank" rel="noopener noreferrer">${src}</a></p>
|
<p>Falls die Einbettung leer bleibt, direkt oeffnen: <a href="${src}" target="_blank" rel="noopener noreferrer">${src}</a></p>
|
||||||
@@ -536,6 +536,14 @@ if (payloadNode) {
|
|||||||
|
|
||||||
node.addEventListener('mousedown', () => focusWindow(record.id));
|
node.addEventListener('mousedown', () => focusWindow(record.id));
|
||||||
|
|
||||||
|
const frame = node.querySelector('.window-app-frame');
|
||||||
|
const frameFallback = node.querySelector('.window-embed-fallback');
|
||||||
|
if (frame && frameFallback) {
|
||||||
|
frame.addEventListener('load', () => {
|
||||||
|
frameFallback.remove();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
node.querySelectorAll('.window-action').forEach((action) => {
|
node.querySelectorAll('.window-action').forEach((action) => {
|
||||||
action.addEventListener('click', (event) => {
|
action.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|||||||
191
src/MiningChecker/LegacyModuleStore.php
Normal file
191
src/MiningChecker/LegacyModuleStore.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MiningChecker;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class LegacyModuleStore
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $projectRoot,
|
||||||
|
private readonly string $userScope,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function loadProject(string $projectKey): array
|
||||||
|
{
|
||||||
|
$state = $this->loadAll();
|
||||||
|
$projects = is_array($state['projects'] ?? null) ? $state['projects'] : [];
|
||||||
|
|
||||||
|
if (!isset($projects[$projectKey]) || !is_array($projects[$projectKey])) {
|
||||||
|
$projects[$projectKey] = $this->defaultProjectState($projectKey);
|
||||||
|
$state['projects'] = $projects;
|
||||||
|
$this->saveAll($state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_replace_recursive($this->defaultProjectState($projectKey), $projects[$projectKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $projectState
|
||||||
|
*/
|
||||||
|
public function saveProject(string $projectKey, array $projectState): void
|
||||||
|
{
|
||||||
|
$state = $this->loadAll();
|
||||||
|
$projects = is_array($state['projects'] ?? null) ? $state['projects'] : [];
|
||||||
|
$projects[$projectKey] = array_replace_recursive($this->defaultProjectState($projectKey), $projectState);
|
||||||
|
$state['projects'] = $projects;
|
||||||
|
$this->saveAll($state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function loadAuth(): array
|
||||||
|
{
|
||||||
|
$state = $this->loadAll();
|
||||||
|
$auth = is_array($state['module_auth'] ?? null) ? $state['module_auth'] : [];
|
||||||
|
|
||||||
|
return array_replace_recursive([
|
||||||
|
'required' => true,
|
||||||
|
'users' => [],
|
||||||
|
'groups' => [],
|
||||||
|
], $auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $auth
|
||||||
|
*/
|
||||||
|
public function saveAuth(array $auth): void
|
||||||
|
{
|
||||||
|
$state = $this->loadAll();
|
||||||
|
$state['module_auth'] = [
|
||||||
|
'required' => (bool) ($auth['required'] ?? true),
|
||||||
|
'users' => array_values(array_filter(array_map('strval', (array) ($auth['users'] ?? [])))),
|
||||||
|
'groups' => array_values(array_filter(array_map('strval', (array) ($auth['groups'] ?? [])))),
|
||||||
|
];
|
||||||
|
$this->saveAll($state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextId(string $projectKey, string $bucket): int
|
||||||
|
{
|
||||||
|
$project = $this->loadProject($projectKey);
|
||||||
|
$counters = is_array($project['_counters'] ?? null) ? $project['_counters'] : [];
|
||||||
|
$next = (int) ($counters[$bucket] ?? 0) + 1;
|
||||||
|
$counters[$bucket] = $next;
|
||||||
|
$project['_counters'] = $counters;
|
||||||
|
$this->saveProject($projectKey, $project);
|
||||||
|
|
||||||
|
return $next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function loadAll(): array
|
||||||
|
{
|
||||||
|
$path = $this->path();
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
return [
|
||||||
|
'module_auth' => [
|
||||||
|
'required' => true,
|
||||||
|
'users' => [],
|
||||||
|
'groups' => [],
|
||||||
|
],
|
||||||
|
'projects' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($path);
|
||||||
|
if ($raw === false || trim($raw) === '') {
|
||||||
|
return [
|
||||||
|
'module_auth' => [
|
||||||
|
'required' => true,
|
||||||
|
'users' => [],
|
||||||
|
'groups' => [],
|
||||||
|
],
|
||||||
|
'projects' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
|
||||||
|
return is_array($data) ? $data : [
|
||||||
|
'module_auth' => [
|
||||||
|
'required' => true,
|
||||||
|
'users' => [],
|
||||||
|
'groups' => [],
|
||||||
|
],
|
||||||
|
'projects' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $state
|
||||||
|
*/
|
||||||
|
private function saveAll(array $state): void
|
||||||
|
{
|
||||||
|
$directory = dirname($this->path());
|
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||||
|
throw new RuntimeException('Mining-Checker Speicherverzeichnis konnte nicht erstellt werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json === false) {
|
||||||
|
throw new RuntimeException('Mining-Checker Speicherinhalt konnte nicht serialisiert werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($this->path(), $json . PHP_EOL, LOCK_EX) === false) {
|
||||||
|
throw new RuntimeException('Mining-Checker Speicherinhalt konnte nicht geschrieben werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function path(): string
|
||||||
|
{
|
||||||
|
$safeScope = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $this->userScope) ?: 'guest';
|
||||||
|
|
||||||
|
return $this->projectRoot . '/data/mining-checker/users/' . $safeScope . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function defaultProjectState(string $projectKey): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'_counters' => [],
|
||||||
|
'project' => [
|
||||||
|
'project_key' => $projectKey,
|
||||||
|
'project_name' => strtoupper(str_replace('-', ' ', $projectKey)),
|
||||||
|
],
|
||||||
|
'settings' => [
|
||||||
|
'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'],
|
||||||
|
],
|
||||||
|
'measurements' => [],
|
||||||
|
'targets' => [],
|
||||||
|
'dashboards' => [],
|
||||||
|
'wallet_snapshots' => [],
|
||||||
|
'cost_plans' => [],
|
||||||
|
'payouts' => [],
|
||||||
|
'miner_offers' => [],
|
||||||
|
'purchased_miners' => [],
|
||||||
|
'fx_history' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user