mining checker
All checks were successful
Deploy / deploy-staging (push) Successful in 27s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-21 00:14:19 +02:00
parent 2cdd14c400
commit b81de785ac
63 changed files with 11549 additions and 1338 deletions

View File

@@ -11,6 +11,7 @@ use Desktop\DesktopState;
use Desktop\SkinResolver;
use Desktop\WidgetRegistry;
use Desktop\WindowManager;
use ModulesCore\ModuleRegistry;
final class App
{
@@ -26,7 +27,8 @@ final class App
{
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
$auth = new KeycloakAuth($keycloakConfig);
$registry = new AppRegistry($this->projectRoot . '/config/apps.php');
$moduleRegistry = new ModuleRegistry($this->projectRoot . '/modules');
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
$skins = SkinResolver::all();
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);

View File

@@ -6,7 +6,7 @@ spl_autoload_register(static function (string $class): void {
$prefixes = [
'App\\' => __DIR__ . '/',
'Desktop\\' => dirname(__DIR__) . '/Desktop/',
'MiningChecker\\' => dirname(__DIR__) . '/MiningChecker/',
'ModulesCore\\' => dirname(__DIR__) . '/ModulesCore/',
];
foreach ($prefixes as $prefix => $baseDir) {

View File

@@ -9,11 +9,14 @@ final class AppRegistry
/** @var array<int, array<string, mixed>> */
private array $apps;
public function __construct(string $configPath)
/**
* @param array<int, array<string, mixed>> $additionalApps
*/
public function __construct(string $configPath, array $additionalApps = [])
{
/** @var array<int, array<string, mixed>> $apps */
$apps = require $configPath;
$this->apps = $apps;
$this->apps = array_values(array_merge($apps, $additionalApps));
}
/**

View File

@@ -1,191 +0,0 @@
<?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' => [],
];
}
}

View File

@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace MiningChecker;
final class MiningCheckerCalculator
{
/**
* @param array<string, mixed> $settings
* @return array<string, float|null>
*/
public function calculate(array $settings): array
{
$hashrate = $this->toHashrateHps(
(float) ($settings['hashrate'] ?? 0),
(string) ($settings['hashrate_unit'] ?? 'MH/s')
);
$networkHashrate = $this->toHashrateHps(
(float) ($settings['network_hashrate'] ?? 0),
(string) ($settings['network_hashrate_unit'] ?? 'TH/s')
);
$powerWatts = max(0.0, (float) ($settings['power_watts'] ?? 0));
$electricityPrice = max(0.0, (float) ($settings['electricity_price'] ?? 0));
$poolFeePercent = max(0.0, min(100.0, (float) ($settings['pool_fee_percent'] ?? 0)));
$coinPrice = max(0.0, (float) ($settings['coin_price'] ?? 0));
$blockReward = max(0.0, (float) ($settings['block_reward'] ?? 0));
$blockTimeSeconds = max(1.0, (float) ($settings['block_time_seconds'] ?? 60));
$hardwareCost = max(0.0, (float) ($settings['hardware_cost'] ?? 0));
$share = $networkHashrate > 0 ? $hashrate / $networkHashrate : null;
$blocksPerDay = 86400 / $blockTimeSeconds;
$dailyCoinsGross = $share !== null ? $share * $blocksPerDay * $blockReward : null;
$dailyCoinsNet = $dailyCoinsGross !== null ? $dailyCoinsGross * (1 - ($poolFeePercent / 100)) : null;
$dailyRevenue = $dailyCoinsNet !== null ? $dailyCoinsNet * $coinPrice : null;
$dailyPowerCost = ($powerWatts / 1000) * 24 * $electricityPrice;
$dailyProfit = $dailyRevenue !== null ? $dailyRevenue - $dailyPowerCost : null;
$monthlyProfit = $dailyProfit !== null ? $dailyProfit * 30 : null;
$annualProfit = $dailyProfit !== null ? $dailyProfit * 365 : null;
$breakEvenDays = ($dailyProfit !== null && $dailyProfit > 0.0 && $hardwareCost > 0.0)
? $hardwareCost / $dailyProfit
: null;
$efficiency = $hashrate > 0 ? $powerWatts / ($hashrate / 1000000) : null;
return [
'hashrate_hps' => $hashrate > 0 ? $hashrate : null,
'network_hashrate_hps' => $networkHashrate > 0 ? $networkHashrate : null,
'share_ratio' => $share,
'blocks_per_day' => $blocksPerDay,
'daily_coins_gross' => $dailyCoinsGross,
'daily_coins_net' => $dailyCoinsNet,
'daily_revenue' => $dailyRevenue,
'daily_power_cost' => $dailyPowerCost,
'daily_profit' => $dailyProfit,
'monthly_profit' => $monthlyProfit,
'annual_profit' => $annualProfit,
'break_even_days' => $breakEvenDays,
'efficiency_w_per_mh' => $efficiency,
];
}
private function toHashrateHps(float $value, string $unit): float
{
if ($value <= 0) {
return 0.0;
}
$normalized = strtoupper(trim($unit));
$multipliers = [
'H/S' => 1,
'KH/S' => 1000,
'MH/S' => 1000000,
'GH/S' => 1000000000,
'TH/S' => 1000000000000,
'PH/S' => 1000000000000000,
'EH/S' => 1000000000000000000,
];
$multiplier = $multipliers[$normalized] ?? 1000000;
return $value * $multiplier;
}
}

View File

@@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
namespace MiningChecker;
use RuntimeException;
final class MiningCheckerStore
{
public function __construct(
private readonly string $projectRoot,
private readonly string $userScope,
) {
}
/**
* @return array<string, mixed>
*/
public function load(): array
{
$path = $this->path();
if (!is_file($path)) {
return $this->defaultState();
}
$raw = file_get_contents($path);
if ($raw === false || trim($raw) === '') {
return $this->defaultState();
}
$data = json_decode($raw, true);
return is_array($data) ? array_replace_recursive($this->defaultState(), $data) : $this->defaultState();
}
/**
* @param array<string, mixed> $state
*/
public function save(array $state): void
{
$directory = dirname($this->path());
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException('Mining-Checker-Verzeichnis 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-Daten konnten nicht serialisiert werden.');
}
if (file_put_contents($this->path(), $json . PHP_EOL, LOCK_EX) === false) {
throw new RuntimeException('Mining-Checker-Daten konnten nicht gespeichert werden.');
}
}
private function path(): string
{
$safeScope = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $this->userScope) ?: 'guest';
return $this->projectRoot . '/data/mining-checker/' . $safeScope . '.json';
}
/**
* @return array<string, mixed>
*/
private function defaultState(): array
{
return [
'settings' => [
'project_name' => 'DOGE Mining',
'coin_symbol' => 'DOGE',
'algorithm' => 'Scrypt',
'currency' => 'EUR',
'hashrate' => 950,
'hashrate_unit' => 'MH/s',
'network_hashrate' => 920,
'network_hashrate_unit' => 'TH/s',
'block_reward' => 10000,
'block_time_seconds' => 60,
'coin_price' => 0.14,
'power_watts' => 3420,
'electricity_price' => 0.32,
'pool_fee_percent' => 1.5,
'hardware_cost' => 8400,
'notes' => '',
],
'history' => [],
];
}
}

View File

@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace MiningChecker;
final class MiningCheckerUserScope
{
public static function current(): string
{
$auth = $_SESSION['desktop_auth'] ?? null;
if (!is_array($auth)) {
return 'guest';
}
$user = is_array($auth['user'] ?? null) ? $auth['user'] : [];
return (string) ($user['sub'] ?? $user['username'] ?? 'guest');
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace ModulesCore;
use App\AccountGate;
use App\ConfigLoader;
use App\KeycloakAuth;
final class ModuleHttp
{
public static function requireDesktopAccess(string $projectRoot, bool $json = false): void
{
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
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();
$message = (string) ($accountCheck['message'] ?? 'Dieses Konto ist noch nicht freigeschaltet.');
if ($json) {
self::respondJson(['error' => $message], 403);
}
$target = (string) ($accountCheck['state'] ?? '') === 'pending'
? '/auth/pending/?username=' . urlencode((string) ($currentUser['username'] ?? ''))
: '/auth/inactive/?message=' . urlencode($message);
header('Location: ' . $target, true, 302);
exit;
}
}
if (!$auth->shouldShowDesktop()) {
if ($json) {
self::respondJson(['error' => 'Nicht autorisiert.'], 401);
}
header('Location: /auth/keycloak', true, 302);
exit;
}
}
public static function currentUserScope(): string
{
$auth = $_SESSION['desktop_auth'] ?? null;
if (!is_array($auth)) {
return 'guest';
}
$user = is_array($auth['user'] ?? null) ? $auth['user'] : [];
return (string) ($user['sub'] ?? $user['username'] ?? 'guest');
}
/**
* @param array<string, mixed> $payload
*/
private static function respondJson(array $payload, int $statusCode): never
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace ModulesCore;
final class ModuleRegistry
{
/** @var array<int, array<string, mixed>>|null */
private ?array $desktopApps = null;
public function __construct(
private readonly string $modulesPath,
) {
}
/**
* @return array<int, array<string, mixed>>
*/
public function desktopApps(): array
{
if ($this->desktopApps !== null) {
return $this->desktopApps;
}
$apps = [];
foreach ($this->moduleDirectories() as $moduleName => $modulePath) {
$desktopConfigPath = $modulePath . '/desktop.php';
if (!is_file($desktopConfigPath)) {
continue;
}
$desktopConfig = require $desktopConfigPath;
if (!is_array($desktopConfig)) {
continue;
}
$manifest = $this->readJson($modulePath . '/module.json');
$desktopConfig['module_id'] = $moduleName;
$desktopConfig['title'] ??= (string) ($manifest['title'] ?? ucfirst($moduleName));
$desktopConfig['summary'] ??= (string) ($manifest['description'] ?? '');
$desktopConfig['required_groups'] ??= array_values(array_map(
'strval',
(array) ($manifest['auth']['groups'] ?? [])
));
$desktopConfig['enabled_by_default'] ??= (bool) ($manifest['enabled_by_default'] ?? false);
$apps[] = $desktopConfig;
}
return $this->desktopApps = $apps;
}
public function modulePath(string $moduleName): ?string
{
$directories = $this->moduleDirectories();
return $directories[$moduleName] ?? null;
}
/**
* @return array<string, string>
*/
private function moduleDirectories(): array
{
$paths = glob(rtrim($this->modulesPath, '/') . '/*', GLOB_ONLYDIR);
if (!is_array($paths)) {
return [];
}
$directories = [];
foreach ($paths as $path) {
$moduleName = basename($path);
if ($moduleName === '' || $moduleName[0] === '.') {
continue;
}
$directories[$moduleName] = $path;
}
ksort($directories);
return $directories;
}
/**
* @return array<string, mixed>
*/
private function readJson(string $path): array
{
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
if ($raw === false || trim($raw) === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
}