registerFunction($moduleName, 'runtime_settings', static function (): array { $moduleBasePath = __DIR__; $config = ModuleConfig::load($moduleBasePath); $projectKey = $config->defaultProjectKey(); $user = auth_user() ?? []; $ownerSub = trim((string) ($user['sub'] ?? '')) !== '' ? trim((string) ($user['sub'] ?? '')) : 'local'; $pdo = ConnectionFactory::make($config); $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); $settings = $repository->getSettings($projectKey) ?? []; $displayTimezone = trim((string) ($settings['display_timezone'] ?? 'Europe/Berlin')); if ($displayTimezone === '') { $displayTimezone = 'Europe/Berlin'; } $baselineMeasuredAt = trim((string) ($settings['baseline_measured_at'] ?? '')); if ($baselineMeasuredAt !== '') { try { $baselineMeasuredAt = (new DateTimeImmutable($baselineMeasuredAt, new DateTimeZone('UTC'))) ->setTimezone(new DateTimeZone($displayTimezone)) ->format('Y-m-d\TH:i'); } catch (\Throwable) { $baselineMeasuredAt = ''; } } return [ 'baseline_measured_at' => $baselineMeasuredAt, 'baseline_coins_total' => isset($settings['baseline_coins_total']) ? (string) $settings['baseline_coins_total'] : '', 'report_currency' => strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR', ]; }); $mm->registerFunction($moduleName, 'save_runtime_settings', static function (array $payload): array { $moduleBasePath = __DIR__; $config = ModuleConfig::load($moduleBasePath); $projectKey = $config->defaultProjectKey(); $user = auth_user() ?? []; $ownerSub = trim((string) ($user['sub'] ?? '')) !== '' ? trim((string) ($user['sub'] ?? '')) : 'local'; $pdo = ConnectionFactory::make($config); $schema = new SchemaManager($pdo, $config->tablePrefix(), $moduleBasePath); $schema->ensureSchema(); $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); $repository->ensureProject($projectKey, strtoupper(str_replace('-', ' ', $projectKey))); $existing = $repository->getSettings($projectKey) ?? []; $displayTimezone = trim((string) ($existing['display_timezone'] ?? 'Europe/Berlin')); if ($displayTimezone === '') { $displayTimezone = 'Europe/Berlin'; } try { $displayTz = new DateTimeZone($displayTimezone); } catch (\Throwable) { throw new RuntimeException('Ungueltige Anzeige-Zeitzone.'); } $baselineInput = trim((string) ($payload['baseline_measured_at'] ?? ($existing['baseline_measured_at'] ?? ''))); if ($baselineInput === '') { throw new RuntimeException('Baseline Zeitpunkt fehlt.'); } try { $baselineUtc = (new DateTimeImmutable($baselineInput, $displayTz)) ->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d H:i:s'); } catch (\Throwable) { throw new RuntimeException('Baseline Zeitpunkt ist ungueltig.'); } $baselineCoins = trim((string) ($payload['baseline_coins_total'] ?? ($existing['baseline_coins_total'] ?? ''))); if ($baselineCoins === '' || !is_numeric($baselineCoins)) { throw new RuntimeException('Baseline Coins muessen numerisch sein.'); } $preferredCurrencies = $existing['preferred_currencies'] ?? ['DOGE', 'USD', 'EUR']; if (is_string($preferredCurrencies)) { $preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: []; } $preferredCurrencies = array_values(array_unique(array_filter(array_map( static fn (mixed $value): string => strtoupper(trim((string) $value)), is_array($preferredCurrencies) ? $preferredCurrencies : [] ), static fn (string $value): bool => $value !== ''))); $settings = [ 'baseline_measured_at' => $baselineUtc, 'baseline_coins_total' => (float) $baselineCoins, 'daily_cost_amount' => isset($existing['daily_cost_amount']) && is_numeric((string) $existing['daily_cost_amount']) ? (float) $existing['daily_cost_amount'] : 0.0, 'daily_cost_currency' => strtoupper(trim((string) ($existing['daily_cost_currency'] ?? 'EUR'))) ?: 'EUR', 'report_currency' => strtoupper(trim((string) ($payload['report_currency'] ?? ($existing['report_currency'] ?? 'EUR')))) ?: 'EUR', 'crypto_currency' => strtoupper(trim((string) ($existing['crypto_currency'] ?? 'DOGE'))) ?: 'DOGE', 'display_timezone' => $displayTimezone, 'fx_max_age_hours' => isset($existing['fx_max_age_hours']) && is_numeric((string) $existing['fx_max_age_hours']) ? (int) $existing['fx_max_age_hours'] : 3, 'module_theme_mode' => in_array((string) ($existing['module_theme_mode'] ?? 'inherit'), ['inherit', 'custom'], true) ? (string) $existing['module_theme_mode'] : 'inherit', 'module_theme_accent' => in_array((string) ($existing['module_theme_accent'] ?? 'teal'), ['teal', 'logo', 'pink', 'cyan', 'orange', 'green'], true) ? (string) $existing['module_theme_accent'] : 'teal', 'preferred_currencies' => $preferredCurrencies, ]; $repository->saveSettings($projectKey, $settings); return module_fn($moduleName, 'runtime_settings'); }); $mm->registerFunction($moduleName, 'setup_actions', static function (): array { return [ [ 'name' => 'connection_test', 'label' => 'DB-Verbindung testen', 'help' => 'Prueft, ob die Projekt-Datenbank fuer den Mining-Checker erreichbar ist.', ], [ 'name' => 'initialize_schema', 'label' => 'Schema initialisieren', 'help' => 'Legt die Mining-Checker Tabellen an, wenn sie noch nicht vorhanden sind.', ], [ 'name' => 'upgrade_schema', 'label' => 'Tabellen auf neuesten Stand bringen', 'help' => 'Fuehrt fehlende Tabellen- und Spalten-Upgrades fuer den Mining-Checker aus.', ], [ 'name' => 'seed_import', 'label' => 'Seed-Daten importieren', 'help' => 'Importiert die vordefinierten Startdaten fuer das Standardprojekt.', ], ]; }); $mm->registerFunction($moduleName, 'run_setup_action', static function (string $action): array { $moduleBasePath = __DIR__; $config = ModuleConfig::load($moduleBasePath); $projectKey = $config->defaultProjectKey(); $user = auth_user() ?? []; $ownerSub = trim((string) ($user['sub'] ?? '')) !== '' ? trim((string) ($user['sub'] ?? '')) : 'local'; $pdo = ConnectionFactory::make($config); $schema = new SchemaManager($pdo, $config->tablePrefix(), $moduleBasePath); $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); return match ($action) { 'connection_test' => (static function () use ($pdo, $config): array { $driver = strtolower((string) $pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); $database = 'n/a'; try { $database = match ($driver) { 'pgsql' => (string) ($pdo->query('SELECT current_database()')->fetchColumn() ?: 'n/a'), 'mysql' => (string) ($pdo->query('SELECT DATABASE()')->fetchColumn() ?: 'n/a'), default => 'n/a', }; } catch (\Throwable) { $database = 'n/a'; } return [ 'message' => 'DB-Verbindung erfolgreich. Driver: ' . $driver . ', Datenbank: ' . $database . ', Tabellenpraefix: ' . $config->tablePrefix() . '.', ]; })(), 'initialize_schema' => (static function () use ($schema): array { $result = $schema->initializeSchema(false); $after = is_array($result['after'] ?? null) ? $result['after'] : []; return [ 'message' => ($result['message'] ?? 'Schema initialisiert.') . ' Vorhanden: ' . (int) ($after['present_count'] ?? 0) . '/' . count((array) ($after['required_tables'] ?? [])) . '.', ]; })(), 'upgrade_schema' => (static function () use ($schema): array { $result = $schema->upgradeSchemaDirect(); $applied = array_values(array_filter(array_map('strval', (array) ($result['upgraded'] ?? [])))); return [ 'message' => ($result['message'] ?? 'Schema-Upgrade ausgefuehrt.') . ($applied !== [] ? ' Upgrades: ' . implode(', ', $applied) . '.' : ''), ]; })(), 'seed_import' => (static function () use ($schema, $repository, $projectKey): array { $schema->ensureSchema(); $repository->ensureProject($projectKey, strtoupper(str_replace('-', ' ', $projectKey))); $result = (new \Modules\MiningChecker\Domain\SeedImporter($repository))->import($projectKey); return [ 'message' => 'Seed-Daten importiert. Eingefuegt: ' . (int) ($result['inserted'] ?? 0) . '.', ]; })(), default => throw new RuntimeException('Unbekannte Setup-Aktion.'), }; }); $mm->registerFunction('mining-checker', 'sql_import_target', static function (): array { $moduleBasePath = __DIR__; $config = ModuleConfig::load($moduleBasePath); return [ 'pdo' => ConnectionFactory::make($config), 'label' => 'Mining-Checker Projekt-Datenbank', ]; });