modules->get($moduleName); $tasks = is_array($module['interval_tasks'] ?? null) ? $module['interval_tasks'] : []; $definitions = []; foreach ($tasks as $task) { if (!is_array($task)) { continue; } $name = trim((string) ($task['name'] ?? '')); $callback = trim((string) ($task['callback'] ?? '')); if ($name === '' || $callback === '') { continue; } $definitions[$name] = [ 'name' => $name, 'label' => trim((string) ($task['label'] ?? $name)), 'callback' => $callback, 'enabled_setting' => trim((string) ($task['enabled_setting'] ?? '')), 'interval_setting' => trim((string) ($task['interval_setting'] ?? '')), 'default_enabled' => array_key_exists('default_enabled', $task) ? (bool) $task['default_enabled'] : false, 'default_interval_hours' => max(1.0, (float) ($task['default_interval_hours'] ?? 6)), 'lock_minutes' => max(1, (int) ($task['lock_minutes'] ?? 10)), ]; } return $definitions; } public function statuses(string $moduleName): array { $definitions = $this->definitions($moduleName); if ($definitions === []) { return []; } $settings = $this->modules->settings($moduleName); $states = $this->fetchStates($moduleName); $nowTs = time(); $statuses = []; foreach ($definitions as $name => $definition) { $state = $states[$name] ?? []; $enabledSetting = $definition['enabled_setting']; $intervalSetting = $definition['interval_setting']; $enabled = $definition['default_enabled']; if ($enabledSetting !== '') { $enabled = $this->settingBool($settings[$enabledSetting] ?? null, $definition['default_enabled']); } $intervalHours = $definition['default_interval_hours']; if ($intervalSetting !== '') { $intervalHours = $this->settingFloat($settings[$intervalSetting] ?? null, $definition['default_interval_hours']); } $intervalHours = max(1.0, $intervalHours); $referenceAt = trim((string) ($state['last_success_at'] ?? '')); $referenceTs = $referenceAt !== '' ? strtotime($referenceAt) : false; $nextDueTs = $referenceTs !== false ? ((int) $referenceTs + (int) round($intervalHours * 3600)) : null; $isLocked = $this->isLockActive($state, $nowTs); $isDue = $enabled && !$isLocked && ($nextDueTs === null || $nextDueTs <= $nowTs); $statuses[] = $definition + [ 'enabled' => $enabled, 'interval_hours' => $intervalHours, 'state' => $state, 'is_due' => $isDue, 'is_locked' => $isLocked, 'next_due_at' => $nextDueTs !== null ? gmdate('Y-m-d H:i:s', $nextDueTs) : null, ]; } return $statuses; } public function runDue(string $moduleName): array { $results = []; foreach ($this->statuses($moduleName) as $task) { if (empty($task['enabled']) || empty($task['is_due'])) { continue; } if (!$this->modules->hasFunction($moduleName, (string) $task['callback'])) { $results[] = [ 'task' => $task['name'], 'ok' => false, 'message' => 'Callback nicht registriert.', ]; continue; } if (!$this->acquireLock($moduleName, (string) $task['name'], (int) $task['lock_minutes'])) { continue; } $startedAt = gmdate('Y-m-d H:i:s'); $this->persistState($moduleName, (string) $task['name'], [ 'last_started_at' => $startedAt, 'last_status' => 'running', 'last_message' => 'Automatischer Lauf gestartet.', ]); try { $result = $this->modules->call($moduleName, (string) $task['callback'], [ 'task' => $task, 'trigger' => 'interval_runner', 'started_at' => $startedAt, ]); $ok = !is_array($result) || !array_key_exists('ok', $result) || !empty($result['ok']); $skipped = is_array($result) && !empty($result['skipped']); $message = is_array($result) ? trim((string) ($result['message'] ?? '')) : ''; $finishedAt = gmdate('Y-m-d H:i:s'); $payload = [ 'last_finished_at' => $finishedAt, 'last_status' => $skipped ? 'skipped' : ($ok ? 'success' : 'error'), 'last_message' => $message, 'lock_until' => null, ]; if ($ok && !$skipped) { $payload['last_success_at'] = $finishedAt; } $this->persistState($moduleName, (string) $task['name'], $payload); $results[] = [ 'task' => $task['name'], 'ok' => $ok, 'message' => $message, ]; } catch (\Throwable $e) { $finishedAt = gmdate('Y-m-d H:i:s'); $this->persistState($moduleName, (string) $task['name'], [ 'last_finished_at' => $finishedAt, 'last_status' => 'error', 'last_message' => $e->getMessage(), 'lock_until' => null, ]); $results[] = [ 'task' => $task['name'], 'ok' => false, 'message' => $e->getMessage(), ]; } } return $results; } private function fetchStates(string $moduleName): array { if (!$this->pdo) { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_module_task_runs WHERE module_name = :module_name" ); $stmt->execute(['module_name' => $moduleName]); $states = []; foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) { $name = trim((string) ($row['task_name'] ?? '')); if ($name !== '') { $states[$name] = $row; } } return $states; } private function acquireLock(string $moduleName, string $taskName, int $lockMinutes): bool { if (!$this->pdo) { return true; } $states = $this->fetchStates($moduleName); $state = $states[$taskName] ?? []; if ($this->isLockActive($state, time())) { return false; } $lockUntil = gmdate('Y-m-d H:i:s', time() + ($lockMinutes * 60)); $this->persistState($moduleName, $taskName, [ 'lock_until' => $lockUntil, ]); return true; } private function persistState(string $moduleName, string $taskName, array $values): void { if (!$this->pdo) { return; } $current = $this->fetchStates($moduleName)[$taskName] ?? []; $payload = array_merge($current, $values, [ 'module_name' => $moduleName, 'task_name' => $taskName, 'updated_at' => gmdate('Y-m-d H:i:s'), ]); $params = [ 'module_name' => $moduleName, 'task_name' => $taskName, 'last_started_at' => $this->nullOrString($payload['last_started_at'] ?? null), 'last_finished_at' => $this->nullOrString($payload['last_finished_at'] ?? null), 'last_success_at' => $this->nullOrString($payload['last_success_at'] ?? null), 'last_status' => $this->nullOrString($payload['last_status'] ?? null), 'last_message' => $this->nullOrString($payload['last_message'] ?? null), 'lock_until' => $this->nullOrString($payload['lock_until'] ?? null), 'updated_at' => $payload['updated_at'], ]; $updateStmt = $this->pdo->prepare( "UPDATE nexus_module_task_runs SET last_started_at = :last_started_at, last_finished_at = :last_finished_at, last_success_at = :last_success_at, last_status = :last_status, last_message = :last_message, lock_until = :lock_until, updated_at = :updated_at WHERE module_name = :module_name AND task_name = :task_name" ); $updateStmt->execute($params); if ($updateStmt->rowCount() > 0) { return; } $insertStmt = $this->pdo->prepare( "INSERT INTO nexus_module_task_runs ( module_name, task_name, last_started_at, last_finished_at, last_success_at, last_status, last_message, lock_until, updated_at ) VALUES ( :module_name, :task_name, :last_started_at, :last_finished_at, :last_success_at, :last_status, :last_message, :lock_until, :updated_at )" ); $insertStmt->execute($params); } private function nullOrString(mixed $value): ?string { $value = trim((string) $value); return $value !== '' ? $value : null; } private function settingBool(mixed $value, bool $default): bool { if ($value === null || $value === '') { return $default; } return in_array((string) $value, ['1', 'true', 'yes', 'on'], true); } private function settingFloat(mixed $value, float $default): float { if (!is_numeric($value)) { return $default; } $numeric = (float) $value; return $numeric > 0 ? $numeric : $default; } private function isLockActive(array $state, int $nowTs): bool { $lockUntil = trim((string) ($state['lock_until'] ?? '')); if ($lockUntil === '') { return false; } $lockTs = strtotime($lockUntil); return $lockTs !== false && $lockTs > $nowTs; } }