sadsad
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-11 02:03:32 +02:00
parent 0f8f9567fe
commit f46de880f4
11 changed files with 178 additions and 22 deletions

View File

@@ -2,4 +2,9 @@ Bitte PROJECT_CONTEXT.md lesen und strikt einhalten.
Wichtig: Modul-spezifischer Code/Assets ausschließlich unter /modules/<modul>/, Wichtig: Modul-spezifischer Code/Assets ausschließlich unter /modules/<modul>/,
keine Änderungen an /public/assets/* für modul-spezifische Features. keine Änderungen an /public/assets/* für modul-spezifische Features.
Staging/Live: /config/<env> im Repo wird nach /app/<env>/config kopiert. Staging/Live: /config/<env> im Repo wird nach /app/<env>/config kopiert.
Modul-Assets müssen über /module/<modul>/asset geladen werden. Modul-Assets müssen über /module/<modul>/asset geladen werden.
Setup-Regel: `Allgemein`, `Datenbank`, `Zugriffsrechte` und `Cron Einstellungen` kommen immer aus dem globalen Setup-System.
Nur `Custom Settings` darf modulspezifisch sein.
Für Zeitzonen und Debug nach Möglichkeit die globalen Helfer aus `src/App/functions.php` nutzen.
Global bereits im Setup vorhanden sind Navigation, Debug-Feld, DB-Logik, Zugriffsschutz, Cron-/Scheduler-Logik, Setup-Aktionen, Statusblöcke und Zeitzonen-Vererbung.
Ein neues Modul soll dafür nur noch `setup.fields`, optionale `scheduler_jobs` / `interval_tasks` und bei Bedarf `setup_actions` / `setup_status` liefern.

View File

@@ -84,17 +84,168 @@ Modulspezifische Assets:
- Mining-Checker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Karten-Boxen, Bereichs-Box - Mining-Checker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Karten-Boxen, Bereichs-Box
- Modulverwaltung: Seitenheader-Box, Submenü-Box, danach Karten-Boxen - Modulverwaltung: Seitenheader-Box, Submenü-Box, danach Karten-Boxen
8) Sicherheits-/Netzwerk-Constraints 8) Globales Setup-System (VERBINDLICH)
- Modul-Setup wird zentral über `partials/landingpages/modules/setup.php` gerendert.
- Die Bereiche
- `Allgemein`
- `Datenbank`
- `Zugriffsrechte`
- `Cron Einstellungen`
muessen fuer alle Module aus dieser gemeinsamen Setup-Logik kommen.
- Nur `Custom Settings` darf modulspezifischen Inhalt enthalten.
- Modul-spezifische Sonderlayouts fuer die Bereiche `Allgemein`, `Datenbank`, `Zugriffsrechte` oder `Cron Einstellungen` sind nicht erlaubt.
- Wenn sich das Verhalten eines dieser Bereiche ändert, muss die Änderung zentral erfolgen, so dass sie automatisch fuer alle Module gilt.
Was global im Setup bereits verfügbar ist:
- gemeinsame Setup-Navigation mit festen Unterseiten
- rechte Aktionsseite mit
- `Nexus Übersicht`
- `Zurück zum Modul`
- gemeinsames Speichern pro Bereich
- gemeinsames Rendering von
- Textfeldern
- Zahlenfeldern
- Checkboxen
- Selects
- Multiselects
- Textareas
- globale `Debug aktivieren`-Option pro Modul im Bereich `Allgemein`
- gemeinsame Datenbank-Logik mit
- `Eigene Modul-DB nutzen`
- Anzeige/Verbergen der Custom-DB-Felder
- optional mehreren DB-Gruppen wie `db.*` und `metadata_db.*`
- `Verbindung testen`
- `Standardwerte laden`
- gemeinsame Auth-/Zugriffslogik mit
- `Login erforderlich`
- erlaubten Benutzern
- erlaubten Gruppen
- bekannten Keycloak-Benutzern und -Gruppen
- gemeinsame Cron-/Scheduler-Logik mit
- Anzeige von Intervall-Tasks
- Anzeige von Cron-Jobs
- Bearbeiten von Cron-Einträgen im Modal
- Cron-Test direkt aus dem Setup
- Mehrfacheinträgen bei `mode = multi`
- Modul-Zeitzonen-Override fuer Crons
- Vererbung globaler Cron-Zeitzonen-Defaults
- gemeinsame Darstellung von Setup-Aktionen und Statusblöcken
- globale Zeitzonen-Datalist aus `nexus_timezones`
Was ein Modul für das Setup nur noch liefern muss:
- `module.json` mit `setup.fields`
- optional `setup.sections.database`
- optional `interval_tasks`
- optional `scheduler_jobs`
- optional `auth`
- optional Bootstrap-Funktionen:
- `setup_actions`
- `run_setup_action`
- `setup_status`
- `runtime_settings`
- `save_runtime_settings`
Was nicht mehr jedes Modul selbst bauen darf:
- eigene Setup-Seitenstruktur fuer `Allgemein`, `Datenbank`, `Zugriffsrechte`, `Cron Einstellungen`
- eigene DB-Toggle-Logik fuer Standard/Custom
- eigene Cron-Editor-Grundlogik
- eigene Debug-UI-Grundlogik
- eigene globale Zeitzonen-Defaults
Setup-Navigation:
- Setup-Routen laufen zentral ueber:
- `/modules/setup/<modul>/general`
- `/modules/setup/<modul>/database`
- `/modules/setup/<modul>/access`
- `/modules/setup/<modul>/cron`
- `/modules/setup/<modul>/custom`
- `Setup` gehoert in der Modulansicht in die rechte Aktionsseite der Submenü-Box.
Steuerung per `module.json`:
- Ein Modul kann ueber `setup.sections.database: true|false` steuern, ob der Menüpunkt `Datenbank` angezeigt wird.
- Wenn `setup.sections.database` fehlt, kann die zentrale Setup-Logik den Punkt implizit aktivieren, sobald DB-Felder vorhanden sind.
- Modulfelder fuer `Allgemein`, `Datenbank`, `Cron` und `Custom Settings` werden zentral nach Feldnamen aufgeteilt:
- `debug_enabled` -> `Allgemein`
- `use_separate_db` und `db.*` / `metadata_db.*` -> `Datenbank`
- `schedule_timezone` -> `Cron Einstellungen`
- alle übrigen Setup-Felder -> `Custom Settings`
Weitere anerkannte Setup-Bausteine:
- `interval_tasks`
- fuer automatische Aufgaben beim Modulaufruf
- `scheduler_jobs`
- fuer zentrale Cron-Jobs ueber den Nexus-Scheduler
- `auth`
- fuer den initialen Modulschutz
- `db_defaults`
- fuer vorbefuellte Standard-DB-Werte im Datenbankbereich
- `metadata_db_defaults`
- fuer weitere globale DB-Gruppen im Datenbankbereich
Speicherregel:
- Beim Speichern eines Setup-Bereichs duerfen nur die in diesem Bereich sichtbaren Felder aktualisiert werden.
- Felder aus anderen Bereichen duerfen nicht mit `null`, `0` oder leeren Strings ueberschrieben werden.
Datenbankbereich:
- `Eigene Modul-DB nutzen` ist der zentrale Standard-Schalter fuer Module mit optionaler eigener DB.
- Wenn der Schalter deaktiviert ist, duerfen keine Custom-DB-Eingabefelder sichtbar sein.
- Datenbankaktionen und Tabellenstatus gehoeren in den Menüpunkt `Datenbank`, nicht in `Custom Settings`.
9) Globale Zeitzonen-Logik (VERBINDLICH)
- Globale Nexus-Einstellungen liegen unter `/settings`.
- Dort werden zentral gepflegt:
- `Anzeige-Zeitzone`
- `Standard-Zeitzone für Crons`
- `Anzeige-Zeitzone`:
- hat eine `Custom`-Checkbox
- ohne Custom wird die System-Zeitzone verwendet
- die aktive Zeitzone soll angezeigt werden
- `Standard-Zeitzone für Crons`:
- ist der globale Default fuer Modul-Crons
- Module duerfen diese Zeitzone im Setup uebersteuern
- einzelne Cron-Einträge duerfen sie ebenfalls uebersteuern
Modul-Cron-Verhalten:
- Im Modul-Setup gibt es keinen nackten Pflicht-Default mehr, der immer direkt eingegeben werden muss.
- Stattdessen:
- Anzeige des aktuell wirksamen Modul-Cron-Defaults
- `Custom-Zeitzone verwenden` fuer Modul-Override
- einzelne Cron-Einträge koennen ebenfalls `Custom-Zeitzone verwenden`
- Ohne Override erbt ein Cron-Eintrag die Modul-Zeitzone, und die Modul-Zeitzone erbt den globalen Nexus-Cron-Default.
Globale PHP-Helfer:
- Neue Module sollen fuer zentrale Zeit-/Debug-Defaults nach Moeglichkeit die globalen Funktionen aus `src/App/functions.php` nutzen:
- `nexus_settings()`
- `nexus_save_settings(array $settings)`
- `nexus_system_timezone_name()`
- `nexus_display_timezone_name()`
- `nexus_cron_timezone_name()`
- `module_debug_enabled(string $module)`
- `module_debug_push(string $module, array $entry)`
- `module_debug_clear(string $module)`
Regel fuer neue Module:
- Keine Zeitzone wie `Europe/Berlin` hart im Modul als Standard erzwingen, wenn dafuer ein globaler Nexus-Default existiert.
- Fuer Anzeige-/Formatierungslogik nach Moeglichkeit `nexus_display_timezone_name()` nutzen.
- Fuer Cron-Fallbacks nach Moeglichkeit `nexus_cron_timezone_name()` nutzen.
10) Globales Debug-System
- Das Debug-Popup ist eine globale Infrastruktur aus dem zentralen Layout.
- Aktivierung bleibt jedoch pro Modul ueber `debug_enabled` im Modul-Setup.
- Das Debug-Symbol darf nur sichtbar sein, wenn fuer das aktuelle Modul Debug aktiv ist.
- Module sollen keine eigene separate Debug-Oberflaeche bauen, wenn der globale Debug-Stream genutzt werden kann.
11) Sicherheits-/Netzwerk-Constraints
- Zugriff im Heimnetz (192.168.178.0/24) per Nginx begrenzt. - Zugriff im Heimnetz (192.168.178.0/24) per Nginx begrenzt.
- SSH-Hosts nur Heimnetz. - SSH-Hosts nur Heimnetz.
9) Pi Control Besonderheiten (konkret) 12) Pi Control Besonderheiten (konkret)
- Worker/Jobs unter /tools/pi_control/ - Worker/Jobs unter /tools/pi_control/
- Check-Updates & Cron nutzen die gleichen SSH-Routinen. - Check-Updates & Cron nutzen die gleichen SSH-Routinen.
- Host-Karten, Befehle und Konsole sind UI im Modul. - Host-Karten, Befehle und Konsole sind UI im Modul.
- Update/Upgrade-Checks liefern Debug-Ausgaben, die als Tooltip oder Debugzeile angezeigt werden. - Update/Upgrade-Checks liefern Debug-Ausgaben, die als Tooltip oder Debugzeile angezeigt werden.
10) Zusammenfassung (kurz) 13) Zusammenfassung (kurz)
Nexus ist modular, mit strikter Trennung zwischen globalem Layout und modulspezifischem Code. Nexus ist modular, mit strikter Trennung zwischen globalem Layout und modulspezifischem Code.
Staging/Live haben eigene /app/<env>/config-Strukturen; /config/<env> im Repo wird beim Deployment kopiert. Staging/Live haben eigene /app/<env>/config-Strukturen; /config/<env> im Repo wird beim Deployment kopiert.
Modul-Assets gehören ausschließlich in den Modul-Ordner und werden dort geladen. Modul-Assets gehören ausschließlich in den Modul-Ordner und werden dort geladen.

View File

@@ -574,7 +574,7 @@ $mm->registerFunction($moduleName, 'format_datetime_for_display', static functio
}); });
$mm->registerFunction($moduleName, 'local_now_input_value', static function (): string { $mm->registerFunction($moduleName, 'local_now_input_value', static function (): string {
return (new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')))->format('Y-m-d\TH:i'); return (new \DateTimeImmutable('now', new \DateTimeZone(nexus_display_timezone_name())))->format('Y-m-d\TH:i');
}); });
$mm->registerFunction($moduleName, 'alpha_vantage_extract_global_quote', static function (array $entry): ?array { $mm->registerFunction($moduleName, 'alpha_vantage_extract_global_quote', static function (array $entry): ?array {

View File

@@ -103,7 +103,7 @@ if ($daily === []) {
$aggregate = static function (array $points, string $format): array { $aggregate = static function (array $points, string $format): array {
$result = []; $result = [];
$timezone = new DateTimeZone('Europe/Berlin'); $timezone = new DateTimeZone(nexus_display_timezone_name());
foreach ($points as $point) { foreach ($points as $point) {
$date = DateTimeImmutable::createFromFormat('Y-m-d', (string) ($point['date'] ?? ''), $timezone); $date = DateTimeImmutable::createFromFormat('Y-m-d', (string) ($point['date'] ?? ''), $timezone);
if (!$date instanceof DateTimeImmutable) { if (!$date instanceof DateTimeImmutable) {

View File

@@ -840,7 +840,7 @@ final class DashboardPage
private function normalizeDateTimeLocal(?string $value): string private function normalizeDateTimeLocal(?string $value): string
{ {
$timezone = new \DateTimeZone('Europe/Berlin'); $timezone = new \DateTimeZone(nexus_display_timezone_name());
$value = trim((string) $value); $value = trim((string) $value);
if ($value === '') { if ($value === '') {
return (new \DateTimeImmutable('now', $timezone))->format('Y-m-d H:i:s'); return (new \DateTimeImmutable('now', $timezone))->format('Y-m-d H:i:s');

View File

@@ -298,7 +298,7 @@ final class InstrumentPage
private function normalizeDateTimeLocal(?string $value): string private function normalizeDateTimeLocal(?string $value): string
{ {
$timezone = new \DateTimeZone('Europe/Berlin'); $timezone = new \DateTimeZone(nexus_display_timezone_name());
$value = trim((string) $value); $value = trim((string) $value);
if ($value === '') { if ($value === '') {
return (new \DateTimeImmutable('now', $timezone))->format('Y-m-d H:i:s'); return (new \DateTimeImmutable('now', $timezone))->format('Y-m-d H:i:s');

View File

@@ -68,7 +68,7 @@ $mm->registerFunction($moduleName, 'settings', static function (): array {
'preferred_currencies' => $preferredCurrencies, 'preferred_currencies' => $preferredCurrencies,
'currency_catalog' => $currencyCatalog, 'currency_catalog' => $currencyCatalog,
'currency_catalog_synced_at' => trim((string) ($saved['currency_catalog_synced_at'] ?? '')), 'currency_catalog_synced_at' => trim((string) ($saved['currency_catalog_synced_at'] ?? '')),
'schedule_timezone' => trim((string) ($saved['schedule_timezone'] ?? 'Europe/Berlin')) ?: 'Europe/Berlin', 'schedule_timezone' => trim((string) ($saved['schedule_timezone'] ?? nexus_cron_timezone_name())) ?: nexus_cron_timezone_name(),
]; ];
}); });

View File

@@ -991,11 +991,11 @@ final class FxRatesService
private function scheduleTimezone(): DateTimeZone private function scheduleTimezone(): DateTimeZone
{ {
$timezone = trim((string) ($this->settings['schedule_timezone'] ?? 'Europe/Berlin')); $timezone = trim((string) ($this->settings['schedule_timezone'] ?? nexus_cron_timezone_name()));
try { try {
return new DateTimeZone($timezone); return new DateTimeZone($timezone);
} catch (\Throwable) { } catch (\Throwable) {
return new DateTimeZone('Europe/Berlin'); return new DateTimeZone(nexus_cron_timezone_name());
} }
} }

View File

@@ -34,9 +34,9 @@ $mm->registerFunction($moduleName, 'runtime_settings', static function (): array
$repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub); $repository = new MiningRepository($pdo, $config->tablePrefix(), null, $ownerSub);
$settings = $repository->getSettings($projectKey) ?? []; $settings = $repository->getSettings($projectKey) ?? [];
$displayTimezone = trim((string) ($settings['display_timezone'] ?? 'Europe/Berlin')); $displayTimezone = trim((string) ($settings['display_timezone'] ?? nexus_display_timezone_name()));
if ($displayTimezone === '') { if ($displayTimezone === '') {
$displayTimezone = 'Europe/Berlin'; $displayTimezone = nexus_display_timezone_name();
} }
$baselineMeasuredAt = trim((string) ($settings['baseline_measured_at'] ?? '')); $baselineMeasuredAt = trim((string) ($settings['baseline_measured_at'] ?? ''));
@@ -72,9 +72,9 @@ $mm->registerFunction($moduleName, 'save_runtime_settings', static function (arr
$repository->ensureProject($projectKey, strtoupper(str_replace('-', ' ', $projectKey))); $repository->ensureProject($projectKey, strtoupper(str_replace('-', ' ', $projectKey)));
$existing = $repository->getSettings($projectKey) ?? []; $existing = $repository->getSettings($projectKey) ?? [];
$displayTimezone = trim((string) ($existing['display_timezone'] ?? 'Europe/Berlin')); $displayTimezone = trim((string) ($existing['display_timezone'] ?? nexus_display_timezone_name()));
if ($displayTimezone === '') { if ($displayTimezone === '') {
$displayTimezone = 'Europe/Berlin'; $displayTimezone = nexus_display_timezone_name();
} }
try { try {

View File

@@ -326,7 +326,7 @@ final class Router
'daily_cost_currency' => 'EUR', 'daily_cost_currency' => 'EUR',
'report_currency' => 'EUR', 'report_currency' => 'EUR',
'crypto_currency' => 'DOGE', 'crypto_currency' => 'DOGE',
'display_timezone' => 'Europe/Berlin', 'display_timezone' => nexus_display_timezone_name(),
'fx_max_age_hours' => self::FX_FETCH_MAX_AGE_HOURS, 'fx_max_age_hours' => self::FX_FETCH_MAX_AGE_HOURS,
'module_theme_mode' => 'inherit', 'module_theme_mode' => 'inherit',
'module_theme_accent' => 'teal', 'module_theme_accent' => 'teal',
@@ -525,7 +525,7 @@ final class Router
'daily_cost_currency' => $backup['settings']['daily_cost_currency'], 'daily_cost_currency' => $backup['settings']['daily_cost_currency'],
'report_currency' => $backup['settings']['report_currency'] ?? 'EUR', 'report_currency' => $backup['settings']['report_currency'] ?? 'EUR',
'crypto_currency' => $backup['settings']['crypto_currency'] ?? 'DOGE', 'crypto_currency' => $backup['settings']['crypto_currency'] ?? 'DOGE',
'display_timezone' => $backup['settings']['display_timezone'] ?? 'Europe/Berlin', 'display_timezone' => $backup['settings']['display_timezone'] ?? nexus_display_timezone_name(),
'fx_max_age_hours' => self::FX_FETCH_MAX_AGE_HOURS, 'fx_max_age_hours' => self::FX_FETCH_MAX_AGE_HOURS,
'module_theme_mode' => $backup['settings']['module_theme_mode'] ?? 'inherit', 'module_theme_mode' => $backup['settings']['module_theme_mode'] ?? 'inherit',
'module_theme_accent' => $backup['settings']['module_theme_accent'] ?? 'teal', 'module_theme_accent' => $backup['settings']['module_theme_accent'] ?? 'teal',
@@ -1158,13 +1158,13 @@ final class Router
'daily_cost_currency' => 'EUR', 'daily_cost_currency' => 'EUR',
'report_currency' => 'EUR', 'report_currency' => 'EUR',
'crypto_currency' => 'DOGE', 'crypto_currency' => 'DOGE',
'display_timezone' => 'Europe/Berlin', 'display_timezone' => nexus_display_timezone_name(),
'module_theme_mode' => 'inherit', 'module_theme_mode' => 'inherit',
'module_theme_accent' => 'teal', 'module_theme_accent' => 'teal',
'preferred_currencies' => $this->preferredCurrencies(), 'preferred_currencies' => $this->preferredCurrencies(),
]; ];
if (!$this->isValidTimezone((string) ($base['display_timezone'] ?? ''))) { if (!$this->isValidTimezone((string) ($base['display_timezone'] ?? ''))) {
$base['display_timezone'] = 'Europe/Berlin'; $base['display_timezone'] = nexus_display_timezone_name();
} }
if (!in_array((string) ($base['module_theme_mode'] ?? ''), ['inherit', 'custom'], true)) { if (!in_array((string) ($base['module_theme_mode'] ?? ''), ['inherit', 'custom'], true)) {
$base['module_theme_mode'] = 'inherit'; $base['module_theme_mode'] = 'inherit';
@@ -1187,7 +1187,7 @@ final class Router
{ {
$existingSettings = $this->repository()->getSettings($projectKey) ?? []; $existingSettings = $this->repository()->getSettings($projectKey) ?? [];
$displayTimezone = $this->requiredTimezone( $displayTimezone = $this->requiredTimezone(
$input['display_timezone'] ?? ($existingSettings['display_timezone'] ?? 'Europe/Berlin'), $input['display_timezone'] ?? ($existingSettings['display_timezone'] ?? nexus_display_timezone_name()),
'display_timezone' 'display_timezone'
); );
$settings = [ $settings = [
@@ -2069,7 +2069,7 @@ final class Router
{ {
$settings = $this->repository()->getSettings($projectKey); $settings = $this->repository()->getSettings($projectKey);
$timezone = is_array($settings) ? (string) ($settings['display_timezone'] ?? '') : ''; $timezone = is_array($settings) ? (string) ($settings['display_timezone'] ?? '') : '';
return $this->isValidTimezone($timezone) ? $timezone : 'Europe/Berlin'; return $this->isValidTimezone($timezone) ? $timezone : nexus_display_timezone_name();
} }
private function utcTimezone(): \DateTimeZone private function utcTimezone(): \DateTimeZone

View File

@@ -103,7 +103,7 @@ final class MiningRepository
'daily_cost_currency' => $settings['daily_cost_currency'], 'daily_cost_currency' => $settings['daily_cost_currency'],
'report_currency' => $settings['report_currency'] ?? 'EUR', 'report_currency' => $settings['report_currency'] ?? 'EUR',
'crypto_currency' => $settings['crypto_currency'] ?? 'DOGE', 'crypto_currency' => $settings['crypto_currency'] ?? 'DOGE',
'display_timezone' => $settings['display_timezone'] ?? 'Europe/Berlin', 'display_timezone' => $settings['display_timezone'] ?? nexus_display_timezone_name(),
'fx_max_age_hours' => $settings['fx_max_age_hours'] ?? 3, 'fx_max_age_hours' => $settings['fx_max_age_hours'] ?? 3,
'module_theme_mode' => $settings['module_theme_mode'] ?? 'inherit', 'module_theme_mode' => $settings['module_theme_mode'] ?? 'inherit',
'module_theme_accent' => $settings['module_theme_accent'] ?? 'teal', 'module_theme_accent' => $settings['module_theme_accent'] ?? 'teal',