Compare commits
2 Commits
27026533ac
...
3ed4fba58c
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ed4fba58c | |||
| 52158ef041 |
@@ -1,4 +1,5 @@
|
|||||||
Bitte PROJECT_CONTEXT.md lesen und strikt einhalten.
|
Bitte `PROJECT_CONTEXT.md` lesen und strikt einhalten.
|
||||||
|
Für Modul-Arbeiten zusätzlich immer `MODULE_DEVELOPMENT.md` lesen und beachten.
|
||||||
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.
|
||||||
|
|||||||
154
MODULE_DEVELOPMENT.md
Normal file
154
MODULE_DEVELOPMENT.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
Projektkontext: Modul-Entwicklung
|
||||||
|
|
||||||
|
1) Modul-Konzept
|
||||||
|
- Jedes klassische Modul lebt unter `/modules/<modulname>/`.
|
||||||
|
- Pflicht-Dateien sind in der Regel:
|
||||||
|
- `module.json`
|
||||||
|
- `bootstrap.php`
|
||||||
|
- `pages/*.php`
|
||||||
|
- `assets/*`
|
||||||
|
|
||||||
|
2) Modulspezifische Assets
|
||||||
|
- Modul-JS und Modul-CSS müssen im Modul-Ordner liegen.
|
||||||
|
- Laden über Modul-Assets, zum Beispiel:
|
||||||
|
- `$assets->addStyle('/module/pi_control/asset?file=pi_control.css');`
|
||||||
|
- `$assets->addScript('/module/pi_control/asset?file=hosts.js', 'footer', true);`
|
||||||
|
- Keine modulspezifischen Änderungen in `/public/assets/*`.
|
||||||
|
|
||||||
|
3) Scope-Regeln
|
||||||
|
- Modul-Aufgaben: nur `/modules/<modul>/` und gegebenenfalls `/tools/<modul>` ändern
|
||||||
|
- globale Layouts: `/partials/structure/` und `/public/assets/css/app.css`
|
||||||
|
- Konfigurationslogik nur bei echtem Globalbedarf in `/config/` oder `/src/`
|
||||||
|
|
||||||
|
4) UI-Regeln für Module
|
||||||
|
- Modulseiten sollen diesem Muster folgen:
|
||||||
|
- Seitenheader-Box
|
||||||
|
- Submenü-Box
|
||||||
|
- danach Bereichs-Boxen und/oder Karten-Boxen
|
||||||
|
- `Setup` gehört in Modulen grundsätzlich in die Submenü-Box
|
||||||
|
- die Optik von Submenü-Aktionen kommt ausschließlich aus dem globalen CSS
|
||||||
|
- Module dürfen dort keine eigenen Farb- oder Variantenlogiken einschleusen
|
||||||
|
|
||||||
|
5) Globales Setup-System
|
||||||
|
- Modul-Setup wird zentral über `partials/landingpages/modules/setup.php` gerendert.
|
||||||
|
- Die Bereiche
|
||||||
|
- `Allgemein`
|
||||||
|
- `Datenbank`
|
||||||
|
- `Zugriffsrechte`
|
||||||
|
- `Cron Einstellungen`
|
||||||
|
müssen für alle Module aus dieser gemeinsamen Setup-Logik kommen.
|
||||||
|
- Nur `Custom Settings` darf modulspezifischen Inhalt enthalten.
|
||||||
|
- Modul-spezifische Sonderlayouts für die Bereiche `Allgemein`, `Datenbank`, `Zugriffsrechte` oder `Cron Einstellungen` sind nicht erlaubt.
|
||||||
|
|
||||||
|
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 oder Verbergen der Custom-DB-Felder
|
||||||
|
- optional mehreren DB-Gruppen wie `db.*` und `metadata_db.*`
|
||||||
|
- `Verbindung testen`
|
||||||
|
- `Standardwerte laden`
|
||||||
|
- gemeinsame Auth- und Zugriffslogik mit
|
||||||
|
- `Login erforderlich`
|
||||||
|
- erlaubten Benutzern
|
||||||
|
- erlaubten Gruppen
|
||||||
|
- bekannten Keycloak-Benutzern und -Gruppen
|
||||||
|
- gemeinsame Cron- und 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 für Crons
|
||||||
|
- Vererbung globaler Cron-Zeitzonen-Defaults
|
||||||
|
- gemeinsame Darstellung von Setup-Aktionen und Statusblöcken
|
||||||
|
- globale Zeitzonen-Datalist aus `nexus_timezones`
|
||||||
|
|
||||||
|
6) Was ein Modul für das Setup liefern darf
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
7) Was ein Modul nicht selbst bauen darf
|
||||||
|
- eigene Setup-Seitenstruktur für `Allgemein`, `Datenbank`, `Zugriffsrechte`, `Cron Einstellungen`
|
||||||
|
- eigene DB-Toggle-Logik für Standard und Custom
|
||||||
|
- eigene Cron-Editor-Grundlogik
|
||||||
|
- eigene Debug-UI-Grundlogik
|
||||||
|
- eigene globale Zeitzonen-Defaults
|
||||||
|
|
||||||
|
8) Setup-Navigation
|
||||||
|
- Setup-Routen laufen zentral über:
|
||||||
|
- `/modules/setup/<modul>/general`
|
||||||
|
- `/modules/setup/<modul>/database`
|
||||||
|
- `/modules/setup/<modul>/access`
|
||||||
|
- `/modules/setup/<modul>/cron`
|
||||||
|
- `/modules/setup/<modul>/custom`
|
||||||
|
- `Setup` gehört in der Modulansicht in die rechte Aktionsseite der Submenü-Box
|
||||||
|
|
||||||
|
9) Steuerung per `module.json`
|
||||||
|
- Ein Modul kann über `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 für `Allgemein`, `Datenbank`, `Cron` und `Custom Settings` werden zentral nach Feldnamen aufgeteilt:
|
||||||
|
- `debug_enabled` -> `Allgemein`
|
||||||
|
- `use_separate_db` und `db.*` oder `metadata_db.*` -> `Datenbank`
|
||||||
|
- `schedule_timezone` -> `Cron Einstellungen`
|
||||||
|
- alle übrigen Setup-Felder -> `Custom Settings`
|
||||||
|
|
||||||
|
Weitere anerkannte Setup-Bausteine:
|
||||||
|
- `interval_tasks`
|
||||||
|
- `scheduler_jobs`
|
||||||
|
- `auth`
|
||||||
|
- `db_defaults`
|
||||||
|
- `metadata_db_defaults`
|
||||||
|
|
||||||
|
10) Speicherregel
|
||||||
|
- Beim Speichern eines Setup-Bereichs dürfen nur die in diesem Bereich sichtbaren Felder aktualisiert werden.
|
||||||
|
- Felder aus anderen Bereichen dürfen nicht mit `null`, `0` oder leeren Strings überschrieben werden.
|
||||||
|
|
||||||
|
11) Datenbankbereich
|
||||||
|
- `Eigene Modul-DB nutzen` ist der zentrale Standard-Schalter für Module mit optionaler eigener DB.
|
||||||
|
- Wenn der Schalter deaktiviert ist, dürfen keine Custom-DB-Eingabefelder sichtbar sein.
|
||||||
|
- Datenbankaktionen und Tabellenstatus gehören in den Menüpunkt `Datenbank`, nicht in `Custom Settings`.
|
||||||
|
|
||||||
|
12) Globale PHP-Helfer für Module
|
||||||
|
- Neue Module sollen für zentrale Zeit- und Debug-Defaults nach Möglichkeit 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)`
|
||||||
|
|
||||||
|
13) Regeln für neue Module
|
||||||
|
- Keine Zeitzone wie `Europe/Berlin` hart im Modul als Standard erzwingen, wenn dafür ein globaler Nexus-Default existiert.
|
||||||
|
- Für Anzeige- und Formatierungslogik nach Möglichkeit `nexus_display_timezone_name()` nutzen.
|
||||||
|
- Für Cron-Fallbacks nach Möglichkeit `nexus_cron_timezone_name()` nutzen.
|
||||||
|
- Neue Module dürfen keine lokalen Zeitzonen direkt in Datenbank-Zeitspalten persistieren.
|
||||||
|
|
||||||
|
14) Pi-Control-Besonderheiten
|
||||||
|
- Worker und Jobs unter `/tools/pi_control/`
|
||||||
|
- Check-Updates und Cron nutzen die gleichen SSH-Routinen
|
||||||
|
- Host-Karten, Befehle und Konsole sind UI im Modul
|
||||||
|
- Update- und Upgrade-Checks liefern Debug-Ausgaben, die als Tooltip oder Debugzeile angezeigt werden
|
||||||
119
NEXUS_SYSTEM.md
Normal file
119
NEXUS_SYSTEM.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
Projektkontext: Nexus-System
|
||||||
|
|
||||||
|
1) Projekt-Zweck und Ziel
|
||||||
|
- Nexus ist ein zentrales, webbasiertes Admin-Interface zur Steuerung einer dynamischen IT-Infrastruktur.
|
||||||
|
- Module kapseln fachliche Funktionen, das Nexus-System stellt das globale Grundgerüst bereit.
|
||||||
|
- Das generelle Nexus-System wird unabhängig von den bestehenden Fachmodulen weiterentwickelt.
|
||||||
|
|
||||||
|
2) Umgebungen und Domains
|
||||||
|
- Live: `nexus.kusche.berlin`
|
||||||
|
- Staging: `staging.nexus.kusche.berlin`
|
||||||
|
|
||||||
|
Container- und Deploy-Layout:
|
||||||
|
- `/app/live/` -> Live-Code
|
||||||
|
- `/app/staging/` -> Staging-Code
|
||||||
|
- jeweils mit eigenem `config`-Unterordner:
|
||||||
|
- `/app/live/config/`
|
||||||
|
- `/app/staging/config/`
|
||||||
|
|
||||||
|
Repo-Layout zu Configs:
|
||||||
|
- `/config/live/` und `/config/staging/` liegen im Repo
|
||||||
|
- beim Deployment werden die Dateien daraus nach `/app/<env>/config` kopiert
|
||||||
|
- wichtig: im laufenden Container existiert `/app/config/` nicht, sondern nur `/app/live/config` und `/app/staging/config`
|
||||||
|
|
||||||
|
3) Verzeichnisstruktur
|
||||||
|
- `/public/` -> Web Root mit globalen Assets
|
||||||
|
- `/api/` -> Backend- und API-Endpunkte
|
||||||
|
- `/src/` -> PHP-Kernklassen und Utilities
|
||||||
|
- `/tools/` -> CLI, Worker und Jobs
|
||||||
|
- `/config/` -> Umgebungs-Configs
|
||||||
|
- `/modules/<modul>/` -> klassische Nexus-Module
|
||||||
|
- `/partials/structure/` -> Header, Footer, Menüs
|
||||||
|
- `/partials/landingpages/` -> komplette Seitenlayouts
|
||||||
|
- `/debug/` -> Custom Logs
|
||||||
|
|
||||||
|
4) Code-Änderungen nach Scope
|
||||||
|
- globale Layouts: `/partials/structure/` und `/public/assets/css/app.css`
|
||||||
|
- Konfigurationslogik: nur wenn nötig `/config/` und `/src/`
|
||||||
|
- Modul-Aufgaben: nur `/modules/<modul>/` und gegebenenfalls `/tools/<modul>`
|
||||||
|
|
||||||
|
5) UI-Naming und Seitenaufbau
|
||||||
|
- `Seitenheader-Box`: oberste globale Header-Box mit Seitentitel, Login und Farbschema
|
||||||
|
- `Submenü-Box`: Box direkt unter der Seitenheader-Box für modul- oder seitenbezogene Aktionen
|
||||||
|
- `Submenü-Aktionen`: rechtsbündige Zusatzbuttons innerhalb der Submenü-Box, z.B. `Setup` oder `Nexus Übersicht`
|
||||||
|
- `Bereichs-Box`: größere Inhaltsbox für einen zusammenhängenden Seitenbereich
|
||||||
|
- `Karten-Box`: kleinere Karte auf derselben Ebene wie Bereichs-Boxen, meist in Grids
|
||||||
|
|
||||||
|
Zentrale CSS-Klassen:
|
||||||
|
- `main-header-box`
|
||||||
|
- `submenu-box`
|
||||||
|
- `module-submenu-actions`
|
||||||
|
- `section-box`
|
||||||
|
- `card-box`
|
||||||
|
|
||||||
|
Globale Struktur:
|
||||||
|
- zuerst Seitenheader-Box
|
||||||
|
- danach Submenü-Box
|
||||||
|
- danach Bereichs-Boxen und/oder Karten-Boxen je nach Seite
|
||||||
|
|
||||||
|
Layout-Regeln:
|
||||||
|
- vertikale Abstände zwischen `main-header-box`, `submenu-box` und den ersten Folge-Boxen müssen aus der globalen Shell kommen
|
||||||
|
- maßgeblich sind `module-page-bg` und `module-page-stack` in `public/assets/css/app.css`
|
||||||
|
- Top-Level-Wrapper wie Grids, Kartencontainer oder Modul-Listen dürfen keinen zusätzlichen `margin-top` oder Sonder-Gap erzeugen, der den Abstand nach dem Submenü verändert
|
||||||
|
- bei Layout-Reviews ist explizit zu prüfen, ob `Main-Header -> Submenü -> erste Section/Card` optisch denselben Rhythmus hat wie auf Referenzseiten
|
||||||
|
- die Optik der Submenü-Aktionsbuttons kommt ausschließlich aus dem globalen CSS
|
||||||
|
|
||||||
|
Beispielstruktur:
|
||||||
|
- Börsenchecker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Bereichs-Box
|
||||||
|
- FX-Rates: Seitenheader-Box, Submenü-Box, danach Bereichs-Boxen
|
||||||
|
- Mining-Checker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Karten-Boxen, Bereichs-Box
|
||||||
|
- Modulverwaltung: Seitenheader-Box, Submenü-Box, danach Karten-Boxen
|
||||||
|
|
||||||
|
6) Nexus-Kerngerüst
|
||||||
|
- Der aktuelle Ausbauschritt betrifft das generelle Nexus-System, nicht die bestehenden Fachmodule.
|
||||||
|
- Bestehende Module sind funktional und strukturell zunächst ausgenommen und dürfen durch Arbeiten am Kerngerüst nicht beeinträchtigt werden.
|
||||||
|
- Neue Kernfunktionen müssen parallel zum bestehenden Modulsystem eingeführt werden.
|
||||||
|
- Zielbild ist ein Nexus-Grundsystem mit flexiblen Benutzer-Dashboards, Integrationen und datengetriebenen Seitenmodulen.
|
||||||
|
|
||||||
|
Produktprinzip:
|
||||||
|
- Nexus ist nicht nur Modul-Launcher, sondern ein persönliches und gruppenfähiges Dashboard-System.
|
||||||
|
- Benutzer sollen eigene Dashboards anlegen, anordnen und konfigurieren können.
|
||||||
|
- Inhalte auf Dashboards sollen aus drei Quellen kommen können:
|
||||||
|
- interne Nexus-Funktionen
|
||||||
|
- externe Integrationen
|
||||||
|
- einfache Seitenmodule ohne eigene Modul-Implementierung
|
||||||
|
|
||||||
|
Abgrenzung zu bestehenden Modulen:
|
||||||
|
- das Verzeichnis `/modules/<modul>/` bleibt das Zuhause klassischer Nexus-Module
|
||||||
|
- das Dashboard-System ist ein zusätzliches globales Kernsystem
|
||||||
|
- neue Kernfunktionen müssen ohne Umbau bestehender Module lauffähig sein
|
||||||
|
|
||||||
|
7) Globale Zeitzonen-Logik
|
||||||
|
- globale Nexus-Einstellungen liegen unter `/settings`
|
||||||
|
- dort werden zentral gepflegt:
|
||||||
|
- `Anzeige-Zeitzone`
|
||||||
|
- `Standard-Zeitzone für Crons`
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- ohne Custom bei der Anzeige-Zeitzone wird die System-Zeitzone verwendet
|
||||||
|
- die aktive Anzeige-Zeitzone soll angezeigt werden
|
||||||
|
- die Standard-Zeitzone für Crons ist der globale Default für Modul-Crons
|
||||||
|
- Module dürfen diese Zeitzone im Setup übersteuern
|
||||||
|
- einzelne Cron-Einträge dürfen sie ebenfalls übersteuern
|
||||||
|
|
||||||
|
UTC-Speicherregel:
|
||||||
|
- Zeitwerte sollen projektweit intern immer in `UTC` gespeichert werden
|
||||||
|
- Anzeige-Zeitzonen dienen nur der Darstellung für Benutzer
|
||||||
|
- Cron-Zeitzonen dienen nur der lokalen Auswertung von Zeitplänen und Fälligkeiten
|
||||||
|
- beim Einlesen lokaler Eingaben muss vor dem Speichern nach `UTC` normalisiert werden
|
||||||
|
- beim Anzeigen gespeicherter Werte muss von `UTC` in die jeweils wirksame Anzeige-Zeitzone umgerechnet werden
|
||||||
|
|
||||||
|
8) Globales Debug-System
|
||||||
|
- das Debug-Popup ist eine globale Infrastruktur aus dem zentralen Layout
|
||||||
|
- die Aktivierung bleibt pro Modul über `debug_enabled` im Modul-Setup steuerbar
|
||||||
|
- das Debug-Symbol darf nur sichtbar sein, wenn für das aktuelle Modul Debug aktiv ist
|
||||||
|
- Module sollen keine eigene separate Debug-Oberfläche bauen, wenn der globale Debug-Stream genutzt werden kann
|
||||||
|
|
||||||
|
9) Sicherheits- und Netzwerk-Constraints
|
||||||
|
- Zugriff im Heimnetz `192.168.178.0/24` per Nginx begrenzt
|
||||||
|
- SSH-Hosts nur Heimnetz
|
||||||
@@ -1,398 +1,41 @@
|
|||||||
Projekt-Zusammenfassung: Nexus Control Panel
|
Projekt-Zusammenfassung: Nexus Control Panel
|
||||||
|
|
||||||
1) Projekt-Zweck & Ziel
|
Diese Datei ist ab jetzt der zentrale Einstieg und verweist auf die drei maßgeblichen Dokumentationsbereiche.
|
||||||
Nexus ist ein zentrales, webbasiertes Admin-Interface zur Steuerung einer dynamischen IT-Infrastruktur.
|
|
||||||
Module kapseln fachliche Funktionen (z.B. KEA DHCP, Pi Control).
|
|
||||||
|
|
||||||
2) Aktive Module (Kurzüberblick)
|
1) Nexus-System
|
||||||
- KEA DHCP: Verwaltung von Hosts/Leases, Statische Reservierungen, Metadaten.
|
- Datei: [NEXUS_SYSTEM.md](NEXUS_SYSTEM.md)
|
||||||
- Pi Control: Verwaltung von SSH-Hosts, Befehle/Preset, Konsole, Host-Status, Update/Upgrade-Checks.
|
- Inhalt:
|
||||||
|
- globales Grundgerüst von Nexus
|
||||||
|
- Umgebungen, Verzeichnisstruktur und globale Scope-Regeln
|
||||||
|
- UI-Naming mit Seitenheader-Box, Submenü-Box, Bereichs-Box und Karten-Box
|
||||||
|
- globale Zeitzonen- und Debug-Regeln
|
||||||
|
- Kerngerüst für das zukünftige Dashboard-System
|
||||||
|
|
||||||
3) Umgebungen & Domains
|
2) Modul-Entwicklung
|
||||||
- Live: nexus.kusche.berlin
|
- Datei: [MODULE_DEVELOPMENT.md](MODULE_DEVELOPMENT.md)
|
||||||
- Staging: staging.nexus.kusche.berlin
|
- Inhalt:
|
||||||
|
- Regeln für klassische Module unter `/modules/<modul>/`
|
||||||
|
- Setup-System und zulässige Modulbausteine
|
||||||
|
- globale Vorgaben für Modul-Layouts, Assets und Helper
|
||||||
|
- Abgrenzung zwischen Modul-Code und globalem Nexus-System
|
||||||
|
|
||||||
Container/Deploy-Layout:
|
3) Widget-Einbindung und Integrationen
|
||||||
- /app/live/ -> Live-Code
|
- Datei: [WIDGET_INTEGRATION.md](WIDGET_INTEGRATION.md)
|
||||||
- /app/staging/ -> Staging-Code
|
- Inhalt:
|
||||||
- Jeweils mit eigenem /config-Unterordner:
|
- Benutzer-Dashboards
|
||||||
- /app/live/config/
|
- Dashboard-Elemente und Widget-Typen
|
||||||
- /app/staging/config/
|
- Integrationen zu Fremdsystemen
|
||||||
|
- on-the-fly Seitenmodule
|
||||||
|
- Kollisionsschutz zu bestehenden Modulen
|
||||||
|
- empfohlene Umsetzungsphasen
|
||||||
|
|
||||||
Repo-Layout zu Configs:
|
4) Wichtige Leitplanken
|
||||||
- /config/live/ und /config/staging/ liegen im Repo.
|
- Änderungen am generellen Nexus-System dürfen nicht in bestehende Module eingreifen, wenn das nicht ausdrücklich verlangt ist.
|
||||||
- Beim Deployment werden die Dateien daraus nach /app/<env>/config kopiert.
|
- Klassische Fachmodule und das neue Dashboard- oder Widget-System sind getrennte Ebenen.
|
||||||
- WICHTIG: Im laufenden Container existiert /app/config/ NICHT, sondern nur /app/live/config und /app/staging/config.
|
- Modulspezifische Assets gehören weiterhin ausschließlich in den jeweiligen Modulordner.
|
||||||
|
- Globale Layout- und Designregeln bleiben zentral in `public/assets/css/app.css` und den globalen Partials.
|
||||||
|
|
||||||
4) Verzeichnisstruktur (Repo)
|
5) Für neue Chats und Arbeitsaufträge
|
||||||
- /public/ -> Web Root (index.php, globale Assets)
|
- Für globale Nexus-Themen zuerst `NEXUS_SYSTEM.md` lesen.
|
||||||
- /api/ -> Backend/API-Endpunkte
|
- Für Arbeiten an klassischen Modulen zuerst `MODULE_DEVELOPMENT.md` lesen.
|
||||||
- /src/ -> PHP-Kernklassen/Utilities
|
- Für Dashboard-, Widget- oder Integrationsarbeiten zuerst `WIDGET_INTEGRATION.md` lesen.
|
||||||
- /tools/ -> CLI/Worker/Jobs (z.B. tools/pi_control)
|
|
||||||
- /config/ -> Umgebungs-Configs (live/staging)
|
|
||||||
- /modules/<modul>/ -> Module (Pages, Assets, Bootstrap)
|
|
||||||
- /partials/structure/ -> Header/Footer/Menüs
|
|
||||||
- /partials/landingpages/ -> Komplette Seitenlayouts
|
|
||||||
- /debug/ -> Custom Logs
|
|
||||||
|
|
||||||
5) Modul-Konzept (WICHTIG)
|
|
||||||
- Jedes Modul lebt unter /modules/<modulname>/.
|
|
||||||
- Pflicht-Dateien i.d.R.:
|
|
||||||
- module.json
|
|
||||||
- bootstrap.php (Schema/Setup)
|
|
||||||
- pages/*.php (UI/Endpoints)
|
|
||||||
- assets/* (modulspezifisches CSS/JS)
|
|
||||||
|
|
||||||
Modulspezifische Assets:
|
|
||||||
- Modul-JS/CSS MUSS im Modul-Ordner liegen.
|
|
||||||
- Laden über Modul-Assets, z.B.:
|
|
||||||
- $assets->addStyle('/module/pi_control/asset?file=pi_control.css');
|
|
||||||
- $assets->addScript('/module/pi_control/asset?file=hosts.js', 'footer', true);
|
|
||||||
- KEINE modulspezifischen Änderungen in /public/assets/*.
|
|
||||||
|
|
||||||
6) Code-Änderungen nach Scope
|
|
||||||
- Modul-Aufgaben: nur /modules/<modul>/ + ggf. /tools/<modul> ändern.
|
|
||||||
- Globale Layouts: /partials/structure und /public/assets/css/app.css.
|
|
||||||
- Konfigurationslogik: nur wenn nötig /config/ und /src/.
|
|
||||||
|
|
||||||
7) UI-Naming und Seitenaufbau
|
|
||||||
- `Seitenheader-Box`: oberste globale Header-Box mit Seitentitel, Login und Farbschema.
|
|
||||||
- `Submenü-Box`: Box direkt unter der Seitenheader-Box für modul- oder seitenbezogene Aktionen.
|
|
||||||
- `Submenü-Aktionen`: rechtsbündige Zusatzbuttons innerhalb der Submenü-Box, z.B. `Setup`, `Nexus Übersicht` oder `Zur Startseite`-Ersatz.
|
|
||||||
- `Bereichs-Box`: größere Inhaltsbox für einen zusammenhängenden Seitenbereich.
|
|
||||||
- `Karten-Box`: kleinere Karte auf derselben Ebene wie Bereichs-Boxen, meist in Grids.
|
|
||||||
- Zentrale CSS-Klassen:
|
|
||||||
- `main-header-box`
|
|
||||||
- `submenu-box`
|
|
||||||
- `module-submenu-actions`
|
|
||||||
- `section-box`
|
|
||||||
- `card-box`
|
|
||||||
- Modulseiten sollen diesem Muster folgen:
|
|
||||||
- zuerst Seitenheader-Box
|
|
||||||
- danach Submenü-Box
|
|
||||||
- danach Bereichs-Boxen und/oder Karten-Boxen je nach Modul
|
|
||||||
- Vertikale Abstände zwischen `main-header-box`, `submenu-box` und den ersten Folge-Boxen muessen aus der globalen Shell kommen.
|
|
||||||
- Maßgeblich sind `module-page-bg` und `module-page-stack` in `public/assets/css/app.css`.
|
|
||||||
- Top-Level-Wrapper wie Grids, Kartencontainer oder Modul-Listen duerfen keinen eigenen zusaetzlichen `margin-top` oder Sonder-Gap erzeugen, der den Abstand nach dem Submenue veraendert.
|
|
||||||
- Bei Layout-Reviews ist explizit zu pruefen, ob `Main-Header -> Submenue -> erste Section/Card` optisch denselben Rhythmus hat wie auf Referenzseiten wie dem Boersenchecker.
|
|
||||||
- `Setup` gehört in Modulen grundsätzlich in die Submenü-Box.
|
|
||||||
- In Verwaltungsseiten soll `Nexus Übersicht` als fester Button in den Submenü-Aktionen vorhanden sein.
|
|
||||||
- Die Optik der Submenü-Aktionsbuttons kommt ausschließlich aus dem globalen CSS. Module sollen dort keine eigenen Farb- oder Variantenlogiken einschleusen.
|
|
||||||
- Beispielstruktur:
|
|
||||||
- Börsenchecker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Bereichs-Box
|
|
||||||
- FX-Rates: Seitenheader-Box, Submenü-Box, danach Bereichs-Boxen
|
|
||||||
- Mining-Checker: Seitenheader-Box, Submenü-Box, Bereichs-Box, Karten-Boxen, Karten-Boxen, Bereichs-Box
|
|
||||||
- Modulverwaltung: Seitenheader-Box, Submenü-Box, danach Karten-Boxen
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
UTC-Speicherregel (VERBINDLICH):
|
|
||||||
- Zeitzonen-Einstellungen haben keinen Einfluss auf das Speicherformat von Zeitwerten in der Datenbank.
|
|
||||||
- Zeitwerte sollen projektweit intern immer in `UTC` gespeichert werden.
|
|
||||||
- Anzeige-Zeitzonen dienen nur der Darstellung fuer Benutzer.
|
|
||||||
- Cron-Zeitzonen dienen nur der lokalen Auswertung von Zeitplaenen und Faelligkeiten.
|
|
||||||
- Beim Einlesen lokaler Eingaben muss vor dem Speichern nach `UTC` normalisiert werden.
|
|
||||||
- Beim Anzeigen gespeicherter Werte muss von `UTC` in die jeweils wirksame Anzeige-Zeitzone umgerechnet werden.
|
|
||||||
- Neue Module duerfen keine lokalen Zeitzonen direkt in Datenbank-Zeitspalten persistieren.
|
|
||||||
|
|
||||||
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.
|
|
||||||
- SSH-Hosts nur Heimnetz.
|
|
||||||
|
|
||||||
12) Pi Control Besonderheiten (konkret)
|
|
||||||
- Worker/Jobs unter /tools/pi_control/
|
|
||||||
- Check-Updates & Cron nutzen die gleichen SSH-Routinen.
|
|
||||||
- Host-Karten, Befehle und Konsole sind UI im Modul.
|
|
||||||
- Update/Upgrade-Checks liefern Debug-Ausgaben, die als Tooltip oder Debugzeile angezeigt werden.
|
|
||||||
|
|
||||||
13) Zusammenfassung (kurz)
|
|
||||||
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.
|
|
||||||
Modul-Assets gehören ausschließlich in den Modul-Ordner und werden dort geladen.
|
|
||||||
|
|
||||||
14) Nexus-Kerngerüst und Dashboard-Zielbild (VERBINDLICH)
|
|
||||||
- Der aktuelle Ausbauschritt betrifft das generelle Nexus-System, nicht die bestehenden Fachmodule.
|
|
||||||
- Bestehende Module sind funktional und strukturell zunächst ausgenommen und dürfen durch Arbeiten am Kerngerüst nicht beeinträchtigt werden.
|
|
||||||
- Neue Kernfunktionen müssen deshalb so eingeführt werden, dass sie parallel zum bestehenden Modulsystem laufen können.
|
|
||||||
- Zielbild ist ein Nexus-Grundsystem mit flexiblen Benutzer-Dashboards, Integrationen und datengetriebenen Seitenmodulen.
|
|
||||||
|
|
||||||
Produktprinzip:
|
|
||||||
- Nexus ist nicht nur Modul-Launcher, sondern ein persönliches und gruppenfähiges Dashboard-System.
|
|
||||||
- Benutzer sollen eigene Dashboards anlegen, anordnen und konfigurieren können.
|
|
||||||
- Inhalte auf Dashboards sollen aus drei Quellen kommen können:
|
|
||||||
- interne Nexus-Funktionen
|
|
||||||
- externe Integrationen
|
|
||||||
- einfache Seitenmodule ohne eigene Modul-Implementierung
|
|
||||||
|
|
||||||
Abgrenzung zu bestehenden Modulen:
|
|
||||||
- Das bestehende Verzeichnis `/modules/<modul>/` bleibt das Zuhause klassischer Nexus-Module.
|
|
||||||
- Das neue Dashboard-System ist ein zusätzliches globales Kernsystem.
|
|
||||||
- Ein Dashboard-Widget darf Daten aus einem Modul anzeigen, aber das Dashboard-System darf keine Modulstruktur voraussetzen oder überschreiben.
|
|
||||||
- Neue Kernfunktionen müssen ohne Umbau bestehender Module lauffähig sein.
|
|
||||||
|
|
||||||
15) Neue globale Kernbausteine (VERBINDLICH)
|
|
||||||
- `Dashboard`
|
|
||||||
- Eine benutzerspezifische oder freigegebene Übersichtsseite.
|
|
||||||
- Ein Benutzer kann mehrere Dashboards besitzen.
|
|
||||||
- Ein Dashboard kann als Standard-Dashboard markiert sein.
|
|
||||||
- `Dashboard-Element`
|
|
||||||
- Ein platzierbares Objekt innerhalb eines Dashboards.
|
|
||||||
- Eigenschaften sind mindestens Typ, Position, Größe, Sichtbarkeit und Konfiguration.
|
|
||||||
- `Widget-Typ`
|
|
||||||
- Beschreibt, welche Art Element gerendert wird.
|
|
||||||
- Beispiele:
|
|
||||||
- Kennzahl
|
|
||||||
- Link-/Bookmark-Gruppe
|
|
||||||
- iFrame/Webseite
|
|
||||||
- Integrationsstatus
|
|
||||||
- Modulansicht im Kleinformat
|
|
||||||
- `Integration`
|
|
||||||
- Zentrale Anbindung an ein externes System.
|
|
||||||
- Beispiele:
|
|
||||||
- Home Assistant
|
|
||||||
- Pi-hole
|
|
||||||
- Proxmox
|
|
||||||
- Docker
|
|
||||||
- Arr-Tools
|
|
||||||
- `Seitenmodul`
|
|
||||||
- Ein datengetriebenes, on-the-fly angelegtes Modul ohne eigenen Code-Ordner unter `/modules/`.
|
|
||||||
- Typische Verwendung:
|
|
||||||
- externer Link
|
|
||||||
- eingebettete Webseite
|
|
||||||
- einfache Startseite für ein Tool
|
|
||||||
|
|
||||||
16) Anforderungen an Benutzer-Dashboards
|
|
||||||
- Jeder Benutzer soll mehrere eigene Dashboards anlegen können.
|
|
||||||
- Jedes Dashboard braucht mindestens:
|
|
||||||
- Name
|
|
||||||
- Slug oder technische ID
|
|
||||||
- Eigentümer
|
|
||||||
- Sichtbarkeit
|
|
||||||
- Sortierung
|
|
||||||
- optional Standardstatus
|
|
||||||
- Dashboards sollen perspektivisch auch teilbar sein:
|
|
||||||
- privat
|
|
||||||
- gruppenbasiert
|
|
||||||
- optional global sichtbar
|
|
||||||
- Ein Benutzer soll seine Dashboards selbst umsortieren, umbenennen, duplizieren und löschen können.
|
|
||||||
- Dashboard-Konfigurationen müssen datenbankbasiert gespeichert werden, nicht in Moduldateien.
|
|
||||||
|
|
||||||
17) Anforderungen an Dashboard-Layout und Editor
|
|
||||||
- Dashboards müssen vom Benutzer flexibel bearbeitet werden können.
|
|
||||||
- Jedes Dashboard-Element braucht mindestens:
|
|
||||||
- Spaltenbreite
|
|
||||||
- Höhe oder Zeilenspanne
|
|
||||||
- Position im Grid
|
|
||||||
- gerätespezifische Layoutdaten, sobald Mobile/Desktop getrennt unterstützt werden
|
|
||||||
- Der Editor soll ein frei konfigurierbares Grid unterstützen.
|
|
||||||
- Größen und Positionen müssen pro Element speicherbar sein.
|
|
||||||
- Ein Wechsel zwischen Anzeige-Modus und Bearbeiten-Modus ist vorzusehen.
|
|
||||||
- Das Layout-System dieses Editors ist globales Nexus-Kerngerüst und darf nicht in einzelnen Modulen dupliziert werden.
|
|
||||||
|
|
||||||
18) Integrationen als globales Kernsystem
|
|
||||||
- Integrationen sind keine Module und keine Widgets, sondern eine eigene Systemschicht.
|
|
||||||
- Eine Integration stellt Verbindung, Zugangsdaten, Basis-URL und technische Einstellungen für ein Fremdsystem bereit.
|
|
||||||
- Widgets oder Seitenmodule greifen nicht direkt auf Rohkonfigurationen zu, sondern auf eine definierte Integration.
|
|
||||||
- Zugangsdaten müssen zentral und getrennt von Widget-Konfigurationen gespeichert werden.
|
|
||||||
- Eine Integration soll mehrfach anlegbar sein, z.B. mehrere Home-Assistant- oder Pi-hole-Instanzen.
|
|
||||||
- Integrationen müssen benennbar und für Benutzer oder Gruppen freigebbar sein.
|
|
||||||
|
|
||||||
19) Seitenmodule on the fly
|
|
||||||
- Ein Seitenmodul ist ein vom Benutzer oder Administrator angelegtes Objekt ohne eigenen Programmcode.
|
|
||||||
- Seitenmodule sind Teil des Nexus-Kerngerüsts und nicht Teil des klassischen Modulordners.
|
|
||||||
- Ein Seitenmodul kann mindestens folgende Typen haben:
|
|
||||||
- `link`
|
|
||||||
- `iframe`
|
|
||||||
- `bookmark_group`
|
|
||||||
- `external_status`
|
|
||||||
- Seitenmodule sollen in Dashboards eingebunden werden können.
|
|
||||||
- Seitenmodule sollen optional auch in der allgemeinen Nexus-Übersicht auftauchen können.
|
|
||||||
- Seitenmodule dürfen keine globale CSS- oder Layoutlogik mitbringen; sie müssen auf dem zentralen Dashboard-/Widget-System aufsetzen.
|
|
||||||
|
|
||||||
20) V1-Datenmodell für das Kerngerüst
|
|
||||||
- Für das generelle Nexus-System sollen neue zentrale Tabellen vorgesehen werden.
|
|
||||||
- Empfohlene V1-Struktur:
|
|
||||||
- `nexus_dashboards`
|
|
||||||
- Dashboard-Metadaten
|
|
||||||
- `nexus_dashboard_items`
|
|
||||||
- platzierte Elemente pro Dashboard
|
|
||||||
- `nexus_integrations`
|
|
||||||
- technische Integrationen und Verbindungsdaten
|
|
||||||
- `nexus_page_modules`
|
|
||||||
- on-the-fly angelegte Seitenmodule
|
|
||||||
- `nexus_dashboard_shares`
|
|
||||||
- optionale Freigaben für Benutzer oder Gruppen
|
|
||||||
- JSON-Konfigurationen sind für flexible Widget- oder Layoutoptionen erlaubt, aber Kerneigenschaften wie Eigentümer, Typ, Position und Sichtbarkeit sollen eigene Spalten behalten.
|
|
||||||
|
|
||||||
21) Kollisionsschutz zu Modulen (VERBINDLICH)
|
|
||||||
- Änderungen am Nexus-Kerngerüst dürfen nicht voraussetzen, dass bestehende Module sofort migriert werden.
|
|
||||||
- Neue Tabellen, Services und Seiten müssen unter globalen Namen aufgebaut werden, nicht unter einem Modulpräfix.
|
|
||||||
- Bestehende Modulrouten, Modul-Assets und Modul-Setups dürfen durch neue Dashboard-Funktionen nicht ersetzt werden.
|
|
||||||
- Integrationen sind global zu denken und dürfen nicht als versteckte Modul-Features in einzelne Module eingestreut werden.
|
|
||||||
- Wenn ein bestehendes Modul später Widgets für Dashboards anbietet, muss das über definierte Adapter oder Provider geschehen, nicht über direkte Eingriffe in das Modul-Layout.
|
|
||||||
|
|
||||||
22) Empfohlene Umsetzungsreihenfolge für das Kerngerüst
|
|
||||||
- Phase 1:
|
|
||||||
- zentrale Begriffe und Datenmodelle festlegen
|
|
||||||
- globale Tabellen für Dashboards, Dashboard-Elemente, Integrationen und Seitenmodule einführen
|
|
||||||
- globale Seiten für Dashboard-Verwaltung anlegen
|
|
||||||
- Phase 2:
|
|
||||||
- erstes Dashboard pro Benutzer
|
|
||||||
- einfacher Grid-Editor
|
|
||||||
- erste Widget-Typen `link`, `iframe`, `bookmark_group`
|
|
||||||
- Phase 3:
|
|
||||||
- Integrationsverwaltung
|
|
||||||
- erste Integrationsadapter, z.B. Home Assistant
|
|
||||||
- widgetfähige Abfrage von externen Daten
|
|
||||||
- Phase 4:
|
|
||||||
- Freigaben, Gruppenrechte, Dashboard-Duplikate
|
|
||||||
- spätere optionale Anbindung bestehender Module über definierte Widget-Provider
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -46,13 +46,12 @@ Das generelle Nexus-System wird unabhängig von den bestehenden Fachmodulen weit
|
|||||||
- zentralen Integrationen zu Fremdsystemen
|
- zentralen Integrationen zu Fremdsystemen
|
||||||
- datengetriebenen Seitenmodulen ohne eigenen Modulordner
|
- datengetriebenen Seitenmodulen ohne eigenen Modulordner
|
||||||
|
|
||||||
Die maßgeblichen Begriffe und Regeln dafür stehen in `PROJECT_CONTEXT.md`:
|
Die maßgeblichen Begriffe und Regeln dafür stehen in:
|
||||||
|
|
||||||
- `Dashboard`
|
- `PROJECT_CONTEXT.md` als Einstieg
|
||||||
- `Dashboard-Element`
|
- `NEXUS_SYSTEM.md`
|
||||||
- `Widget-Typ`
|
- `MODULE_DEVELOPMENT.md`
|
||||||
- `Integration`
|
- `WIDGET_INTEGRATION.md`
|
||||||
- `Seitenmodul`
|
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
|
|
||||||
|
|||||||
126
WIDGET_INTEGRATION.md
Normal file
126
WIDGET_INTEGRATION.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
Projektkontext: Widget-Einbindung und Integrationen
|
||||||
|
|
||||||
|
1) Zielbild
|
||||||
|
- Nexus soll ein flexibles Dashboard-System im Stil moderner Startseitenlösungen werden.
|
||||||
|
- Benutzer sollen eigene Dashboards anlegen, anordnen und konfigurieren können.
|
||||||
|
- Inhalte auf Dashboards sollen aus internen Nexus-Funktionen, externen Integrationen und Seitenmodulen kommen können.
|
||||||
|
|
||||||
|
2) Begriffe
|
||||||
|
- `Dashboard`
|
||||||
|
- eine benutzerspezifische oder freigegebene Übersichtsseite
|
||||||
|
- `Dashboard-Element`
|
||||||
|
- ein platzierbares Objekt innerhalb eines Dashboards
|
||||||
|
- `Widget-Typ`
|
||||||
|
- beschreibt, welche Art Element gerendert wird
|
||||||
|
- `Integration`
|
||||||
|
- zentrale Anbindung an ein externes System
|
||||||
|
- `Seitenmodul`
|
||||||
|
- datengetriebenes, on-the-fly angelegtes Modul ohne eigenen Code-Ordner unter `/modules/`
|
||||||
|
|
||||||
|
3) Anforderungen an Benutzer-Dashboards
|
||||||
|
- jeder Benutzer soll mehrere eigene Dashboards anlegen können
|
||||||
|
- jedes Dashboard braucht mindestens:
|
||||||
|
- Name
|
||||||
|
- Slug oder technische ID
|
||||||
|
- Eigentümer
|
||||||
|
- Sichtbarkeit
|
||||||
|
- Sortierung
|
||||||
|
- optional Standardstatus
|
||||||
|
- Dashboards sollen perspektivisch auch teilbar sein:
|
||||||
|
- privat
|
||||||
|
- gruppenbasiert
|
||||||
|
- optional global sichtbar
|
||||||
|
- ein Benutzer soll seine Dashboards selbst umsortieren, umbenennen, duplizieren und löschen können
|
||||||
|
- Dashboard-Konfigurationen müssen datenbankbasiert gespeichert werden, nicht in Moduldateien
|
||||||
|
|
||||||
|
4) Anforderungen an Dashboard-Layout und Editor
|
||||||
|
- Dashboards müssen vom Benutzer flexibel bearbeitet werden können.
|
||||||
|
- Jedes Dashboard-Element braucht mindestens:
|
||||||
|
- Spaltenbreite
|
||||||
|
- Höhe oder Zeilenspanne
|
||||||
|
- Position im Grid
|
||||||
|
- gerätespezifische Layoutdaten, sobald Mobile/Desktop getrennt unterstützt werden
|
||||||
|
- der Editor soll ein frei konfigurierbares Grid unterstützen
|
||||||
|
- Größen und Positionen müssen pro Element speicherbar sein
|
||||||
|
- ein Wechsel zwischen Anzeige-Modus und Bearbeiten-Modus ist vorzusehen
|
||||||
|
- das Layout-System dieses Editors ist globales Nexus-Kerngerüst und darf nicht in einzelnen Modulen dupliziert werden
|
||||||
|
|
||||||
|
5) Widget-Typen V1
|
||||||
|
- `link`
|
||||||
|
- einzelner Verweis zu einer internen oder externen Seite
|
||||||
|
- `iframe`
|
||||||
|
- eingebettete Webseite oder Tool-Oberfläche
|
||||||
|
- `bookmark_group`
|
||||||
|
- Sammlung mehrerer Links oder Schnellzugriffe
|
||||||
|
- `external_status`
|
||||||
|
- kompakte Zustandsanzeige aus einer Integration
|
||||||
|
- perspektivisch zusätzlich:
|
||||||
|
- Kennzahl
|
||||||
|
- Integrationsstatus
|
||||||
|
- Modulansicht im Kleinformat
|
||||||
|
|
||||||
|
6) Integrationen als Kernsystem
|
||||||
|
- Integrationen sind keine Module und keine Widgets, sondern eine eigene Systemschicht.
|
||||||
|
- Eine Integration stellt Verbindung, Zugangsdaten, Basis-URL und technische Einstellungen für ein Fremdsystem bereit.
|
||||||
|
- Widgets oder Seitenmodule greifen nicht direkt auf Rohkonfigurationen zu, sondern auf eine definierte Integration.
|
||||||
|
- Zugangsdaten müssen zentral und getrennt von Widget-Konfigurationen gespeichert werden.
|
||||||
|
- Eine Integration soll mehrfach anlegbar sein, zum Beispiel mehrere Home-Assistant- oder Pi-hole-Instanzen.
|
||||||
|
- Integrationen müssen benennbar und für Benutzer oder Gruppen freigebbar sein.
|
||||||
|
|
||||||
|
Beispiele für Integrationen:
|
||||||
|
- Home Assistant
|
||||||
|
- Pi-hole
|
||||||
|
- Proxmox
|
||||||
|
- Docker
|
||||||
|
- Arr-Tools
|
||||||
|
|
||||||
|
7) Seitenmodule on the fly
|
||||||
|
- Ein Seitenmodul ist ein vom Benutzer oder Administrator angelegtes Objekt ohne eigenen Programmcode.
|
||||||
|
- Seitenmodule sind Teil des Nexus-Kerngerüsts und nicht Teil des klassischen Modulordners.
|
||||||
|
- Ein Seitenmodul kann mindestens folgende Typen haben:
|
||||||
|
- `link`
|
||||||
|
- `iframe`
|
||||||
|
- `bookmark_group`
|
||||||
|
- `external_status`
|
||||||
|
- Seitenmodule sollen in Dashboards eingebunden werden können.
|
||||||
|
- Seitenmodule sollen optional auch in der allgemeinen Nexus-Übersicht auftauchen können.
|
||||||
|
- Seitenmodule dürfen keine globale CSS- oder Layoutlogik mitbringen; sie müssen auf dem zentralen Dashboard- und Widget-System aufsetzen.
|
||||||
|
|
||||||
|
8) V1-Datenmodell
|
||||||
|
- Für das generelle Nexus-System sollen neue zentrale Tabellen vorgesehen werden.
|
||||||
|
- Empfohlene V1-Struktur:
|
||||||
|
- `nexus_dashboards`
|
||||||
|
- Dashboard-Metadaten
|
||||||
|
- `nexus_dashboard_items`
|
||||||
|
- platzierte Elemente pro Dashboard
|
||||||
|
- `nexus_integrations`
|
||||||
|
- technische Integrationen und Verbindungsdaten
|
||||||
|
- `nexus_page_modules`
|
||||||
|
- on-the-fly angelegte Seitenmodule
|
||||||
|
- `nexus_dashboard_shares`
|
||||||
|
- optionale Freigaben für Benutzer oder Gruppen
|
||||||
|
- JSON-Konfigurationen sind für flexible Widget- oder Layoutoptionen erlaubt, aber Kerneigenschaften wie Eigentümer, Typ, Position und Sichtbarkeit sollen eigene Spalten behalten.
|
||||||
|
|
||||||
|
9) Kollisionsschutz zu Modulen
|
||||||
|
- Änderungen am Nexus-Kerngerüst dürfen nicht voraussetzen, dass bestehende Module sofort migriert werden.
|
||||||
|
- Neue Tabellen, Services und Seiten müssen unter globalen Namen aufgebaut werden, nicht unter einem Modulpräfix.
|
||||||
|
- Bestehende Modulrouten, Modul-Assets und Modul-Setups dürfen durch neue Dashboard-Funktionen nicht ersetzt werden.
|
||||||
|
- Integrationen sind global zu denken und dürfen nicht als versteckte Modul-Features in einzelne Module eingestreut werden.
|
||||||
|
- Wenn ein bestehendes Modul später Widgets für Dashboards anbietet, muss das über definierte Adapter oder Provider geschehen, nicht über direkte Eingriffe in das Modul-Layout.
|
||||||
|
|
||||||
|
10) Empfohlene Umsetzungsreihenfolge
|
||||||
|
- Phase 1:
|
||||||
|
- zentrale Begriffe und Datenmodelle festlegen
|
||||||
|
- globale Tabellen für Dashboards, Dashboard-Elemente, Integrationen und Seitenmodule einführen
|
||||||
|
- globale Seiten für Dashboard-Verwaltung anlegen
|
||||||
|
- Phase 2:
|
||||||
|
- erstes Dashboard pro Benutzer
|
||||||
|
- einfacher Grid-Editor
|
||||||
|
- erste Widget-Typen `link`, `iframe`, `bookmark_group`
|
||||||
|
- Phase 3:
|
||||||
|
- Integrationsverwaltung
|
||||||
|
- erste Integrationsadapter, zum Beispiel Home Assistant
|
||||||
|
- widgetfähige Abfrage von externen Daten
|
||||||
|
- Phase 4:
|
||||||
|
- Freigaben, Gruppenrechte, Dashboard-Duplikate
|
||||||
|
- spätere optionale Anbindung bestehender Module über definierte Widget-Provider
|
||||||
261
partials/landingpages/dashboard.php
Normal file
261
partials/landingpages/dashboard.php
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_auth();
|
||||||
|
|
||||||
|
$service = dashboards();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$groups = auth_groups();
|
||||||
|
$notice = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if (!$service->available() || $ownerKey === '') {
|
||||||
|
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Dashboard-System nicht verfügbar.</section></div></div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = trim((string) ($_POST['action'] ?? ''));
|
||||||
|
try {
|
||||||
|
if ($action === 'add_item') {
|
||||||
|
$dashboardId = (int) ($_POST['dashboard_id'] ?? 0);
|
||||||
|
$itemType = trim((string) ($_POST['item_type'] ?? 'link'));
|
||||||
|
$config = [];
|
||||||
|
if ($itemType === 'page_module') {
|
||||||
|
$config['page_module_id'] = (int) ($_POST['page_module_id'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$config['url'] = trim((string) ($_POST['target_url'] ?? ''));
|
||||||
|
}
|
||||||
|
$service->createItem($dashboardId, $ownerKey, [
|
||||||
|
'item_type' => $itemType,
|
||||||
|
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||||
|
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||||
|
'grid_column' => trim((string) ($_POST['grid_column'] ?? '')),
|
||||||
|
'grid_row' => trim((string) ($_POST['grid_row'] ?? '')),
|
||||||
|
'column_span' => (int) ($_POST['column_span'] ?? 1),
|
||||||
|
'row_span' => (int) ($_POST['row_span'] ?? 1),
|
||||||
|
'config' => $config,
|
||||||
|
]);
|
||||||
|
$notice = 'Dashboard-Element hinzugefügt.';
|
||||||
|
} elseif ($action === 'delete_item') {
|
||||||
|
$service->deleteItem((int) ($_POST['item_id'] ?? 0), (int) ($_POST['dashboard_id'] ?? 0), $ownerKey);
|
||||||
|
$notice = 'Dashboard-Element entfernt.';
|
||||||
|
}
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$error = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessibleDashboards = $service->listAccessibleDashboards($ownerKey, $groups);
|
||||||
|
$selectedDashboardId = (int) ($_GET['id'] ?? 0);
|
||||||
|
$currentDashboard = null;
|
||||||
|
foreach ($accessibleDashboards as $dashboard) {
|
||||||
|
if ((int) ($dashboard['id'] ?? 0) === $selectedDashboardId) {
|
||||||
|
$currentDashboard = $dashboard;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($currentDashboard === null) {
|
||||||
|
$currentDashboard = $service->ensureDefaultDashboard($ownerKey, 'Mein Dashboard');
|
||||||
|
}
|
||||||
|
$currentDashboardId = (int) ($currentDashboard['id'] ?? 0);
|
||||||
|
$dashboardItems = $service->listItems($currentDashboardId);
|
||||||
|
$ownPageModules = $service->listPageModulesForOwner($ownerKey);
|
||||||
|
$pageModuleMap = [];
|
||||||
|
foreach ($ownPageModules as $pageModule) {
|
||||||
|
$pageModuleMap[(int) ($pageModule['id'] ?? 0)] = $pageModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = 'Dashboard';
|
||||||
|
$GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierbaren Dashboard-Elementen.';
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Dashboard Navigation">
|
||||||
|
<a class="module-button module-button--tab-active" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||||
|
<?php if (auth_is_admin()): ?>
|
||||||
|
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/settings">Nexus Einstellungen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<section class="section-box"><?= e($error) ?></section>
|
||||||
|
<?php elseif ($notice !== null): ?>
|
||||||
|
<section class="section-box"><?= e($notice) ?></section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2><?= e((string) ($currentDashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||||
|
<p class="muted"><?= e((string) ($currentDashboard['description'] ?? 'Dein aktuelles Standard-Dashboard.')) ?></p>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Aktives Dashboard</span>
|
||||||
|
<select onchange="if (this.value) window.location.href='/dashboard?id=' + this.value;">
|
||||||
|
<?php foreach ($accessibleDashboards as $dashboard): ?>
|
||||||
|
<option value="<?= (int) ($dashboard['id'] ?? 0) ?>" <?= (int) ($dashboard['id'] ?? 0) === $currentDashboardId ? 'selected' : '' ?>>
|
||||||
|
<?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Element hinzufügen</h2>
|
||||||
|
<p class="muted">V1 unterstützt direkte Links, iFrames und gespeicherte Seitenmodule.</p>
|
||||||
|
<form method="post" class="setup-form">
|
||||||
|
<input type="hidden" name="action" value="add_item">
|
||||||
|
<input type="hidden" name="dashboard_id" value="<?= $currentDashboardId ?>">
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Titel</span>
|
||||||
|
<input type="text" name="title" required>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Typ</span>
|
||||||
|
<select name="item_type" data-dashboard-item-type>
|
||||||
|
<option value="link">Link</option>
|
||||||
|
<option value="iframe">iFrame</option>
|
||||||
|
<option value="page_module">Seitenmodul</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Beschreibung</span>
|
||||||
|
<input type="text" name="description">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted" data-dashboard-target-url>
|
||||||
|
<span>Ziel-URL</span>
|
||||||
|
<input type="url" name="target_url" placeholder="https://...">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted" data-dashboard-page-module hidden>
|
||||||
|
<span>Seitenmodul</span>
|
||||||
|
<select name="page_module_id">
|
||||||
|
<option value="0">Bitte wählen</option>
|
||||||
|
<?php foreach ($ownPageModules as $pageModule): ?>
|
||||||
|
<option value="<?= (int) ($pageModule['id'] ?? 0) ?>"><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Spaltenbreite</span>
|
||||||
|
<select name="column_span">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2" selected>2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Zeilenhöhe</span>
|
||||||
|
<select name="row_span">
|
||||||
|
<option value="1" selected>1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Grid-Spalte optional</span>
|
||||||
|
<input type="number" name="grid_column" min="1" max="4">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-actions setup-actions--footer">
|
||||||
|
<button class="cta-button" type="submit">Element speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if ($dashboardItems === []): ?>
|
||||||
|
<section class="section-box dashboard-empty">Noch keine Elemente vorhanden.</section>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<?php foreach ($dashboardItems as $item): ?>
|
||||||
|
<?php
|
||||||
|
$itemType = (string) ($item['item_type'] ?? 'link');
|
||||||
|
$config = is_array($item['config'] ?? null) ? $item['config'] : [];
|
||||||
|
$columnSpan = max(1, min(4, (int) ($item['column_span'] ?? 1)));
|
||||||
|
$rowSpan = max(1, min(4, (int) ($item['row_span'] ?? 1)));
|
||||||
|
$gridStyles = 'grid-column: span ' . $columnSpan . '; grid-row: span ' . $rowSpan . ';';
|
||||||
|
if (!empty($item['grid_column'])) {
|
||||||
|
$gridStyles .= 'grid-column-start:' . (int) $item['grid_column'] . ';';
|
||||||
|
}
|
||||||
|
if (!empty($item['grid_row'])) {
|
||||||
|
$gridStyles .= 'grid-row-start:' . (int) $item['grid_row'] . ';';
|
||||||
|
}
|
||||||
|
$pageModule = null;
|
||||||
|
if ($itemType === 'page_module' && !empty($config['page_module_id'])) {
|
||||||
|
$pageModule = $pageModuleMap[(int) $config['page_module_id']] ?? null;
|
||||||
|
}
|
||||||
|
$targetUrl = trim((string) ($config['url'] ?? ''));
|
||||||
|
if ($pageModule !== null) {
|
||||||
|
$targetUrl = trim((string) ($pageModule['target_url'] ?? $targetUrl));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<article class="card-box dashboard-widget" style="<?= e($gridStyles) ?>">
|
||||||
|
<div class="dashboard-widget__head">
|
||||||
|
<div>
|
||||||
|
<span class="module-admin-meta__label"><?= e(strtoupper($itemType)) ?></span>
|
||||||
|
<h2><?= e((string) ($item['title'] ?? 'Element')) ?></h2>
|
||||||
|
<?php if (!empty($item['description'])): ?>
|
||||||
|
<p><?= e((string) $item['description']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="delete_item">
|
||||||
|
<input type="hidden" name="dashboard_id" value="<?= $currentDashboardId ?>">
|
||||||
|
<input type="hidden" name="item_id" value="<?= (int) ($item['id'] ?? 0) ?>">
|
||||||
|
<button class="module-button module-button--secondary module-button--small" type="submit">Entfernen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (($itemType === 'iframe' || ($pageModule !== null && (string) ($pageModule['module_type'] ?? '') === 'iframe')) && $targetUrl !== ''): ?>
|
||||||
|
<iframe class="dashboard-widget__frame" src="<?= e($targetUrl) ?>" loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||||
|
<?php elseif ($pageModule !== null): ?>
|
||||||
|
<div class="dashboard-widget__meta">
|
||||||
|
<p><?= e((string) ($pageModule['description'] ?? 'Seitenmodul aus der globalen Nexus-Verwaltung.')) ?></p>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/page-modules/view/<?= (int) ($pageModule['id'] ?? 0) ?>">Öffnen</a>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($targetUrl !== ''): ?>
|
||||||
|
<div class="dashboard-widget__meta">
|
||||||
|
<p><?= e($targetUrl) ?></p>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="dashboard-widget__meta"><p>Für dieses Element ist noch kein Inhalt hinterlegt.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div></div></div>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const typeSelect = document.querySelector('[data-dashboard-item-type]');
|
||||||
|
const urlField = document.querySelector('[data-dashboard-target-url]');
|
||||||
|
const pageModuleField = document.querySelector('[data-dashboard-page-module]');
|
||||||
|
if (!typeSelect || !urlField || !pageModuleField) return;
|
||||||
|
const sync = () => {
|
||||||
|
const isPageModule = typeSelect.value === 'page_module';
|
||||||
|
urlField.hidden = isPageModule;
|
||||||
|
pageModuleField.hidden = !isPageModule;
|
||||||
|
};
|
||||||
|
typeSelect.addEventListener('change', sync);
|
||||||
|
sync();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
142
partials/landingpages/dashboards.php
Normal file
142
partials/landingpages/dashboards.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_auth();
|
||||||
|
|
||||||
|
$service = dashboards();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$notice = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if (!$service->available() || $ownerKey === '') {
|
||||||
|
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Dashboard-System nicht verfügbar.</section></div></div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = trim((string) ($_POST['action'] ?? ''));
|
||||||
|
try {
|
||||||
|
if ($action === 'create_dashboard') {
|
||||||
|
$service->createDashboard($ownerKey, [
|
||||||
|
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||||
|
'slug' => trim((string) ($_POST['slug'] ?? '')),
|
||||||
|
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||||
|
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||||
|
'is_default' => isset($_POST['is_default']),
|
||||||
|
]);
|
||||||
|
$notice = 'Dashboard angelegt.';
|
||||||
|
} elseif ($action === 'set_default') {
|
||||||
|
$service->setDefaultDashboard($ownerKey, (int) ($_POST['dashboard_id'] ?? 0));
|
||||||
|
$notice = 'Standard-Dashboard gesetzt.';
|
||||||
|
} elseif ($action === 'delete_dashboard') {
|
||||||
|
$service->deleteDashboard((int) ($_POST['dashboard_id'] ?? 0), $ownerKey);
|
||||||
|
$notice = 'Dashboard gelöscht.';
|
||||||
|
}
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$error = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboardsList = $service->listDashboardsForOwner($ownerKey);
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = 'Dashboards';
|
||||||
|
$GLOBALS['layout_header_text'] = 'Verwaltung persönlicher und später freigebbarer Dashboard-Flächen.';
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Dashboard Verwaltung">
|
||||||
|
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab-active" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<section class="section-box"><?= e($error) ?></section>
|
||||||
|
<?php elseif ($notice !== null): ?>
|
||||||
|
<section class="section-box"><?= e($notice) ?></section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Neues Dashboard</h2>
|
||||||
|
<form method="post" class="setup-form">
|
||||||
|
<input type="hidden" name="action" value="create_dashboard">
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Titel</span>
|
||||||
|
<input type="text" name="title" required>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Slug optional</span>
|
||||||
|
<input type="text" name="slug">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Beschreibung</span>
|
||||||
|
<input type="text" name="description">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Sichtbarkeit</span>
|
||||||
|
<select name="visibility">
|
||||||
|
<option value="private">Privat</option>
|
||||||
|
<option value="public">Öffentlich</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<input type="checkbox" name="is_default" value="1">
|
||||||
|
<span>Als Standard-Dashboard setzen</span>
|
||||||
|
</label>
|
||||||
|
<div class="setup-actions setup-actions--footer">
|
||||||
|
<button class="cta-button" type="submit">Dashboard anlegen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="module-admin-grid">
|
||||||
|
<?php foreach ($dashboardsList as $dashboard): ?>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2><?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||||
|
<p><?= e((string) ($dashboard['description'] ?? 'Persönliche Dashboard-Fläche.')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta">
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Sichtbarkeit</span>
|
||||||
|
<strong class="module-admin-badge"><?= e(ucfirst((string) ($dashboard['visibility'] ?? 'private'))) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Standard</span>
|
||||||
|
<strong class="module-admin-badge<?= !empty($dashboard['is_default']) ? ' module-admin-badge--success' : '' ?>"><?= !empty($dashboard['is_default']) ? 'Ja' : 'Nein' ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($dashboard['id'] ?? 0) ?>">Öffnen</a>
|
||||||
|
<?php if (empty($dashboard['is_default'])): ?>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="set_default">
|
||||||
|
<input type="hidden" name="dashboard_id" value="<?= (int) ($dashboard['id'] ?? 0) ?>">
|
||||||
|
<button class="module-button module-button--secondary module-button--small" type="submit">Als Standard</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="delete_dashboard">
|
||||||
|
<input type="hidden" name="dashboard_id" value="<?= (int) ($dashboard['id'] ?? 0) ?>">
|
||||||
|
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div></div></div>
|
||||||
197
partials/landingpages/index.php
Executable file → Normal file
197
partials/landingpages/index.php
Executable file → Normal file
@@ -3,29 +3,160 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
$auth = app()->auth();
|
$auth = app()->auth();
|
||||||
$authUser = $auth->user();
|
$authUser = $auth->user();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$groups = auth_groups();
|
||||||
|
$service = dashboards();
|
||||||
$modules = array_values(array_filter(
|
$modules = array_values(array_filter(
|
||||||
$auth->filterModules(modules()->all()),
|
$auth->filterModules(modules()->all()),
|
||||||
static fn (array $module): bool => !empty($module['enabled'])
|
static fn (array $module): bool => !empty($module['enabled'])
|
||||||
));
|
));
|
||||||
?>
|
|
||||||
<section class="module-list-section" data-reveal>
|
|
||||||
<?php if ($authUser !== null): ?>
|
|
||||||
<div class="section-head">
|
|
||||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
|
||||||
<a class="nav-link" href="/modules">Module verwalten</a>
|
|
||||||
<?php if (auth_is_admin()): ?>
|
|
||||||
<a class="nav-link" href="/settings">Nexus Einstellungen</a>
|
|
||||||
<a class="nav-link" href="/exports/database.sql">SQL-Export</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($modules === []): ?>
|
$dashboardsList = [];
|
||||||
<div class="empty-state" data-reveal>
|
$pageModules = [];
|
||||||
Keine Module für den aktuellen Zugriff sichtbar.
|
$defaultDashboard = null;
|
||||||
|
$dashboardItems = [];
|
||||||
|
|
||||||
|
if ($authUser !== null && $service->available() && $ownerKey !== '') {
|
||||||
|
$defaultDashboard = $service->ensureDefaultDashboard($ownerKey, 'Mein Dashboard');
|
||||||
|
$dashboardsList = $service->listAccessibleDashboards($ownerKey, $groups);
|
||||||
|
$pageModules = $service->listPageModulesForOwner($ownerKey);
|
||||||
|
if ($defaultDashboard !== []) {
|
||||||
|
$dashboardItems = $service->listItems((int) ($defaultDashboard['id'] ?? 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = 'Übersicht';
|
||||||
|
$GLOBALS['layout_header_text'] = $authUser === null
|
||||||
|
? 'Zentraler Einstieg in Nexus und die verfügbaren Bereiche.'
|
||||||
|
: 'Persönliche Übersicht aus Dashboards, Seitenmodulen und klassischen Modulen.';
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<?php if ($authUser !== null): ?>
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Nexus Navigation">
|
||||||
|
<a class="module-button module-button--tab-active" href="/">Nexus Übersicht</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||||
|
<?php if (auth_is_admin()): ?>
|
||||||
|
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/settings">Nexus Einstellungen</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Mein Standard-Dashboard</h2>
|
||||||
|
<p class="muted">Der schnellste Einstieg in deine wichtigsten Nexus-Inhalte.</p>
|
||||||
|
<?php if ($defaultDashboard === null || $defaultDashboard === []): ?>
|
||||||
|
<div class="dashboard-empty">Kein Dashboard verfügbar.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<div class="nexus-quick-grid">
|
||||||
|
<article class="card-box nexus-stat-card">
|
||||||
|
<span class="module-admin-meta__label">Dashboard</span>
|
||||||
|
<strong><?= e((string) ($defaultDashboard['title'] ?? 'Mein Dashboard')) ?></strong>
|
||||||
|
<p class="muted"><?= e((string) ($defaultDashboard['description'] ?? 'Persönliches Standard-Dashboard')) ?></p>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($defaultDashboard['id'] ?? 0) ?>">Öffnen</a>
|
||||||
|
</article>
|
||||||
|
<article class="card-box nexus-stat-card">
|
||||||
|
<span class="module-admin-meta__label">Elemente</span>
|
||||||
|
<strong><?= count($dashboardItems) ?></strong>
|
||||||
|
<p class="muted">Aktive Dashboard-Elemente im Standard-Dashboard.</p>
|
||||||
|
</article>
|
||||||
|
<article class="card-box nexus-stat-card">
|
||||||
|
<span class="module-admin-meta__label">Seitenmodule</span>
|
||||||
|
<strong><?= count($pageModules) ?></strong>
|
||||||
|
<p class="muted">Eigene, on-the-fly angelegte Seitenmodule.</p>
|
||||||
|
</article>
|
||||||
|
<article class="card-box nexus-stat-card">
|
||||||
|
<span class="module-admin-meta__label">Klassische Module</span>
|
||||||
|
<strong><?= count($modules) ?></strong>
|
||||||
|
<p class="muted">Aktuell sichtbare, klassische Nexus-Module.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Dashboards</h2>
|
||||||
|
<p class="muted">Eigene und freigegebene Dashboards im globalen Nexus-System.</p>
|
||||||
|
<div class="module-admin-grid module-admin-grid--compact">
|
||||||
|
<?php foreach (array_slice($dashboardsList, 0, 6) as $dashboard): ?>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2><?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||||
|
<p><?= e((string) ($dashboard['description'] ?? 'Flexible Dashboard-Fläche für Widgets und Seitenmodule.')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta">
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Sichtbarkeit</span>
|
||||||
|
<strong class="module-admin-badge"><?= e(ucfirst((string) ($dashboard['visibility'] ?? 'private'))) ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($dashboard['id'] ?? 0) ?>">Öffnen</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="module-admin-grid">
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Seitenmodule</h2>
|
||||||
|
<p>On-the-fly angelegte Zielseiten und eingebettete Tools.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta">
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Anzahl</span>
|
||||||
|
<strong class="module-admin-badge module-admin-badge--success"><?= count($pageModules) ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/page-modules">Verwalten</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Integrationen</h2>
|
||||||
|
<p>Zentrale Anbindungen an Home Assistant, Pi-hole, Proxmox und andere Systeme.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/integrations">Verwalten</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Dashboards konfigurieren</h2>
|
||||||
|
<p>Eigene Dashboard-Flächen, Reihenfolge und Standard-Dashboard verwalten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/dashboards">Öffnen</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Klassische Module</h2>
|
||||||
|
<p class="muted">Die bisherigen Nexus-Module bleiben parallel zum neuen Grundgerüst bestehen.</p>
|
||||||
<div class="module-list">
|
<div class="module-list">
|
||||||
<?php foreach ($modules as $module): ?>
|
<?php foreach ($modules as $module): ?>
|
||||||
<a class="module-row" href="<?= e((string) ($module['entry'] ?? ('/module/' . $module['name']))) ?>">
|
<a class="module-row" href="<?= e((string) ($module['entry'] ?? ('/module/' . $module['name']))) ?>">
|
||||||
@@ -37,5 +168,37 @@ $modules = array_values(array_filter(
|
|||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
</section>
|
||||||
|
<?php else: ?>
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Nexus Einstieg</h2>
|
||||||
|
<p class="muted">Nach der Anmeldung stehen persönliche Dashboards, Integrationen und Seitenmodule bereit.</p>
|
||||||
|
<div class="module-admin-grid">
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Persönliche Dashboards</h2>
|
||||||
|
<p>Mehrere frei konfigurierbare Übersichten pro Benutzer.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Integrationen</h2>
|
||||||
|
<p>Zentrale Anbindungen für Fremdsysteme und externe Datenquellen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2>Seitenmodule</h2>
|
||||||
|
<p>On-the-fly angelegte Links und eingebettete Weboberflächen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div></div></div>
|
||||||
|
|||||||
149
partials/landingpages/integrations.php
Normal file
149
partials/landingpages/integrations.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_auth();
|
||||||
|
|
||||||
|
$service = dashboards();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$notice = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if (!$service->available() || $ownerKey === '') {
|
||||||
|
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Integrationssystem nicht verfügbar.</section></div></div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = trim((string) ($_POST['action'] ?? ''));
|
||||||
|
try {
|
||||||
|
if ($action === 'create_integration') {
|
||||||
|
$service->createIntegration($ownerKey, [
|
||||||
|
'type' => trim((string) ($_POST['type'] ?? 'generic')),
|
||||||
|
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||||
|
'base_url' => trim((string) ($_POST['base_url'] ?? '')),
|
||||||
|
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||||
|
'is_active' => isset($_POST['is_active']),
|
||||||
|
'config' => [
|
||||||
|
'notes' => trim((string) ($_POST['notes'] ?? '')),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$notice = 'Integration angelegt.';
|
||||||
|
} elseif ($action === 'delete_integration') {
|
||||||
|
$service->deleteIntegration((int) ($_POST['integration_id'] ?? 0), $ownerKey);
|
||||||
|
$notice = 'Integration gelöscht.';
|
||||||
|
}
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$error = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = 'Integrationen';
|
||||||
|
$GLOBALS['layout_header_text'] = 'Zentrale Anbindungen an externe Systeme, getrennt vom klassischen Modulsystem.';
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Integration Navigation">
|
||||||
|
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab-active" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<section class="section-box"><?= e($error) ?></section>
|
||||||
|
<?php elseif ($notice !== null): ?>
|
||||||
|
<section class="section-box"><?= e($notice) ?></section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Neue Integration</h2>
|
||||||
|
<form method="post" class="setup-form">
|
||||||
|
<input type="hidden" name="action" value="create_integration">
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Name</span>
|
||||||
|
<input type="text" name="name" required>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Typ</span>
|
||||||
|
<select name="type">
|
||||||
|
<option value="home_assistant">Home Assistant</option>
|
||||||
|
<option value="pi_hole">Pi-hole</option>
|
||||||
|
<option value="proxmox">Proxmox</option>
|
||||||
|
<option value="docker">Docker</option>
|
||||||
|
<option value="generic">Generic</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Basis-URL</span>
|
||||||
|
<input type="url" name="base_url" placeholder="https://...">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Sichtbarkeit</span>
|
||||||
|
<select name="visibility">
|
||||||
|
<option value="private">Privat</option>
|
||||||
|
<option value="public">Öffentlich</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Notizen</span>
|
||||||
|
<input type="text" name="notes">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<input type="checkbox" name="is_active" value="1" checked>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-actions setup-actions--footer">
|
||||||
|
<button class="cta-button" type="submit">Integration speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="module-admin-grid">
|
||||||
|
<?php foreach ($integrations as $integration): ?>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2><?= e((string) ($integration['name'] ?? 'Integration')) ?></h2>
|
||||||
|
<p><?= e((string) (($integration['config']['notes'] ?? '') ?: (string) ($integration['base_url'] ?? 'Zentrale externe Anbindung.'))) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta">
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Typ</span>
|
||||||
|
<strong class="module-admin-badge"><?= e((string) ($integration['type'] ?? 'generic')) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Status</span>
|
||||||
|
<strong class="module-admin-badge<?= !empty($integration['is_active']) ? ' module-admin-badge--success' : ' module-admin-badge--warning' ?>"><?= !empty($integration['is_active']) ? 'Aktiv' : 'Inaktiv' ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<?php if (!empty($integration['base_url'])): ?>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="<?= e((string) $integration['base_url']) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="delete_integration">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= (int) ($integration['id'] ?? 0) ?>">
|
||||||
|
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div></div></div>
|
||||||
158
partials/landingpages/page_modules.php
Normal file
158
partials/landingpages/page_modules.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_auth();
|
||||||
|
|
||||||
|
$service = dashboards();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$notice = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if (!$service->available() || $ownerKey === '') {
|
||||||
|
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Seitenmodul-System nicht verfügbar.</section></div></div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = trim((string) ($_POST['action'] ?? ''));
|
||||||
|
try {
|
||||||
|
if ($action === 'create_page_module') {
|
||||||
|
$service->createPageModule($ownerKey, [
|
||||||
|
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||||
|
'slug' => trim((string) ($_POST['slug'] ?? '')),
|
||||||
|
'module_type' => trim((string) ($_POST['module_type'] ?? 'link')),
|
||||||
|
'target_url' => trim((string) ($_POST['target_url'] ?? '')),
|
||||||
|
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||||
|
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||||
|
'open_mode' => trim((string) ($_POST['open_mode'] ?? 'embed')),
|
||||||
|
'is_active' => isset($_POST['is_active']),
|
||||||
|
]);
|
||||||
|
$notice = 'Seitenmodul angelegt.';
|
||||||
|
} elseif ($action === 'delete_page_module') {
|
||||||
|
$service->deletePageModule((int) ($_POST['page_module_id'] ?? 0), $ownerKey);
|
||||||
|
$notice = 'Seitenmodul gelöscht.';
|
||||||
|
}
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$error = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageModules = $service->listPageModulesForOwner($ownerKey);
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = 'Seitenmodule';
|
||||||
|
$GLOBALS['layout_header_text'] = 'On-the-fly angelegte Link- und iFrame-Module ohne eigenen Modulordner.';
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Seitenmodule Navigation">
|
||||||
|
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab-active" href="/page-modules">Seitenmodule</a>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($error !== null): ?>
|
||||||
|
<section class="section-box"><?= e($error) ?></section>
|
||||||
|
<?php elseif ($notice !== null): ?>
|
||||||
|
<section class="section-box"><?= e($notice) ?></section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2>Neues Seitenmodul</h2>
|
||||||
|
<form method="post" class="setup-form">
|
||||||
|
<input type="hidden" name="action" value="create_page_module">
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Titel</span>
|
||||||
|
<input type="text" name="title" required>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Slug optional</span>
|
||||||
|
<input type="text" name="slug">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Typ</span>
|
||||||
|
<select name="module_type">
|
||||||
|
<option value="link">Link</option>
|
||||||
|
<option value="iframe">iFrame</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Ziel-URL</span>
|
||||||
|
<input type="url" name="target_url" required placeholder="https://...">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Beschreibung</span>
|
||||||
|
<input type="text" name="description">
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Öffnen als</span>
|
||||||
|
<select name="open_mode">
|
||||||
|
<option value="embed">Im Nexus einbetten</option>
|
||||||
|
<option value="new_tab">Neuer Tab</option>
|
||||||
|
<option value="same_tab">Direkt öffnen</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-grid">
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<span>Sichtbarkeit</span>
|
||||||
|
<select name="visibility">
|
||||||
|
<option value="private">Privat</option>
|
||||||
|
<option value="public">Öffentlich</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="setup-field muted">
|
||||||
|
<input type="checkbox" name="is_active" value="1" checked>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setup-actions setup-actions--footer">
|
||||||
|
<button class="cta-button" type="submit">Seitenmodul speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="module-admin-grid">
|
||||||
|
<?php foreach ($pageModules as $pageModule): ?>
|
||||||
|
<article class="card-box module-admin-card">
|
||||||
|
<div class="module-admin-card__head">
|
||||||
|
<div class="module-admin-card__title">
|
||||||
|
<h2><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></h2>
|
||||||
|
<p><?= e((string) ($pageModule['description'] ?? ($pageModule['target_url'] ?? ''))) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta">
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Typ</span>
|
||||||
|
<strong class="module-admin-badge"><?= e((string) ($pageModule['module_type'] ?? 'link')) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-meta__item">
|
||||||
|
<span class="module-admin-meta__label">Öffnen</span>
|
||||||
|
<strong class="module-admin-badge"><?= e((string) ($pageModule['open_mode'] ?? 'embed')) ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-admin-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/page-modules/view/<?= (int) ($pageModule['id'] ?? 0) ?>">Öffnen</a>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="delete_page_module">
|
||||||
|
<input type="hidden" name="page_module_id" value="<?= (int) ($pageModule['id'] ?? 0) ?>">
|
||||||
|
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div></div></div>
|
||||||
58
partials/landingpages/page_modules_view.php
Normal file
58
partials/landingpages/page_modules_view.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_auth();
|
||||||
|
|
||||||
|
$service = dashboards();
|
||||||
|
$ownerKey = auth_user_key();
|
||||||
|
$groups = auth_groups();
|
||||||
|
$pageModuleId = (int) ($_GET['id'] ?? 0);
|
||||||
|
$pageModule = $service->getPageModule($pageModuleId, $ownerKey, $groups);
|
||||||
|
|
||||||
|
if ($pageModule === null) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Seitenmodul nicht gefunden.</section></div></div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetUrl = trim((string) ($pageModule['target_url'] ?? ''));
|
||||||
|
$openMode = (string) ($pageModule['open_mode'] ?? 'embed');
|
||||||
|
if ($targetUrl !== '' && $openMode === 'same_tab') {
|
||||||
|
redirect($targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||||
|
$GLOBALS['layout_header_context'] = (string) ($pageModule['title'] ?? 'Seitenmodul');
|
||||||
|
$GLOBALS['layout_header_text'] = (string) ($pageModule['description'] ?? 'On-the-fly angelegtes Seitenmodul.');
|
||||||
|
?>
|
||||||
|
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||||
|
<header class="module-hero submenu-box">
|
||||||
|
<div class="module-hero-top module-hero-top--compact">
|
||||||
|
<nav class="module-tabs" aria-label="Seitenmodul Navigation">
|
||||||
|
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||||
|
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||||
|
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||||
|
<a class="module-button module-button--tab-active" href="/page-modules">Seitenmodule</a>
|
||||||
|
</nav>
|
||||||
|
<div class="module-hero-actions module-submenu-actions">
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section-box">
|
||||||
|
<h2><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></h2>
|
||||||
|
<p class="muted"><?= e((string) ($pageModule['description'] ?? '')) ?></p>
|
||||||
|
<?php if ($targetUrl === ''): ?>
|
||||||
|
<div class="dashboard-empty">Dieses Seitenmodul hat noch keine Ziel-URL.</div>
|
||||||
|
<?php elseif ((string) ($pageModule['module_type'] ?? 'link') === 'iframe' || $openMode === 'embed'): ?>
|
||||||
|
<iframe class="dashboard-widget__frame dashboard-widget__frame--page" src="<?= e($targetUrl) ?>" loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="dashboard-widget__meta">
|
||||||
|
<p><?= e($targetUrl) ?></p>
|
||||||
|
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">Extern öffnen</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div></div></div>
|
||||||
@@ -776,6 +776,108 @@ body.has-modal-open {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nexus-quick-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-stat-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-stat-card strong {
|
||||||
|
font-size: clamp(1.35rem, 2vw, 2rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__head {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__head h2 {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__head p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--text-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__meta p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-soft);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__frame {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-soft) 85%, transparent);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__frame--page {
|
||||||
|
min-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-empty {
|
||||||
|
padding: 18px 20px;
|
||||||
|
border: 1px dashed color-mix(in srgb, var(--border-soft) 85%, transparent);
|
||||||
|
border-radius: 16px;
|
||||||
|
color: var(--text-soft);
|
||||||
|
background: rgba(255, 255, 255, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget {
|
||||||
|
grid-column: auto !important;
|
||||||
|
grid-row: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget__head {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.setup-shell {
|
.setup-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ $publicPaths = [
|
|||||||
'auth/me',
|
'auth/me',
|
||||||
'module/pi_control/terminal_info',
|
'module/pi_control/terminal_info',
|
||||||
];
|
];
|
||||||
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql'], true)
|
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql', 'dashboard', 'dashboards', 'integrations', 'page-modules'], true)
|
||||||
|| str_starts_with($uriPath, 'modules/setup/')
|
|| str_starts_with($uriPath, 'modules/setup/')
|
||||||
|| str_starts_with($uriPath, 'modules/access/');
|
|| str_starts_with($uriPath, 'modules/access/');
|
||||||
|
if (str_starts_with($uriPath, 'page-modules/view/')) {
|
||||||
|
$requiresGlobalAuth = true;
|
||||||
|
}
|
||||||
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
|
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
|
||||||
$user = auth_user();
|
$user = auth_user();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
@@ -274,6 +277,17 @@ if (str_starts_with($uriPath, 'modules/install')) {
|
|||||||
$target = $pagesBase . '/users/settings.php';
|
$target = $pagesBase . '/users/settings.php';
|
||||||
} elseif ($uriPath === 'users') {
|
} elseif ($uriPath === 'users') {
|
||||||
$target = $pagesBase . '/users/index.php';
|
$target = $pagesBase . '/users/index.php';
|
||||||
|
} elseif ($uriPath === 'dashboard') {
|
||||||
|
$target = $pagesBase . '/dashboard.php';
|
||||||
|
} elseif ($uriPath === 'dashboards') {
|
||||||
|
$target = $pagesBase . '/dashboards.php';
|
||||||
|
} elseif ($uriPath === 'integrations') {
|
||||||
|
$target = $pagesBase . '/integrations.php';
|
||||||
|
} elseif ($uriPath === 'page-modules') {
|
||||||
|
$target = $pagesBase . '/page_modules.php';
|
||||||
|
} elseif (preg_match('~^page-modules/view/(\d+)$~', $uriPath, $pageModuleMatch)) {
|
||||||
|
$_GET['id'] = (string) $pageModuleMatch[1];
|
||||||
|
$target = $pagesBase . '/page_modules_view.php';
|
||||||
} elseif ($uriPath === 'debug') {
|
} elseif ($uriPath === 'debug') {
|
||||||
$target = $pagesBase . '/retool/debug.php';
|
$target = $pagesBase . '/retool/debug.php';
|
||||||
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
|
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ final class App
|
|||||||
private ?\PDO $basePdo;
|
private ?\PDO $basePdo;
|
||||||
private ModuleManager $modules;
|
private ModuleManager $modules;
|
||||||
private AuthService $auth;
|
private AuthService $auth;
|
||||||
|
private NexusDashboardService $dashboards;
|
||||||
|
|
||||||
private function __construct(private Config $config)
|
private function __construct(private Config $config)
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,7 @@ final class App
|
|||||||
$this->modules = new ModuleManager($this->basePdo, $modulesPath);
|
$this->modules = new ModuleManager($this->basePdo, $modulesPath);
|
||||||
$this->modules->bootEnabled();
|
$this->modules->bootEnabled();
|
||||||
$this->auth = new AuthService($this);
|
$this->auth = new AuthService($this);
|
||||||
|
$this->dashboards = new NexusDashboardService($this->basePdo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function init(Config $config): self
|
public static function init(Config $config): self
|
||||||
@@ -71,4 +73,5 @@ final class App
|
|||||||
public function basePdo(): ?\PDO { return $this->basePdo; }
|
public function basePdo(): ?\PDO { return $this->basePdo; }
|
||||||
public function modules(): ModuleManager { return $this->modules; }
|
public function modules(): ModuleManager { return $this->modules; }
|
||||||
public function auth(): AuthService { return $this->auth; }
|
public function auth(): AuthService { return $this->auth; }
|
||||||
|
public function dashboards(): NexusDashboardService { return $this->dashboards; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,88 @@ final class BaseSchema
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_dashboards_owner_slug_idx ON nexus_dashboards (owner_key, slug)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
dashboard_id BIGINT NOT NULL,
|
||||||
|
item_type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
grid_column INTEGER NULL,
|
||||||
|
grid_row INTEGER NULL,
|
||||||
|
column_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
row_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_items_dashboard_idx ON nexus_dashboard_items (dashboard_id, sort_order, id)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_integrations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
base_url TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
secrets TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
module_type TEXT NOT NULL,
|
||||||
|
target_url TEXT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
open_mode TEXT NOT NULL DEFAULT 'embed',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_page_modules_owner_slug_idx ON nexus_page_modules (owner_key, slug)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
dashboard_id BIGINT NOT NULL,
|
||||||
|
share_type TEXT NOT NULL,
|
||||||
|
share_value TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function ensureSqlite(\PDO $pdo): void
|
private static function ensureSqlite(\PDO $pdo): void
|
||||||
@@ -276,6 +358,88 @@ final class BaseSchema
|
|||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_dashboards_owner_slug_idx ON nexus_dashboards (owner_key, slug)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dashboard_id INTEGER NOT NULL,
|
||||||
|
item_type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
grid_column INTEGER NULL,
|
||||||
|
grid_row INTEGER NULL,
|
||||||
|
column_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
row_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_items_dashboard_idx ON nexus_dashboard_items (dashboard_id, sort_order, id)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_integrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
base_url TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
secrets TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_key TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
module_type TEXT NOT NULL,
|
||||||
|
target_url TEXT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
open_mode TEXT NOT NULL DEFAULT 'embed',
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
config TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_page_modules_owner_slug_idx ON nexus_page_modules (owner_key, slug)");
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dashboard_id INTEGER NOT NULL,
|
||||||
|
share_type TEXT NOT NULL,
|
||||||
|
share_value TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function ensureGeneric(\PDO $pdo): void
|
private static function ensureGeneric(\PDO $pdo): void
|
||||||
@@ -404,6 +568,88 @@ final class BaseSchema
|
|||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
owner_key VARCHAR(190) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(190) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_default TINYINT NOT NULL DEFAULT 0,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY nexus_dashboards_owner_slug_idx (owner_key, slug)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
dashboard_id BIGINT NOT NULL,
|
||||||
|
item_type VARCHAR(64) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
grid_column INT NULL,
|
||||||
|
grid_row INT NULL,
|
||||||
|
column_span INT NOT NULL DEFAULT 1,
|
||||||
|
row_span INT NOT NULL DEFAULT 1,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY nexus_dashboard_items_dashboard_idx (dashboard_id, sort_order, id)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_integrations (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
owner_key VARCHAR(190) NOT NULL,
|
||||||
|
type VARCHAR(64) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
base_url TEXT NULL,
|
||||||
|
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||||
|
is_active TINYINT NOT NULL DEFAULT 1,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
secrets TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
owner_key VARCHAR(190) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(190) NOT NULL,
|
||||||
|
module_type VARCHAR(64) NOT NULL,
|
||||||
|
target_url TEXT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||||
|
open_mode VARCHAR(32) NOT NULL DEFAULT 'embed',
|
||||||
|
is_active TINYINT NOT NULL DEFAULT 1,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY nexus_page_modules_owner_slug_idx (owner_key, slug)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
dashboard_id BIGINT NOT NULL,
|
||||||
|
share_type VARCHAR(32) NOT NULL,
|
||||||
|
share_value VARCHAR(255) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY nexus_dashboard_shares_dashboard_idx (dashboard_id, share_type, share_value)
|
||||||
|
)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function seedTimezones(\PDO $pdo): void
|
private static function seedTimezones(\PDO $pdo): void
|
||||||
|
|||||||
628
src/App/NexusDashboardService.php
Normal file
628
src/App/NexusDashboardService.php
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class NexusDashboardService
|
||||||
|
{
|
||||||
|
public function __construct(private ?\PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function available(): bool
|
||||||
|
{
|
||||||
|
return $this->pdo instanceof \PDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensureDefaultDashboard(string $ownerKey, string $title = 'Mein Dashboard'): array
|
||||||
|
{
|
||||||
|
$dashboards = $this->listDashboardsForOwner($ownerKey);
|
||||||
|
if ($dashboards !== []) {
|
||||||
|
foreach ($dashboards as $dashboard) {
|
||||||
|
if (!empty($dashboard['is_default'])) {
|
||||||
|
return $dashboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->setDefaultDashboard($ownerKey, (int) $dashboards[0]['id']);
|
||||||
|
return $this->getDashboardById((int) $dashboards[0]['id']) ?? $dashboards[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->createDashboard($ownerKey, [
|
||||||
|
'title' => $title,
|
||||||
|
'slug' => 'mein-dashboard',
|
||||||
|
'description' => 'Persönliches Nexus-Dashboard.',
|
||||||
|
'visibility' => 'private',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->getDashboardById($id) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listDashboardsForOwner(string $ownerKey): array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT *
|
||||||
|
FROM nexus_dashboards
|
||||||
|
WHERE owner_key = :owner
|
||||||
|
ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute(['owner' => $ownerKey]);
|
||||||
|
|
||||||
|
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listAccessibleDashboards(string $ownerKey, array $groups = []): array
|
||||||
|
{
|
||||||
|
if (!$this->available()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($this->listDashboardsForOwner($ownerKey) as $dashboard) {
|
||||||
|
$id = (int) ($dashboard['id'] ?? 0);
|
||||||
|
if ($id > 0) {
|
||||||
|
$rows[] = $dashboard;
|
||||||
|
$seen[$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT *
|
||||||
|
FROM nexus_dashboards
|
||||||
|
WHERE visibility = 'public'
|
||||||
|
ORDER BY sort_order ASC, title ASC, id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute();
|
||||||
|
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $dashboard) {
|
||||||
|
$id = (int) ($dashboard['id'] ?? 0);
|
||||||
|
if ($id > 0 && !isset($seen[$id])) {
|
||||||
|
$rows[] = $dashboard;
|
||||||
|
$seen[$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ownerKey !== '') {
|
||||||
|
$shared = $this->listSharedDashboardIds($ownerKey, $groups);
|
||||||
|
foreach ($shared as $dashboardId) {
|
||||||
|
if (isset($seen[$dashboardId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dashboard = $this->getDashboardById($dashboardId);
|
||||||
|
if ($dashboard !== null) {
|
||||||
|
$rows[] = $dashboard;
|
||||||
|
$seen[$dashboardId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($rows, static function (array $left, array $right): int {
|
||||||
|
return [$left['sort_order'] ?? 0, (string) ($left['title'] ?? ''), (int) ($left['id'] ?? 0)]
|
||||||
|
<=> [$right['sort_order'] ?? 0, (string) ($right['title'] ?? ''), (int) ($right['id'] ?? 0)];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDashboardById(int $id): ?array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM nexus_dashboards WHERE id = :id LIMIT 1");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return is_array($row) ? $this->hydrateRow($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDashboard(string $ownerKey, array $data): int
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim((string) ($data['title'] ?? 'Neues Dashboard'));
|
||||||
|
$slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), 0);
|
||||||
|
$description = trim((string) ($data['description'] ?? ''));
|
||||||
|
$visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? 'private'));
|
||||||
|
$sortOrder = (int) ($data['sort_order'] ?? $this->nextSortOrder('nexus_dashboards', 'owner_key', $ownerKey));
|
||||||
|
$isDefault = !empty($data['is_default']);
|
||||||
|
$config = $this->encodeJson((array) ($data['config'] ?? []));
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_dashboards
|
||||||
|
(owner_key, title, slug, description, visibility, sort_order, is_default, config, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:owner_key, :title, :slug, :description, :visibility, :sort_order, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'owner_key' => $ownerKey,
|
||||||
|
'title' => $title !== '' ? $title : 'Neues Dashboard',
|
||||||
|
'slug' => $slug,
|
||||||
|
'description' => $description,
|
||||||
|
'visibility' => $visibility,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_default' => $isDefault ? 1 : 0,
|
||||||
|
'config' => $config,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$id = (int) $this->pdo->lastInsertId();
|
||||||
|
if ($isDefault) {
|
||||||
|
$this->setDefaultDashboard($ownerKey, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateDashboard(int $id, string $ownerKey, array $data): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0 || $ownerKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboard = $this->getOwnedDashboard($id, $ownerKey);
|
||||||
|
if ($dashboard === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim((string) ($data['title'] ?? ($dashboard['title'] ?? 'Dashboard')));
|
||||||
|
$slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), $id);
|
||||||
|
$description = trim((string) ($data['description'] ?? ($dashboard['description'] ?? '')));
|
||||||
|
$visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? ($dashboard['visibility'] ?? 'private')));
|
||||||
|
$sortOrder = isset($data['sort_order']) ? (int) $data['sort_order'] : (int) ($dashboard['sort_order'] ?? 0);
|
||||||
|
$config = $this->encodeJson((array) ($data['config'] ?? ($dashboard['config'] ?? [])));
|
||||||
|
$isDefault = !empty($data['is_default']);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"UPDATE nexus_dashboards
|
||||||
|
SET title = :title,
|
||||||
|
slug = :slug,
|
||||||
|
description = :description,
|
||||||
|
visibility = :visibility,
|
||||||
|
sort_order = :sort_order,
|
||||||
|
is_default = :is_default,
|
||||||
|
config = :config,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = :id AND owner_key = :owner_key"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'id' => $id,
|
||||||
|
'owner_key' => $ownerKey,
|
||||||
|
'title' => $title !== '' ? $title : 'Dashboard',
|
||||||
|
'slug' => $slug,
|
||||||
|
'description' => $description,
|
||||||
|
'visibility' => $visibility,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_default' => $isDefault ? 1 : 0,
|
||||||
|
'config' => $config,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($isDefault) {
|
||||||
|
$this->setDefaultDashboard($ownerKey, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteDashboard(int $id, string $ownerKey): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0 || $ownerKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboard = $this->getOwnedDashboard($id, $ownerKey);
|
||||||
|
if ($dashboard === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id");
|
||||||
|
$stmt->execute(['dashboard_id' => $id]);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_shares WHERE dashboard_id = :dashboard_id");
|
||||||
|
$stmt->execute(['dashboard_id' => $id]);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboards WHERE id = :id AND owner_key = :owner_key");
|
||||||
|
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
|
||||||
|
|
||||||
|
if (!empty($dashboard['is_default'])) {
|
||||||
|
$remaining = $this->listDashboardsForOwner($ownerKey);
|
||||||
|
if ($remaining !== []) {
|
||||||
|
$this->setDefaultDashboard($ownerKey, (int) $remaining[0]['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDefaultDashboard(string $ownerKey, int $dashboardId): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '' || $dashboardId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key");
|
||||||
|
$stmt->execute(['owner_key' => $ownerKey]);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key");
|
||||||
|
$stmt->execute(['id' => $dashboardId, 'owner_key' => $ownerKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listItems(int $dashboardId): array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $dashboardId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT *
|
||||||
|
FROM nexus_dashboard_items
|
||||||
|
WHERE dashboard_id = :dashboard_id
|
||||||
|
ORDER BY sort_order ASC, id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute(['dashboard_id' => $dashboardId]);
|
||||||
|
|
||||||
|
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createItem(int $dashboardId, string $ownerKey, array $data): int
|
||||||
|
{
|
||||||
|
if (!$this->available() || $dashboardId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey);
|
||||||
|
if ($dashboard === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemType = $this->normalizeItemType((string) ($data['item_type'] ?? 'link'));
|
||||||
|
$title = trim((string) ($data['title'] ?? 'Element'));
|
||||||
|
$description = trim((string) ($data['description'] ?? ''));
|
||||||
|
$gridColumn = $this->nullableInt($data['grid_column'] ?? null);
|
||||||
|
$gridRow = $this->nullableInt($data['grid_row'] ?? null);
|
||||||
|
$columnSpan = max(1, min(4, (int) ($data['column_span'] ?? 1)));
|
||||||
|
$rowSpan = max(1, min(4, (int) ($data['row_span'] ?? 1)));
|
||||||
|
$sortOrder = (int) ($data['sort_order'] ?? $this->nextItemSortOrder($dashboardId));
|
||||||
|
$config = $this->encodeJson((array) ($data['config'] ?? []));
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_dashboard_items
|
||||||
|
(dashboard_id, item_type, title, description, grid_column, grid_row, column_span, row_span, sort_order, config, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:dashboard_id, :item_type, :title, :description, :grid_column, :grid_row, :column_span, :row_span, :sort_order, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'dashboard_id' => $dashboardId,
|
||||||
|
'item_type' => $itemType,
|
||||||
|
'title' => $title !== '' ? $title : 'Element',
|
||||||
|
'description' => $description,
|
||||||
|
'grid_column' => $gridColumn,
|
||||||
|
'grid_row' => $gridRow,
|
||||||
|
'column_span' => $columnSpan,
|
||||||
|
'row_span' => $rowSpan,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'config' => $config,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteItem(int $itemId, int $dashboardId, string $ownerKey): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $itemId <= 0 || $dashboardId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey);
|
||||||
|
if ($dashboard === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE id = :id AND dashboard_id = :dashboard_id");
|
||||||
|
$stmt->execute([
|
||||||
|
'id' => $itemId,
|
||||||
|
'dashboard_id' => $dashboardId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listIntegrationsForOwner(string $ownerKey): array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT *
|
||||||
|
FROM nexus_integrations
|
||||||
|
WHERE owner_key = :owner_key
|
||||||
|
ORDER BY is_active DESC, name ASC, id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute(['owner_key' => $ownerKey]);
|
||||||
|
|
||||||
|
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createIntegration(string $ownerKey, array $data): int
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_integrations
|
||||||
|
(owner_key, type, name, base_url, visibility, is_active, config, secrets, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:owner_key, :type, :name, :base_url, :visibility, :is_active, :config, :secrets, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'owner_key' => $ownerKey,
|
||||||
|
'type' => trim((string) ($data['type'] ?? 'generic')) ?: 'generic',
|
||||||
|
'name' => trim((string) ($data['name'] ?? 'Integration')) ?: 'Integration',
|
||||||
|
'base_url' => trim((string) ($data['base_url'] ?? '')),
|
||||||
|
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
|
||||||
|
'is_active' => !empty($data['is_active']) ? 1 : 0,
|
||||||
|
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
|
||||||
|
'secrets' => $this->encodeJson((array) ($data['secrets'] ?? [])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteIntegration(int $id, string $ownerKey): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0 || $ownerKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_integrations WHERE id = :id AND owner_key = :owner_key");
|
||||||
|
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listPageModulesForOwner(string $ownerKey): array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT *
|
||||||
|
FROM nexus_page_modules
|
||||||
|
WHERE owner_key = :owner_key
|
||||||
|
ORDER BY is_active DESC, title ASC, id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute(['owner_key' => $ownerKey]);
|
||||||
|
|
||||||
|
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createPageModule(string $ownerKey, array $data): int
|
||||||
|
{
|
||||||
|
if (!$this->available() || $ownerKey === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim((string) ($data['title'] ?? 'Seitenmodul')) ?: 'Seitenmodul';
|
||||||
|
$slug = $this->uniqueSlug('nexus_page_modules', $ownerKey, (string) ($data['slug'] ?? $title), 0);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_page_modules
|
||||||
|
(owner_key, title, slug, module_type, target_url, description, visibility, open_mode, is_active, config, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:owner_key, :title, :slug, :module_type, :target_url, :description, :visibility, :open_mode, :is_active, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'owner_key' => $ownerKey,
|
||||||
|
'title' => $title,
|
||||||
|
'slug' => $slug,
|
||||||
|
'module_type' => $this->normalizePageModuleType((string) ($data['module_type'] ?? 'link')),
|
||||||
|
'target_url' => trim((string) ($data['target_url'] ?? '')),
|
||||||
|
'description' => trim((string) ($data['description'] ?? '')),
|
||||||
|
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
|
||||||
|
'open_mode' => $this->normalizeOpenMode((string) ($data['open_mode'] ?? 'embed')),
|
||||||
|
'is_active' => !empty($data['is_active']) ? 1 : 0,
|
||||||
|
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deletePageModule(int $id, string $ownerKey): void
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0 || $ownerKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM nexus_page_modules WHERE id = :id AND owner_key = :owner_key");
|
||||||
|
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageModule(int $id, string $ownerKey = '', array $groups = []): ?array
|
||||||
|
{
|
||||||
|
if (!$this->available() || $id <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM nexus_page_modules WHERE id = :id LIMIT 1");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($row)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = $this->hydrateRow($row);
|
||||||
|
if ($module['owner_key'] === $ownerKey || $module['visibility'] === 'public') {
|
||||||
|
return $module;
|
||||||
|
}
|
||||||
|
if ($module['visibility'] === 'group' && $ownerKey !== '') {
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) {
|
||||||
|
return $module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function listSharedDashboardIds(string $ownerKey, array $groups): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT dashboard_id, share_type, share_value
|
||||||
|
FROM nexus_dashboard_shares
|
||||||
|
WHERE share_type = 'user' AND share_value = :share_value"
|
||||||
|
);
|
||||||
|
$stmt->execute(['share_value' => $ownerKey]);
|
||||||
|
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||||
|
$result[] = (int) ($row['dashboard_id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$group = trim((string) $group);
|
||||||
|
if ($group === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT dashboard_id
|
||||||
|
FROM nexus_dashboard_shares
|
||||||
|
WHERE share_type = 'group' AND share_value = :share_value"
|
||||||
|
);
|
||||||
|
$stmt->execute(['share_value' => $group]);
|
||||||
|
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||||
|
$result[] = (int) ($row['dashboard_id'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter($result, static fn (int $id): bool => $id > 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOwnedDashboard(int $id, string $ownerKey): ?array
|
||||||
|
{
|
||||||
|
$dashboard = $this->getDashboardById($id);
|
||||||
|
if ($dashboard === null || (string) ($dashboard['owner_key'] ?? '') !== $ownerKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uniqueSlug(string $table, string $ownerKey, string $candidate, int $ignoreId): string
|
||||||
|
{
|
||||||
|
$slug = $this->slugify($candidate);
|
||||||
|
$suffix = 1;
|
||||||
|
while ($this->slugExists($table, $ownerKey, $slug, $ignoreId)) {
|
||||||
|
$suffix++;
|
||||||
|
$slug = $this->slugify($candidate) . '-' . $suffix;
|
||||||
|
}
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function slugExists(string $table, string $ownerKey, string $slug, int $ignoreId): bool
|
||||||
|
{
|
||||||
|
$sql = "SELECT id FROM {$table} WHERE owner_key = :owner_key AND slug = :slug";
|
||||||
|
if ($ignoreId > 0) {
|
||||||
|
$sql .= " AND id <> :id";
|
||||||
|
}
|
||||||
|
$sql .= " LIMIT 1";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$params = ['owner_key' => $ownerKey, 'slug' => $slug];
|
||||||
|
if ($ignoreId > 0) {
|
||||||
|
$params['id'] = $ignoreId;
|
||||||
|
}
|
||||||
|
$stmt->execute($params);
|
||||||
|
return (bool) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function slugify(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim(mb_strtolower($value));
|
||||||
|
$value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?? '';
|
||||||
|
$value = trim($value, '-');
|
||||||
|
return $value !== '' ? $value : 'eintrag';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextSortOrder(string $table, string $field, string $value): int
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM {$table} WHERE {$field} = :value");
|
||||||
|
$stmt->execute(['value' => $value]);
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextItemSortOrder(int $dashboardId): int
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id");
|
||||||
|
$stmt->execute(['dashboard_id' => $dashboardId]);
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeVisibility(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
return in_array($value, ['private', 'group', 'public'], true) ? $value : 'private';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeItemType(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link'], true) ? $value : 'link';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePageModuleType(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
return in_array($value, ['link', 'iframe', 'bookmark_group', 'external_status'], true) ? $value : 'link';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeOpenMode(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
return in_array($value, ['embed', 'new_tab', 'same_tab'], true) ? $value : 'embed';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encodeJson(array $value): string
|
||||||
|
{
|
||||||
|
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
return $encoded === false ? '{}' : $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeJson(mixed $value): array
|
||||||
|
{
|
||||||
|
if (!is_string($value) || trim($value) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateRows(array $rows): array
|
||||||
|
{
|
||||||
|
return array_values(array_map(fn (array $row): array => $this->hydrateRow($row), $rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateRow(array $row): array
|
||||||
|
{
|
||||||
|
foreach (['config', 'secrets'] as $jsonField) {
|
||||||
|
if (array_key_exists($jsonField, $row)) {
|
||||||
|
$row[$jsonField] = $this->decodeJson($row[$jsonField]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['is_default', 'is_active'] as $boolField) {
|
||||||
|
if (array_key_exists($boolField, $row)) {
|
||||||
|
$row[$boolField] = !empty($row[$boolField]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nullableInt(mixed $value): ?int
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,23 @@ function auth_display_name(): string
|
|||||||
return $email;
|
return $email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function auth_user_key(): string
|
||||||
|
{
|
||||||
|
$user = auth_user();
|
||||||
|
if (!$user) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['sub', 'email', 'username', 'name'] as $field) {
|
||||||
|
$value = trim((string) ($user[$field] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function auth_initials(): string
|
function auth_initials(): string
|
||||||
{
|
{
|
||||||
$name = auth_display_name();
|
$name = auth_display_name();
|
||||||
@@ -188,6 +205,11 @@ function modules(): \App\ModuleManager
|
|||||||
return app()->modules();
|
return app()->modules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashboards(): \App\NexusDashboardService
|
||||||
|
{
|
||||||
|
return app()->dashboards();
|
||||||
|
}
|
||||||
|
|
||||||
function module_fn(string $module, string $name, mixed ...$args): mixed
|
function module_fn(string $module, string $name, mixed ...$args): mixed
|
||||||
{
|
{
|
||||||
return modules()->call($module, $name, ...$args);
|
return modules()->call($module, $name, ...$args);
|
||||||
|
|||||||
Reference in New Issue
Block a user