Main update
All checks were successful
Deploy / deploy-staging (push) Successful in 25s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-24 02:24:39 +02:00
parent 5ff4c3fb2b
commit d121f74bd0
34 changed files with 2548 additions and 67 deletions

View File

@@ -436,6 +436,28 @@ namespace {
}
}
if (!function_exists('nexus_cron_timezone_name')) {
function nexus_cron_timezone_name(): string
{
$candidates = [
$_COOKIE['desktop_timezone'] ?? null,
$_SESSION['desktop_timezone'] ?? null,
getenv('DESKTOP_CRON_TIMEZONE') ?: null,
getenv('DESKTOP_TIMEZONE') ?: null,
'Europe/Berlin',
];
foreach ($candidates as $candidate) {
$timezone = trim((string) $candidate);
if ($timezone !== '' && in_array($timezone, \DateTimeZone::listIdentifiers(), true)) {
return $timezone;
}
}
return 'Europe/Berlin';
}
}
if (!function_exists('auth_enabled')) {
function auth_enabled(): bool
{

View File

@@ -8,6 +8,12 @@ final class ModuleRegistry
{
/** @var array<int, array<string, mixed>>|null */
private ?array $desktopApps = null;
/** @var array<int, array<string, mixed>>|null */
private ?array $widgets = null;
/** @var array<int, array<string, mixed>>|null */
private ?array $cronJobs = null;
/** @var array<string, array<string, mixed>>|null */
private ?array $manifests = null;
public function __construct(
private readonly string $modulesPath,
@@ -26,6 +32,12 @@ final class ModuleRegistry
$apps = [];
foreach ($this->moduleDirectories() as $moduleName => $modulePath) {
$manifest = $this->manifest($moduleName, $modulePath);
$desktopMeta = is_array($manifest['desktop'] ?? null) ? $manifest['desktop'] : [];
if (array_key_exists('available', $desktopMeta) && !$desktopMeta['available']) {
continue;
}
$desktopConfigPath = $modulePath . '/desktop.php';
if (!is_file($desktopConfigPath)) {
continue;
@@ -36,7 +48,6 @@ final class ModuleRegistry
continue;
}
$manifest = $this->readJson($modulePath . '/module.json');
$desktopConfig['module_id'] = $moduleName;
$desktopConfig['title'] ??= (string) ($manifest['title'] ?? ucfirst($moduleName));
$desktopConfig['summary'] ??= (string) ($manifest['description'] ?? '');
@@ -45,12 +56,67 @@ final class ModuleRegistry
(array) ($manifest['auth']['groups'] ?? [])
));
$desktopConfig['enabled_by_default'] ??= (bool) ($manifest['enabled_by_default'] ?? false);
$desktopConfig['app_scope'] ??= (string) ($manifest['app_scope'] ?? 'module');
$desktopConfig['installable'] ??= array_key_exists('installable', $manifest) ? (bool) $manifest['installable'] : true;
$desktopConfig['show_on_desktop'] ??= !array_key_exists('show_on_desktop', $desktopMeta) || (bool) $desktopMeta['show_on_desktop'];
$desktopConfig['show_in_start_menu'] ??= !array_key_exists('show_in_start_menu', $desktopMeta) || (bool) $desktopMeta['show_in_start_menu'];
$desktopConfig['supports_widget'] ??= $this->moduleWidgetsForManifest($moduleName, $manifest, $desktopConfig) !== [];
$desktopConfig['supports_cron'] ??= $this->moduleCronJobsForManifest($moduleName, $manifest, $desktopConfig) !== [];
$apps[] = $desktopConfig;
}
return $this->desktopApps = $apps;
}
/**
* @return array<int, array<string, mixed>>
*/
public function widgets(): array
{
if ($this->widgets !== null) {
return $this->widgets;
}
$widgets = [];
foreach ($this->moduleDirectories() as $moduleName => $modulePath) {
$manifest = $this->manifest($moduleName, $modulePath);
foreach ($this->moduleWidgetsForManifest($moduleName, $manifest, null) as $widget) {
$widgets[] = $widget;
}
}
return $this->widgets = $widgets;
}
/**
* @return array<int, array<string, mixed>>
*/
public function cronJobs(): array
{
if ($this->cronJobs !== null) {
return $this->cronJobs;
}
$jobs = [];
foreach ($this->moduleDirectories() as $moduleName => $modulePath) {
$manifest = $this->manifest($moduleName, $modulePath);
$desktopConfig = null;
$desktopConfigPath = $modulePath . '/desktop.php';
if (is_file($desktopConfigPath)) {
$loaded = require $desktopConfigPath;
if (is_array($loaded)) {
$desktopConfig = $loaded;
}
}
foreach ($this->moduleCronJobsForManifest($moduleName, $manifest, $desktopConfig) as $job) {
$jobs[] = $job;
}
}
return $this->cronJobs = $jobs;
}
public function modulePath(string $moduleName): ?string
{
$directories = $this->moduleDirectories();
@@ -84,6 +150,108 @@ final class ModuleRegistry
return $directories;
}
/**
* @return array<string, mixed>
*/
private function manifest(string $moduleName, string $modulePath): array
{
if ($this->manifests === null) {
$this->manifests = [];
}
if (!array_key_exists($moduleName, $this->manifests)) {
$this->manifests[$moduleName] = $this->readJson($modulePath . '/module.json');
}
return $this->manifests[$moduleName];
}
/**
* @param array<string, mixed> $manifest
* @param array<string, mixed>|null $desktopConfig
* @return array<int, array<string, mixed>>
*/
private function moduleWidgetsForManifest(string $moduleName, array $manifest, ?array $desktopConfig): array
{
$appId = (string) (($desktopConfig['app_id'] ?? null) ?: ($manifest['app_id'] ?? $moduleName));
$widgets = [];
foreach ((array) ($manifest['widgets'] ?? []) as $widget) {
if (!is_array($widget)) {
continue;
}
$widgetId = trim((string) ($widget['widget_id'] ?? ''));
$title = trim((string) ($widget['title'] ?? ''));
if ($widgetId === '' || $title === '') {
continue;
}
$widgets[] = [
'widget_id' => $widgetId,
'title' => $title,
'icon' => (string) ($widget['icon'] ?? strtoupper(substr($appId, 0, 2))),
'zone' => (string) ($widget['zone'] ?? 'sidebar'),
'default_enabled' => (bool) ($widget['default_enabled'] ?? false),
'supports_public_home' => (bool) ($widget['supports_public_home'] ?? false),
'summary' => (string) ($widget['summary'] ?? ''),
'content' => (string) ($widget['content'] ?? ''),
'launch_app_id' => (string) ($widget['launch_app_id'] ?? $appId),
'action_label' => (string) ($widget['action_label'] ?? 'Oeffnen'),
'widget_type' => (string) ($widget['widget_type'] ?? 'default'),
'status_api' => isset($widget['status_api']) ? (string) $widget['status_api'] : null,
'refresh_api' => isset($widget['refresh_api']) ? (string) $widget['refresh_api'] : null,
'source_app_id' => $appId,
'source_module_id' => $moduleName,
'actions' => is_array($widget['actions'] ?? null) ? array_values($widget['actions']) : [],
];
}
return $widgets;
}
/**
* @param array<string, mixed> $manifest
* @param array<string, mixed>|null $desktopConfig
* @return array<int, array<string, mixed>>
*/
private function moduleCronJobsForManifest(string $moduleName, array $manifest, ?array $desktopConfig): array
{
$appId = (string) (($desktopConfig['app_id'] ?? null) ?: ($manifest['app_id'] ?? $moduleName));
$jobs = [];
foreach ((array) ($manifest['cron_jobs'] ?? []) as $job) {
if (!is_array($job)) {
continue;
}
$jobId = trim((string) ($job['job_id'] ?? $job['name'] ?? ''));
$endpointPath = trim((string) ($job['endpoint_path'] ?? ''));
if ($jobId === '' || $endpointPath === '') {
continue;
}
$jobs[] = [
'job_key' => $moduleName . '.' . $jobId,
'job_id' => $jobId,
'module_id' => $moduleName,
'app_id' => $appId,
'app_scope' => (string) ($manifest['app_scope'] ?? 'module'),
'label' => trim((string) ($job['label'] ?? $jobId)),
'description' => trim((string) ($job['description'] ?? '')),
'endpoint_path' => $endpointPath,
'method' => strtoupper(trim((string) ($job['method'] ?? 'POST'))) ?: 'POST',
'payload' => is_array($job['payload'] ?? null) ? $job['payload'] : [],
'default_enabled' => (bool) ($job['default_enabled'] ?? false),
'default_cron' => trim((string) ($job['default_cron'] ?? '0 * * * *')) ?: '0 * * * *',
'default_timezone' => trim((string) ($job['default_timezone'] ?? '')) ?: 'Europe/Berlin',
'lock_minutes' => max(1, (int) ($job['lock_minutes'] ?? 10)),
];
}
return $jobs;
}
/**
* @return array<string, mixed>
*/