cron
This commit is contained in:
@@ -567,6 +567,136 @@ $mm->registerFunction($moduleName, 'alpha_vantage_fetch_quotes', static function
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'scheduled_refresh_quotes', static function (array $context = []): array {
|
||||||
|
$pdo = module_fn('boersenchecker', 'pdo');
|
||||||
|
$settings = modules()->settings('boersenchecker');
|
||||||
|
$instrumentTable = module_fn('boersenchecker', 'table', 'instruments');
|
||||||
|
$positionTable = module_fn('boersenchecker', 'table', 'positions');
|
||||||
|
$quoteTable = module_fn('boersenchecker', 'table', 'quotes');
|
||||||
|
|
||||||
|
$defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR';
|
||||||
|
$minIntervalMinutes = (int) (($settings['alpha_vantage_min_interval_minutes'] ?? null) ?: 60);
|
||||||
|
if ($minIntervalMinutes <= 0) {
|
||||||
|
$minIntervalMinutes = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->query(
|
||||||
|
'SELECT DISTINCT
|
||||||
|
i.id,
|
||||||
|
i.name,
|
||||||
|
i.symbol,
|
||||||
|
i.quote_currency
|
||||||
|
FROM ' . $positionTable . ' p
|
||||||
|
INNER JOIN ' . $instrumentTable . ' i ON i.id = p.instrument_id
|
||||||
|
WHERE i.symbol IS NOT NULL
|
||||||
|
AND i.symbol <> \'\'
|
||||||
|
ORDER BY i.name ASC'
|
||||||
|
);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
if ($rows === []) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'message' => 'Kein automatischer Kursabruf: keine Aktien mit Symbol vorhanden.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$instrumentIds = array_values(array_map(static fn (array $row): int => (int) ($row['id'] ?? 0), $rows));
|
||||||
|
$instrumentIds = array_values(array_filter($instrumentIds, static fn (int $id): bool => $id > 0));
|
||||||
|
$latestQuotes = [];
|
||||||
|
if ($instrumentIds !== []) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($instrumentIds), '?'));
|
||||||
|
$latestStmt = $pdo->prepare(
|
||||||
|
'SELECT *
|
||||||
|
FROM ' . $quoteTable . '
|
||||||
|
WHERE instrument_id IN (' . $placeholders . ')
|
||||||
|
AND source LIKE ?
|
||||||
|
ORDER BY quoted_at DESC, created_at DESC, id DESC'
|
||||||
|
);
|
||||||
|
$latestStmt->execute([...$instrumentIds, 'alphavantage:%']);
|
||||||
|
foreach ($latestStmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||||
|
$instrumentId = (int) ($row['instrument_id'] ?? 0);
|
||||||
|
if ($instrumentId > 0 && !isset($latestQuotes[$instrumentId])) {
|
||||||
|
$latestQuotes[$instrumentId] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reused = 0;
|
||||||
|
$candidates = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$instrumentId = (int) ($row['id'] ?? 0);
|
||||||
|
$latest = $latestQuotes[$instrumentId] ?? null;
|
||||||
|
$latestTimestamp = is_array($latest) ? strtotime((string) ($latest['quoted_at'] ?? '')) : false;
|
||||||
|
if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($minIntervalMinutes * 60)) {
|
||||||
|
$reused++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$candidates[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidates === []) {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'message' => 'Automatischer Kursabruf uebersprungen: alle Kurse liegen noch innerhalb des Mindestabstands.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bulkResult = module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $candidates);
|
||||||
|
if (empty($bulkResult['ok'])) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => (string) ($bulkResult['message'] ?? 'Automatischer Alpha-Vantage-Abruf fehlgeschlagen.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : [];
|
||||||
|
$errors = is_array($bulkResult['errors'] ?? null) ? $bulkResult['errors'] : [];
|
||||||
|
$updated = 0;
|
||||||
|
foreach ($candidates as $row) {
|
||||||
|
$instrumentId = (int) ($row['id'] ?? 0);
|
||||||
|
$quote = $quotes[$instrumentId] ?? null;
|
||||||
|
if (!is_array($quote) || !is_numeric($quote['price'] ?? null)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storeResult = module_fn(
|
||||||
|
'boersenchecker',
|
||||||
|
'store_market_quote',
|
||||||
|
$instrumentId,
|
||||||
|
(float) $quote['price'],
|
||||||
|
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $defaultReportCurrency))) ?: $defaultReportCurrency,
|
||||||
|
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
|
||||||
|
(string) ($quote['source'] ?? 'alphavantage:global_quote')
|
||||||
|
);
|
||||||
|
if (!empty($storeResult['inserted'])) {
|
||||||
|
$updated++;
|
||||||
|
} else {
|
||||||
|
$reused++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = 'Automatischer Kursabruf: ' . $updated . ' neu, ' . $reused . ' wiederverwendet, ' . count($errors) . ' Fehler.';
|
||||||
|
if ($errors !== []) {
|
||||||
|
$message .= ' ' . implode(' | ', array_slice($errors, 0, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
module_debug_push('boersenchecker', [
|
||||||
|
'label' => 'Intervall-Aufgabe',
|
||||||
|
'type' => 'scheduler:run',
|
||||||
|
'task' => 'auto_refresh_quotes',
|
||||||
|
'context' => $context,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => $errors === [],
|
||||||
|
'message' => $message,
|
||||||
|
'updated' => $updated,
|
||||||
|
'reused' => $reused,
|
||||||
|
'errors' => $errors,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
$mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static function (string $keywords): array {
|
$mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static function (string $keywords): array {
|
||||||
$keywords = trim($keywords);
|
$keywords = trim($keywords);
|
||||||
if ($keywords === '') {
|
if ($keywords === '') {
|
||||||
|
|||||||
@@ -17,9 +17,23 @@
|
|||||||
{ "name": "fx_max_age_hours", "label": "Maximales FX-Alter (Stunden)", "type": "number", "required": false, "help": "Wird bei manueller Aktualisierung ueber den Mining-Checker genutzt." },
|
{ "name": "fx_max_age_hours", "label": "Maximales FX-Alter (Stunden)", "type": "number", "required": false, "help": "Wird bei manueller Aktualisierung ueber den Mining-Checker genutzt." },
|
||||||
{ "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." },
|
{ "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." },
|
||||||
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
|
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
|
||||||
{ "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." }
|
{ "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." },
|
||||||
|
{ "name": "auto_refresh_quotes_enabled", "label": "Automatischen Kursabruf aktivieren", "type": "checkbox", "required": false, "help": "Fuehrt Kursupdates automatisch beim ersten Modulaufruf nach Ablauf des Intervalls aus." },
|
||||||
|
{ "name": "auto_refresh_quotes_interval_hours", "label": "Intervall fuer automatischen Kursabruf (Stunden)", "type": "number", "required": false, "help": "Nach Ablauf dieses Intervalls wird beim naechsten Modulaufruf ein automatischer Kursabruf gestartet." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"interval_tasks": [
|
||||||
|
{
|
||||||
|
"name": "auto_refresh_quotes",
|
||||||
|
"label": "Automatischer Kursabruf",
|
||||||
|
"callback": "scheduled_refresh_quotes",
|
||||||
|
"enabled_setting": "auto_refresh_quotes_enabled",
|
||||||
|
"interval_setting": "auto_refresh_quotes_interval_hours",
|
||||||
|
"default_enabled": false,
|
||||||
|
"default_interval_hours": 6,
|
||||||
|
"lock_minutes": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
"db_defaults": {
|
"db_defaults": {
|
||||||
"driver": "pgsql",
|
"driver": "pgsql",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ foreach ($fields as $field) {
|
|||||||
$fieldMeta[$fname] = $field;
|
$fieldMeta[$fname] = $field;
|
||||||
}
|
}
|
||||||
$current = modules()->settings($moduleName);
|
$current = modules()->settings($moduleName);
|
||||||
|
$intervalTaskStatuses = modules()->intervalTaskStatuses($moduleName);
|
||||||
$defaults = $module['db_defaults'] ?? [];
|
$defaults = $module['db_defaults'] ?? [];
|
||||||
if (empty($current['db']) && is_array($defaults)) {
|
if (empty($current['db']) && is_array($defaults)) {
|
||||||
$current['db'] = $defaults;
|
$current['db'] = $defaults;
|
||||||
@@ -251,6 +252,20 @@ $normalizeDriver = static function (mixed $value): mixed {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$formatRunTimestamp = static function (?string $value): string {
|
||||||
|
$value = trim((string) $value);
|
||||||
|
if ($value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ts = strtotime($value);
|
||||||
|
if ($ts === false) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d H:i:s', $ts);
|
||||||
|
};
|
||||||
|
|
||||||
$renderField = function (array $field) use (&$current, $getNested, $driverOptions): void {
|
$renderField = function (array $field) use (&$current, $getNested, $driverOptions): void {
|
||||||
$name = (string)($field['name'] ?? '');
|
$name = (string)($field['name'] ?? '');
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
@@ -435,6 +450,35 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
|||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($intervalTaskStatuses !== []): ?>
|
||||||
|
<section class="setup-panel">
|
||||||
|
<div class="setup-panel__head">
|
||||||
|
<div>
|
||||||
|
<span class="pill">Automationen</span>
|
||||||
|
<h2>Intervall-Aufgaben</h2>
|
||||||
|
<p class="muted">Diese Aufgaben werden beim ersten gueltigen Modulaufruf nach Ablauf des Intervalls automatisch ausgefuehrt.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<?php foreach ($intervalTaskStatuses as $task): ?>
|
||||||
|
<?php $state = is_array($task['state'] ?? null) ? $task['state'] : []; ?>
|
||||||
|
<div class="setup-field muted">
|
||||||
|
<span><?= e((string) ($task['label'] ?? $task['name'] ?? 'Intervall-Aufgabe')) ?></span>
|
||||||
|
<input type="text" readonly value="<?= e(!empty($task['enabled']) ? 'Aktiv' : 'Deaktiviert') ?>">
|
||||||
|
<small class="muted">Intervall: <?= e(number_format((float) ($task['interval_hours'] ?? 0), 2, ',', '')) ?> Stunden</small>
|
||||||
|
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($state['last_started_at'] ?? ''))) ?></small>
|
||||||
|
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($state['last_success_at'] ?? ''))) ?></small>
|
||||||
|
<small class="muted">Naechster Lauf: <?= e($formatRunTimestamp((string) ($task['next_due_at'] ?? ''))) ?></small>
|
||||||
|
<small class="muted">Status: <?= e((string) (($state['last_status'] ?? '') !== '' ? $state['last_status'] : '-')) ?></small>
|
||||||
|
<?php if (trim((string) ($state['last_message'] ?? '')) !== ''): ?>
|
||||||
|
<small class="muted">Meldung: <?= e((string) $state['last_message']) ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (is_array($moduleStatusPanel)): ?>
|
<?php if (is_array($moduleStatusPanel)): ?>
|
||||||
<section class="setup-panel setup-panel--flat">
|
<section class="setup-panel setup-panel--flat">
|
||||||
<div class="setup-panel__head">
|
<div class="setup-panel__head">
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ if (str_starts_with($uriPath, 'modules/install')) {
|
|||||||
require_once $moduleBootstrap;
|
require_once $moduleBootstrap;
|
||||||
}
|
}
|
||||||
if ($modulePage) {
|
if ($modulePage) {
|
||||||
|
app()->modules()->runDueIntervalTasks($module);
|
||||||
$target = $modulePage;
|
$target = $modulePage;
|
||||||
} else {
|
} else {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ final class BaseSchema
|
|||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_module_task_runs (
|
||||||
|
module_name TEXT NOT NULL,
|
||||||
|
task_name TEXT NOT NULL,
|
||||||
|
last_started_at TIMESTAMPTZ NULL,
|
||||||
|
last_finished_at TIMESTAMPTZ NULL,
|
||||||
|
last_success_at TIMESTAMPTZ NULL,
|
||||||
|
last_status TEXT NULL,
|
||||||
|
last_message TEXT NULL,
|
||||||
|
lock_until TIMESTAMPTZ NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (module_name, task_name)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
$pdo->exec(
|
$pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
||||||
sub TEXT PRIMARY KEY,
|
sub TEXT PRIMARY KEY,
|
||||||
@@ -149,6 +164,21 @@ final class BaseSchema
|
|||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_module_task_runs (
|
||||||
|
module_name TEXT NOT NULL,
|
||||||
|
task_name TEXT NOT NULL,
|
||||||
|
last_started_at TEXT NULL,
|
||||||
|
last_finished_at TEXT NULL,
|
||||||
|
last_success_at TEXT NULL,
|
||||||
|
last_status TEXT NULL,
|
||||||
|
last_message TEXT NULL,
|
||||||
|
lock_until TEXT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
PRIMARY KEY (module_name, task_name)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
$pdo->exec(
|
$pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
||||||
sub TEXT PRIMARY KEY,
|
sub TEXT PRIMARY KEY,
|
||||||
@@ -237,6 +267,21 @@ final class BaseSchema
|
|||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_module_task_runs (
|
||||||
|
module_name VARCHAR(190) NOT NULL,
|
||||||
|
task_name VARCHAR(190) NOT NULL,
|
||||||
|
last_started_at DATETIME NULL,
|
||||||
|
last_finished_at DATETIME NULL,
|
||||||
|
last_success_at DATETIME NULL,
|
||||||
|
last_status VARCHAR(32) NULL,
|
||||||
|
last_message TEXT NULL,
|
||||||
|
lock_until DATETIME NULL,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (module_name, task_name)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
$pdo->exec(
|
$pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
"CREATE TABLE IF NOT EXISTS nexus_auth_users (
|
||||||
sub VARCHAR(190) PRIMARY KEY,
|
sub VARCHAR(190) PRIMARY KEY,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ final class ModuleManager
|
|||||||
{
|
{
|
||||||
private array $modules = [];
|
private array $modules = [];
|
||||||
private array $callbacks = [];
|
private array $callbacks = [];
|
||||||
|
private ?ModuleTaskScheduler $taskScheduler = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ?\PDO $basePdo,
|
private ?\PDO $basePdo,
|
||||||
@@ -243,6 +244,21 @@ final class ModuleManager
|
|||||||
return isset($this->callbacks[$module . ':' . $name]);
|
return isset($this->callbacks[$module . ':' . $name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function intervalTasks(string $name): array
|
||||||
|
{
|
||||||
|
return $this->taskScheduler()->definitions($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function intervalTaskStatuses(string $name): array
|
||||||
|
{
|
||||||
|
return $this->taskScheduler()->statuses($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runDueIntervalTasks(string $name): array
|
||||||
|
{
|
||||||
|
return $this->taskScheduler()->runDue($name);
|
||||||
|
}
|
||||||
|
|
||||||
private function scanModules(): void
|
private function scanModules(): void
|
||||||
{
|
{
|
||||||
$this->modules = [];
|
$this->modules = [];
|
||||||
@@ -274,6 +290,7 @@ final class ModuleManager
|
|||||||
'version' => $data['version'] ?? '',
|
'version' => $data['version'] ?? '',
|
||||||
'description' => $data['description'] ?? '',
|
'description' => $data['description'] ?? '',
|
||||||
'setup' => $data['setup'] ?? [],
|
'setup' => $data['setup'] ?? [],
|
||||||
|
'interval_tasks' => $data['interval_tasks'] ?? [],
|
||||||
'menu' => $data['menu'] ?? [],
|
'menu' => $data['menu'] ?? [],
|
||||||
'sidebar' => $data['sidebar'] ?? [],
|
'sidebar' => $data['sidebar'] ?? [],
|
||||||
'db_defaults' => $data['db_defaults'] ?? [],
|
'db_defaults' => $data['db_defaults'] ?? [],
|
||||||
@@ -470,4 +487,13 @@ final class ModuleManager
|
|||||||
|
|
||||||
return array_values(array_unique($normalized));
|
return array_values(array_unique($normalized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function taskScheduler(): ModuleTaskScheduler
|
||||||
|
{
|
||||||
|
if (!$this->taskScheduler instanceof ModuleTaskScheduler) {
|
||||||
|
$this->taskScheduler = new ModuleTaskScheduler($this, $this->basePdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->taskScheduler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
312
src/App/ModuleTaskScheduler.php
Normal file
312
src/App/ModuleTaskScheduler.php
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class ModuleTaskScheduler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ModuleManager $modules,
|
||||||
|
private ?\PDO $pdo
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function definitions(string $moduleName): array
|
||||||
|
{
|
||||||
|
$module = $this->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'] ?? $state['last_finished_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']);
|
||||||
|
$message = is_array($result) ? trim((string) ($result['message'] ?? '')) : '';
|
||||||
|
$finishedAt = gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'last_finished_at' => $finishedAt,
|
||||||
|
'last_status' => $ok ? 'success' : 'error',
|
||||||
|
'last_message' => $message,
|
||||||
|
'lock_until' => null,
|
||||||
|
];
|
||||||
|
if ($ok) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user