Compare commits
24 Commits
27026533ac
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b07d1f00c | |||
| d8892e63a9 | |||
| 7020ae0b1c | |||
| a408765e76 | |||
| 72774c05aa | |||
| 6b0ffd7aeb | |||
| ae837bc6df | |||
| 94a4e3616c | |||
| c5fd8b39fb | |||
| 3c1cc30fe9 | |||
| aa7ec1d321 | |||
| 7783f65b26 | |||
| 99cf5eaef2 | |||
| 66f9176ea8 | |||
| 3f712fc09d | |||
| 4a6f23cde0 | |||
| efcaa88b31 | |||
| 56d85e417b | |||
| 127a0e71e1 | |||
| 7323673158 | |||
| 813dd86811 | |||
| 81d1e486e8 | |||
| 3ed4fba58c | |||
| 52158ef041 |
@@ -15,10 +15,10 @@ jobs:
|
||||
CONFIG_BASE_DIR: "config"
|
||||
CONFIG_ENV_DIR: "config/staging"
|
||||
DEPLOY_ENV: "staging"
|
||||
LOCAL_ROOT: "/deploy/nexus"
|
||||
LOCAL_ROOT: "/deploy/nexus/nexus-old"
|
||||
RSYNC_OPTS: "--delete --inplace --no-whole-file --no-owner --no-group --no-perms --omit-dir-times --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r"
|
||||
SITE_DOMAIN_DIR: "${{ vars.SITE_DOMAIN_DIR }}"
|
||||
TARGET_PATH: "/deploy/nexus/staging/"
|
||||
TARGET_PATH: "/deploy/nexus/nexus-old/staging/"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -134,10 +134,10 @@ jobs:
|
||||
CONFIG_BASE_DIR: "config"
|
||||
CONFIG_ENV_DIR: "config/prod"
|
||||
DEPLOY_ENV: "production"
|
||||
LOCAL_ROOT: "/deploy/nexus"
|
||||
LOCAL_ROOT: "/deploy/nexus/nexus-old"
|
||||
RSYNC_OPTS: "--delete --inplace --no-whole-file --no-owner --no-group --no-perms --omit-dir-times --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r"
|
||||
SITE_DOMAIN_DIR: "${{ vars.SITE_DOMAIN_DIR }}"
|
||||
TARGET_PATH: "/deploy/nexus/live/"
|
||||
TARGET_PATH: "/deploy/nexus/nexus-old/live/"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -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>/,
|
||||
keine Änderungen an /public/assets/* für modul-spezifische Features.
|
||||
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
|
||||
|
||||
1) Projekt-Zweck & Ziel
|
||||
Nexus ist ein zentrales, webbasiertes Admin-Interface zur Steuerung einer dynamischen IT-Infrastruktur.
|
||||
Module kapseln fachliche Funktionen (z.B. KEA DHCP, Pi Control).
|
||||
Diese Datei ist ab jetzt der zentrale Einstieg und verweist auf die drei maßgeblichen Dokumentationsbereiche.
|
||||
|
||||
2) Aktive Module (Kurzüberblick)
|
||||
- KEA DHCP: Verwaltung von Hosts/Leases, Statische Reservierungen, Metadaten.
|
||||
- Pi Control: Verwaltung von SSH-Hosts, Befehle/Preset, Konsole, Host-Status, Update/Upgrade-Checks.
|
||||
1) Nexus-System
|
||||
- Datei: [NEXUS_SYSTEM.md](NEXUS_SYSTEM.md)
|
||||
- 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
|
||||
- Live: nexus.kusche.berlin
|
||||
- Staging: staging.nexus.kusche.berlin
|
||||
2) Modul-Entwicklung
|
||||
- Datei: [MODULE_DEVELOPMENT.md](MODULE_DEVELOPMENT.md)
|
||||
- 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:
|
||||
- /app/live/ -> Live-Code
|
||||
- /app/staging/ -> Staging-Code
|
||||
- Jeweils mit eigenem /config-Unterordner:
|
||||
- /app/live/config/
|
||||
- /app/staging/config/
|
||||
3) Widget-Einbindung und Integrationen
|
||||
- Datei: [WIDGET_INTEGRATION.md](WIDGET_INTEGRATION.md)
|
||||
- Inhalt:
|
||||
- Benutzer-Dashboards
|
||||
- Dashboard-Elemente und Widget-Typen
|
||||
- Integrationen zu Fremdsystemen
|
||||
- on-the-fly Seitenmodule
|
||||
- Kollisionsschutz zu bestehenden Modulen
|
||||
- empfohlene Umsetzungsphasen
|
||||
|
||||
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.
|
||||
4) Wichtige Leitplanken
|
||||
- Änderungen am generellen Nexus-System dürfen nicht in bestehende Module eingreifen, wenn das nicht ausdrücklich verlangt ist.
|
||||
- Klassische Fachmodule und das neue Dashboard- oder Widget-System sind getrennte Ebenen.
|
||||
- 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)
|
||||
- /public/ -> Web Root (index.php, globale Assets)
|
||||
- /api/ -> Backend/API-Endpunkte
|
||||
- /src/ -> PHP-Kernklassen/Utilities
|
||||
- /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
|
||||
5) Für neue Chats und Arbeitsaufträge
|
||||
- Für globale Nexus-Themen zuerst `NEXUS_SYSTEM.md` lesen.
|
||||
- Für Arbeiten an klassischen Modulen zuerst `MODULE_DEVELOPMENT.md` lesen.
|
||||
- Für Dashboard-, Widget- oder Integrationsarbeiten zuerst `WIDGET_INTEGRATION.md` lesen.
|
||||
|
||||
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
|
||||
- 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`
|
||||
- `Dashboard-Element`
|
||||
- `Widget-Typ`
|
||||
- `Integration`
|
||||
- `Seitenmodul`
|
||||
- `PROJECT_CONTEXT.md` als Einstieg
|
||||
- `NEXUS_SYSTEM.md`
|
||||
- `MODULE_DEVELOPMENT.md`
|
||||
- `WIDGET_INTEGRATION.md`
|
||||
|
||||
Wichtig:
|
||||
|
||||
|
||||
70
Umsetzung Neue UI/01_ZIELBILD_DESKTOP_UI.md
Normal file
70
Umsetzung Neue UI/01_ZIELBILD_DESKTOP_UI.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Zielbild Desktop UI
|
||||
|
||||
## Produktidee
|
||||
|
||||
Die neue Oberfläche soll sich wie ein Desktop-System verhalten, nicht wie eine klassische Admin-Webseite.
|
||||
|
||||
Die Oberfläche besteht aus:
|
||||
|
||||
- Desktop-Fläche
|
||||
- frei platzierbaren App-Icons
|
||||
- Fenster-Manager
|
||||
- Statusleiste oder Dock
|
||||
- Startmenü oder Hauptmenü
|
||||
- Mini-Widget-Bereich
|
||||
- Skin-System
|
||||
|
||||
## Bedienmodell
|
||||
|
||||
- Root `/` ist immer die Desktop-Oberfläche
|
||||
- Apps werden als Fenster geöffnet
|
||||
- Widgets können auf dem Desktop oder in einer Leiste erscheinen
|
||||
- persönliche Startseite ist immer das persönliche Desktop-Dashboard
|
||||
- für nicht eingeloggte Nutzer kann ein öffentliches Desktop-Home angezeigt werden
|
||||
|
||||
## Skins
|
||||
|
||||
Es gibt zunächst drei Skins:
|
||||
|
||||
- `Windows`
|
||||
- `Apple`
|
||||
- `Linux`
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Das sind Skins, keine drei getrennten Implementierungen
|
||||
- Verhalten und Optik sollen sich an echten Desktop-Systemen orientieren
|
||||
- die technische Basis bleibt aber eine gemeinsame `Desktop Shell`
|
||||
|
||||
## Kernbausteine
|
||||
|
||||
- `Desktop Shell`
|
||||
- `Window Manager`
|
||||
- `Dock oder Taskbar`
|
||||
- `System Tray`
|
||||
- `Widget Zones`
|
||||
- `App Registry`
|
||||
- `Theme and Skin Engine`
|
||||
- `Desktop Settings`
|
||||
|
||||
## Öffentliche und persönliche Nutzung
|
||||
|
||||
- Admin kann ein globales öffentliches Desktop-Home-Dashboard anlegen
|
||||
- eingeloggte Nutzer sehen ihren persönlichen Desktop
|
||||
- Nutzer können mehrere persönliche Dashboards oder Workspaces haben
|
||||
|
||||
## Inhalte
|
||||
|
||||
Der Desktop soll langfristig enthalten können:
|
||||
|
||||
- klassische Nexus-Module
|
||||
- Links
|
||||
- eingebettete Webseiten
|
||||
- Apps
|
||||
- globale Widgets
|
||||
- persönliche Linklisten
|
||||
- spätere Integrationsdaten per API
|
||||
|
||||
## Technische Leitidee
|
||||
|
||||
Nicht das alte Nexus-Layout erweitern, sondern eine neue Frontend- und UX-Schicht bauen, die bestehende Kernlogik später konsumiert.
|
||||
87
Umsetzung Neue UI/02_ORDNERSTRUKTUR_UND_GRUNDREGELN.md
Normal file
87
Umsetzung Neue UI/02_ORDNERSTRUKTUR_UND_GRUNDREGELN.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Ordnerstruktur und Grundregeln
|
||||
|
||||
## Grundsatz
|
||||
|
||||
Die Ordnerstruktur soll sich so nah wie sinnvoll an der bestehenden Nexus-Struktur orientieren.
|
||||
|
||||
## Beibehaltene Strukturidee
|
||||
|
||||
- `/public/` -> Web Root, globale Assets und Einstieg
|
||||
- `/api/` -> Backend- und API-Endpunkte
|
||||
- `/src/` -> Kernlogik, Services, Utilities
|
||||
- `/config/` -> Umgebungs-Configs
|
||||
- `/modules/<modul>/` -> klassische Module
|
||||
- `/partials/structure/` -> globale Layout-Bausteine
|
||||
- `/partials/landingpages/` -> Seiten oder Views
|
||||
- `/tools/` -> Worker, CLI, Hilfstools
|
||||
- `/debug/` -> Debug-Ausgaben
|
||||
|
||||
## Neue zusätzliche Bereiche für die Desktop-UI
|
||||
|
||||
Empfohlene Ergänzungen:
|
||||
|
||||
- `/src/Desktop/`
|
||||
- Window Manager
|
||||
- Desktop Registry
|
||||
- Skin Resolver
|
||||
- Widget Layout Engine
|
||||
- `/public/assets/desktop/`
|
||||
- globale Desktop-CSS
|
||||
- JavaScript für Fenster, Dock, Tray, Desktop-Interaktion
|
||||
- `/partials/desktop/`
|
||||
- Desktop Shell
|
||||
- Window Templates
|
||||
- Dock
|
||||
- Tray
|
||||
- Menu
|
||||
|
||||
Optional für die Übergangsphase:
|
||||
|
||||
- `/Old-Nexus/`
|
||||
- komplette Altbasis zur Sichtprüfung und als Entwicklungserinnerung
|
||||
- nur temporär
|
||||
|
||||
## Wichtige Regel
|
||||
|
||||
Die klassische Modulstruktur bleibt erhalten.
|
||||
|
||||
Das bedeutet:
|
||||
|
||||
- Module bleiben unter `/modules/<modul>/`
|
||||
- der neue Desktop-Layer ist nur die neue Oberfläche
|
||||
- Module dürfen nicht ungeordnet in den Desktop-Core verschoben werden
|
||||
- gemeinsame Desktop-Mechaniken liegen nicht in Modulen, sondern im globalen Kern
|
||||
|
||||
## Trennung
|
||||
|
||||
Es gibt drei Ebenen:
|
||||
|
||||
1. globales System
|
||||
2. Desktop-UI-Schicht
|
||||
3. klassische Module
|
||||
|
||||
## Was nicht passieren soll
|
||||
|
||||
- keine Vermischung von Fenster-Manager-Code mit Modulbusinesslogik
|
||||
- keine Skin-spezifischen Sonderlösungen in einzelnen Modulen
|
||||
- keine harte Kopplung zwischen alter Seitenstruktur und neuem Desktop-Verhalten
|
||||
- keine technische Abhängigkeit zu `Old-Nexus/`
|
||||
- keine Includes, Imports, Autoloading-Pfade oder Assets aus `Old-Nexus/`
|
||||
|
||||
## Architekturregel
|
||||
|
||||
Die neue Oberfläche muss so gebaut sein, dass:
|
||||
|
||||
- Module später als Fenster gerendert werden können
|
||||
- Apps, Widgets und Seitenmodule aus einer gemeinsamen Registry kommen
|
||||
- Skins ausschließlich Layout, Stil und Interaktionsdetails definieren
|
||||
|
||||
## Sonderregel für `Old-Nexus/`
|
||||
|
||||
Wenn `Old-Nexus/` im neuen Projekt vorhanden ist, dann gilt verbindlich:
|
||||
|
||||
- nur lesen und vergleichen
|
||||
- niemals direkt einbinden
|
||||
- niemals als Referenzpfad im Produktivcode verwenden
|
||||
- niemals als Dauerbestandteil der neuen Architektur einplanen
|
||||
- nach Abschluss der Umsetzung wieder entfernen
|
||||
81
Umsetzung Neue UI/03_MIGRATION_BESTEHENDE_MODULE.md
Normal file
81
Umsetzung Neue UI/03_MIGRATION_BESTEHENDE_MODULE.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Migration bestehende Module
|
||||
|
||||
## Ziel
|
||||
|
||||
Die bestehenden Module aus dem alten Nexus sollen als Basis in `desktop.kusche.berlin` verfügbar sein, damit sie dort später in die neue Oberfläche überführt werden können.
|
||||
|
||||
## Wichtige Regel
|
||||
|
||||
Die Übernahme erfolgt zweistufig:
|
||||
|
||||
1. Rohkopie als Referenzbasis
|
||||
2. anschließende separate Anpassung im neuen Projekt
|
||||
|
||||
## Empfohlener Zielordner im neuen Projekt
|
||||
|
||||
- `temp/nexus-module-import/`
|
||||
|
||||
## Was kopiert werden soll
|
||||
|
||||
Mindestens:
|
||||
|
||||
- `/modules/`
|
||||
- relevante globale Dokumentationsdateien
|
||||
- gegebenenfalls Referenzdateien aus `/src/`, wenn Modul-Helfer benötigt werden
|
||||
|
||||
Zusätzlich möglich:
|
||||
|
||||
- kompletter Altstand unter `Old-Nexus/`
|
||||
|
||||
## Was zunächst nicht aktiv umgebaut werden soll
|
||||
|
||||
- keine direkten Eingriffe in das alte Nexus-Projekt
|
||||
- keine halb fertige Mischform aus alter UI und neuer Desktop-UI
|
||||
- keine automatische Massenmigration ohne Sichtprüfung pro Modul
|
||||
- keine technische Nutzung von `Old-Nexus/` im neuen Produktivcode
|
||||
|
||||
## Ziel nach dem Kopieren
|
||||
|
||||
Im neuen Projekt gibt es:
|
||||
|
||||
- die neue Desktop-Shell
|
||||
- die importierten Module als Rohbasis
|
||||
- optional `Old-Nexus/` als reine Sicht- und Erinnerungsbasis
|
||||
- eine schrittweise Anbindung pro Modul an das neue Fenstersystem
|
||||
|
||||
## Empfohlener Migrationsablauf je Modul
|
||||
|
||||
1. Modul aus Import-Basis prüfen
|
||||
2. benötigte Assets und Seiten identifizieren
|
||||
3. Modul in neue App-Registry eintragen
|
||||
4. Modul als Desktop-App definieren
|
||||
5. Modulansichten in Fensterlogik integrieren
|
||||
6. Skin-Verhalten testen
|
||||
|
||||
## Wichtig für spätere Umsetzung
|
||||
|
||||
Module werden nicht einfach als alte Seite verlinkt, sondern:
|
||||
|
||||
- als App registriert
|
||||
- im Desktop geöffnet
|
||||
- mit Fenstertitel, Icon, Startverhalten und optionalen Widget-Anknüpfungen beschrieben
|
||||
|
||||
Wenn `Old-Nexus/` vorhanden ist:
|
||||
|
||||
- nur zum Nachschlagen alter Lösungswege verwenden
|
||||
- keine Pfadabhängigkeit darauf erzeugen
|
||||
- nichts daraus zur Laufzeit laden
|
||||
- nichts daraus direkt includen oder importieren
|
||||
|
||||
## Mögliche App-Metadaten je Modul
|
||||
|
||||
- `app_id`
|
||||
- `title`
|
||||
- `icon`
|
||||
- `entry_route`
|
||||
- `window_mode`
|
||||
- `default_width`
|
||||
- `default_height`
|
||||
- `supports_widget`
|
||||
- `supports_tray`
|
||||
- `module_name`
|
||||
75
Umsetzung Neue UI/04_UMSETZUNGSPLAN_V1.md
Normal file
75
Umsetzung Neue UI/04_UMSETZUNGSPLAN_V1.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Umsetzungsplan V1
|
||||
|
||||
## Phase 1: Fundament
|
||||
|
||||
- neues Projekt technisch vorbereiten
|
||||
- bestehende Ordnerstruktur anlegen
|
||||
- Desktop Shell als Root-Oberfläche bauen
|
||||
- Skin-System mit `Windows`, `Apple`, `Linux` vorbereiten
|
||||
- Keycloak-Anbindung einhängen
|
||||
|
||||
## Phase 2: Desktop-Grundfunktionen
|
||||
|
||||
- Desktop-Hintergrund
|
||||
- App-Icons
|
||||
- frei verschiebbare Icons
|
||||
- Dock oder Taskbar
|
||||
- Statusbereich
|
||||
- Uhr
|
||||
- erstes Hauptmenü
|
||||
|
||||
## Phase 3: Fenster-Manager
|
||||
|
||||
- Fenster öffnen und schließen
|
||||
- Fenster fokussieren
|
||||
- Fenster minimieren
|
||||
- Fenster maximieren
|
||||
- z-index-Verwaltung
|
||||
- Position und Größe merken
|
||||
|
||||
## Phase 4: Inhaltssystem
|
||||
|
||||
- App Registry
|
||||
- Widget Registry
|
||||
- Seitenmodule
|
||||
- persönliche Linklisten
|
||||
- öffentliches Home-Dashboard
|
||||
- persönliche Workspaces
|
||||
|
||||
## Phase 5: Admin-Bereiche
|
||||
|
||||
- globale Einstellungen
|
||||
- Widget-Verwaltung
|
||||
- Integrationen
|
||||
- Suchmaschinen
|
||||
- Apps
|
||||
- öffentliches Home-Dashboard
|
||||
|
||||
## Phase 6: Modul-Anbindung
|
||||
|
||||
- importierte Nexus-Module sichten
|
||||
- erste Module als Apps in der Desktop-Shell öffnen
|
||||
- Fenstertauglichkeit je Modul prüfen
|
||||
- langfristig modulweise an neues UX-Modell anpassen
|
||||
|
||||
## V1-Minimalziel
|
||||
|
||||
V1 ist erreicht, wenn:
|
||||
|
||||
- Root die Desktop-Oberfläche ist
|
||||
- Login vorhanden ist
|
||||
- Desktop-Skins umschaltbar sind
|
||||
- Apps als Fenster geöffnet werden können
|
||||
- Admin globale Apps, Widgets, Integrationen und Suchmaschinen verwalten kann
|
||||
- mindestens ein persönlicher Desktop pro Nutzer funktioniert
|
||||
- ein öffentliches Home-Dashboard existiert
|
||||
|
||||
## Spätere Ausbaustufen
|
||||
|
||||
- echte API-Datenquellen für Widgets
|
||||
- Benachrichtigungen
|
||||
- Kontextmenüs
|
||||
- Multi-Workspace
|
||||
- Drag-and-drop Widget-Layout
|
||||
- Desktop-Shortcuts
|
||||
- Datei- oder Objektverwaltung
|
||||
48
Umsetzung Neue UI/05_KEYCLOAK_LOGIN_UND_DESKTOP_LOGIN.md
Normal file
48
Umsetzung Neue UI/05_KEYCLOAK_LOGIN_UND_DESKTOP_LOGIN.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Keycloak Login und Desktop Login
|
||||
|
||||
## Ziel
|
||||
|
||||
Nexus beziehungsweise `desktop.kusche.berlin` soll einen eigenen Login-Screen erhalten, der optisch wie ein Desktop-Login wirkt.
|
||||
|
||||
## Technische Grundlage
|
||||
|
||||
Keycloak bleibt weiterhin das Auth-System.
|
||||
|
||||
Für `desktop.kusche.berlin` soll aber ein eigenes Keycloak-Theme verwendet werden.
|
||||
|
||||
## Wichtige Leitlinie
|
||||
|
||||
- technisch bleibt der Login ein Keycloak-Login
|
||||
- optisch wird er als eigenständiger Desktop-Login gebrandet
|
||||
- das Theme soll nur für diese Anwendung gelten
|
||||
|
||||
## Zieloptik
|
||||
|
||||
Der Login darf sich an Desktop-Systemen orientieren:
|
||||
|
||||
- Hintergrundbild oder Wallpaper
|
||||
- zentrierte Login-Kachel
|
||||
- Benutzerwahl oder Benutzeranzeige
|
||||
- große Uhr oder Statusanzeige
|
||||
- Desktop-artige Übergänge
|
||||
|
||||
## Was im neuen Projekt vorbereitet werden soll
|
||||
|
||||
- Branding-Farben
|
||||
- Logos
|
||||
- Desktop-Login-Konzept
|
||||
- Übergabepunkte für Keycloak-Theme
|
||||
- Beschreibung der gewünschten UX
|
||||
|
||||
## Was später am Keycloak-System zu tun ist
|
||||
|
||||
- eigenes Login-Theme anlegen
|
||||
- Theme dem Nexus-Client oder Realm zuordnen
|
||||
- Templates, CSS und Assets anpassen
|
||||
- Desktop-Login visuell mit der Shell abstimmen
|
||||
|
||||
## Wichtig
|
||||
|
||||
Das Login-Theme gehört technisch nicht in die normale Desktop-Shell, sondern in das Keycloak-Theme-System.
|
||||
|
||||
Trotzdem soll es gestalterisch so wirken, als gehöre es direkt zu `desktop.kusche.berlin`.
|
||||
44
Umsetzung Neue UI/06_STARTPROMPT_NEUE_SESSION.md
Normal file
44
Umsetzung Neue UI/06_STARTPROMPT_NEUE_SESSION.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Startprompt für neue Session
|
||||
|
||||
Nutze den folgenden Prompt in der neuen Session im Projekt `desktop.kusche.berlin`.
|
||||
|
||||
```text
|
||||
Bitte arbeite ausschließlich im neuen Projekt `desktop.kusche.berlin`.
|
||||
|
||||
Wichtige Einstiegsdatei:
|
||||
- `START_HIER.md`
|
||||
|
||||
Lies danach in dieser Reihenfolge:
|
||||
- `01_ZIELBILD_DESKTOP_UI.md`
|
||||
- `02_ORDNERSTRUKTUR_UND_GRUNDREGELN.md`
|
||||
- `03_MIGRATION_BESTEHENDE_MODULE.md`
|
||||
- `04_UMSETZUNGSPLAN_V1.md`
|
||||
- `05_KEYCLOAK_LOGIN_UND_DESKTOP_LOGIN.md`
|
||||
|
||||
Rahmenbedingungen:
|
||||
- Die bestehende Ordnerstruktur soll sich an Nexus orientieren.
|
||||
- Die neuen Dateien im Projekt beschreiben das Zielbild verbindlich.
|
||||
- Die Umsetzung erfolgt ausschließlich im neuen Projekt.
|
||||
- Die bestehenden Nexus-Module sollen als Basis aus einer Import-Struktur verfügbar gemacht werden.
|
||||
- Falls ein Ordner `Old-Nexus/` vorhanden ist, dient er ausschließlich als menschliche Referenz und Erinnerung an das alte System.
|
||||
- `Old-Nexus/` darf niemals technisch eingebunden, importiert, included oder als Asset-/Code-Referenz verwendet werden.
|
||||
- Die eigentliche Anpassung der Module erfolgt danach separat im neuen Projekt.
|
||||
- Die neue Oberfläche ist eine Desktop-Shell, keine klassische Website.
|
||||
- Root `/` soll die Desktop-Oberfläche sein.
|
||||
- Skins sollen als `Windows`, `Apple`, `Linux` angelegt werden, technisch aber auf einer gemeinsamen Shell beruhen.
|
||||
- Spätere Modul-Anbindung muss vorbereitet werden, aber die Shell ist zuerst zu bauen.
|
||||
|
||||
Erste Aufgabe:
|
||||
1. Projektstruktur im neuen Projekt prüfen.
|
||||
2. Falls nötig die Zielstruktur anlegen.
|
||||
3. Eine V1-Desktop-Shell scaffolden.
|
||||
4. Skin-System vorbereiten.
|
||||
5. Fenster-Manager, Dock oder Taskbar und Desktop-Icons als Kern aufsetzen.
|
||||
6. Danach die Import-Basis der alten Module in `temp/nexus-module-import/` vorbereiten.
|
||||
|
||||
Wichtig:
|
||||
- keine Arbeiten mehr im alten Nexus durchführen
|
||||
- keine Modulmigration im Blindflug
|
||||
- keine Vermischung von alter Website-Navigation und neuer Desktop-Interaktion
|
||||
- `Old-Nexus/` nur lesen, nie technisch verwenden
|
||||
```
|
||||
62
Umsetzung Neue UI/START_HIER.md
Normal file
62
Umsetzung Neue UI/START_HIER.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Start Hier
|
||||
|
||||
Diese Datei ist der Einstieg für die Umsetzung der neuen Desktop-Oberfläche in `desktop.kusche.berlin`.
|
||||
|
||||
## Ziel
|
||||
|
||||
Nexus soll als neue Oberfläche nicht mehr wie eine klassische Website aufgebaut sein, sondern wie ein Desktop-System:
|
||||
|
||||
- Desktop-Shell als Hauptoberfläche
|
||||
- Fenster für Apps und Widgets
|
||||
- Skin-System für `Windows`, `Apple`, `Linux`
|
||||
- persönliche Dashboards
|
||||
- öffentliches Home-Dashboard für nicht eingeloggte Nutzer
|
||||
- globale Verwaltung für Widgets, Integrationen, Suchmaschinen und Apps
|
||||
|
||||
Die bestehenden klassischen Module aus Nexus sollen später in diese neue Oberfläche überführt werden, aber die Umsetzung erfolgt ausschließlich im neuen Projekt.
|
||||
|
||||
## Reihenfolge der Dokumente
|
||||
|
||||
Bitte diese Dateien in genau dieser Reihenfolge lesen:
|
||||
|
||||
1. `START_HIER.md`
|
||||
2. `01_ZIELBILD_DESKTOP_UI.md`
|
||||
3. `02_ORDNERSTRUKTUR_UND_GRUNDREGELN.md`
|
||||
4. `03_MIGRATION_BESTEHENDE_MODULE.md`
|
||||
5. `04_UMSETZUNGSPLAN_V1.md`
|
||||
6. `05_KEYCLOAK_LOGIN_UND_DESKTOP_LOGIN.md`
|
||||
7. `06_STARTPROMPT_NEUE_SESSION.md`
|
||||
|
||||
## Wichtig
|
||||
|
||||
- Die bestehende Ordnerlogik aus Nexus soll grundsätzlich beibehalten werden.
|
||||
- Die Umsetzung erfolgt **nicht** mehr im alten Nexus-Projekt.
|
||||
- Die bestehenden Module aus Nexus dienen als Basis und Referenz, sollen aber in `desktop.kusche.berlin` separat weiterentwickelt werden.
|
||||
- Der neue UI-Layer ist eine neue Anwendungsschicht, kein Theme-Umbau des alten Systems.
|
||||
|
||||
## Erwartetes Vorgehen im neuen Projekt
|
||||
|
||||
- zuerst Architektur und Shell anlegen
|
||||
- dann Desktop-Grundsystem bauen
|
||||
- dann globale Verwaltungsseiten
|
||||
- dann klassische Module schrittweise in die neue Shell einhängen
|
||||
|
||||
## Basisübernahme aus Nexus
|
||||
|
||||
Im neuen Projekt sollte ein temporärer Import-Ordner angelegt werden, zum Beispiel:
|
||||
|
||||
- `temp/nexus-module-import/`
|
||||
|
||||
Dorthin werden die bestehenden Module aus Nexus zunächst nur kopiert, damit die Basis verfügbar ist. Die eigentliche Anpassung erfolgt danach separat im neuen Projekt.
|
||||
|
||||
## Old-Nexus
|
||||
|
||||
Zusätzlich kann im neuen Projekt ein Ordner `Old-Nexus/` liegen, in dem das alte System vollständig enthalten ist.
|
||||
|
||||
Wichtige Regel:
|
||||
|
||||
- `Old-Nexus/` dient ausschließlich als menschliche Referenz und Erinnerung an die alte Entwicklung
|
||||
- dort dürfen Beispiele, frühere Funktionen, Strukturen oder Hilfslogik nachgesehen werden
|
||||
- `Old-Nexus/` darf aber niemals als technische Laufzeit- oder Build-Abhängigkeit verwendet werden
|
||||
- es dürfen keine Includes, keine Imports, keine Asset-Referenzen, keine Pfadverweise und keine direkte Code-Nutzung auf `Old-Nexus/` zeigen
|
||||
- der Ordner ist nur temporär und wird nach der Umsetzung wieder gelöscht
|
||||
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
|
||||
@@ -3,12 +3,22 @@ declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Base database for Nexus core (users, settings, modules).
|
||||
* Sync copies the correct file into /config.
|
||||
* Prefer the deployed root config, but fall back to checked-in env defaults.
|
||||
*/
|
||||
|
||||
$path = __DIR__ . '/db_settings_basic.php';
|
||||
if (!file_exists($path)) {
|
||||
throw new RuntimeException('Missing base DB config: expected config/db_settings_basic.php');
|
||||
$candidates = [
|
||||
__DIR__ . '/db_settings_basic.php',
|
||||
__DIR__ . '/staging/db_settings_basic.php',
|
||||
__DIR__ . '/prod/db_settings_basic.php',
|
||||
];
|
||||
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return require $path;
|
||||
}
|
||||
}
|
||||
|
||||
return require $path;
|
||||
throw new RuntimeException(
|
||||
'Missing base DB config. Expected one of: '
|
||||
. implode(', ', array_map(static fn (string $path): string => basename(dirname($path)) . '/' . basename($path), $candidates))
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ $pgsql = [
|
||||
'host' => 'db_nexus',
|
||||
'port' => 5432,
|
||||
'dbname' => 'nexus_live',
|
||||
'connect_timeout' => 5,
|
||||
|
||||
// optional: schema/search_path (commonly "public")
|
||||
'schema' => 'public',
|
||||
|
||||
@@ -26,6 +26,7 @@ $pgsql = [
|
||||
'host' => 'staging_db_nexus',
|
||||
'port' => 5432,
|
||||
'dbname' => 'nexus_staging',
|
||||
'connect_timeout' => 5,
|
||||
|
||||
// optional: schema/search_path (commonly "public")
|
||||
'schema' => 'public',
|
||||
|
||||
0
debug/testoutput.irgendwas
Normal file
0
debug/testoutput.irgendwas
Normal file
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
const h = React.createElement;
|
||||
const { useEffect, useMemo, useState } = React;
|
||||
const { useEffect, useMemo, useRef, useState } = React;
|
||||
const apiBase = root.dataset.apiBase || '/api/mining-checker/v1';
|
||||
const initialProjectKey = root.dataset.defaultProjectKey || 'doge-main';
|
||||
const initialActiveTab = String(root.dataset.activeView || 'overview').trim() || 'overview';
|
||||
@@ -131,6 +131,38 @@
|
||||
}).format(Number(value));
|
||||
}
|
||||
|
||||
function recentMeasurementWindow(rows, windowDays) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedWindowDays = Number(windowDays);
|
||||
if (!Number.isFinite(normalizedWindowDays) || normalizedWindowDays <= 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const sortedRows = rows
|
||||
.filter((row) => row && row.measured_at)
|
||||
.slice()
|
||||
.sort((left, right) => new Date(left.measured_at).getTime() - new Date(right.measured_at).getTime());
|
||||
if (sortedRows.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const latestTs = new Date(sortedRows[sortedRows.length - 1].measured_at).getTime();
|
||||
if (!Number.isFinite(latestTs)) {
|
||||
return sortedRows;
|
||||
}
|
||||
|
||||
const minTs = latestTs - (normalizedWindowDays * 86400 * 1000);
|
||||
const filteredRows = sortedRows.filter((row) => {
|
||||
const measuredTs = new Date(row.measured_at).getTime();
|
||||
return Number.isFinite(measuredTs) && measuredTs >= minTs;
|
||||
});
|
||||
|
||||
return filteredRows.length ? filteredRows : [sortedRows[sortedRows.length - 1]];
|
||||
}
|
||||
|
||||
function parseStoredUtcDate(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
@@ -227,8 +259,8 @@
|
||||
const requestOptions = options && typeof options === 'object' ? { ...options } : {};
|
||||
const debugEnabled = !!debugBus.enabled;
|
||||
const timeoutMs = typeof requestOptions.timeoutMs === 'number'
|
||||
? (debugEnabled ? Math.max(requestOptions.timeoutMs, 20000) : requestOptions.timeoutMs)
|
||||
: (debugEnabled ? 20000 : 8000);
|
||||
? (debugEnabled ? Math.max(requestOptions.timeoutMs, 30000) : requestOptions.timeoutMs)
|
||||
: (debugEnabled ? 30000 : 20000);
|
||||
delete requestOptions.timeoutMs;
|
||||
|
||||
const controller = new AbortController();
|
||||
@@ -702,6 +734,7 @@
|
||||
const [error, setError] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null));
|
||||
const bootstrapCacheRef = useRef(new Map());
|
||||
const [initForm, setInitForm] = useState({ drop_existing: false });
|
||||
const [sqlImportFile, setSqlImportFile] = useState(null);
|
||||
const [dbCheck, setDbCheck] = useState(null);
|
||||
@@ -772,8 +805,7 @@
|
||||
runtime_months: 1,
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
auto_renew: true,
|
||||
base_price_amount: '',
|
||||
payment_type: 'fiat',
|
||||
@@ -793,10 +825,7 @@
|
||||
const [minerOfferForm, setMinerOfferForm] = useState({
|
||||
label: '',
|
||||
runtime_months: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
base_price_amount: '',
|
||||
base_price_currency: 'USD',
|
||||
payment_type: 'fiat',
|
||||
@@ -808,9 +837,14 @@
|
||||
const [purchaseMinerModalOpen, setPurchaseMinerModalOpen] = useState(false);
|
||||
const [purchaseMinerForm, setPurchaseMinerForm] = useState({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: 'USD',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
@@ -851,6 +885,9 @@
|
||||
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
|
||||
const currentMinerOffers = Array.isArray(currentSettings.miner_offers) ? currentSettings.miner_offers : [];
|
||||
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
|
||||
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null;
|
||||
const currentWalletMiningBalance = walletAssetBalance(latestWalletSnapshot, currentMiningCurrency);
|
||||
const renewableOfferIds = new Set(
|
||||
currentMinerOffers
|
||||
.filter((offer) => !!offer.auto_renew)
|
||||
@@ -866,7 +903,7 @@
|
||||
: currencies;
|
||||
const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies;
|
||||
const evaluatedMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : [];
|
||||
const availableMinerOffers = evaluatedMinerOffers.filter((offer) => !!offer.is_active);
|
||||
const availableMinerOffers = evaluatedMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance));
|
||||
const filteredMinerOffers = availableMinerOffers.filter((offer) => {
|
||||
const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min);
|
||||
const speedUnit = String(minerOfferFilters.speed_unit || 'auto');
|
||||
@@ -908,7 +945,7 @@
|
||||
: cryptoCurrencies;
|
||||
const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies;
|
||||
const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies;
|
||||
const selectedMinerScenario = availableMinerOffers.find((offer) => Number(offer.id) === Number(selectedMinerScenarioId)) || null;
|
||||
const selectedMinerScenario = availableMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null;
|
||||
const activeMinerRows = currentPurchasedMiners.map((miner) => ({
|
||||
id: `purchase-${miner.id}`,
|
||||
source: 'miete',
|
||||
@@ -929,7 +966,7 @@
|
||||
: 'fiat',
|
||||
is_active: miner.is_active !== false,
|
||||
can_toggle_auto_renew: Number(miner.runtime_months) > 0 && renewableOfferIds.has(Number(miner.miner_offer_id)),
|
||||
hashrate_text: `${fmtNumber(((Number(miner.mining_speed_value) || 0) + (Number(miner.bonus_speed_value) || 0)), 4)} ${miner.mining_speed_unit || miner.bonus_speed_unit || ''}`.trim(),
|
||||
hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit),
|
||||
type_label: 'Aus Angebot gemietet',
|
||||
})).concat(currentCostPlans.map((plan) => ({
|
||||
id: `plan-${plan.id}`,
|
||||
@@ -945,10 +982,7 @@
|
||||
payment_type: plan.payment_type,
|
||||
is_active: !!plan.is_active,
|
||||
can_toggle_auto_renew: false,
|
||||
hashrate_text: [
|
||||
formatSpeed(plan.mining_speed_value, plan.mining_speed_unit, 'Basis'),
|
||||
formatSpeed(plan.bonus_speed_value, plan.bonus_speed_unit, 'Bonus'),
|
||||
].filter(Boolean).join(' · ') || 'n/a',
|
||||
hashrate_text: formatHashrateWithBonus(plan.mining_speed_value, plan.mining_speed_unit, plan.bonus_speed_value, plan.bonus_speed_unit),
|
||||
type_label: 'Manuell eingetragen',
|
||||
}))).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || '')));
|
||||
|
||||
@@ -963,7 +997,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = availableMinerOffers.some((offer) => Number(offer.id) === Number(selectedMinerScenarioId));
|
||||
const exists = availableMinerOffers.some((offer) => String(offer.id) === String(selectedMinerScenarioId));
|
||||
if (!exists) {
|
||||
setSelectedMinerScenarioId(null);
|
||||
}
|
||||
@@ -984,6 +1018,11 @@
|
||||
setPurchaseMinerForm((current) => ({
|
||||
...current,
|
||||
offer_id: current.offer_id || String(selectedOffer.id),
|
||||
base_offer_id: current.base_offer_id || String(selectedOffer.base_offer_id || selectedOffer.id || ''),
|
||||
label: current.label || String(selectedOffer.label || ''),
|
||||
mining_speed_value: current.mining_speed_value || String(selectedOffer.mining_speed_value || ''),
|
||||
mining_speed_unit: current.mining_speed_unit || String(selectedOffer.mining_speed_unit || ''),
|
||||
bonus_percent: current.bonus_percent || (selectedOffer.bonus_percent !== null && selectedOffer.bonus_percent !== undefined ? String(selectedOffer.bonus_percent) : ''),
|
||||
currency: current.currency || (!selectedOffer.auto_renew ? (currentSettings.crypto_currency || 'DOGE') : (selectedOffer.effective_price_currency || selectedOffer.base_price_currency || 'USD')),
|
||||
total_cost_amount: current.total_cost_amount || (selectedOffer.effective_price_amount !== null && selectedOffer.effective_price_amount !== undefined ? String(selectedOffer.effective_price_amount) : ''),
|
||||
reference_price_amount: current.reference_price_amount || (selectedOffer.reference_price_amount !== null && selectedOffer.reference_price_amount !== undefined ? String(selectedOffer.reference_price_amount) : ''),
|
||||
@@ -1088,6 +1127,43 @@
|
||||
return rate === null ? null : numericValue * rate;
|
||||
}
|
||||
|
||||
function walletAssetBalance(snapshot, currency) {
|
||||
const code = String(currency || '').toUpperCase();
|
||||
if (!snapshot || !code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assets = snapshot.balances_json && typeof snapshot.balances_json === 'object' ? snapshot.balances_json : {};
|
||||
const asset = assets[code] ?? assets[code.toLowerCase()] ?? assets[code.toUpperCase()];
|
||||
const snapshotCurrency = String(snapshot.wallet_currency || '').toUpperCase();
|
||||
const rawBalance = asset && typeof asset === 'object'
|
||||
? asset.balance
|
||||
: (asset ?? (snapshotCurrency === code ? snapshot.wallet_balance : null));
|
||||
const balance = Number(rawBalance);
|
||||
return Number.isFinite(balance) ? balance : null;
|
||||
}
|
||||
|
||||
function isOfferAvailableForWallet(offer, miningCurrency, walletBalance) {
|
||||
if (!offer || !offer.is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String(offer.payment_type || '').toLowerCase() !== 'crypto') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currency = String(offer.effective_price_currency || offer.price_currency || '').toUpperCase();
|
||||
const expectedCurrency = String(miningCurrency || '').toUpperCase();
|
||||
const price = Number(offer.effective_price_amount);
|
||||
const balance = Number(walletBalance);
|
||||
|
||||
if (!currency || !expectedCurrency || currency !== expectedCurrency || !Number.isFinite(price) || !Number.isFinite(balance)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return price <= balance + 0.00000001;
|
||||
}
|
||||
|
||||
function latestFxHistoryRate(fromCurrency, toCurrency) {
|
||||
const from = String(fromCurrency || '').toUpperCase();
|
||||
const to = String(toCurrency || '').toUpperCase();
|
||||
@@ -1127,31 +1203,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBootstrap(key) {
|
||||
setLoading(true);
|
||||
async function loadBootstrap(key, options) {
|
||||
const suppressError = !!(options && options.suppressError);
|
||||
const cacheKey = `${key}:${activeTab || 'overview'}`;
|
||||
const cachedPayload = bootstrapCacheRef.current.get(cacheKey) || null;
|
||||
|
||||
setLoading(!cachedPayload);
|
||||
setError('');
|
||||
setPayload((previous) => previous || normalizeBootstrap(null, key));
|
||||
if (cachedPayload) {
|
||||
setPayload(cachedPayload);
|
||||
} else {
|
||||
setPayload((previous) => previous || normalizeBootstrap(null, key));
|
||||
}
|
||||
let loadGuardTriggered = false;
|
||||
const loadGuard = window.setTimeout(() => {
|
||||
loadGuardTriggered = true;
|
||||
setLoading(false);
|
||||
setPayload((previous) => previous || normalizeBootstrap(null, key));
|
||||
setError((previous) => previous || 'Bootstrap-Request haengt oder braucht zu lange.');
|
||||
}, 12000);
|
||||
setPayload((previous) => previous || cachedPayload || normalizeBootstrap(null, key));
|
||||
if (!suppressError) {
|
||||
setError((previous) => previous || 'Bootstrap-Request haengt oder braucht zu lange.');
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
try {
|
||||
const schema = await request(`${apiBase}/projects/${encodeURIComponent(key)}/schema-status`, { timeoutMs: 4000 });
|
||||
const nextSchemaStatus = normalizeSchemaStatus(schema);
|
||||
setSchemaStatus(nextSchemaStatus);
|
||||
if (!nextSchemaStatus.all_present) {
|
||||
if (schemaStatus.missing_count > 0 || schemaStatus.pending_upgrade_count > 0) {
|
||||
setPayload(normalizeBootstrap(null, key));
|
||||
setError('Mining-Checker Schema ist noch nicht initialisiert. Bitte im Tab Settings die Datenbank initialisieren.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ view: activeTab || 'overview' });
|
||||
const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap?${params.toString()}`, { timeoutMs: 10000 });
|
||||
const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap?${params.toString()}`, { timeoutMs: 25000 });
|
||||
const normalized = normalizeBootstrap(data, key);
|
||||
bootstrapCacheRef.current.set(cacheKey, normalized);
|
||||
setPayload(normalized);
|
||||
setSettingsForm({
|
||||
baseline_measured_at: normalized.settings.baseline_measured_at || '',
|
||||
@@ -1162,7 +1246,6 @@
|
||||
setFxSelection(Array.isArray(normalized.settings.preferred_currencies) && normalized.settings.preferred_currencies.length
|
||||
? normalized.settings.preferred_currencies
|
||||
: ['DOGE', 'USD', 'EUR']);
|
||||
loadFxHistory(key);
|
||||
setTargetForm((previous) => ({
|
||||
...previous,
|
||||
currency: normalized.settings.currencies?.[0]?.code || previous.currency || 'EUR',
|
||||
@@ -1171,9 +1254,13 @@
|
||||
...previous,
|
||||
currency: normalized.settings.currencies?.[0]?.code || previous.currency || 'EUR',
|
||||
}));
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
if (!suppressError) {
|
||||
setError(err.message);
|
||||
}
|
||||
setPayload(normalizeBootstrap(null, key));
|
||||
return false;
|
||||
} finally {
|
||||
window.clearTimeout(loadGuard);
|
||||
if (!loadGuardTriggered) {
|
||||
@@ -1182,10 +1269,73 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadBootstrapAfterMutation(successMessage) {
|
||||
const refreshed = await loadBootstrap(projectKey, { suppressError: true });
|
||||
if (!refreshed) {
|
||||
setError('Speichern war erfolgreich, aber die Ansicht konnte nicht automatisch aktualisiert werden.');
|
||||
if (successMessage) {
|
||||
setMessage(successMessage);
|
||||
}
|
||||
}
|
||||
return refreshed;
|
||||
}
|
||||
|
||||
function invalidateProjectBootstrapCache(key) {
|
||||
const prefix = `${key}:`;
|
||||
Array.from(bootstrapCacheRef.current.keys()).forEach((cacheKey) => {
|
||||
if (String(cacheKey).startsWith(prefix)) {
|
||||
bootstrapCacheRef.current.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applySavedPayout(savedPayout) {
|
||||
if (!savedPayout || typeof savedPayout !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
setPayload((previous) => {
|
||||
const current = previous || normalizeBootstrap(null, projectKey);
|
||||
const previousPayouts = Array.isArray(current.settings?.payouts) ? current.settings.payouts : [];
|
||||
const savedId = String(savedPayout.id ?? '');
|
||||
const payouts = previousPayouts
|
||||
.filter((row) => savedId === '' || String(row?.id ?? '') !== savedId)
|
||||
.concat(savedPayout)
|
||||
.sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || '')));
|
||||
const totalCoins = payouts.reduce((sum, row) => {
|
||||
const amount = Number(row?.coins_amount);
|
||||
return Number.isFinite(amount) ? sum + amount : sum;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
...current,
|
||||
settings: {
|
||||
...(current.settings || {}),
|
||||
payouts,
|
||||
},
|
||||
summary: {
|
||||
...(current.summary || {}),
|
||||
payouts: {
|
||||
...(current.summary?.payouts || {}),
|
||||
total_count: payouts.length,
|
||||
total_coins: totalCoins,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadBootstrap(projectKey);
|
||||
}, [projectKey, activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'mining') {
|
||||
return;
|
||||
}
|
||||
loadFxHistory(projectKey);
|
||||
}, [activeTab, projectKey]);
|
||||
|
||||
useEffect(() => {
|
||||
syncMiningCheckerTabButtons(activeTab);
|
||||
|
||||
@@ -1258,8 +1408,10 @@
|
||||
}, [payload, projectKey]);
|
||||
|
||||
const overviewCharts = useMemo(() => {
|
||||
const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15);
|
||||
const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays);
|
||||
const chartCoinCurrency = String(currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const comparisonRows = measurements.filter((row) => {
|
||||
const comparisonRows = overviewRows.filter((row) => {
|
||||
const miningRate = Number(row.doge_per_hour_per_mh_interval);
|
||||
const price = Number(row.effective_price_per_coin ?? row.price_per_coin);
|
||||
return Number.isFinite(miningRate) && miningRate > 0 && Number.isFinite(price) && price > 0;
|
||||
@@ -1269,10 +1421,10 @@
|
||||
const basePrice = baseComparison ? Number(baseComparison.effective_price_per_coin ?? baseComparison.price_per_coin) : null;
|
||||
|
||||
return {
|
||||
mining: measurements.map((row) => ({ x: fmtDate(row.measured_at), y: row.coins_total })),
|
||||
performance: measurements.filter((row) => row.doge_per_day_interval !== null)
|
||||
mining: overviewRows.map((row) => ({ x: fmtDate(row.measured_at), y: row.coins_total })),
|
||||
performance: overviewRows.filter((row) => row.doge_per_day_interval !== null)
|
||||
.map((row) => ({ x: fmtDate(row.measured_at), y: row.doge_per_day_interval })),
|
||||
pricing: measurements.filter((row) => row.price_per_coin !== null)
|
||||
pricing: overviewRows.filter((row) => row.price_per_coin !== null)
|
||||
.map((row) => ({ x: fmtDate(row.measured_at), y: row.price_per_coin })),
|
||||
miningVsPrice: baseMining && basePrice ? [
|
||||
{
|
||||
@@ -1295,7 +1447,7 @@
|
||||
},
|
||||
] : [],
|
||||
};
|
||||
}, [currentSettings.crypto_currency, measurements]);
|
||||
}, [currentSettings.crypto_currency, measurements, payload?.bootstrap_meta?.overview_window_days]);
|
||||
|
||||
async function submitMeasurement(fromPreview) {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
@@ -1326,7 +1478,7 @@
|
||||
source: 'manual',
|
||||
});
|
||||
setOcrPreview(null);
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation(fromPreview ? 'OCR-Vorschlag bestaetigt und gespeichert.' : 'Messpunkt gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1355,7 +1507,7 @@
|
||||
});
|
||||
setMessage('Wallet-Snapshot gespeichert.');
|
||||
setOcrPreview(null);
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Wallet-Snapshot gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1380,7 +1532,7 @@
|
||||
method: 'DELETE',
|
||||
});
|
||||
setMessage('Messpunkt geloescht.');
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Messpunkt geloescht.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1411,7 +1563,7 @@
|
||||
if (!result.error_count) {
|
||||
setImportForm((previous) => ({ ...previous, rows_text: '' }));
|
||||
}
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation(`Import abgeschlossen: ${summary}.`);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1472,7 +1624,7 @@
|
||||
}),
|
||||
});
|
||||
setMessage('Dashboard gespeichert.');
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Dashboard gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1501,7 +1653,7 @@
|
||||
}),
|
||||
});
|
||||
setMessage('Settings gespeichert.');
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Settings gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1558,7 +1710,7 @@
|
||||
setMessage('Ziel gespeichert.');
|
||||
setTargetForm({ label: '', target_amount_fiat: '', currency: currencies[0]?.code || 'EUR', miner_offer_id: '', is_active: true, sort_order: 0 });
|
||||
setTargetModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Ziel gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1579,7 +1731,7 @@
|
||||
method: 'DELETE',
|
||||
});
|
||||
setMessage('Ziel geloescht.');
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Ziel geloescht.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1601,7 +1753,7 @@
|
||||
body: JSON.stringify({ auto_renew: !row.auto_renew }),
|
||||
});
|
||||
setMessage(`Automatische Verlängerung ${row.auto_renew ? 'deaktiviert' : 'aktiviert'}.`);
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation(`Automatische Verlaengerung ${row.auto_renew ? 'deaktiviert' : 'aktiviert'}.`);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1626,8 +1778,7 @@
|
||||
runtime_months: 1,
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
auto_renew: true,
|
||||
base_price_amount: '',
|
||||
payment_type: 'fiat',
|
||||
@@ -1637,7 +1788,7 @@
|
||||
is_active: true,
|
||||
});
|
||||
setCostPlanModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
await reloadBootstrapAfterMutation('Miner gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1649,16 +1800,19 @@
|
||||
event.preventDefault();
|
||||
setSaving(true);
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, {
|
||||
const savedPayout = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payoutForm),
|
||||
timeoutMs: 8000,
|
||||
});
|
||||
applySavedPayout(savedPayout);
|
||||
invalidateProjectBootstrapCache(projectKey);
|
||||
setMessage('Auszahlung gespeichert.');
|
||||
setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: currentSettings.crypto_currency || 'DOGE', note: '' });
|
||||
setPayoutModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -1671,19 +1825,20 @@
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const offerPayload = {
|
||||
...minerOfferForm,
|
||||
base_price_currency: minerOfferForm.payment_type === 'crypto' ? 'USD' : minerOfferForm.base_price_currency,
|
||||
};
|
||||
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(minerOfferForm),
|
||||
body: JSON.stringify(offerPayload),
|
||||
});
|
||||
setMessage('Miner-Angebot gespeichert.');
|
||||
setMinerOfferForm({
|
||||
label: '',
|
||||
runtime_months: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
base_price_amount: '',
|
||||
base_price_currency: 'USD',
|
||||
payment_type: 'fiat',
|
||||
@@ -1710,6 +1865,21 @@
|
||||
body: JSON.stringify(overrides || { purchased_at: nowDateTimeLocalValue() }),
|
||||
});
|
||||
setMessage('Miner als gemietet erfasst.');
|
||||
setPurchaseMinerForm({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
note: '',
|
||||
});
|
||||
setPurchaseMinerModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
} catch (err) {
|
||||
@@ -1726,7 +1896,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await purchaseMinerOffer(purchaseMinerForm.offer_id, {
|
||||
const selectedOffer = availableMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id));
|
||||
if (!selectedOffer) {
|
||||
setError('Bitte ein gueltiges Miner-Angebot auswaehlen.');
|
||||
return;
|
||||
}
|
||||
|
||||
await purchaseMinerOffer(String(selectedOffer.base_offer_id || selectedOffer.id), {
|
||||
label: purchaseMinerForm.label || null,
|
||||
mining_speed_value: purchaseMinerForm.mining_speed_value || null,
|
||||
mining_speed_unit: purchaseMinerForm.mining_speed_unit || null,
|
||||
bonus_percent: purchaseMinerForm.bonus_percent || null,
|
||||
purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(),
|
||||
total_cost_amount: purchaseMinerForm.total_cost_amount || null,
|
||||
currency: purchaseMinerForm.currency || null,
|
||||
@@ -1918,9 +2098,11 @@
|
||||
function renderSharedOcrPanel() {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
const isWalletPreview = preview.kind === 'wallet';
|
||||
const hasMiningSuggestion = preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null;
|
||||
const hasWalletSuggestion = preview.suggested_wallet.wallet_balance !== '' && preview.suggested_wallet.wallet_balance !== null;
|
||||
const hasUsableOcrSuggestion = isWalletPreview
|
||||
? (preview.suggested_wallet.wallet_balance !== '' && preview.suggested_wallet.wallet_balance !== null)
|
||||
: (preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null);
|
||||
? hasWalletSuggestion
|
||||
: hasMiningSuggestion;
|
||||
const ocrStatus = getOcrStatusMessage(preview);
|
||||
|
||||
return panel('OCR Upload', 'Screenshot auswaehlen, Ergebnis direkt pruefen und speichern.', [
|
||||
@@ -1983,6 +2165,24 @@
|
||||
onClick: () => isWalletPreview ? submitWalletSnapshotFromPreview() : submitMeasurement(true),
|
||||
disabled: saving || !hasUsableOcrSuggestion,
|
||||
}, saving ? 'Speichert …' : 'Ergebnis speichern'),
|
||||
isWalletPreview && hasMiningSuggestion
|
||||
? h('button', {
|
||||
key: 'force-mining',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--ghost',
|
||||
onClick: () => submitMeasurement(true),
|
||||
disabled: saving,
|
||||
}, 'Als Mining speichern')
|
||||
: null,
|
||||
!isWalletPreview && hasWalletSuggestion
|
||||
? h('button', {
|
||||
key: 'force-wallet',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--ghost',
|
||||
onClick: () => submitWalletSnapshotFromPreview(),
|
||||
disabled: saving,
|
||||
}, 'Als Wallet speichern')
|
||||
: null,
|
||||
])
|
||||
: h('div', { key: 'ocr-empty', className: 'mc-empty' },
|
||||
'Noch kein Screenshot ausgewaehlt.'),
|
||||
@@ -2000,7 +2200,7 @@
|
||||
]);
|
||||
|
||||
function renderTab() {
|
||||
const currentCoinCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const currentCoinCurrency = currentMiningCurrency;
|
||||
const perDayLabel = `${currentCoinCurrency} pro Tag`;
|
||||
|
||||
if (activeTab === 'overview') {
|
||||
@@ -2068,6 +2268,16 @@
|
||||
value: latest ? fmtNumber(latest.doge_per_day_interval, 4) : 'n/a',
|
||||
sub: payload?.summary?.current_hashrate_mh ? `Hashrate ${fmtNumber(payload.summary.current_hashrate_mh, 4)} MH/s` : (latest ? `Trend ${latest.trend_label}` : ''),
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'perday-since-payout',
|
||||
label: `${currentCoinCurrency} pro Tag seit letzter Auszahlung`,
|
||||
value: latest && latest.doge_per_day_since_last_payout !== null && latest.doge_per_day_since_last_payout !== undefined
|
||||
? fmtNumber(latest.doge_per_day_since_last_payout, 4)
|
||||
: 'n/a',
|
||||
sub: latest && latest.last_payout_at
|
||||
? `Seit ${fmtDate(latest.last_payout_at)} · ${fmtNumber(latest.coins_since_last_payout, 6)} ${currentCoinCurrency}`
|
||||
: 'Noch keine Auszahlung vor dem letzten Upload',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'value',
|
||||
label: 'Aktueller Gegenwert',
|
||||
@@ -2138,7 +2348,7 @@
|
||||
panel('Mining-History', 'Die letzten 10 Mining-Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [
|
||||
h('table', { key: 'table', className: 'mc-table' }, [
|
||||
h('thead', { key: 'thead' }, h('tr', null, [
|
||||
'Zeit', 'Coins', 'Kurs', 'Quelle', perDayLabel, 'Trend', 'Notiz', 'Aktion'
|
||||
'Zeit', 'Coins', 'Kurs', 'Quelle', perDayLabel, 'Seit Auszahlung/Tag', 'Trend', 'Notiz', 'Aktion'
|
||||
].map((label) => h('th', { key: label }, label)))),
|
||||
h('tbody', { key: 'tbody' },
|
||||
measurements.slice(-10).reverse().map((row) => h('tr', { key: row.id }, [
|
||||
@@ -2147,6 +2357,9 @@
|
||||
h('td', { key: 'price' }, row.price_per_coin ? `${fmtNumber(row.price_per_coin, 6)} ${row.price_currency}` : 'n/a'),
|
||||
h('td', { key: 'source' }, row.source),
|
||||
h('td', { key: 'rate' }, fmtNumber(row.doge_per_day_interval, 4)),
|
||||
h('td', { key: 'rate-payout' }, row.doge_per_day_since_last_payout !== null && row.doge_per_day_since_last_payout !== undefined
|
||||
? `${fmtNumber(row.doge_per_day_since_last_payout, 4)}${row.last_payout_at ? ` seit ${fmtDate(row.last_payout_at)}` : ''}`
|
||||
: 'n/a'),
|
||||
h('td', { key: 'trend' }, row.trend_label),
|
||||
h('td', { key: 'note' }, row.note || row.ocr_flags.join(', ') || '—'),
|
||||
h('td', { key: 'action' }, h('button', {
|
||||
@@ -2347,7 +2560,24 @@
|
||||
key: 'rent-miner',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--ghost',
|
||||
onClick: () => setPurchaseMinerModalOpen(true),
|
||||
onClick: () => {
|
||||
setPurchaseMinerForm({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
note: '',
|
||||
});
|
||||
setPurchaseMinerModalOpen(true);
|
||||
},
|
||||
disabled: !availableMinerOffers.length,
|
||||
}, 'Neuen Miner mieten'),
|
||||
]),
|
||||
@@ -2427,14 +2657,17 @@
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
panel('Miner-Angebote', 'Angebote fuer neue Miner und eine grobe Reinvestitionsbewertung auf Basis der aktuellen Leistung.', [
|
||||
panel('Miner-Angebote', currentWalletMiningBalance !== null
|
||||
? `Hier werden nur Basis-Miner gepflegt. Krypto-Angebote werden gegen deinen aktuellen Wallet-Bestand (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}) gefiltert.`
|
||||
: 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.',
|
||||
[
|
||||
h('div', { key: 'actions', className: 'mc-inline-row' }, [
|
||||
h('button', {
|
||||
key: 'add-offer',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--secondary',
|
||||
onClick: () => setMinerOfferModalOpen(true),
|
||||
}, 'Miner-Angebot anlegen'),
|
||||
}, 'Basis-Angebot anlegen'),
|
||||
]),
|
||||
h('div', { key: 'filters', className: 'mc-filter-grid' }, [
|
||||
inputField(`Min. Geschwindigkeit (${minerOfferFilters.speed_unit === 'kh' ? 'kH/s' : 'MH/s'})`, 'number', minerOfferFilters.speed_min, (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_min: value }), '0.0001'),
|
||||
@@ -2472,7 +2705,7 @@
|
||||
h('td', { key: 'break' }, offer.break_even_days !== null ? `${fmtNumber(offer.break_even_days, 2)} Tage` : 'n/a'),
|
||||
h('td', { key: 'rec' }, [
|
||||
h('div', { key: 'rec-main' }, offer.recommendation),
|
||||
offer.base_price_amount !== null && offer.base_price_currency && offer.payment_type !== 'crypto'
|
||||
offer.base_price_amount !== null && offer.base_price_currency
|
||||
? h('div', { key: 'rec-ref', className: 'mc-kicker' }, `Basis ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`)
|
||||
: null,
|
||||
offer.payment_type ? h('div', { key: 'paytype', className: 'mc-kicker' }, offer.payment_type === 'crypto' ? `Zahlung in Krypto (${currentSettings.crypto_currency || 'DOGE'})` : `Zahlung in FIAT (${offer.base_price_currency || 'EUR'})`) : null,
|
||||
@@ -2482,7 +2715,7 @@
|
||||
h('button', {
|
||||
key: 'scenario',
|
||||
type: 'button',
|
||||
className: cx('mc-button', Number(selectedMinerScenario?.id) === Number(offer.id) ? 'mc-button--secondary' : 'mc-button--ghost'),
|
||||
className: cx('mc-button', String(selectedMinerScenario?.id) === String(offer.id) ? 'mc-button--secondary' : 'mc-button--ghost'),
|
||||
onClick: () => setSelectedMinerScenarioId(offer.id),
|
||||
}, 'Szenario'),
|
||||
h('button', {
|
||||
@@ -2494,7 +2727,7 @@
|
||||
label: offer.label,
|
||||
target_amount_fiat: String(offer.base_price_amount ?? offer.effective_price_amount ?? ''),
|
||||
currency: offer.base_price_currency || offer.effective_price_currency || 'EUR',
|
||||
miner_offer_id: String(offer.id),
|
||||
miner_offer_id: '',
|
||||
is_active: true,
|
||||
sort_order: 0,
|
||||
});
|
||||
@@ -2508,7 +2741,12 @@
|
||||
onClick: () => {
|
||||
setPurchaseMinerForm({
|
||||
offer_id: String(offer.id),
|
||||
base_offer_id: String(offer.base_offer_id || offer.id || ''),
|
||||
purchased_at: nowDateTimeLocalValue(),
|
||||
label: String(offer.label || ''),
|
||||
mining_speed_value: String(offer.mining_speed_value || ''),
|
||||
mining_speed_unit: String(offer.mining_speed_unit || ''),
|
||||
bonus_percent: offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '',
|
||||
total_cost_amount: offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '',
|
||||
currency: offer.effective_price_currency || offer.base_price_currency || 'USD',
|
||||
reference_price_amount: offer.base_price_amount !== null && offer.base_price_amount !== undefined ? String(offer.base_price_amount) : '',
|
||||
@@ -2636,8 +2874,7 @@
|
||||
inputField('Laufzeit in Monaten', 'number', String(costPlanForm.runtime_months), (value) => setCostPlanForm({ ...costPlanForm, runtime_months: Number(value) || 0 })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', costPlanForm.mining_speed_value, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', costPlanForm.mining_speed_unit, speedUnits, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Geschwindigkeit', 'number', costPlanForm.bonus_speed_value, (value) => setCostPlanForm({ ...costPlanForm, bonus_speed_value: value }), '0.0001'),
|
||||
selectField('Bonus-Einheit', costPlanForm.bonus_speed_unit, speedUnits, (value) => setCostPlanForm({ ...costPlanForm, bonus_speed_unit: value })),
|
||||
inputField('Bonus-Hashrate in %', 'number', costPlanForm.bonus_percent, (value) => setCostPlanForm({ ...costPlanForm, bonus_percent: value }), '0.01'),
|
||||
inputField(`Basispreis in ${settingsForm.report_currency || 'EUR'}`, 'number', costPlanForm.base_price_amount, (value) => setCostPlanForm({ ...costPlanForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Zahlungsart', costPlanForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setCostPlanForm({ ...costPlanForm, payment_type: value })),
|
||||
textareaField('Notiz', costPlanForm.note, (value) => setCostPlanForm({ ...costPlanForm, note: value })),
|
||||
@@ -2667,17 +2904,23 @@
|
||||
]),
|
||||
]),
|
||||
], () => setPayoutModalOpen(false)) : null,
|
||||
minerOfferModalOpen ? renderModal('Miner-Angebot anlegen', [
|
||||
minerOfferModalOpen ? renderModal('Basis-Miner-Angebot anlegen', [
|
||||
h('form', { key: 'form', className: 'mc-form', onSubmit: submitMinerOffer }, [
|
||||
inputField('Label', 'text', minerOfferForm.label, (value) => setMinerOfferForm({ ...minerOfferForm, label: value })),
|
||||
inputField('Laufzeit in Monaten', 'number', minerOfferForm.runtime_months, (value) => setMinerOfferForm({ ...minerOfferForm, runtime_months: value })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', minerOfferForm.mining_speed_value, (value) => setMinerOfferForm({ ...minerOfferForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', minerOfferForm.mining_speed_unit, speedUnits, (value) => setMinerOfferForm({ ...minerOfferForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Geschwindigkeit', 'number', minerOfferForm.bonus_speed_value, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_speed_value: value }), '0.0001'),
|
||||
selectField('Bonus-Einheit', minerOfferForm.bonus_speed_unit, speedUnits, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_speed_unit: value })),
|
||||
inputField('Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Basiswährung', minerOfferForm.base_price_currency, selectableFiatCurrencies.map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })),
|
||||
selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({ ...minerOfferForm, payment_type: value })),
|
||||
displayField('Basis-Geschwindigkeit', baseOfferSpeedLabel(minerOfferForm.payment_type)),
|
||||
inputField('Bonus-Hashrate in %', 'number', minerOfferForm.bonus_percent, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_percent: value }), '0.01'),
|
||||
inputField(minerOfferForm.payment_type === 'crypto' ? 'Basispreis in USD' : 'Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({
|
||||
...minerOfferForm,
|
||||
payment_type: value,
|
||||
base_price_currency: value === 'crypto'
|
||||
? 'USD'
|
||||
: (selectableFiatCurrencies[0]?.code || 'USD'),
|
||||
})),
|
||||
minerOfferForm.payment_type === 'crypto'
|
||||
? displayField('Kostenwaehrung', 'USD')
|
||||
: selectField('Basiswährung', minerOfferForm.base_price_currency, selectableFiatCurrencies.map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })),
|
||||
h('label', { className: 'mc-checkbox' }, [
|
||||
h('input', { type: 'checkbox', checked: !!minerOfferForm.auto_renew, onChange: (event) => setMinerOfferForm({ ...minerOfferForm, auto_renew: event.target.checked }) }),
|
||||
'Automatische Verlängerung',
|
||||
@@ -2702,7 +2945,12 @@
|
||||
const offer = availableMinerOffers.find((item) => String(item.id) === String(value));
|
||||
setPurchaseMinerForm({
|
||||
offer_id: value,
|
||||
base_offer_id: offer ? String(offer.base_offer_id || offer.id || '') : '',
|
||||
purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(),
|
||||
label: offer ? String(offer.label || '') : '',
|
||||
mining_speed_value: offer && offer.mining_speed_value !== null && offer.mining_speed_value !== undefined ? String(offer.mining_speed_value) : '',
|
||||
mining_speed_unit: offer?.mining_speed_unit || '',
|
||||
bonus_percent: offer && offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '',
|
||||
total_cost_amount: offer && offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '',
|
||||
currency: offer?.effective_price_currency || offer?.base_price_currency || 'USD',
|
||||
reference_price_amount: offer && offer.reference_price_amount !== null && offer.reference_price_amount !== undefined ? String(offer.reference_price_amount) : '',
|
||||
@@ -2712,6 +2960,10 @@
|
||||
});
|
||||
}),
|
||||
inputField('Mietdatum/-zeit', 'datetime-local', purchaseMinerForm.purchased_at, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, purchased_at: value })),
|
||||
inputField('Label', 'text', purchaseMinerForm.label, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, label: value })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', purchaseMinerForm.mining_speed_value, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', purchaseMinerForm.mining_speed_unit, speedUnits, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Hashrate in %', 'number', purchaseMinerForm.bonus_percent, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, bonus_percent: value }), '0.01'),
|
||||
inputField('Exakter Mietpreis', 'number', purchaseMinerForm.total_cost_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, total_cost_amount: value }), '0.000001'),
|
||||
selectField('Mietwährung', purchaseMinerForm.currency, selectableCurrencies.map((currency) => currency.code), (value) => setPurchaseMinerForm({ ...purchaseMinerForm, currency: value })),
|
||||
inputField('Referenzpreis', 'number', purchaseMinerForm.reference_price_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, reference_price_amount: value }), '0.000001'),
|
||||
@@ -2730,11 +2982,11 @@
|
||||
targetModalOpen ? renderModal('Ziel anlegen', [
|
||||
h('form', { key: 'form', className: 'mc-form', onSubmit: submitTarget }, [
|
||||
inputField('Label', 'text', targetForm.label, (value) => setTargetForm({ ...targetForm, label: value })),
|
||||
selectField('Angebots-Verknuepfung', targetForm.miner_offer_id || '', [{ value: '', label: 'Kein verknuepftes Angebot' }].concat(availableMinerOffers.map((offer) => ({
|
||||
selectField('Angebots-Verknuepfung', targetForm.miner_offer_id || '', [{ value: '', label: 'Kein verknuepftes Angebot' }].concat(currentMinerOffers.map((offer) => ({
|
||||
value: String(offer.id),
|
||||
label: `${offer.label} · ${fmtNumber(offer.base_price_amount ?? offer.effective_price_amount, 6)} ${offer.base_price_currency || offer.effective_price_currency}`,
|
||||
label: `${offer.label} · ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`,
|
||||
}))), (value) => {
|
||||
const offer = availableMinerOffers.find((item) => String(item.id) === String(value));
|
||||
const offer = currentMinerOffers.find((item) => String(item.id) === String(value));
|
||||
setTargetForm({
|
||||
...targetForm,
|
||||
miner_offer_id: value,
|
||||
@@ -2985,6 +3237,18 @@
|
||||
return `${label ? label + ' ' : ''}${fmtNumber(value, 4)} ${unit}`;
|
||||
}
|
||||
|
||||
function baseOfferSpeedLabel(paymentType) {
|
||||
return String(paymentType || 'fiat') === 'crypto' ? '75 kH/s' : '50 kH/s';
|
||||
}
|
||||
|
||||
function formatHashrateWithBonus(speedValue, speedUnit, bonusValue, bonusUnit) {
|
||||
const parts = [
|
||||
formatSpeed(speedValue, speedUnit, 'Basis'),
|
||||
Number(bonusValue) > 0 ? formatSpeed(bonusValue, bonusUnit, 'Bonus') : '',
|
||||
].filter(Boolean);
|
||||
return parts.length ? parts.join(' · ') : 'n/a';
|
||||
}
|
||||
|
||||
function formatAdaptiveSpeed(valueMh) {
|
||||
const numericValue = Number(valueMh);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
|
||||
@@ -53,7 +53,7 @@ $moduleJs = str_replace('</script>', '<\/script>', $moduleJs);
|
||||
data-sections-json="<?= e(is_string($sectionsJson) ? $sectionsJson : '[]') ?>"></div>
|
||||
</div>
|
||||
<style><?= $moduleCss ?></style>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="/assets/vendor/react/react.production.min.js"></script>
|
||||
<script src="/assets/vendor/react/react-dom.production.min.js"></script>
|
||||
<script><?= $moduleJs ?></script>
|
||||
<?= module_shell_footer() ?>
|
||||
|
||||
@@ -21,9 +21,13 @@ final class Router
|
||||
{
|
||||
private const BOOTSTRAP_MEASUREMENT_LIMIT = 150;
|
||||
private const BOOTSTRAP_SNAPSHOT_LIMIT = 40;
|
||||
private const LONG_REQUEST_BUDGET_SECONDS = 8.0;
|
||||
private const LONG_REQUEST_BUDGET_SECONDS = 20.0;
|
||||
private const OVERVIEW_WINDOW_DAYS = 15;
|
||||
private const FX_FETCH_MAX_AGE_HOURS = 3.0;
|
||||
private const BASE_OFFER_SPEEDS = [
|
||||
'fiat' => ['value' => 50.0, 'unit' => 'kH/s'],
|
||||
'crypto' => ['value' => 75.0, 'unit' => 'kH/s'],
|
||||
];
|
||||
|
||||
private string $moduleBasePath;
|
||||
private ModuleConfig $config;
|
||||
@@ -318,7 +322,7 @@ final class Router
|
||||
'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT,
|
||||
]);
|
||||
|
||||
$settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey), [
|
||||
$settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey, $this->bootstrapSettingsOptions($view)), [
|
||||
'project_key' => $projectKey,
|
||||
'baseline_measured_at' => null,
|
||||
'baseline_coins_total' => null,
|
||||
@@ -358,7 +362,7 @@ final class Router
|
||||
'view' => $view,
|
||||
'measurement_count' => is_array($measurements) ? count($measurements) : 0,
|
||||
]);
|
||||
$summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $view), [
|
||||
$summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $walletSnapshots, $view), [
|
||||
'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null,
|
||||
'baseline' => $settings,
|
||||
'targets' => [],
|
||||
@@ -1147,8 +1151,14 @@ final class Router
|
||||
return abs($leftTs - $rightTs);
|
||||
}
|
||||
|
||||
private function settings(string $projectKey): array
|
||||
private function settings(string $projectKey, array $options = []): array
|
||||
{
|
||||
$includeCostPlans = !array_key_exists('cost_plans', $options) || (bool) $options['cost_plans'];
|
||||
$includeCurrencies = !array_key_exists('currencies', $options) || (bool) $options['currencies'];
|
||||
$includePayouts = !array_key_exists('payouts', $options) || (bool) $options['payouts'];
|
||||
$includeMinerOffers = !array_key_exists('miner_offers', $options) || (bool) $options['miner_offers'];
|
||||
$includePurchasedMiners = !array_key_exists('purchased_miners', $options) || (bool) $options['purchased_miners'];
|
||||
|
||||
$settings = $this->repository()->getSettings($projectKey);
|
||||
$base = is_array($settings) ? $settings : [
|
||||
'project_key' => $projectKey,
|
||||
@@ -1173,12 +1183,12 @@ final class Router
|
||||
$base['module_theme_accent'] = 'teal';
|
||||
}
|
||||
|
||||
$base['cost_plans'] = $this->costPlans($projectKey);
|
||||
$base['currencies'] = $this->currencies();
|
||||
$base['cost_plans'] = $includeCostPlans ? $this->costPlans($projectKey) : [];
|
||||
$base['currencies'] = $includeCurrencies ? $this->currencies() : [];
|
||||
$base['preferred_currencies'] = $this->preferredCurrencies($base['preferred_currencies'] ?? null);
|
||||
$base['payouts'] = $this->payouts($projectKey);
|
||||
$base['miner_offers'] = $this->minerOffers($projectKey);
|
||||
$base['purchased_miners'] = $this->purchasedMiners($projectKey);
|
||||
$base['payouts'] = $includePayouts ? $this->payouts($projectKey) : [];
|
||||
$base['miner_offers'] = $includeMinerOffers ? $this->minerOffers($projectKey) : [];
|
||||
$base['purchased_miners'] = $includePurchasedMiners ? $this->purchasedMiners($projectKey) : [];
|
||||
$base['measurement_rates'] = [];
|
||||
return $base;
|
||||
}
|
||||
@@ -1228,9 +1238,13 @@ final class Router
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = in_array($view, ['overview', 'mining'], true)
|
||||
? $this->repository()->listRecentMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT)
|
||||
: $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT);
|
||||
if (in_array($view, ['overview', 'mining'], true)) {
|
||||
$rows = $this->repository()->listRecentMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT);
|
||||
} elseif ($view === 'measurements') {
|
||||
$rows = $this->repository()->listRecentMeasurements($projectKey, 10);
|
||||
} else {
|
||||
$rows = $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT);
|
||||
}
|
||||
|
||||
if (in_array($view, ['overview', 'mining'], true)) {
|
||||
$rows = $this->filterMeasurementsToRecentWindow($rows, self::OVERVIEW_WINDOW_DAYS);
|
||||
@@ -1242,16 +1256,22 @@ final class Router
|
||||
'row_count' => count($rows),
|
||||
'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT,
|
||||
]);
|
||||
return $this->analytics()->enrichMeasurements($rows, $settings);
|
||||
return $this->analytics()->enrichMeasurements($rows, $settings, [
|
||||
'full_latest_only' => in_array($view, ['overview', 'mining'], true),
|
||||
]);
|
||||
}
|
||||
|
||||
private function bootstrapWalletSnapshots(string $projectKey, string $view): array
|
||||
{
|
||||
if (!in_array($view, ['wallet'], true)) {
|
||||
if ($view === 'wallet') {
|
||||
return $this->repository()->listWalletSnapshots($projectKey, 50);
|
||||
}
|
||||
|
||||
if (!in_array($view, ['overview', 'mining'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->repository()->listWalletSnapshots($projectKey, 50);
|
||||
return $this->repository()->listWalletSnapshots($projectKey, 1);
|
||||
}
|
||||
|
||||
private function bootstrapTargets(string $projectKey, string $view): array
|
||||
@@ -1266,14 +1286,19 @@ final class Router
|
||||
|
||||
private function bootstrapFxSnapshots(array $measurements, string $view): array
|
||||
{
|
||||
if (!in_array($view, ['overview', 'mining', 'measurements'], true)) {
|
||||
if (!in_array($view, ['overview', 'mining'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT);
|
||||
$limit = match ($view) {
|
||||
'overview' => 1,
|
||||
default => self::BOOTSTRAP_SNAPSHOT_LIMIT,
|
||||
};
|
||||
|
||||
return $this->measurementFxSnapshots($measurements, $limit);
|
||||
}
|
||||
|
||||
private function bootstrapSummary(array $measurements, array $settings, array $targets, string $view): array
|
||||
private function bootstrapSummary(array $measurements, array $settings, array $targets, array $walletSnapshots, string $view): array
|
||||
{
|
||||
if (!in_array($view, ['overview', 'mining'], true)) {
|
||||
return [
|
||||
@@ -1285,7 +1310,11 @@ final class Router
|
||||
];
|
||||
}
|
||||
|
||||
return $this->analytics()->buildSummary($measurements, $settings, $targets);
|
||||
return $this->analytics()->buildSummary($measurements, $settings, $targets, [
|
||||
'include_offer_scenarios' => $view === 'mining',
|
||||
'include_long_term_projection' => $view === 'mining',
|
||||
'wallet_snapshots' => $walletSnapshots,
|
||||
]);
|
||||
}
|
||||
|
||||
private function filterMeasurementsToRecentWindow(array $rows, int $windowDays): array
|
||||
@@ -1318,6 +1347,54 @@ final class Router
|
||||
: 'overview';
|
||||
}
|
||||
|
||||
private function bootstrapSettingsOptions(string $view): array
|
||||
{
|
||||
return match ($view) {
|
||||
'overview' => [
|
||||
'cost_plans' => true,
|
||||
'currencies' => true,
|
||||
'payouts' => true,
|
||||
'miner_offers' => false,
|
||||
'purchased_miners' => true,
|
||||
],
|
||||
'upload' => [
|
||||
'cost_plans' => false,
|
||||
'currencies' => true,
|
||||
'payouts' => false,
|
||||
'miner_offers' => false,
|
||||
'purchased_miners' => false,
|
||||
],
|
||||
'measurements' => [
|
||||
'cost_plans' => false,
|
||||
'currencies' => false,
|
||||
'payouts' => true,
|
||||
'miner_offers' => false,
|
||||
'purchased_miners' => false,
|
||||
],
|
||||
'wallet' => [
|
||||
'cost_plans' => false,
|
||||
'currencies' => false,
|
||||
'payouts' => false,
|
||||
'miner_offers' => false,
|
||||
'purchased_miners' => false,
|
||||
],
|
||||
'dashboards' => [
|
||||
'cost_plans' => false,
|
||||
'currencies' => false,
|
||||
'payouts' => false,
|
||||
'miner_offers' => false,
|
||||
'purchased_miners' => false,
|
||||
],
|
||||
default => [
|
||||
'cost_plans' => true,
|
||||
'currencies' => true,
|
||||
'payouts' => true,
|
||||
'miner_offers' => true,
|
||||
'purchased_miners' => true,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private function createMeasurement(string $projectKey, array $input): array
|
||||
{
|
||||
$projectTimezone = $this->projectTimezone($projectKey);
|
||||
@@ -1606,14 +1683,18 @@ final class Router
|
||||
private function saveCostPlan(string $projectKey, array $input): array
|
||||
{
|
||||
$projectTimezone = $this->projectTimezone($projectKey);
|
||||
$miningSpeedValue = $this->optionalDecimal($input['mining_speed_value'] ?? null);
|
||||
$miningSpeedUnit = $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null);
|
||||
$bonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
|
||||
$payload = [
|
||||
'label' => $this->requiredString($input['label'] ?? null, 'label', 120),
|
||||
'purchased_at' => $this->requiredDateTime($input['starts_at'] ?? null, 'starts_at', $projectTimezone),
|
||||
'runtime_months' => $this->requiredPositiveInt($input['runtime_months'] ?? null, 'runtime_months'),
|
||||
'mining_speed_value' => $this->optionalDecimal($input['mining_speed_value'] ?? null),
|
||||
'mining_speed_unit' => $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null),
|
||||
'bonus_speed_value' => $this->optionalDecimal($input['bonus_speed_value'] ?? null),
|
||||
'bonus_speed_unit' => $this->optionalSpeedUnit($input['bonus_speed_unit'] ?? null),
|
||||
'mining_speed_value' => $miningSpeedValue,
|
||||
'mining_speed_unit' => $miningSpeedUnit,
|
||||
'bonus_speed_value' => $this->bonusSpeedFromPercent($miningSpeedValue, $miningSpeedUnit, $bonusPercent),
|
||||
'bonus_speed_unit' => $bonusPercent !== null ? $miningSpeedUnit : null,
|
||||
'bonus_percent' => $bonusPercent,
|
||||
'auto_renew' => !empty($input['auto_renew']) ? 1 : 0,
|
||||
'base_price_amount' => $this->requiredDecimal($input['base_price_amount'] ?? ($input['total_cost_amount'] ?? null), 'base_price_amount'),
|
||||
'payment_type' => $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'),
|
||||
@@ -1625,8 +1706,11 @@ final class Router
|
||||
throw new ApiException('Mining-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
|
||||
}
|
||||
|
||||
if (($payload['bonus_speed_value'] === null) xor ($payload['bonus_speed_unit'] === null)) {
|
||||
throw new ApiException('Bonus-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
|
||||
if ($bonusPercent !== null && ($miningSpeedValue === null || $miningSpeedUnit === null)) {
|
||||
throw new ApiException('Bonus-Prozent setzt eine Mining-Geschwindigkeit voraus.', 422);
|
||||
}
|
||||
if ($bonusPercent !== null && $bonusPercent < 0) {
|
||||
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
|
||||
}
|
||||
|
||||
$settings = $this->settings($projectKey);
|
||||
@@ -1714,21 +1798,32 @@ final class Router
|
||||
|
||||
private function saveMinerOffer(string $projectKey, array $input): array
|
||||
{
|
||||
$paymentType = $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type');
|
||||
$baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat'];
|
||||
$bonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
|
||||
$basePriceCurrency = $paymentType === 'crypto'
|
||||
? 'USD'
|
||||
: $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency');
|
||||
$payload = [
|
||||
'label' => $this->requiredString($input['label'] ?? null, 'label', 120),
|
||||
'runtime_months' => $this->optionalPositiveInt($input['runtime_months'] ?? null),
|
||||
'mining_speed_value' => $this->optionalDecimal($input['mining_speed_value'] ?? null),
|
||||
'mining_speed_unit' => $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null),
|
||||
'bonus_speed_value' => $this->optionalDecimal($input['bonus_speed_value'] ?? null),
|
||||
'bonus_speed_unit' => $this->optionalSpeedUnit($input['bonus_speed_unit'] ?? null),
|
||||
'mining_speed_value' => $baseSpeed['value'],
|
||||
'mining_speed_unit' => $baseSpeed['unit'],
|
||||
'bonus_speed_value' => $this->bonusSpeedFromPercent((float) $baseSpeed['value'], (string) $baseSpeed['unit'], $bonusPercent),
|
||||
'bonus_speed_unit' => $bonusPercent !== null ? (string) $baseSpeed['unit'] : null,
|
||||
'bonus_percent' => $bonusPercent,
|
||||
'base_price_amount' => $this->requiredDecimal($input['base_price_amount'] ?? ($input['reference_price_amount'] ?? $input['price_amount'] ?? null), 'base_price_amount'),
|
||||
'base_price_currency' => $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency'),
|
||||
'payment_type' => $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'),
|
||||
'base_price_currency' => $basePriceCurrency,
|
||||
'payment_type' => $paymentType,
|
||||
'auto_renew' => !empty($input['auto_renew']) ? 1 : 0,
|
||||
'note' => $this->optionalString($input['note'] ?? null, 1000),
|
||||
'is_active' => !empty($input['is_active']) ? 1 : 0,
|
||||
];
|
||||
|
||||
if ($bonusPercent !== null && $bonusPercent < 0) {
|
||||
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
|
||||
}
|
||||
|
||||
$this->assertCurrencyType($payload['base_price_currency'], false, 'base_price_currency');
|
||||
|
||||
return $this->repository()->saveMinerOffer($projectKey, $payload);
|
||||
@@ -1744,31 +1839,53 @@ final class Router
|
||||
$settings = $this->settings($projectKey);
|
||||
$cryptoCurrency = $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
|
||||
$isAutoRenew = array_key_exists('auto_renew', $input) ? !empty($input['auto_renew']) : !empty($offer['auto_renew']);
|
||||
$paymentType = $this->enumValue($offer['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type');
|
||||
$selectedSpeedValue = $this->optionalDecimal($input['mining_speed_value'] ?? null);
|
||||
$selectedSpeedUnit = $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null);
|
||||
if (($selectedSpeedValue === null) xor ($selectedSpeedUnit === null)) {
|
||||
throw new ApiException('Mining-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
|
||||
}
|
||||
|
||||
$resolvedSpeedValue = $selectedSpeedValue ?? $this->optionalDecimal($offer['mining_speed_value'] ?? null);
|
||||
$resolvedSpeedUnit = $selectedSpeedUnit ?? $this->optionalSpeedUnit($offer['mining_speed_unit'] ?? null);
|
||||
$selectedBonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
|
||||
if ($selectedBonusPercent !== null && $selectedBonusPercent < 0) {
|
||||
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
|
||||
}
|
||||
$resolvedBonusPercent = $selectedBonusPercent ?? $this->offerBonusPercent($offer);
|
||||
$resolvedBonusValue = $this->bonusSpeedFromPercent($resolvedSpeedValue, $resolvedSpeedUnit, $resolvedBonusPercent);
|
||||
$resolvedBonusUnit = $resolvedBonusValue !== null ? $resolvedSpeedUnit : null;
|
||||
|
||||
$purchaseCurrency = $this->optionalCurrency($input['currency'] ?? null) ?? (string) ($offer['effective_price_currency'] ?? $offer['price_currency'] ?? $offer['base_price_currency'] ?? '');
|
||||
if (!$isAutoRenew) {
|
||||
if ($paymentType === 'crypto' || !$isAutoRenew) {
|
||||
$purchaseCurrency = $cryptoCurrency;
|
||||
}
|
||||
$purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null);
|
||||
if ($purchaseCost === null || !$isAutoRenew) {
|
||||
$purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [
|
||||
if ($purchaseCost === null) {
|
||||
$purchaseCost = $this->resolveOfferPurchaseCost($projectKey, array_merge($offer, [
|
||||
'mining_speed_value' => $resolvedSpeedValue,
|
||||
'mining_speed_unit' => $resolvedSpeedUnit,
|
||||
'bonus_speed_value' => $resolvedBonusValue,
|
||||
'bonus_speed_unit' => $resolvedBonusUnit,
|
||||
'base_price_amount' => $this->optionalDecimal($input['reference_price_amount'] ?? null) ?? ($offer['base_price_amount'] ?? null),
|
||||
'base_price_currency' => $this->optionalCurrency($input['reference_price_currency'] ?? null) ?? ($offer['base_price_currency'] ?? null),
|
||||
'price_currency' => $purchaseCurrency !== '' ? $purchaseCurrency : ($offer['price_currency'] ?? ''),
|
||||
]));
|
||||
}
|
||||
$referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null));
|
||||
$referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null)));
|
||||
$referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null));
|
||||
$referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null)));
|
||||
$purchasedAt = array_key_exists('purchased_at', $input)
|
||||
? $this->requiredDateTime($input['purchased_at'], 'purchased_at', $this->projectTimezone($projectKey))
|
||||
: $this->currentTimestamp();
|
||||
|
||||
return $this->repository()->purchaseMiner($projectKey, $offerId, [
|
||||
'purchased_at' => $purchasedAt,
|
||||
'label' => $offer['label'],
|
||||
'label' => $this->optionalString($input['label'] ?? ($offer['label'] ?? null), 120) ?? $offer['label'],
|
||||
'runtime_months' => $offer['runtime_months'] ?? null,
|
||||
'mining_speed_value' => $offer['mining_speed_value'] ?? null,
|
||||
'mining_speed_unit' => $offer['mining_speed_unit'] ?? null,
|
||||
'bonus_speed_value' => $offer['bonus_speed_value'] ?? null,
|
||||
'bonus_speed_unit' => $offer['bonus_speed_unit'] ?? null,
|
||||
'mining_speed_value' => $resolvedSpeedValue,
|
||||
'mining_speed_unit' => $resolvedSpeedUnit,
|
||||
'bonus_speed_value' => $resolvedBonusValue,
|
||||
'bonus_speed_unit' => $resolvedBonusUnit,
|
||||
'total_cost_amount' => $purchaseCost,
|
||||
'currency' => $purchaseCurrency !== '' ? $purchaseCurrency : $offer['price_currency'],
|
||||
'usd_reference_amount' => $offer['usd_reference_amount'] ?? null,
|
||||
@@ -2347,7 +2464,7 @@ final class Router
|
||||
return abs(time() - $parsed) <= (int) round(max(0.25, $maxAgeHours) * 3600);
|
||||
}
|
||||
|
||||
private function resolveOfferPurchaseCost(array $offer): float
|
||||
private function resolveOfferPurchaseCost(string $projectKey, array $offer): float
|
||||
{
|
||||
$purchaseCurrency = (string) ($offer['price_currency'] ?? '');
|
||||
$baseAmount = is_numeric($offer['base_price_amount'] ?? null)
|
||||
@@ -2358,6 +2475,11 @@ final class Router
|
||||
$baseCurrency = (string) ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ''));
|
||||
|
||||
if ($purchaseCurrency !== '' && $baseAmount !== null && $baseAmount > 0 && $baseCurrency !== '') {
|
||||
$latestConverted = $this->convertWithLatestMeasurement($projectKey, $baseAmount, $baseCurrency, $purchaseCurrency);
|
||||
if ($latestConverted !== null && $latestConverted > 0) {
|
||||
return $latestConverted;
|
||||
}
|
||||
|
||||
$converted = $this->fx()->convert($baseAmount, $baseCurrency, $purchaseCurrency);
|
||||
if (is_numeric($converted) && (float) $converted > 0) {
|
||||
return (float) $converted;
|
||||
@@ -2367,6 +2489,89 @@ final class Router
|
||||
return (float) ($baseAmount ?? $offer['price_amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function convertWithLatestMeasurement(string $projectKey, float $amount, string $fromCurrency, string $toCurrency): ?float
|
||||
{
|
||||
$from = strtoupper(trim($fromCurrency));
|
||||
$to = strtoupper(trim($toCurrency));
|
||||
if ($from === '' || $to === '' || $amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
if ($from === $to) {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
$latestRows = $this->repository()->listRecentMeasurements($projectKey, 1);
|
||||
$latest = $latestRows[0] ?? null;
|
||||
if (!is_array($latest)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$coinCurrency = strtoupper(trim((string) ($latest['coin_currency'] ?? $this->settings($projectKey)['crypto_currency'] ?? '')));
|
||||
$priceCurrency = strtoupper(trim((string) ($latest['price_currency'] ?? '')));
|
||||
$pricePerCoin = is_numeric($latest['price_per_coin'] ?? null) ? (float) $latest['price_per_coin'] : null;
|
||||
if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($from === $priceCurrency && $to === $coinCurrency) {
|
||||
return $amount / $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $to === $priceCurrency) {
|
||||
return $amount * $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($to === $coinCurrency) {
|
||||
$convertedFiat = $this->fx()->convert($amount, $from, $priceCurrency);
|
||||
if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) {
|
||||
return (float) $convertedFiat / $pricePerCoin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency) {
|
||||
$convertedFiat = $amount * $pricePerCoin;
|
||||
$convertedTarget = $this->fx()->convert($convertedFiat, $priceCurrency, $to);
|
||||
if (is_numeric($convertedTarget)) {
|
||||
return (float) $convertedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function bonusSpeedFromPercent(?float $speedValue, ?string $speedUnit, ?float $bonusPercent): ?float
|
||||
{
|
||||
if ($speedValue === null || $speedUnit === null || $bonusPercent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round($speedValue * ($bonusPercent / 100), 4);
|
||||
}
|
||||
|
||||
private function offerBonusPercent(array $offer): ?float
|
||||
{
|
||||
$speedValue = $this->optionalDecimal($offer['mining_speed_value'] ?? null);
|
||||
$speedUnit = $this->optionalSpeedUnit($offer['mining_speed_unit'] ?? null);
|
||||
$bonusValue = $this->optionalDecimal($offer['bonus_speed_value'] ?? null);
|
||||
$bonusUnit = $this->optionalSpeedUnit($offer['bonus_speed_unit'] ?? null);
|
||||
if ($speedValue === null || $speedUnit === null || $bonusValue === null || $bonusUnit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$speedMh = $this->hashrateToMh($speedValue, $speedUnit);
|
||||
$bonusMh = $this->hashrateToMh($bonusValue, $bonusUnit);
|
||||
if ($speedMh <= 0 || $bonusMh <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($bonusMh / $speedMh) * 100;
|
||||
}
|
||||
|
||||
private function hashrateToMh(float $value, string $unit): float
|
||||
{
|
||||
return $unit === 'kH/s' ? $value / 1000 : $value;
|
||||
}
|
||||
|
||||
private function pdo(): PDO
|
||||
{
|
||||
if ($this->pdo === null) {
|
||||
|
||||
@@ -7,6 +7,16 @@ use Modules\MiningChecker\Support\ApiException;
|
||||
|
||||
final class AnalyticsService
|
||||
{
|
||||
private const BASE_OFFER_SPEEDS = [
|
||||
'fiat' => ['value' => 50.0, 'unit' => 'kH/s'],
|
||||
'crypto' => ['value' => 75.0, 'unit' => 'kH/s'],
|
||||
];
|
||||
|
||||
private const OFFER_SPEED_MULTIPLIERS = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
];
|
||||
|
||||
private ?FxService $fx;
|
||||
|
||||
public function __construct(?FxService $fx = null)
|
||||
@@ -14,8 +24,9 @@ final class AnalyticsService
|
||||
$this->fx = $fx;
|
||||
}
|
||||
|
||||
public function enrichMeasurements(array $measurements, array $settings): array
|
||||
public function enrichMeasurements(array $measurements, array $settings, array $options = []): array
|
||||
{
|
||||
$fullLatestOnly = !empty($options['full_latest_only']);
|
||||
$baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0);
|
||||
$baselineAt = (string) ($settings['baseline_measured_at'] ?? '');
|
||||
$costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [];
|
||||
@@ -32,15 +43,18 @@ final class AnalyticsService
|
||||
$previous = null;
|
||||
$previousMeasuredTs = null;
|
||||
$previousIntervalRate = null;
|
||||
$previousCoinCurrency = null;
|
||||
$result = [];
|
||||
$payoutIndex = 0;
|
||||
$lastPayoutTs = null;
|
||||
$payoutsByAsset = [];
|
||||
$latestPayoutByAsset = [];
|
||||
$latestPriceByCurrency = [];
|
||||
|
||||
foreach ($measurements as $row) {
|
||||
$lastIndex = count($measurements) - 1;
|
||||
foreach ($measurements as $index => $row) {
|
||||
$measuredTs = $this->utcTimestamp((string) $row['measured_at']);
|
||||
$coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')));
|
||||
$includeFullDetail = !$fullLatestOnly || $index === $lastIndex;
|
||||
while (isset($payouts[$payoutIndex])) {
|
||||
$payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? ''));
|
||||
if ($payoutTs <= 0 || $payoutTs > $measuredTs) {
|
||||
@@ -48,8 +62,13 @@ final class AnalyticsService
|
||||
}
|
||||
|
||||
$payoutAsset = strtoupper(trim((string) ($payouts[$payoutIndex]['payout_currency'] ?? $coinCurrency)));
|
||||
$payoutsByAsset[$payoutAsset] = ($payoutsByAsset[$payoutAsset] ?? 0.0) + (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
|
||||
$lastPayoutTs = $payoutTs;
|
||||
$payoutAmount = (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
|
||||
$payoutsByAsset[$payoutAsset] = ($payoutsByAsset[$payoutAsset] ?? 0.0) + $payoutAmount;
|
||||
$latestPayoutByAsset[$payoutAsset] = [
|
||||
'payout_at' => (string) ($payouts[$payoutIndex]['payout_at'] ?? ''),
|
||||
'payout_ts' => $payoutTs,
|
||||
'coins_amount' => $payoutAmount,
|
||||
];
|
||||
$payoutIndex++;
|
||||
}
|
||||
|
||||
@@ -68,18 +87,21 @@ final class AnalyticsService
|
||||
$perDayInterval = null;
|
||||
$perHourPerMhInterval = null;
|
||||
$perDayPerMhInterval = null;
|
||||
$hoursSinceLastPayout = null;
|
||||
$coinsSinceLastPayout = null;
|
||||
$perHourSinceLastPayout = null;
|
||||
$perDaySinceLastPayout = null;
|
||||
$lastPayoutAt = null;
|
||||
|
||||
if (is_array($previous) && $previousMeasuredTs !== null) {
|
||||
$intervalStartTs = $previousMeasuredTs;
|
||||
$intervalStartCoins = (float) ($previous['coins_total'] ?? 0.0);
|
||||
|
||||
if ($lastPayoutTs !== null && $lastPayoutTs > $previousMeasuredTs && $lastPayoutTs <= $measuredTs) {
|
||||
$intervalStartTs = $lastPayoutTs;
|
||||
$intervalStartCoins = 0.0;
|
||||
$intervalStartCoins = (float) ($previous['coins_total_effective'] ?? $previous['coins_total'] ?? 0.0);
|
||||
if ($previousCoinCurrency !== null && $previousCoinCurrency !== $coinCurrency) {
|
||||
$intervalStartCoins = $effectiveCoinsTotal;
|
||||
}
|
||||
|
||||
$intervalHours = max(0.0, ($measuredTs - $intervalStartTs) / 3600);
|
||||
$intervalGrowth = $visibleCoinsTotal - $intervalStartCoins;
|
||||
$intervalGrowth = $effectiveCoinsTotal - $intervalStartCoins;
|
||||
$perHourInterval = $intervalHours > 0 ? $intervalGrowth / $intervalHours : null;
|
||||
$perDayInterval = $perHourInterval !== null ? $perHourInterval * 24 : null;
|
||||
if ($perHourInterval !== null && $activeHashrateMh > 0) {
|
||||
@@ -88,6 +110,18 @@ final class AnalyticsService
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($latestPayoutByAsset[$coinCurrency]) && is_array($latestPayoutByAsset[$coinCurrency])) {
|
||||
$lastPayout = $latestPayoutByAsset[$coinCurrency];
|
||||
$lastPayoutTs = (int) ($lastPayout['payout_ts'] ?? 0);
|
||||
if ($lastPayoutTs > 0 && $measuredTs > $lastPayoutTs) {
|
||||
$hoursSinceLastPayout = ($measuredTs - $lastPayoutTs) / 3600;
|
||||
$coinsSinceLastPayout = $visibleCoinsTotal;
|
||||
$perHourSinceLastPayout = $hoursSinceLastPayout > 0 ? $coinsSinceLastPayout / $hoursSinceLastPayout : null;
|
||||
$perDaySinceLastPayout = $perHourSinceLastPayout !== null ? $perHourSinceLastPayout * 24 : null;
|
||||
$lastPayoutAt = (string) ($lastPayout['payout_at'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$trendLabel = 'stabil';
|
||||
if ($perHourInterval !== null && $previousIntervalRate !== null) {
|
||||
$delta = $perHourInterval - $previousIntervalRate;
|
||||
@@ -134,20 +168,23 @@ final class AnalyticsService
|
||||
}
|
||||
}
|
||||
|
||||
$effectiveDailyCost = $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row);
|
||||
$currentValue = $price !== null ? $visibleCoinsTotal * $price : null;
|
||||
$currentValueEffective = $price !== null ? $effectiveCoinsTotal * $price : null;
|
||||
$theoreticalDailyRevenue = ($price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null;
|
||||
$effectiveDailyCost = $includeFullDetail ? $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row) : null;
|
||||
$currentValue = ($includeFullDetail && $price !== null) ? $visibleCoinsTotal * $price : null;
|
||||
$currentValueEffective = ($includeFullDetail && $price !== null) ? $effectiveCoinsTotal * $price : null;
|
||||
$theoreticalDailyRevenue = ($includeFullDetail && $price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null;
|
||||
$theoreticalDailyProfit = (
|
||||
$includeFullDetail &&
|
||||
$theoreticalDailyRevenue !== null &&
|
||||
$effectiveDailyCost !== null
|
||||
) ? $theoreticalDailyRevenue - $effectiveDailyCost : null;
|
||||
$breakEvenPricePerCoin = (
|
||||
$includeFullDetail &&
|
||||
$effectiveDailyCost !== null &&
|
||||
$perDayInterval !== null &&
|
||||
$perDayInterval > 0
|
||||
) ? $effectiveDailyCost / $perDayInterval : null;
|
||||
$profitMarginPercent = (
|
||||
$includeFullDetail &&
|
||||
$theoreticalDailyRevenue !== null &&
|
||||
$theoreticalDailyRevenue > 0 &&
|
||||
$theoreticalDailyProfit !== null
|
||||
@@ -159,7 +196,7 @@ final class AnalyticsService
|
||||
$normalizedFlags = is_array($decoded) ? $decoded : [$normalizedFlags];
|
||||
}
|
||||
|
||||
$result[] = array_merge($row, [
|
||||
$enrichedRow = array_merge($row, [
|
||||
'coins_total_visible' => $this->roundOrNull($visibleCoinsTotal, 6),
|
||||
'coins_total_effective' => $this->roundOrNull($effectiveCoinsTotal, 6),
|
||||
'coin_currency' => $coinCurrency,
|
||||
@@ -175,6 +212,11 @@ final class AnalyticsService
|
||||
'doge_per_day_interval' => $this->roundOrNull($perDayInterval, 6),
|
||||
'doge_per_hour_per_mh_interval' => $this->roundOrNull($perHourPerMhInterval, 8),
|
||||
'doge_per_day_per_mh_interval' => $this->roundOrNull($perDayPerMhInterval, 8),
|
||||
'last_payout_at' => $lastPayoutAt,
|
||||
'hours_since_last_payout' => $this->roundOrNull($hoursSinceLastPayout, 4),
|
||||
'coins_since_last_payout' => $this->roundOrNull($coinsSinceLastPayout, 6),
|
||||
'doge_per_hour_since_last_payout' => $this->roundOrNull($perHourSinceLastPayout, 6),
|
||||
'doge_per_day_since_last_payout' => $this->roundOrNull($perDaySinceLastPayout, 6),
|
||||
'trend_label' => $trendLabel,
|
||||
'effective_price_per_coin' => $this->roundOrNull($price, 8),
|
||||
'effective_price_currency' => $priceCurrency,
|
||||
@@ -190,19 +232,24 @@ final class AnalyticsService
|
||||
'measured_date' => substr((string) $row['measured_at'], 0, 10),
|
||||
'ocr_flags' => is_array($normalizedFlags) ? $normalizedFlags : [],
|
||||
]);
|
||||
$result[] = $enrichedRow;
|
||||
|
||||
if ($perHourInterval !== null) {
|
||||
$previousIntervalRate = $perHourInterval;
|
||||
}
|
||||
$previous = $row;
|
||||
$previous = $enrichedRow;
|
||||
$previousMeasuredTs = $measuredTs > 0 ? $measuredTs : null;
|
||||
$previousCoinCurrency = $coinCurrency;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function buildSummary(array $measurements, array $settings, array $targets): array
|
||||
public function buildSummary(array $measurements, array $settings, array $targets, array $options = []): array
|
||||
{
|
||||
$includeOfferScenarios = !array_key_exists('include_offer_scenarios', $options) || (bool) $options['include_offer_scenarios'];
|
||||
$includeLongTermProjection = !array_key_exists('include_long_term_projection', $options) || (bool) $options['include_long_term_projection'];
|
||||
|
||||
if ($measurements === []) {
|
||||
return [
|
||||
'latest_measurement' => null,
|
||||
@@ -237,7 +284,7 @@ final class AnalyticsService
|
||||
$purchasedMiners
|
||||
));
|
||||
$offerSummary = [];
|
||||
foreach ($minerOffers as $offer) {
|
||||
foreach ($this->expandOfferVariants($minerOffers, $settings) as $offer) {
|
||||
$offerSummary[] = $this->evaluateMinerOffer($offer, $latest, $latestPriceByCurrency, $currentHashrateMh, $settings);
|
||||
}
|
||||
|
||||
@@ -248,7 +295,7 @@ final class AnalyticsService
|
||||
$linkedOffer = null;
|
||||
if (is_numeric($target['miner_offer_id'] ?? null)) {
|
||||
foreach ($offerSummary as $offer) {
|
||||
if ((int) ($offer['id'] ?? 0) === (int) $target['miner_offer_id']) {
|
||||
if ((int) ($offer['base_offer_id'] ?? $offer['id'] ?? 0) === (int) $target['miner_offer_id']) {
|
||||
$linkedOffer = $offer;
|
||||
break;
|
||||
}
|
||||
@@ -322,9 +369,22 @@ final class AnalyticsService
|
||||
)
|
||||
: null;
|
||||
$walletBalances = $this->walletBalances($payouts, $purchasedMiners, $latestMeasuredTs);
|
||||
$latestWalletSnapshot = $this->latestWalletSnapshot(is_array($options['wallet_snapshots'] ?? null) ? $options['wallet_snapshots'] : []);
|
||||
if (is_array($latestWalletSnapshot)) {
|
||||
$snapshotBalances = $this->walletSnapshotBalances($latestWalletSnapshot);
|
||||
if ($snapshotBalances !== []) {
|
||||
$walletBalances = $snapshotBalances;
|
||||
}
|
||||
}
|
||||
$walletBalanceCurrentAsset = (float) ($walletBalances[strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')))] ?? 0.0);
|
||||
$holdingsCurrentAsset = $walletBalanceCurrentAsset + (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0);
|
||||
$walletValue = $latestCurrency !== '' ? $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) : null;
|
||||
$walletValue = $latestCurrency !== ''
|
||||
? (
|
||||
is_array($latestWalletSnapshot)
|
||||
? $this->walletSnapshotValue($latestWalletSnapshot, $latestCurrency, $latest)
|
||||
: $this->walletBalanceValue($walletBalances, $latestCurrency, $latest)
|
||||
)
|
||||
: null;
|
||||
$currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null;
|
||||
$totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null)
|
||||
? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0)
|
||||
@@ -350,33 +410,41 @@ final class AnalyticsService
|
||||
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
|
||||
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
|
||||
'wallet_value' => $this->roundOrNull($walletValue, 8),
|
||||
'wallet_snapshot_measured_at' => is_array($latestWalletSnapshot) ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null,
|
||||
'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8),
|
||||
'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8),
|
||||
'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4),
|
||||
'break_even_eta_at' => $breakEvenProjection['eta'] ?? null,
|
||||
]);
|
||||
$currentProjection = $this->projectPerformance(
|
||||
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
|
||||
$purchasedMiners,
|
||||
$latestSummary,
|
||||
730
|
||||
);
|
||||
$latestSummary = array_merge($latestSummary, [
|
||||
'projection_days' => $currentProjection['days'],
|
||||
'projection_two_year_revenue' => $this->roundOrNull($currentProjection['revenue'], 8),
|
||||
'projection_two_year_cost' => $this->roundOrNull($currentProjection['cost'], 8),
|
||||
'projection_two_year_profit' => $this->roundOrNull($currentProjection['profit'], 8),
|
||||
]);
|
||||
$offerSummary = array_map(
|
||||
fn (array $offer): array => $this->enrichOfferScenario(
|
||||
$offer,
|
||||
$latestSummary,
|
||||
$currentHashrateMh,
|
||||
if ($includeLongTermProjection) {
|
||||
$currentProjection = $this->projectPerformance(
|
||||
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
|
||||
$purchasedMiners
|
||||
),
|
||||
$offerSummary
|
||||
);
|
||||
$purchasedMiners,
|
||||
$latestSummary,
|
||||
730
|
||||
);
|
||||
$latestSummary = array_merge($latestSummary, [
|
||||
'projection_days' => $currentProjection['days'],
|
||||
'projection_two_year_revenue' => $this->roundOrNull($currentProjection['revenue'], 8),
|
||||
'projection_two_year_cost' => $this->roundOrNull($currentProjection['cost'], 8),
|
||||
'projection_two_year_profit' => $this->roundOrNull($currentProjection['profit'], 8),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($includeOfferScenarios) {
|
||||
$offerSummary = array_map(
|
||||
fn (array $offer): array => $this->enrichOfferScenario(
|
||||
$offer,
|
||||
$latestSummary,
|
||||
$currentHashrateMh,
|
||||
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
|
||||
$purchasedMiners
|
||||
),
|
||||
$offerSummary
|
||||
);
|
||||
} else {
|
||||
$offerSummary = [];
|
||||
}
|
||||
|
||||
return [
|
||||
'latest_measurement' => $latestSummary,
|
||||
@@ -595,6 +663,11 @@ final class AnalyticsService
|
||||
return $amount;
|
||||
}
|
||||
|
||||
$measurementConverted = $this->convertAmountFromMeasurementPrice($amount, $from, $to, $fxContext);
|
||||
if ($measurementConverted !== null) {
|
||||
return $measurementConverted;
|
||||
}
|
||||
|
||||
if ($this->fx === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -607,6 +680,62 @@ final class AnalyticsService
|
||||
return $this->fx->convertAt($amount, $from, $to, $at, null, $fetchId);
|
||||
}
|
||||
|
||||
private function convertAmountFromMeasurementPrice(float $amount, string $from, string $to, ?array $fxContext = null): ?float
|
||||
{
|
||||
if (!is_array($fxContext)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$coinCurrency = strtoupper(trim((string) ($fxContext['coin_currency'] ?? '')));
|
||||
$priceCurrency = strtoupper(trim((string) ($fxContext['effective_price_currency'] ?? $fxContext['price_currency'] ?? '')));
|
||||
$pricePerCoin = is_numeric($fxContext['effective_price_per_coin'] ?? null)
|
||||
? (float) $fxContext['effective_price_per_coin']
|
||||
: (is_numeric($fxContext['price_per_coin'] ?? null) ? (float) $fxContext['price_per_coin'] : null);
|
||||
|
||||
if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $to === $priceCurrency) {
|
||||
return $amount * $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($from === $priceCurrency && $to === $coinCurrency) {
|
||||
return $amount / $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($to === $coinCurrency && $this->fx !== null) {
|
||||
$convertedFiat = $this->fx->convertAt(
|
||||
$amount,
|
||||
$from,
|
||||
$priceCurrency,
|
||||
is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null,
|
||||
null,
|
||||
isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null
|
||||
);
|
||||
if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) {
|
||||
return (float) $convertedFiat / $pricePerCoin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $this->fx !== null) {
|
||||
$convertedFiat = $amount * $pricePerCoin;
|
||||
$convertedTarget = $this->fx->convertAt(
|
||||
$convertedFiat,
|
||||
$priceCurrency,
|
||||
$to,
|
||||
is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null,
|
||||
null,
|
||||
isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null
|
||||
);
|
||||
if (is_numeric($convertedTarget)) {
|
||||
return (float) $convertedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function totalHashrateMh(array $entries): float
|
||||
{
|
||||
$total = 0.0;
|
||||
@@ -794,6 +923,102 @@ final class AnalyticsService
|
||||
};
|
||||
}
|
||||
|
||||
private function expandOfferVariants(array $offers, array $settings): array
|
||||
{
|
||||
$expanded = [];
|
||||
foreach ($offers as $offer) {
|
||||
$paymentType = $this->normalizeOfferPaymentType($offer);
|
||||
$baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat'];
|
||||
$baseSpeedValue = (float) $baseSpeed['value'];
|
||||
$baseSpeedUnit = (string) $baseSpeed['unit'];
|
||||
$baseBonusPercent = $this->deriveBonusPercent($offer, $baseSpeedValue, $baseSpeedUnit);
|
||||
$basePriceAmount = is_numeric($offer['base_price_amount'] ?? null)
|
||||
? (float) $offer['base_price_amount']
|
||||
: (is_numeric($offer['reference_price_amount'] ?? null)
|
||||
? (float) $offer['reference_price_amount']
|
||||
: (is_numeric($offer['usd_reference_amount'] ?? null)
|
||||
? (float) $offer['usd_reference_amount']
|
||||
: (is_numeric($offer['price_amount'] ?? null) ? (float) $offer['price_amount'] : null)));
|
||||
|
||||
foreach (self::OFFER_SPEED_MULTIPLIERS as $multiplier) {
|
||||
$derivedSpeedValue = $baseSpeedValue * $multiplier;
|
||||
$derivedBonusValue = $baseBonusPercent !== null
|
||||
? round($derivedSpeedValue * ($baseBonusPercent / 100), 4)
|
||||
: null;
|
||||
$derivedPriceAmount = $basePriceAmount !== null
|
||||
? round($basePriceAmount * $multiplier, 8)
|
||||
: null;
|
||||
|
||||
$expanded[] = array_merge($offer, [
|
||||
'id' => sprintf('%s:%s:%s', (string) ($offer['id'] ?? 'offer'), $paymentType, rtrim(rtrim(number_format($derivedSpeedValue, 4, '.', ''), '0'), '.')),
|
||||
'base_offer_id' => $offer['id'] ?? null,
|
||||
'base_offer_label' => $offer['label'] ?? null,
|
||||
'payment_type' => $paymentType,
|
||||
'label' => trim(((string) ($offer['label'] ?? 'Miner-Angebot')) . ' · ' . $this->formatSpeedLabel($derivedSpeedValue, $baseSpeedUnit)),
|
||||
'mining_speed_value' => $derivedSpeedValue,
|
||||
'mining_speed_unit' => $baseSpeedUnit,
|
||||
'bonus_speed_value' => $derivedBonusValue,
|
||||
'bonus_speed_unit' => $derivedBonusValue !== null ? $baseSpeedUnit : null,
|
||||
'bonus_percent' => $baseBonusPercent,
|
||||
'speed_factor' => $multiplier,
|
||||
'base_speed_value' => $baseSpeedValue,
|
||||
'base_speed_unit' => $baseSpeedUnit,
|
||||
'base_price_amount' => $derivedPriceAmount,
|
||||
'reference_price_amount' => $derivedPriceAmount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
usort($expanded, static function (array $left, array $right): int {
|
||||
$paymentCompare = strcmp((string) ($left['payment_type'] ?? ''), (string) ($right['payment_type'] ?? ''));
|
||||
if ($paymentCompare !== 0) {
|
||||
return $paymentCompare;
|
||||
}
|
||||
|
||||
$runtimeCompare = ((int) ($left['runtime_months'] ?? 0)) <=> ((int) ($right['runtime_months'] ?? 0));
|
||||
if ($runtimeCompare !== 0) {
|
||||
return $runtimeCompare;
|
||||
}
|
||||
|
||||
return ((int) ($left['speed_factor'] ?? 0)) <=> ((int) ($right['speed_factor'] ?? 0));
|
||||
});
|
||||
|
||||
return $expanded;
|
||||
}
|
||||
|
||||
private function normalizeOfferPaymentType(array $offer): string
|
||||
{
|
||||
$paymentType = strtolower(trim((string) ($offer['payment_type'] ?? '')));
|
||||
if (in_array($paymentType, ['fiat', 'crypto'], true)) {
|
||||
return $paymentType;
|
||||
}
|
||||
|
||||
return !empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true)
|
||||
? 'crypto'
|
||||
: 'fiat';
|
||||
}
|
||||
|
||||
private function deriveBonusPercent(array $offer, float $speedValue, string $speedUnit): ?float
|
||||
{
|
||||
if (is_numeric($offer['bonus_percent'] ?? null)) {
|
||||
$numeric = (float) $offer['bonus_percent'];
|
||||
return $numeric >= 0 ? $numeric : null;
|
||||
}
|
||||
|
||||
$baseSpeedMh = $this->normalizeHashrateMh($speedValue, $speedUnit);
|
||||
$bonusMh = $this->normalizeHashrateMh($offer['bonus_speed_value'] ?? null, $offer['bonus_speed_unit'] ?? null);
|
||||
if ($baseSpeedMh <= 0 || $bonusMh <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($bonusMh / $baseSpeedMh) * 100;
|
||||
}
|
||||
|
||||
private function formatSpeedLabel(float $value, string $unit): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 4, '.', ''), '0'), '.') . ' ' . $unit;
|
||||
}
|
||||
|
||||
private function evaluateMinerOffer(array $offer, array $latest, array $latestPriceByCurrency, float $currentHashrateMh, array $settings): array
|
||||
{
|
||||
$offerHashrateMh = $this->normalizeHashrateMh($offer['mining_speed_value'] ?? null, $offer['mining_speed_unit'] ?? null)
|
||||
@@ -821,6 +1046,8 @@ final class AnalyticsService
|
||||
$convertedReference = $this->convertAmount($basePriceAmount, $basePriceCurrency, $effectivePriceCurrency, $latest);
|
||||
if ($convertedReference !== null && $convertedReference > 0) {
|
||||
$effectivePriceAmount = $convertedReference;
|
||||
} else {
|
||||
$effectivePriceAmount = null;
|
||||
}
|
||||
}
|
||||
$referencePriceAmount = $basePriceAmount;
|
||||
@@ -1111,6 +1338,114 @@ final class AnalyticsService
|
||||
return array_map(fn (float $value): float => round($value, 8), $balances);
|
||||
}
|
||||
|
||||
private function latestWalletSnapshot(array $walletSnapshots): ?array
|
||||
{
|
||||
foreach ($walletSnapshots as $snapshot) {
|
||||
if (is_array($snapshot)) {
|
||||
return $snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function walletSnapshotBalances(array $snapshot): array
|
||||
{
|
||||
$balances = [];
|
||||
$rawBalances = is_array($snapshot['balances_json'] ?? null) ? $snapshot['balances_json'] : [];
|
||||
foreach ($rawBalances as $code => $asset) {
|
||||
$normalizedCode = strtoupper(trim((string) $code));
|
||||
if ($normalizedCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$balance = is_array($asset)
|
||||
? ($asset['balance'] ?? null)
|
||||
: $asset;
|
||||
if (!is_numeric($balance)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$balances[$normalizedCode] = round((float) $balance, 8);
|
||||
}
|
||||
|
||||
ksort($balances);
|
||||
return $balances;
|
||||
}
|
||||
|
||||
private function walletSnapshotValue(array $snapshot, string $targetCurrency, ?array $fxContext = null): ?float
|
||||
{
|
||||
$target = strtoupper(trim($targetCurrency));
|
||||
if ($target === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reportedTotal = is_numeric($snapshot['total_value_amount'] ?? null)
|
||||
? (float) $snapshot['total_value_amount']
|
||||
: null;
|
||||
$reportedCurrency = strtoupper(trim((string) ($snapshot['total_value_currency'] ?? '')));
|
||||
if ($reportedTotal !== null && $reportedTotal >= 0 && $reportedCurrency !== '') {
|
||||
$convertedTotal = $reportedCurrency === $target
|
||||
? $reportedTotal
|
||||
: $this->convertAmount($reportedTotal, $reportedCurrency, $target, $fxContext);
|
||||
if ($convertedTotal !== null) {
|
||||
return $convertedTotal;
|
||||
}
|
||||
}
|
||||
|
||||
$balances = is_array($snapshot['balances_json'] ?? null) ? $snapshot['balances_json'] : [];
|
||||
if ($balances === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$total = 0.0;
|
||||
$matched = false;
|
||||
foreach ($balances as $code => $asset) {
|
||||
$currency = strtoupper(trim((string) $code));
|
||||
if ($currency === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$balance = is_array($asset) ? ($asset['balance'] ?? null) : $asset;
|
||||
if (!is_numeric($balance)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$numericBalance = (float) $balance;
|
||||
if ($currency === $target) {
|
||||
$total += $numericBalance;
|
||||
$matched = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshotPrice = is_array($asset) && is_numeric($asset['price_amount'] ?? null)
|
||||
? (float) $asset['price_amount']
|
||||
: null;
|
||||
$snapshotPriceCurrency = is_array($asset)
|
||||
? strtoupper(trim((string) ($asset['price_currency'] ?? '')))
|
||||
: '';
|
||||
|
||||
if ($snapshotPrice !== null && $snapshotPrice > 0 && $snapshotPriceCurrency !== '') {
|
||||
$convertedPrice = $snapshotPriceCurrency === $target
|
||||
? $snapshotPrice
|
||||
: $this->convertAmount($snapshotPrice, $snapshotPriceCurrency, $target, $fxContext);
|
||||
if ($convertedPrice !== null) {
|
||||
$total += $numericBalance * $convertedPrice;
|
||||
$matched = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$convertedBalance = $this->convertAmount($numericBalance, $currency, $target, $fxContext);
|
||||
if ($convertedBalance !== null) {
|
||||
$total += $convertedBalance;
|
||||
$matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $matched ? $total : null;
|
||||
}
|
||||
|
||||
private function walletBalanceValue(array $balances, string $targetCurrency, ?array $fxContext = null): ?float
|
||||
{
|
||||
$target = strtoupper(trim($targetCurrency));
|
||||
@@ -1179,44 +1514,116 @@ final class AnalyticsService
|
||||
}
|
||||
|
||||
$dogePerDayPerMh = $dogePerDay / $currentHashrateMh;
|
||||
$cumulativeRevenue = 0.0;
|
||||
$maxDays = 3650;
|
||||
$horizonTs = $baseTs + ($maxDays * 86400);
|
||||
$entries = [];
|
||||
|
||||
for ($day = 0; $day <= $maxDays; $day++) {
|
||||
$dayHashrate = 0.0;
|
||||
$dayTs = $baseTs + ($day * 86400);
|
||||
foreach ($costPlans as $plan) {
|
||||
if (!empty($plan['is_active']) && $this->entryIsCovered($plan, $dayTs)) {
|
||||
$dayHashrate += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null);
|
||||
$dayHashrate += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
|
||||
}
|
||||
}
|
||||
foreach ($purchasedMiners as $miner) {
|
||||
if ((array_key_exists('is_active', $miner) && empty($miner['is_active'])) || !$this->entryIsCovered($miner, $dayTs, 'purchased_at')) {
|
||||
continue;
|
||||
}
|
||||
$dayHashrate += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null);
|
||||
$dayHashrate += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
|
||||
}
|
||||
|
||||
if ($dayHashrate <= 0) {
|
||||
foreach ($costPlans as $plan) {
|
||||
if (empty($plan['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = ['data' => $plan, 'start_field' => 'starts_at'];
|
||||
}
|
||||
|
||||
$dayRevenue = $dayHashrate * $dogePerDayPerMh * $pricePerCoin;
|
||||
$cumulativeRevenue += $dayRevenue;
|
||||
if ($cumulativeRevenue >= $remainingAmount) {
|
||||
$etaTs = (int) round($baseTs + ($day * 86400));
|
||||
return [
|
||||
'days' => (float) $day,
|
||||
'eta' => $this->formatUtcTimestamp($etaTs),
|
||||
];
|
||||
foreach ($purchasedMiners as $miner) {
|
||||
if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = ['data' => $miner, 'start_field' => 'purchased_at'];
|
||||
}
|
||||
|
||||
$events = [$baseTs, $horizonTs];
|
||||
foreach ($entries as $entryMeta) {
|
||||
$entry = $entryMeta['data'];
|
||||
$startField = $entryMeta['start_field'];
|
||||
$startTs = $this->utcTimestamp((string) ($entry[$startField] ?? ''));
|
||||
if ($startTs > $baseTs && $startTs < $horizonTs) {
|
||||
$events[] = $startTs;
|
||||
}
|
||||
|
||||
$endTs = $this->entryCoverageEndTimestamp($entry, $startField);
|
||||
if ($endTs !== null) {
|
||||
$afterEndTs = $endTs + 1;
|
||||
if ($afterEndTs > $baseTs && $afterEndTs < $horizonTs) {
|
||||
$events[] = $afterEndTs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$events = array_values(array_unique(array_map('intval', $events)));
|
||||
sort($events, SORT_NUMERIC);
|
||||
|
||||
$cumulativeRevenue = 0.0;
|
||||
for ($index = 0, $maxIndex = count($events) - 1; $index < $maxIndex; $index++) {
|
||||
$segmentStartTs = $events[$index];
|
||||
$segmentEndTs = $events[$index + 1];
|
||||
if ($segmentEndTs <= $segmentStartTs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$segmentHashrate = 0.0;
|
||||
foreach ($entries as $entryMeta) {
|
||||
$entry = $entryMeta['data'];
|
||||
$startField = $entryMeta['start_field'];
|
||||
if (!$this->entryIsCovered($entry, $segmentStartTs, $startField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$segmentHashrate += $this->normalizeHashrateMh($entry['mining_speed_value'] ?? null, $entry['mining_speed_unit'] ?? null);
|
||||
$segmentHashrate += $this->normalizeHashrateMh($entry['bonus_speed_value'] ?? null, $entry['bonus_speed_unit'] ?? null);
|
||||
}
|
||||
|
||||
if ($segmentHashrate <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$segmentDays = ($segmentEndTs - $segmentStartTs) / 86400;
|
||||
if ($segmentDays <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$segmentRevenuePerDay = $segmentHashrate * $dogePerDayPerMh * $pricePerCoin;
|
||||
if ($segmentRevenuePerDay <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$segmentRevenue = $segmentRevenuePerDay * $segmentDays;
|
||||
if ($cumulativeRevenue + $segmentRevenue >= $remainingAmount) {
|
||||
$remainingSegmentAmount = $remainingAmount - $cumulativeRevenue;
|
||||
$segmentOffsetDays = $remainingSegmentAmount / $segmentRevenuePerDay;
|
||||
$etaTs = (int) round($segmentStartTs + ($segmentOffsetDays * 86400));
|
||||
return [
|
||||
'days' => round(($etaTs - $baseTs) / 86400, 4),
|
||||
'eta' => $this->formatUtcTimestamp($etaTs),
|
||||
];
|
||||
}
|
||||
|
||||
$cumulativeRevenue += $segmentRevenue;
|
||||
}
|
||||
|
||||
return ['days' => null, 'eta' => null];
|
||||
}
|
||||
|
||||
private function entryCoverageEndTimestamp(array $entry, string $startField = 'starts_at'): ?int
|
||||
{
|
||||
if (!empty($entry['auto_renew'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$runtimeMonths = (int) ($entry['runtime_months'] ?? 0);
|
||||
if ($runtimeMonths <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$startTs = $this->utcTimestamp((string) ($entry[$startField] ?? ''));
|
||||
if ($startTs <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
return (int) round($startTs + ($runtimeDays * 86400));
|
||||
}
|
||||
|
||||
private function utcTimestamp(?string $value): int
|
||||
{
|
||||
$normalized = trim((string) $value);
|
||||
|
||||
@@ -437,6 +437,14 @@ final class OcrService
|
||||
}
|
||||
}
|
||||
|
||||
$measurementIndicators = 0;
|
||||
$normalizedLower = strtolower($normalizedText);
|
||||
foreach (['mining-guthaben', 'mining guthaben', 'mining-balance', 'mining balance', 'doge /', 'bonus', 'verlauf'] as $indicator) {
|
||||
if (str_contains($normalizedLower, $indicator)) {
|
||||
$measurementIndicators++;
|
||||
}
|
||||
}
|
||||
|
||||
$matchedFields = 0;
|
||||
foreach ([$coinsTotal, $price, $currency] as $field) {
|
||||
if ($field !== null) {
|
||||
@@ -444,7 +452,8 @@ final class OcrService
|
||||
}
|
||||
}
|
||||
|
||||
$confidence = max(0.05, min(0.99, ($matchedFields / 3) - (count($flags) * 0.04)));
|
||||
$score = $matchedFields + min(3, $measurementIndicators);
|
||||
$confidence = max(0.05, min(0.99, ($matchedFields / 3) + (min(3, $measurementIndicators) * 0.08) - (count($flags) * 0.04)));
|
||||
|
||||
return [
|
||||
'suggested' => [
|
||||
@@ -457,7 +466,7 @@ final class OcrService
|
||||
],
|
||||
'confidence' => round($confidence, 4),
|
||||
'flags' => $flags,
|
||||
'score' => $matchedFields,
|
||||
'score' => $score,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -529,7 +538,11 @@ final class OcrService
|
||||
}
|
||||
}
|
||||
|
||||
$priceAmount = $this->pickWalletUnitPrice($balanceAmount, $usdCandidates);
|
||||
$priceAmount = $this->pickWalletUnitPrice(
|
||||
$balanceAmount,
|
||||
$usdCandidates,
|
||||
$totalValueCurrency === 'USD' ? $totalValueAmount : null
|
||||
);
|
||||
$balances[$currency] = [
|
||||
'balance' => $balanceAmount,
|
||||
'price_amount' => $priceAmount,
|
||||
@@ -604,7 +617,7 @@ final class OcrService
|
||||
/**
|
||||
* @param list<float|int> $candidates
|
||||
*/
|
||||
private function pickWalletUnitPrice(float $balance, array $candidates): ?float
|
||||
private function pickWalletUnitPrice(float $balance, array $candidates, ?float $walletTotal = null): ?float
|
||||
{
|
||||
$candidates = array_values(array_filter(array_map(
|
||||
static fn (mixed $value): float => round((float) $value, 8),
|
||||
@@ -615,7 +628,11 @@ final class OcrService
|
||||
return null;
|
||||
}
|
||||
if (count($candidates) === 1) {
|
||||
return $candidates[0];
|
||||
$candidate = $candidates[0];
|
||||
if ($walletTotal !== null && $walletTotal > 0 && ($balance * $candidate) > ($walletTotal * 1.5)) {
|
||||
return round($candidate / $balance, 8);
|
||||
}
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
$bestPrice = null;
|
||||
|
||||
347
partials/landingpages/dashboard.php
Normal file
347
partials/landingpages/dashboard.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?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);
|
||||
} elseif ($itemType === 'app') {
|
||||
$config['app_id'] = (int) ($_POST['app_id'] ?? 0);
|
||||
} elseif ($itemType === 'widget_template') {
|
||||
$config['widget_template_id'] = (int) ($_POST['widget_template_id'] ?? 0);
|
||||
} elseif ($itemType === 'bookmark_group') {
|
||||
$config['bookmarks'] = trim((string) ($_POST['bookmarks'] ?? ''));
|
||||
} 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);
|
||||
$availableApps = $service->listApps($ownerKey, true);
|
||||
$availableWidgetTemplates = $service->listWidgetTemplates($ownerKey, true);
|
||||
$pageModuleMap = [];
|
||||
foreach ($ownPageModules as $pageModule) {
|
||||
$pageModuleMap[(int) ($pageModule['id'] ?? 0)] = $pageModule;
|
||||
}
|
||||
$appMap = [];
|
||||
foreach ($availableApps as $appEntry) {
|
||||
$appMap[(int) ($appEntry['id'] ?? 0)] = $appEntry;
|
||||
}
|
||||
$widgetTemplateMap = [];
|
||||
foreach ($availableWidgetTemplates as $template) {
|
||||
$widgetTemplateMap[(int) ($template['id'] ?? 0)] = $template;
|
||||
}
|
||||
|
||||
$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">Verfügbar sind direkte Links, iFrames, persönliche Linklisten, globale Apps, Seitenmodule und wiederverwendbare Widgets.</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="bookmark_group">Linkliste</option>
|
||||
<option value="app">App</option>
|
||||
<option value="page_module">Seitenmodul</option>
|
||||
<option value="widget_template">Widget-Vorlage</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-bookmarks hidden>
|
||||
<span>Linkliste</span>
|
||||
<textarea name="bookmarks" rows="5" placeholder="Bezeichnung | https://ziel.example Nexus | /dashboard"></textarea>
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-app hidden>
|
||||
<span>App</span>
|
||||
<select name="app_id">
|
||||
<option value="0">Bitte wählen</option>
|
||||
<?php foreach ($availableApps as $appEntry): ?>
|
||||
<option value="<?= (int) ($appEntry['id'] ?? 0) ?>"><?= e((string) ($appEntry['name'] ?? 'App')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</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>
|
||||
<label class="setup-field muted" data-dashboard-widget-template hidden>
|
||||
<span>Widget-Vorlage</span>
|
||||
<select name="widget_template_id">
|
||||
<option value="0">Bitte wählen</option>
|
||||
<?php foreach ($availableWidgetTemplates as $template): ?>
|
||||
<option value="<?= (int) ($template['id'] ?? 0) ?>"><?= e((string) ($template['name'] ?? 'Widget')) ?></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'] : [];
|
||||
if ($itemType === 'widget_template' && !empty($config['widget_template_id']) && isset($widgetTemplateMap[(int) $config['widget_template_id']])) {
|
||||
$template = $widgetTemplateMap[(int) $config['widget_template_id']];
|
||||
$itemType = (string) ($template['widget_type'] ?? $itemType);
|
||||
$config = array_merge(is_array($template['config'] ?? null) ? $template['config'] : [], $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;
|
||||
}
|
||||
$appEntry = null;
|
||||
if ($itemType === 'app' && !empty($config['app_id'])) {
|
||||
$appEntry = $appMap[(int) $config['app_id']] ?? null;
|
||||
}
|
||||
$targetUrl = trim((string) ($config['url'] ?? ''));
|
||||
if ($pageModule !== null) {
|
||||
$targetUrl = trim((string) ($pageModule['target_url'] ?? $targetUrl));
|
||||
}
|
||||
if ($appEntry !== null) {
|
||||
$targetUrl = trim((string) ($appEntry['app_url'] ?? $targetUrl));
|
||||
}
|
||||
$bookmarks = [];
|
||||
if ($itemType === 'bookmark_group') {
|
||||
foreach (preg_split('/\r\n|\r|\n/', trim((string) ($config['bookmarks'] ?? ''))) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
[$label, $url] = array_pad(array_map('trim', explode('|', $line, 2)), 2, '');
|
||||
if ($label !== '' && $url !== '') {
|
||||
$bookmarks[] = ['label' => $label, 'url' => $url];
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<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 === 'bookmark_group' && $bookmarks !== []): ?>
|
||||
<div class="dashboard-links">
|
||||
<?php foreach ($bookmarks as $bookmark): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($bookmark['url']) ?>" target="_blank" rel="noreferrer"><?= e($bookmark['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif (($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 ($appEntry !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<?php if (!empty($appEntry['icon_url'])): ?>
|
||||
<img class="dashboard-app-icon" src="<?= e((string) $appEntry['icon_url']) ?>" alt="">
|
||||
<?php endif; ?>
|
||||
<p><?= e((string) ($appEntry['description'] ?? $targetUrl)) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">App öffnen</a>
|
||||
</div>
|
||||
<?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]');
|
||||
const bookmarksField = document.querySelector('[data-dashboard-bookmarks]');
|
||||
const appField = document.querySelector('[data-dashboard-app]');
|
||||
const widgetTemplateField = document.querySelector('[data-dashboard-widget-template]');
|
||||
if (!typeSelect || !urlField || !pageModuleField || !bookmarksField || !appField || !widgetTemplateField) return;
|
||||
const sync = () => {
|
||||
const value = typeSelect.value;
|
||||
urlField.hidden = !['link', 'iframe'].includes(value);
|
||||
pageModuleField.hidden = value !== 'page_module';
|
||||
bookmarksField.hidden = value !== 'bookmark_group';
|
||||
appField.hidden = value !== 'app';
|
||||
widgetTemplateField.hidden = value !== 'widget_template';
|
||||
};
|
||||
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>
|
||||
196
partials/landingpages/index.php
Executable file → Normal file
196
partials/landingpages/index.php
Executable file → Normal file
@@ -3,39 +3,173 @@ declare(strict_types=1);
|
||||
|
||||
$auth = app()->auth();
|
||||
$authUser = $auth->user();
|
||||
$modules = array_values(array_filter(
|
||||
$auth->filterModules(modules()->all()),
|
||||
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; ?>
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$groups = auth_groups();
|
||||
$selectedDashboardId = (int) ($_GET['board'] ?? 0);
|
||||
$currentDashboard = $service->available()
|
||||
? $service->resolveHomeDashboard($ownerKey !== '' ? $ownerKey : null, $groups, $authUser !== null, $selectedDashboardId)
|
||||
: null;
|
||||
$dashboardItems = $currentDashboard !== null ? $service->listItems((int) ($currentDashboard['id'] ?? 0)) : [];
|
||||
$accessibleApps = ($authUser !== null && $service->available()) ? $service->listApps($ownerKey, true) : [];
|
||||
$appsById = [];
|
||||
foreach ($accessibleApps as $appEntry) {
|
||||
$appsById[(int) ($appEntry['id'] ?? 0)] = $appEntry;
|
||||
}
|
||||
$widgetTemplates = ($authUser !== null && $service->available()) ? $service->listWidgetTemplates($ownerKey, true) : [];
|
||||
$widgetTemplatesById = [];
|
||||
foreach ($widgetTemplates as $template) {
|
||||
$widgetTemplatesById[(int) ($template['id'] ?? 0)] = $template;
|
||||
}
|
||||
$priorityModuleEntries = [];
|
||||
if ($authUser !== null) {
|
||||
$accessibleModules = $auth->filterModules(array_values(modules()->all()));
|
||||
$accessibleModulesByName = [];
|
||||
foreach ($accessibleModules as $module) {
|
||||
$name = trim((string) ($module['name'] ?? ''));
|
||||
if ($name !== '' && !empty($module['enabled'])) {
|
||||
$accessibleModulesByName[$name] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
<?php if ($modules === []): ?>
|
||||
<div class="empty-state" data-reveal>
|
||||
Keine Module für den aktuellen Zugriff sichtbar.
|
||||
</div>
|
||||
foreach (['mining-checker', 'boersenchecker'] as $moduleName) {
|
||||
if (isset($accessibleModulesByName[$moduleName])) {
|
||||
$priorityModuleEntries[] = $accessibleModulesByName[$moduleName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = $currentDashboard !== null ? (string) ($currentDashboard['title'] ?? 'Dashboard') : 'Dashboard';
|
||||
$GLOBALS['layout_header_text'] = $currentDashboard !== null
|
||||
? (string) ($currentDashboard['description'] ?? '')
|
||||
: 'Kein öffentliches oder persönliches Home-Dashboard verfügbar.';
|
||||
|
||||
$renderBookmarks = static function (array $config): array {
|
||||
$items = [];
|
||||
$raw = trim((string) ($config['bookmarks'] ?? ''));
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
foreach (preg_split('/\r\n|\r|\n/', $raw) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
[$label, $url] = array_pad(array_map('trim', explode('|', $line, 2)), 2, '');
|
||||
if ($label !== '' && $url !== '') {
|
||||
$items[] = ['label' => $label, 'url' => $url];
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
};
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<?php if ($priorityModuleEntries !== []): ?>
|
||||
<section class="section-box">
|
||||
<h2>Schnellzugriff</h2>
|
||||
<p class="muted">Wichtige Module für deinen direkten Einstieg.</p>
|
||||
<div class="dashboard-links">
|
||||
<?php foreach ($priorityModuleEntries as $moduleEntry): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/module/<?= rawurlencode((string) ($moduleEntry['name'] ?? '')) ?>">
|
||||
<?= e((string) ($moduleEntry['title'] ?? $moduleEntry['name'] ?? 'Modul')) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<?php if ($currentDashboard === null): ?>
|
||||
<section class="section-box">
|
||||
<h2>Kein Home-Dashboard verfügbar</h2>
|
||||
<p class="muted">Lege als Administrator ein öffentliches Dashboard fest oder richte dein persönliches Dashboard ein.</p>
|
||||
<?php if ($authUser !== null): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/dashboard">Zum Dashboard</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php elseif ($dashboardItems === []): ?>
|
||||
<section class="section-box">
|
||||
<h2><?= e((string) ($currentDashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||
<p class="muted">Dieses Dashboard enthält noch keine Elemente.</p>
|
||||
<?php if ($authUser !== null): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($currentDashboard['id'] ?? 0) ?>">Dashboard bearbeiten</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<div class="module-list">
|
||||
<?php foreach ($modules as $module): ?>
|
||||
<a class="module-row" href="<?= e((string)($module['entry'] ?? ('/module/' . $module['name']))) ?>">
|
||||
<span class="module-row__icon"><?= e(strtoupper(substr((string)($module['title'] ?? $module['name']), 0, 1))) ?></span>
|
||||
<span class="module-row__content">
|
||||
<strong class="module-title"><?= e((string)($module['title'] ?? $module['name'] ?? 'Modul')) ?></strong>
|
||||
<span class="module-desc"><?= e((string)($module['description'] ?? '')) ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="dashboard-grid">
|
||||
<?php foreach ($dashboardItems as $item): ?>
|
||||
<?php
|
||||
$itemType = (string) ($item['item_type'] ?? 'link');
|
||||
$config = is_array($item['config'] ?? null) ? $item['config'] : [];
|
||||
if ($itemType === 'widget_template' && !empty($config['widget_template_id']) && isset($widgetTemplatesById[(int) $config['widget_template_id']])) {
|
||||
$template = $widgetTemplatesById[(int) $config['widget_template_id']];
|
||||
$itemType = (string) ($template['widget_type'] ?? $itemType);
|
||||
$config = array_merge(is_array($template['config'] ?? null) ? $template['config'] : [], $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'] . ';';
|
||||
}
|
||||
$targetUrl = trim((string) ($config['url'] ?? ''));
|
||||
$pageModule = null;
|
||||
if ($itemType === 'page_module' && !empty($config['page_module_id'])) {
|
||||
$pageModule = $service->getPageModule((int) $config['page_module_id'], $ownerKey, $groups);
|
||||
$targetUrl = trim((string) ($pageModule['target_url'] ?? $targetUrl));
|
||||
}
|
||||
$appEntry = null;
|
||||
if ($itemType === 'app' && !empty($config['app_id'])) {
|
||||
$appEntry = $appsById[(int) $config['app_id']] ?? null;
|
||||
$targetUrl = trim((string) ($appEntry['app_url'] ?? $targetUrl));
|
||||
}
|
||||
$bookmarks = $itemType === 'bookmark_group' ? $renderBookmarks($config) : [];
|
||||
?>
|
||||
<article class="card-box dashboard-widget" style="<?= e($gridStyles) ?>">
|
||||
<div class="dashboard-widget__head">
|
||||
<div>
|
||||
<span class="module-admin-meta__label"><?= e(strtoupper(str_replace('_', ' ', $itemType))) ?></span>
|
||||
<h2><?= e((string) ($item['title'] ?? 'Element')) ?></h2>
|
||||
<?php if (!empty($item['description'])): ?>
|
||||
<p><?= e((string) $item['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($itemType === 'bookmark_group' && $bookmarks !== []): ?>
|
||||
<div class="dashboard-links">
|
||||
<?php foreach ($bookmarks as $bookmark): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($bookmark['url']) ?>" target="_blank" rel="noreferrer"><?= e($bookmark['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif (($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 ($appEntry !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<?php if (!empty($appEntry['icon_url'])): ?>
|
||||
<img class="dashboard-app-icon" src="<?= e((string) $appEntry['icon_url']) ?>" alt="">
|
||||
<?php endif; ?>
|
||||
<p><?= e((string) ($appEntry['description'] ?? $targetUrl)) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">App öffnen</a>
|
||||
</div>
|
||||
<?php elseif ($pageModule !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e((string) ($pageModule['description'] ?? 'Seitenmodul aus dem Nexus-System.')) ?></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-empty">Für dieses Element ist noch kein Inhalt hinterlegt.</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div></div></div>
|
||||
|
||||
150
partials/landingpages/integrations.php
Normal file
150
partials/landingpages/integrations.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$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 Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Integrationen';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Integrationen für spätere Widgets, Apps und Suchfunktionen.';
|
||||
?>
|
||||
<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="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab-active" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</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="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>
|
||||
@@ -12,6 +12,7 @@ $timezoneOptions = modules()->timezones();
|
||||
$nexusSettings = nexus_settings();
|
||||
$isAdmin = auth_is_admin();
|
||||
$notice = null;
|
||||
$publicDashboards = $isAdmin ? dashboards()->listPublicDashboards() : [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$settingsSection = trim((string) ($_POST['settings_section'] ?? 'theme'));
|
||||
@@ -19,6 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$displayTimezoneCustom = isset($_POST['display_timezone_custom']) ? '1' : '0';
|
||||
$displayTimezone = trim((string) ($_POST['display_timezone'] ?? ''));
|
||||
$cronTimezone = trim((string) ($_POST['cron_timezone'] ?? ''));
|
||||
$globalHomeDashboardId = (int) ($_POST['global_home_dashboard_id'] ?? 0);
|
||||
|
||||
foreach (['displayTimezone' => $displayTimezone, 'cronTimezone' => $cronTimezone] as $key => $value) {
|
||||
if ($value !== '') {
|
||||
@@ -33,6 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nexusSettings['display_timezone_custom'] = $displayTimezoneCustom;
|
||||
$nexusSettings['display_timezone'] = $displayTimezoneCustom === '1' ? $displayTimezone : '';
|
||||
$nexusSettings['cron_timezone'] = $cronTimezone;
|
||||
$nexusSettings['global_home_dashboard_id'] = $globalHomeDashboardId > 0 ? (string) $globalHomeDashboardId : '';
|
||||
nexus_save_settings($nexusSettings);
|
||||
$nexusSettings = nexus_settings();
|
||||
$notice = 'Nexus-Einstellungen gespeichert.';
|
||||
@@ -53,11 +56,35 @@ $effectiveCronTimezone = nexus_cron_timezone_name();
|
||||
$displayTimezoneCustom = !empty($nexusSettings['display_timezone_custom']);
|
||||
$savedDisplayTimezone = trim((string) ($nexusSettings['display_timezone'] ?? ''));
|
||||
$savedCronTimezone = trim((string) ($nexusSettings['cron_timezone'] ?? ''));
|
||||
$globalHomeDashboardId = (int) ($nexusSettings['global_home_dashboard_id'] ?? 0);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Allgemein';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Grundeinstellungen, Home-Dashboard und systemweite Standardwerte.';
|
||||
?>
|
||||
<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="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab-active" href="/settings">Allgemein</a>
|
||||
<?php if ($isAdmin): ?>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section-box">
|
||||
<h1>Nexus Einstellungen</h1>
|
||||
<p class="muted">Persönliche Anzeige und systemweite Standardwerte.</p>
|
||||
<p class="muted">Persönliche Anzeige, Home-Dashboard und systemweite Standardwerte.</p>
|
||||
|
||||
<?php if ($notice): ?>
|
||||
<div class="section-box" style="margin-top:1rem; border-color:var(--accent-2);">
|
||||
@@ -115,6 +142,18 @@ $savedCronTimezone = trim((string) ($nexusSettings['cron_timezone'] ?? ''));
|
||||
<div><?= e($systemTimezone) ?></div>
|
||||
<small class="muted">Diese Zeitzone wird genutzt, wenn keine globale Anzeige-Zeitzone gesetzt ist.</small>
|
||||
</div>
|
||||
<label class="setup-field muted">
|
||||
<span>Öffentliches Home-Dashboard</span>
|
||||
<select name="global_home_dashboard_id">
|
||||
<option value="0">Kein öffentliches Home-Dashboard</option>
|
||||
<?php foreach ($publicDashboards as $dashboard): ?>
|
||||
<option value="<?= (int) ($dashboard['id'] ?? 0) ?>" <?= (int) ($dashboard['id'] ?? 0) === $globalHomeDashboardId ? 'selected' : '' ?>>
|
||||
<?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="muted">Dieses öffentliche Dashboard wird am Root-Pfad angezeigt, wenn Nutzer nicht eingeloggt sind.</small>
|
||||
</label>
|
||||
<div class="setup-field muted">
|
||||
<span>Anzeige-Zeitzone</span>
|
||||
<label style="display:flex; align-items:center; gap:10px;">
|
||||
|
||||
125
partials/landingpages/users/settings_apps.php
Normal file
125
partials/landingpages/users/settings_apps.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_app') {
|
||||
$service->createApp($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'app_url' => trim((string) ($_POST['app_url'] ?? '')),
|
||||
'icon_url' => trim((string) ($_POST['icon_url'] ?? '')),
|
||||
'integration_id' => (int) ($_POST['integration_id'] ?? 0),
|
||||
'visibility' => 'public',
|
||||
]);
|
||||
$notice = 'App gespeichert.';
|
||||
} elseif ($action === 'delete_app') {
|
||||
$service->deleteApp((int) ($_POST['app_id'] ?? 0), $ownerKey);
|
||||
$notice = 'App gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$apps = $service->listApps($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Apps';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Apps, die Nutzer später ihren Dashboards hinzufügen können.';
|
||||
?>
|
||||
<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="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</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 App</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_app">
|
||||
<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>App-URL</span>
|
||||
<input type="url" name="app_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>Icon-URL optional</span>
|
||||
<input type="url" name="icon_url" placeholder="https://.../icon.png">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Integration optional</span>
|
||||
<select name="integration_id">
|
||||
<option value="0">Keine Integration</option>
|
||||
<?php foreach ($integrations as $integration): ?>
|
||||
<option value="<?= (int) ($integration['id'] ?? 0) ?>"><?= e((string) ($integration['name'] ?? 'Integration')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">App speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($apps as $appEntry): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($appEntry['name'] ?? 'App')) ?></h2>
|
||||
<p><?= e((string) ($appEntry['description'] ?? ($appEntry['app_url'] ?? ''))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e((string) ($appEntry['app_url'] ?? '#')) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_app">
|
||||
<input type="hidden" name="app_id" value="<?= (int) ($appEntry['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>
|
||||
142
partials/landingpages/users/settings_search_engines.php
Normal file
142
partials/landingpages/users/settings_search_engines.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_search_engine') {
|
||||
$service->createSearchEngine($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'short_code' => trim((string) ($_POST['short_code'] ?? '')),
|
||||
'engine_type' => trim((string) ($_POST['engine_type'] ?? 'template')),
|
||||
'template_url' => trim((string) ($_POST['template_url'] ?? '')),
|
||||
'integration_id' => (int) ($_POST['integration_id'] ?? 0),
|
||||
'visibility' => 'public',
|
||||
'is_default' => isset($_POST['is_default']),
|
||||
]);
|
||||
$notice = 'Suchmaschine gespeichert.';
|
||||
} elseif ($action === 'delete_search_engine') {
|
||||
$service->deleteSearchEngine((int) ($_POST['engine_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Suchmaschine gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$engines = $service->listSearchEngines($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Suchmaschinen';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Suchmaschinen für die spätere Suche im Nexus-System.';
|
||||
?>
|
||||
<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="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</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 Suchmaschine</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_search_engine">
|
||||
<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>Kurzcode</span>
|
||||
<input type="text" name="short_code" placeholder="g" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="engine_type">
|
||||
<option value="template">Such-Template</option>
|
||||
<option value="integration">Integration</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Such-URL</span>
|
||||
<input type="url" name="template_url" placeholder="https://www.google.com/search?q=%s">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Integration optional</span>
|
||||
<select name="integration_id">
|
||||
<option value="0">Keine Integration</option>
|
||||
<?php foreach ($integrations as $integration): ?>
|
||||
<option value="<?= (int) ($integration['id'] ?? 0) ?>"><?= e((string) ($integration['name'] ?? 'Integration')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<input type="checkbox" name="is_default" value="1">
|
||||
<span>Als globale Standard-Suche setzen</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Suchmaschine speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($engines as $engine): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($engine['name'] ?? 'Suche')) ?></h2>
|
||||
<p><?= e((string) (($engine['template_url'] ?? '') ?: 'Integration-basierte Suche.')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Kurzcode</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($engine['short_code'] ?? '')) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Standard</span>
|
||||
<strong class="module-admin-badge<?= !empty($engine['is_default']) ? ' module-admin-badge--success' : '' ?>"><?= !empty($engine['is_default']) ? 'Ja' : 'Nein' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_search_engine">
|
||||
<input type="hidden" name="engine_id" value="<?= (int) ($engine['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>
|
||||
130
partials/landingpages/users/settings_widgets.php
Normal file
130
partials/landingpages/users/settings_widgets.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_widget') {
|
||||
$service->createWidgetTemplate($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'widget_type' => trim((string) ($_POST['widget_type'] ?? 'link')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'visibility' => 'public',
|
||||
'config' => [
|
||||
'url' => trim((string) ($_POST['target_url'] ?? '')),
|
||||
'bookmarks' => trim((string) ($_POST['bookmarks'] ?? '')),
|
||||
],
|
||||
]);
|
||||
$notice = 'Widget-Vorlage gespeichert.';
|
||||
} elseif ($action === 'delete_widget') {
|
||||
$service->deleteWidgetTemplate((int) ($_POST['widget_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Widget-Vorlage gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$widgets = $service->listWidgetTemplates($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Widgets';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Widget-Vorlagen, die andere Nutzer in ihre Dashboards übernehmen können.';
|
||||
?>
|
||||
<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="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</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 Widget-Vorlage</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_widget">
|
||||
<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="widget_type">
|
||||
<option value="link">Link</option>
|
||||
<option value="iframe">iFrame</option>
|
||||
<option value="bookmark_group">Linkliste</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">
|
||||
<span>Ziel-URL</span>
|
||||
<input type="url" name="target_url" placeholder="https://...">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Linkliste optional</span>
|
||||
<textarea name="bookmarks" rows="5" placeholder="Name | https://ziel.example Monitoring | https://grafana.example"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Widget speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($widgets as $widget): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($widget['name'] ?? 'Widget')) ?></h2>
|
||||
<p><?= e((string) ($widget['description'] ?? 'Globale Widget-Vorlage.')) ?></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) ($widget['widget_type'] ?? 'link')) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_widget">
|
||||
<input type="hidden" name="widget_id" value="<?= (int) ($widget['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>
|
||||
@@ -776,6 +776,122 @@ body.has-modal-open {
|
||||
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-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dashboard-widget__meta p {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dashboard-app-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-soft) 85%, transparent);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
267
public/assets/vendor/react/react-dom.production.min.js
vendored
Normal file
267
public/assets/vendor/react/react-dom.production.min.js
vendored
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
(function(){/*
|
||||
Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
'use strict';(function(Q,zb){"object"===typeof exports&&"undefined"!==typeof module?zb(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],zb):(Q=Q||self,zb(Q.ReactDOM={},Q.React))})(this,function(Q,zb){function m(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
|
||||
function mb(a,b){Ab(a,b);Ab(a+"Capture",b)}function Ab(a,b){$b[a]=b;for(a=0;a<b.length;a++)cg.add(b[a])}function bj(a){if(Zd.call(dg,a))return!0;if(Zd.call(eg,a))return!1;if(cj.test(a))return dg[a]=!0;eg[a]=!0;return!1}function dj(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function ej(a,b,c,d){if(null===
|
||||
b||"undefined"===typeof b||dj(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function Y(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}function $d(a,b,c,d){var e=R.hasOwnProperty(b)?R[b]:null;if(null!==e?0!==e.type:d||!(2<b.length)||"o"!==
|
||||
b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1])ej(b,c,e,d)&&(c=null),d||null===e?bj(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c)))}function ac(a){if(null===a||"object"!==typeof a)return null;a=fg&&a[fg]||a["@@iterator"];return"function"===typeof a?a:null}function bc(a,b,
|
||||
c){if(void 0===ae)try{throw Error();}catch(d){ae=(b=d.stack.trim().match(/\n( *(at )?)/))&&b[1]||""}return"\n"+ae+a}function be(a,b){if(!a||ce)return"";ce=!0;var c=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(b)if(b=function(){throw Error();},Object.defineProperty(b.prototype,"props",{set:function(){throw Error();}}),"object"===typeof Reflect&&Reflect.construct){try{Reflect.construct(b,[])}catch(n){var d=n}Reflect.construct(a,[],b)}else{try{b.call()}catch(n){d=n}a.call(b.prototype)}else{try{throw Error();
|
||||
}catch(n){d=n}a()}}catch(n){if(n&&d&&"string"===typeof n.stack){for(var e=n.stack.split("\n"),f=d.stack.split("\n"),g=e.length-1,h=f.length-1;1<=g&&0<=h&&e[g]!==f[h];)h--;for(;1<=g&&0<=h;g--,h--)if(e[g]!==f[h]){if(1!==g||1!==h){do if(g--,h--,0>h||e[g]!==f[h]){var k="\n"+e[g].replace(" at new "," at ");a.displayName&&k.includes("<anonymous>")&&(k=k.replace("<anonymous>",a.displayName));return k}while(1<=g&&0<=h)}break}}}finally{ce=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:"")?bc(a):
|
||||
""}function fj(a){switch(a.tag){case 5:return bc(a.type);case 16:return bc("Lazy");case 13:return bc("Suspense");case 19:return bc("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function de(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Bb:return"Fragment";case Cb:return"Portal";case ee:return"Profiler";case fe:return"StrictMode";
|
||||
case ge:return"Suspense";case he:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case gg:return(a.displayName||"Context")+".Consumer";case hg:return(a._context.displayName||"Context")+".Provider";case ie:var b=a.render;a=a.displayName;a||(a=b.displayName||b.name||"",a=""!==a?"ForwardRef("+a+")":"ForwardRef");return a;case je:return b=a.displayName||null,null!==b?b:de(a.type)||"Memo";case Ta:b=a._payload;a=a._init;try{return de(a(b))}catch(c){}}return null}function gj(a){var b=a.type;
|
||||
switch(a.tag){case 24:return"Cache";case 9:return(b.displayName||"Context")+".Consumer";case 10:return(b._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=b.render,a=a.displayName||a.name||"",b.displayName||(""!==a?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return b;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return de(b);case 8:return b===fe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";
|
||||
case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"===typeof b)return b.displayName||b.name||null;if("string"===typeof b)return b}return null}function Ua(a){switch(typeof a){case "boolean":case "number":case "string":case "undefined":return a;case "object":return a;default:return""}}function ig(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===
|
||||
b)}function hj(a){var b=ig(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=
|
||||
null;delete a[b]}}}}function Pc(a){a._valueTracker||(a._valueTracker=hj(a))}function jg(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=ig(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Qc(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function ke(a,b){var c=b.checked;return E({},b,{defaultChecked:void 0,defaultValue:void 0,
|
||||
value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function kg(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function lg(a,b){b=b.checked;null!=b&&$d(a,"checked",b,!1)}function le(a,b){lg(a,b);var c=Ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=
|
||||
c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?me(a,b.type,c):b.hasOwnProperty("defaultValue")&&me(a,b.type,Ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function mg(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;
|
||||
c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function me(a,b,c){if("number"!==b||Qc(a.ownerDocument)!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function Db(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=
|
||||
!0)}else{c=""+Ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function ne(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(m(91));return E({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}function ng(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(m(92));if(cc(c)){if(1<c.length)throw Error(m(93));
|
||||
c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:Ua(c)}}function og(a,b){var c=Ua(b.value),d=Ua(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function pg(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==b&&null!==b&&(a.value=b)}function qg(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}
|
||||
function oe(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?qg(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function rg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||dc.hasOwnProperty(a)&&dc[a]?(""+b).trim():b+"px"}function sg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=rg(c,b[c],d);"float"===c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function pe(a,b){if(b){if(ij[a]&&
|
||||
(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(m(137,a));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(m(60));if("object"!==typeof b.dangerouslySetInnerHTML||!("__html"in b.dangerouslySetInnerHTML))throw Error(m(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(m(62));}}function qe(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
|
||||
default:return!0}}function re(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function tg(a){if(a=ec(a)){if("function"!==typeof se)throw Error(m(280));var b=a.stateNode;b&&(b=Rc(b),se(a.stateNode,a.type,b))}}function ug(a){Eb?Fb?Fb.push(a):Fb=[a]:Eb=a}function vg(){if(Eb){var a=Eb,b=Fb;Fb=Eb=null;tg(a);if(b)for(a=0;a<b.length;a++)tg(b[a])}}function wg(a,b,c){if(te)return a(b,c);te=!0;try{return xg(a,b,c)}finally{if(te=
|
||||
!1,null!==Eb||null!==Fb)yg(),vg()}}function fc(a,b){var c=a.stateNode;if(null===c)return null;var d=Rc(c);if(null===d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;
|
||||
if(c&&"function"!==typeof c)throw Error(m(231,b,typeof c));return c}function jj(a,b,c,d,e,f,g,h,k){gc=!1;Sc=null;kj.apply(lj,arguments)}function mj(a,b,c,d,e,f,g,h,k){jj.apply(this,arguments);if(gc){if(gc){var n=Sc;gc=!1;Sc=null}else throw Error(m(198));Tc||(Tc=!0,ue=n)}}function nb(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.flags&4098)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function zg(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,
|
||||
null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Ag(a){if(nb(a)!==a)throw Error(m(188));}function nj(a){var b=a.alternate;if(!b){b=nb(a);if(null===b)throw Error(m(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Ag(e),a;if(f===d)return Ag(e),b;f=f.sibling}throw Error(m(188));}if(c.return!==d.return)c=e,d=f;
|
||||
else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(m(189));}}if(c.alternate!==d)throw Error(m(190));}if(3!==c.tag)throw Error(m(188));return c.stateNode.current===c?a:b}function Bg(a){a=nj(a);return null!==a?Cg(a):null}function Cg(a){if(5===a.tag||6===a.tag)return a;for(a=a.child;null!==a;){var b=Cg(a);if(null!==b)return b;a=a.sibling}return null}
|
||||
function oj(a,b){if(Ca&&"function"===typeof Ca.onCommitFiberRoot)try{Ca.onCommitFiberRoot(Uc,a,void 0,128===(a.current.flags&128))}catch(c){}}function pj(a){a>>>=0;return 0===a?32:31-(qj(a)/rj|0)|0}function hc(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a&
|
||||
4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function Vc(a,b){var c=a.pendingLanes;if(0===c)return 0;var d=0,e=a.suspendedLanes,f=a.pingedLanes,g=c&268435455;if(0!==g){var h=g&~e;0!==h?d=hc(h):(f&=g,0!==f&&(d=hc(f)))}else g=c&~e,0!==g?d=hc(g):0!==f&&(d=hc(f));if(0===d)return 0;if(0!==b&&b!==d&&0===(b&e)&&
|
||||
(e=d&-d,f=b&-b,e>=f||16===e&&0!==(f&4194240)))return b;0!==(d&4)&&(d|=c&16);b=a.entangledLanes;if(0!==b)for(a=a.entanglements,b&=d;0<b;)c=31-ta(b),e=1<<c,d|=a[c],b&=~e;return d}function sj(a,b){switch(a){case 1:case 2:case 4:return b+250;case 8:case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return b+5E3;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return-1;
|
||||
case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function tj(a,b){for(var c=a.suspendedLanes,d=a.pingedLanes,e=a.expirationTimes,f=a.pendingLanes;0<f;){var g=31-ta(f),h=1<<g,k=e[g];if(-1===k){if(0===(h&c)||0!==(h&d))e[g]=sj(h,b)}else k<=b&&(a.expiredLanes|=h);f&=~h}}function ve(a){a=a.pendingLanes&-1073741825;return 0!==a?a:a&1073741824?1073741824:0}function Dg(){var a=Wc;Wc<<=1;0===(Wc&4194240)&&(Wc=64);return a}function we(a){for(var b=[],c=0;31>c;c++)b.push(a);
|
||||
return b}function ic(a,b,c){a.pendingLanes|=b;536870912!==b&&(a.suspendedLanes=0,a.pingedLanes=0);a=a.eventTimes;b=31-ta(b);a[b]=c}function uj(a,b){var c=a.pendingLanes&~b;a.pendingLanes=b;a.suspendedLanes=0;a.pingedLanes=0;a.expiredLanes&=b;a.mutableReadLanes&=b;a.entangledLanes&=b;b=a.entanglements;var d=a.eventTimes;for(a=a.expirationTimes;0<c;){var e=31-ta(c),f=1<<e;b[e]=0;d[e]=-1;a[e]=-1;c&=~f}}function xe(a,b){var c=a.entangledLanes|=b;for(a=a.entanglements;c;){var d=31-ta(c),e=1<<d;e&b|a[d]&
|
||||
b&&(a[d]|=b);c&=~e}}function Eg(a){a&=-a;return 1<a?4<a?0!==(a&268435455)?16:536870912:4:1}function Fg(a,b){switch(a){case "focusin":case "focusout":Va=null;break;case "dragenter":case "dragleave":Wa=null;break;case "mouseover":case "mouseout":Xa=null;break;case "pointerover":case "pointerout":jc.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":kc.delete(b.pointerId)}}function lc(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a={blockedOn:b,domEventName:c,eventSystemFlags:d,
|
||||
nativeEvent:f,targetContainers:[e]},null!==b&&(b=ec(b),null!==b&&Gg(b)),a;a.eventSystemFlags|=d;b=a.targetContainers;null!==e&&-1===b.indexOf(e)&&b.push(e);return a}function vj(a,b,c,d,e){switch(b){case "focusin":return Va=lc(Va,a,b,c,d,e),!0;case "dragenter":return Wa=lc(Wa,a,b,c,d,e),!0;case "mouseover":return Xa=lc(Xa,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;jc.set(f,lc(jc.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,kc.set(f,lc(kc.get(f)||null,a,b,
|
||||
c,d,e)),!0}return!1}function Hg(a){var b=ob(a.target);if(null!==b){var c=nb(b);if(null!==c)if(b=c.tag,13===b){if(b=zg(c),null!==b){a.blockedOn=b;wj(a.priority,function(){xj(c)});return}}else if(3===b&&c.stateNode.current.memoizedState.isDehydrated){a.blockedOn=3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function Xc(a){if(null!==a.blockedOn)return!1;for(var b=a.targetContainers;0<b.length;){var c=ye(a.domEventName,a.eventSystemFlags,b[0],a.nativeEvent);if(null===c){c=a.nativeEvent;
|
||||
var d=new c.constructor(c.type,c);ze=d;c.target.dispatchEvent(d);ze=null}else return b=ec(c),null!==b&&Gg(b),a.blockedOn=c,!1;b.shift()}return!0}function Ig(a,b,c){Xc(a)&&c.delete(b)}function yj(){Ae=!1;null!==Va&&Xc(Va)&&(Va=null);null!==Wa&&Xc(Wa)&&(Wa=null);null!==Xa&&Xc(Xa)&&(Xa=null);jc.forEach(Ig);kc.forEach(Ig)}function mc(a,b){a.blockedOn===b&&(a.blockedOn=null,Ae||(Ae=!0,Jg(Kg,yj)))}function nc(a){if(0<Yc.length){mc(Yc[0],a);for(var b=1;b<Yc.length;b++){var c=Yc[b];c.blockedOn===a&&(c.blockedOn=
|
||||
null)}}null!==Va&&mc(Va,a);null!==Wa&&mc(Wa,a);null!==Xa&&mc(Xa,a);b=function(b){return mc(b,a)};jc.forEach(b);kc.forEach(b);for(b=0;b<Ya.length;b++)c=Ya[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Ya.length&&(b=Ya[0],null===b.blockedOn);)Hg(b),null===b.blockedOn&&Ya.shift()}function zj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=1,Be(a,b,c,d)}finally{z=e,Gb.transition=f}}function Aj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=4,Be(a,b,c,d)}finally{z=e,Gb.transition=
|
||||
f}}function Be(a,b,c,d){if(Zc){var e=ye(a,b,c,d);if(null===e)Ce(a,b,d,$c,c),Fg(a,d);else if(vj(e,a,b,c,d))d.stopPropagation();else if(Fg(a,d),b&4&&-1<Bj.indexOf(a)){for(;null!==e;){var f=ec(e);null!==f&&Cj(f);f=ye(a,b,c,d);null===f&&Ce(a,b,d,$c,c);if(f===e)break;e=f}null!==e&&d.stopPropagation()}else Ce(a,b,d,null,c)}}function ye(a,b,c,d){$c=null;a=re(d);a=ob(a);if(null!==a)if(b=nb(a),null===b)a=null;else if(c=b.tag,13===c){a=zg(b);if(null!==a)return a;a=null}else if(3===c){if(b.stateNode.current.memoizedState.isDehydrated)return 3===
|
||||
b.tag?b.stateNode.containerInfo:null;a=null}else b!==a&&(a=null);$c=a;return null}function Lg(a){switch(a){case "cancel":case "click":case "close":case "contextmenu":case "copy":case "cut":case "auxclick":case "dblclick":case "dragend":case "dragstart":case "drop":case "focusin":case "focusout":case "input":case "invalid":case "keydown":case "keypress":case "keyup":case "mousedown":case "mouseup":case "paste":case "pause":case "play":case "pointercancel":case "pointerdown":case "pointerup":case "ratechange":case "reset":case "resize":case "seeked":case "submit":case "touchcancel":case "touchend":case "touchstart":case "volumechange":case "change":case "selectionchange":case "textInput":case "compositionstart":case "compositionend":case "compositionupdate":case "beforeblur":case "afterblur":case "beforeinput":case "blur":case "fullscreenchange":case "focus":case "hashchange":case "popstate":case "select":case "selectstart":return 1;
|
||||
case "drag":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "mousemove":case "mouseout":case "mouseover":case "pointermove":case "pointerout":case "pointerover":case "scroll":case "toggle":case "touchmove":case "wheel":case "mouseenter":case "mouseleave":case "pointerenter":case "pointerleave":return 4;case "message":switch(Dj()){case De:return 1;case Mg:return 4;case ad:case Ej:return 16;case Ng:return 536870912;default:return 16}default:return 16}}function Og(){if(bd)return bd;
|
||||
var a,b=Ee,c=b.length,d,e="value"in Za?Za.value:Za.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return bd=e.slice(a,1<d?1-d:void 0)}function cd(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function dd(){return!0}function Pg(){return!1}function ka(a){function b(b,d,e,f,g){this._reactName=b;this._targetInst=e;this.type=d;this.nativeEvent=f;this.target=g;this.currentTarget=null;
|
||||
for(var c in a)a.hasOwnProperty(c)&&(b=a[c],this[c]=b?b(f):f[c]);this.isDefaultPrevented=(null!=f.defaultPrevented?f.defaultPrevented:!1===f.returnValue)?dd:Pg;this.isPropagationStopped=Pg;return this}E(b.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=dd)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():
|
||||
"unknown"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=dd)},persist:function(){},isPersistent:dd});return b}function Fj(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Gj[a])?!!b[a]:!1}function Fe(a){return Fj}function Qg(a,b){switch(a){case "keyup":return-1!==Hj.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "focusout":return!0;default:return!1}}function Rg(a){a=a.detail;return"object"===typeof a&&
|
||||
"data"in a?a.data:null}function Ij(a,b){switch(a){case "compositionend":return Rg(b);case "keypress":if(32!==b.which)return null;Sg=!0;return Tg;case "textInput":return a=b.data,a===Tg&&Sg?null:a;default:return null}}function Jj(a,b){if(Hb)return"compositionend"===a||!Ge&&Qg(a,b)?(a=Og(),bd=Ee=Za=null,Hb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
|
||||
case "compositionend":return Ug&&"ko"!==b.locale?null:b.data;default:return null}}function Vg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Kj[a.type]:"textarea"===b?!0:!1}function Lj(a){if(!Ia)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Wg(a,b,c,d){ug(d);b=ed(b,"onChange");0<b.length&&(c=new He("onChange","change",null,c,d),a.push({event:c,listeners:b}))}function Mj(a){Xg(a,
|
||||
0)}function fd(a){var b=Ib(a);if(jg(b))return a}function Nj(a,b){if("change"===a)return b}function Yg(){oc&&(oc.detachEvent("onpropertychange",Zg),pc=oc=null)}function Zg(a){if("value"===a.propertyName&&fd(pc)){var b=[];Wg(b,pc,a,re(a));wg(Mj,b)}}function Oj(a,b,c){"focusin"===a?(Yg(),oc=b,pc=c,oc.attachEvent("onpropertychange",Zg)):"focusout"===a&&Yg()}function Pj(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return fd(pc)}function Qj(a,b){if("click"===a)return fd(b)}function Rj(a,b){if("input"===
|
||||
a||"change"===a)return fd(b)}function Sj(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function qc(a,b){if(ua(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++){var e=c[d];if(!Zd.call(b,e)||!ua(a[e],b[e]))return!1}return!0}function $g(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ah(a,b){var c=$g(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;
|
||||
if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=$g(c)}}function bh(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?bh(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function ch(){for(var a=window,b=Qc();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;
|
||||
b=Qc(a.document)}return b}function Ie(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Tj(a){var b=ch(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&bh(c.ownerDocument.documentElement,c)){if(null!==d&&Ie(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);
|
||||
else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=ah(c,f);var g=ah(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),
|
||||
a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function dh(a,b,c){var d=c.window===c?c.document:9===c.nodeType?c:c.ownerDocument;Je||null==Jb||Jb!==Qc(d)||(d=Jb,"selectionStart"in d&&Ie(d)?d={start:d.selectionStart,end:d.selectionEnd}:(d=(d.ownerDocument&&d.ownerDocument.defaultView||window).getSelection(),d=
|
||||
{anchorNode:d.anchorNode,anchorOffset:d.anchorOffset,focusNode:d.focusNode,focusOffset:d.focusOffset}),rc&&qc(rc,d)||(rc=d,d=ed(Ke,"onSelect"),0<d.length&&(b=new He("onSelect","select",null,b,c),a.push({event:b,listeners:d}),b.target=Jb)))}function gd(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function hd(a){if(Le[a])return Le[a];if(!Kb[a])return a;var b=Kb[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in eh)return Le[a]=b[c];return a}function $a(a,
|
||||
b){fh.set(a,b);mb(b,[a])}function gh(a,b,c){var d=a.type||"unknown-event";a.currentTarget=c;mj(d,b,void 0,a);a.currentTarget=null}function Xg(a,b){b=0!==(b&4);for(var c=0;c<a.length;c++){var d=a[c],e=d.event;d=d.listeners;a:{var f=void 0;if(b)for(var g=d.length-1;0<=g;g--){var h=d[g],k=h.instance,n=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;gh(e,h,n);f=k}else for(g=0;g<d.length;g++){h=d[g];k=h.instance;n=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;
|
||||
gh(e,h,n);f=k}}}if(Tc)throw a=ue,Tc=!1,ue=null,a;}function B(a,b){var c=b[Me];void 0===c&&(c=b[Me]=new Set);var d=a+"__bubble";c.has(d)||(hh(b,a,2,!1),c.add(d))}function Ne(a,b,c){var d=0;b&&(d|=4);hh(c,a,d,b)}function sc(a){if(!a[id]){a[id]=!0;cg.forEach(function(b){"selectionchange"!==b&&(Uj.has(b)||Ne(b,!1,a),Ne(b,!0,a))});var b=9===a.nodeType?a:a.ownerDocument;null===b||b[id]||(b[id]=!0,Ne("selectionchange",!1,b))}}function hh(a,b,c,d,e){switch(Lg(b)){case 1:e=zj;break;case 4:e=Aj;break;default:e=
|
||||
Be}c=e.bind(null,b,c,a);e=void 0;!Oe||"touchstart"!==b&&"touchmove"!==b&&"wheel"!==b||(e=!0);d?void 0!==e?a.addEventListener(b,c,{capture:!0,passive:e}):a.addEventListener(b,c,!0):void 0!==e?a.addEventListener(b,c,{passive:e}):a.addEventListener(b,c,!1)}function Ce(a,b,c,d,e){var f=d;if(0===(b&1)&&0===(b&2)&&null!==d)a:for(;;){if(null===d)return;var g=d.tag;if(3===g||4===g){var h=d.stateNode.containerInfo;if(h===e||8===h.nodeType&&h.parentNode===e)break;if(4===g)for(g=d.return;null!==g;){var k=g.tag;
|
||||
if(3===k||4===k)if(k=g.stateNode.containerInfo,k===e||8===k.nodeType&&k.parentNode===e)return;g=g.return}for(;null!==h;){g=ob(h);if(null===g)return;k=g.tag;if(5===k||6===k){d=f=g;continue a}h=h.parentNode}}d=d.return}wg(function(){var d=f,e=re(c),g=[];a:{var h=fh.get(a);if(void 0!==h){var k=He,m=a;switch(a){case "keypress":if(0===cd(c))break a;case "keydown":case "keyup":k=Vj;break;case "focusin":m="focus";k=Pe;break;case "focusout":m="blur";k=Pe;break;case "beforeblur":case "afterblur":k=Pe;break;
|
||||
case "click":if(2===c.button)break a;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":k=ih;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":k=Wj;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":k=Xj;break;case jh:case kh:case lh:k=Yj;break;case mh:k=Zj;break;case "scroll":k=ak;break;case "wheel":k=bk;break;case "copy":case "cut":case "paste":k=
|
||||
ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":k=nh}var l=0!==(b&4),p=!l&&"scroll"===a,w=l?null!==h?h+"Capture":null:h;l=[];for(var A=d,t;null!==A;){t=A;var M=t.stateNode;5===t.tag&&null!==M&&(t=M,null!==w&&(M=fc(A,w),null!=M&&l.push(tc(A,M,t))));if(p)break;A=A.return}0<l.length&&(h=new k(h,m,null,c,e),g.push({event:h,listeners:l}))}}if(0===(b&7)){a:{h="mouseover"===a||"pointerover"===
|
||||
a;k="mouseout"===a||"pointerout"===a;if(h&&c!==ze&&(m=c.relatedTarget||c.fromElement)&&(ob(m)||m[Ja]))break a;if(k||h){h=e.window===e?e:(h=e.ownerDocument)?h.defaultView||h.parentWindow:window;if(k){if(m=c.relatedTarget||c.toElement,k=d,m=m?ob(m):null,null!==m&&(p=nb(m),m!==p||5!==m.tag&&6!==m.tag))m=null}else k=null,m=d;if(k!==m){l=ih;M="onMouseLeave";w="onMouseEnter";A="mouse";if("pointerout"===a||"pointerover"===a)l=nh,M="onPointerLeave",w="onPointerEnter",A="pointer";p=null==k?h:Ib(k);t=null==
|
||||
m?h:Ib(m);h=new l(M,A+"leave",k,c,e);h.target=p;h.relatedTarget=t;M=null;ob(e)===d&&(l=new l(w,A+"enter",m,c,e),l.target=t,l.relatedTarget=p,M=l);p=M;if(k&&m)b:{l=k;w=m;A=0;for(t=l;t;t=Lb(t))A++;t=0;for(M=w;M;M=Lb(M))t++;for(;0<A-t;)l=Lb(l),A--;for(;0<t-A;)w=Lb(w),t--;for(;A--;){if(l===w||null!==w&&l===w.alternate)break b;l=Lb(l);w=Lb(w)}l=null}else l=null;null!==k&&oh(g,h,k,l,!1);null!==m&&null!==p&&oh(g,p,m,l,!0)}}}a:{h=d?Ib(d):window;k=h.nodeName&&h.nodeName.toLowerCase();if("select"===k||"input"===
|
||||
k&&"file"===h.type)var ma=Nj;else if(Vg(h))if(ph)ma=Rj;else{ma=Pj;var va=Oj}else(k=h.nodeName)&&"input"===k.toLowerCase()&&("checkbox"===h.type||"radio"===h.type)&&(ma=Qj);if(ma&&(ma=ma(a,d))){Wg(g,ma,c,e);break a}va&&va(a,h,d);"focusout"===a&&(va=h._wrapperState)&&va.controlled&&"number"===h.type&&me(h,"number",h.value)}va=d?Ib(d):window;switch(a){case "focusin":if(Vg(va)||"true"===va.contentEditable)Jb=va,Ke=d,rc=null;break;case "focusout":rc=Ke=Jb=null;break;case "mousedown":Je=!0;break;case "contextmenu":case "mouseup":case "dragend":Je=
|
||||
!1;dh(g,c,e);break;case "selectionchange":if(dk)break;case "keydown":case "keyup":dh(g,c,e)}var ab;if(Ge)b:{switch(a){case "compositionstart":var da="onCompositionStart";break b;case "compositionend":da="onCompositionEnd";break b;case "compositionupdate":da="onCompositionUpdate";break b}da=void 0}else Hb?Qg(a,c)&&(da="onCompositionEnd"):"keydown"===a&&229===c.keyCode&&(da="onCompositionStart");da&&(Ug&&"ko"!==c.locale&&(Hb||"onCompositionStart"!==da?"onCompositionEnd"===da&&Hb&&(ab=Og()):(Za=e,Ee=
|
||||
"value"in Za?Za.value:Za.textContent,Hb=!0)),va=ed(d,da),0<va.length&&(da=new qh(da,a,null,c,e),g.push({event:da,listeners:va}),ab?da.data=ab:(ab=Rg(c),null!==ab&&(da.data=ab))));if(ab=ek?Ij(a,c):Jj(a,c))d=ed(d,"onBeforeInput"),0<d.length&&(e=new fk("onBeforeInput","beforeinput",null,c,e),g.push({event:e,listeners:d}),e.data=ab)}Xg(g,b)})}function tc(a,b,c){return{instance:a,listener:b,currentTarget:c}}function ed(a,b){for(var c=b+"Capture",d=[];null!==a;){var e=a,f=e.stateNode;5===e.tag&&null!==
|
||||
f&&(e=f,f=fc(a,c),null!=f&&d.unshift(tc(a,f,e)),f=fc(a,b),null!=f&&d.push(tc(a,f,e)));a=a.return}return d}function Lb(a){if(null===a)return null;do a=a.return;while(a&&5!==a.tag);return a?a:null}function oh(a,b,c,d,e){for(var f=b._reactName,g=[];null!==c&&c!==d;){var h=c,k=h.alternate,n=h.stateNode;if(null!==k&&k===d)break;5===h.tag&&null!==n&&(h=n,e?(k=fc(c,f),null!=k&&g.unshift(tc(c,k,h))):e||(k=fc(c,f),null!=k&&g.push(tc(c,k,h))));c=c.return}0!==g.length&&a.push({event:b,listeners:g})}function rh(a){return("string"===
|
||||
typeof a?a:""+a).replace(gk,"\n").replace(hk,"")}function jd(a,b,c,d){b=rh(b);if(rh(a)!==b&&c)throw Error(m(425));}function kd(){}function Qe(a,b){return"textarea"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function ik(a){setTimeout(function(){throw a;})}function Re(a,b){var c=b,d=0;do{var e=c.nextSibling;a.removeChild(c);if(e&&8===e.nodeType)if(c=
|
||||
e.data,"/$"===c){if(0===d){a.removeChild(e);nc(b);return}d--}else"$"!==c&&"$?"!==c&&"$!"!==c||d++;c=e}while(c);nc(b)}function Ka(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break;if(8===b){b=a.data;if("$"===b||"$!"===b||"$?"===b)break;if("/$"===b)return null}}return a}function sh(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if("$"===c||"$!"===c||"$?"===c){if(0===b)return a;b--}else"/$"===c&&b++}a=a.previousSibling}return null}function ob(a){var b=a[Da];
|
||||
if(b)return b;for(var c=a.parentNode;c;){if(b=c[Ja]||c[Da]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=sh(a);null!==a;){if(c=a[Da])return c;a=sh(a)}return b}a=c;c=a.parentNode}return null}function ec(a){a=a[Da]||a[Ja];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Ib(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(m(33));}function Rc(a){return a[uc]||null}function bb(a){return{current:a}}function v(a,b){0>Mb||(a.current=Se[Mb],Se[Mb]=null,Mb--)}
|
||||
function y(a,b,c){Mb++;Se[Mb]=a.current;a.current=b}function Nb(a,b){var c=a.type.contextTypes;if(!c)return cb;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function ea(a){a=a.childContextTypes;return null!==a&&void 0!==a}function th(a,b,c){if(J.current!==cb)throw Error(m(168));
|
||||
y(J,b);y(S,c)}function uh(a,b,c){var d=a.stateNode;b=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in b))throw Error(m(108,gj(a)||"Unknown",e));return E({},c,d)}function ld(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||cb;pb=J.current;y(J,a);y(S,S.current);return!0}function vh(a,b,c){var d=a.stateNode;if(!d)throw Error(m(169));c?(a=uh(a,b,pb),d.__reactInternalMemoizedMergedChildContext=a,v(S),v(J),y(J,a)):v(S);
|
||||
y(S,c)}function wh(a){null===La?La=[a]:La.push(a)}function jk(a){md=!0;wh(a)}function db(){if(!Te&&null!==La){Te=!0;var a=0,b=z;try{var c=La;for(z=1;a<c.length;a++){var d=c[a];do d=d(!0);while(null!==d)}La=null;md=!1}catch(e){throw null!==La&&(La=La.slice(a+1)),xh(De,db),e;}finally{z=b,Te=!1}}return null}function qb(a,b){Ob[Pb++]=nd;Ob[Pb++]=od;od=a;nd=b}function yh(a,b,c){na[oa++]=Ma;na[oa++]=Na;na[oa++]=rb;rb=a;var d=Ma;a=Na;var e=32-ta(d)-1;d&=~(1<<e);c+=1;var f=32-ta(b)+e;if(30<f){var g=e-e%5;
|
||||
f=(d&(1<<g)-1).toString(32);d>>=g;e-=g;Ma=1<<32-ta(b)+e|c<<e|d;Na=f+a}else Ma=1<<f|c<<e|d,Na=a}function Ue(a){null!==a.return&&(qb(a,1),yh(a,1,0))}function Ve(a){for(;a===od;)od=Ob[--Pb],Ob[Pb]=null,nd=Ob[--Pb],Ob[Pb]=null;for(;a===rb;)rb=na[--oa],na[oa]=null,Na=na[--oa],na[oa]=null,Ma=na[--oa],na[oa]=null}function zh(a,b){var c=pa(5,null,null,0);c.elementType="DELETED";c.stateNode=b;c.return=a;b=a.deletions;null===b?(a.deletions=[c],a.flags|=16):b.push(c)}function Ah(a,b){switch(a.tag){case 5:var c=
|
||||
a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,la=a,fa=Ka(b.firstChild),!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,la=a,fa=null,!0):!1;case 13:return b=8!==b.nodeType?null:b,null!==b?(c=null!==rb?{id:Ma,overflow:Na}:null,a.memoizedState={dehydrated:b,treeContext:c,retryLane:1073741824},c=pa(18,null,null,0),c.stateNode=b,c.return=a,a.child=c,la=a,fa=null,!0):!1;default:return!1}}function We(a){return 0!==
|
||||
(a.mode&1)&&0===(a.flags&128)}function Xe(a){if(D){var b=fa;if(b){var c=b;if(!Ah(a,b)){if(We(a))throw Error(m(418));b=Ka(c.nextSibling);var d=la;b&&Ah(a,b)?zh(d,c):(a.flags=a.flags&-4097|2,D=!1,la=a)}}else{if(We(a))throw Error(m(418));a.flags=a.flags&-4097|2;D=!1;la=a}}}function Bh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;la=a}function pd(a){if(a!==la)return!1;if(!D)return Bh(a),D=!0,!1;var b;(b=3!==a.tag)&&!(b=5!==a.tag)&&(b=a.type,b="head"!==b&&"body"!==b&&!Qe(a.type,
|
||||
a.memoizedProps));if(b&&(b=fa)){if(We(a)){for(a=fa;a;)a=Ka(a.nextSibling);throw Error(m(418));}for(;b;)zh(a,b),b=Ka(b.nextSibling)}Bh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(m(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if("/$"===c){if(0===b){fa=Ka(a.nextSibling);break a}b--}else"$"!==c&&"$!"!==c&&"$?"!==c||b++}a=a.nextSibling}fa=null}}else fa=la?Ka(a.stateNode.nextSibling):null;return!0}function Qb(){fa=la=null;D=!1}function Ye(a){null===
|
||||
wa?wa=[a]:wa.push(a)}function vc(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(m(309));var d=c.stateNode}if(!d)throw Error(m(147,a));var e=d,f=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===f)return b.ref;b=function(a){var b=e.refs;null===a?delete b[f]:b[f]=a};b._stringRef=f;return b}if("string"!==typeof a)throw Error(m(284));if(!c._owner)throw Error(m(290,a));}return a}function qd(a,b){a=
|
||||
Object.prototype.toString.call(b);throw Error(m(31,"[object Object]"===a?"object with keys {"+Object.keys(b).join(", ")+"}":a));}function Ch(a){var b=a._init;return b(a._payload)}function Dh(a){function b(b,c){if(a){var d=b.deletions;null===d?(b.deletions=[c],b.flags|=16):d.push(c)}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b){a=eb(a,b);a.index=
|
||||
0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return b.flags|=1048576,c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.flags|=2,c):d;b.flags|=2;return c}function g(b){a&&null===b.alternate&&(b.flags|=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=Ze(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function k(a,b,c,d){var f=c.type;if(f===Bb)return l(a,b,c.props.children,d,c.key);if(null!==b&&(b.elementType===f||"object"===typeof f&&null!==f&&f.$$typeof===Ta&&
|
||||
Ch(f)===b.type))return d=e(b,c.props),d.ref=vc(a,b,c),d.return=a,d;d=rd(c.type,c.key,c.props,null,a.mode,d);d.ref=vc(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=$e(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=sb(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function u(a,b,c){if("string"===
|
||||
typeof b&&""!==b||"number"===typeof b)return b=Ze(""+b,a.mode,c),b.return=a,b;if("object"===typeof b&&null!==b){switch(b.$$typeof){case sd:return c=rd(b.type,b.key,b.props,null,a.mode,c),c.ref=vc(a,null,b),c.return=a,c;case Cb:return b=$e(b,a.mode,c),b.return=a,b;case Ta:var d=b._init;return u(a,d(b._payload),c)}if(cc(b)||ac(b))return b=sb(b,a.mode,c,null),b.return=a,b;qd(a,b)}return null}function r(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c&&""!==c||"number"===typeof c)return null!==
|
||||
e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case sd:return c.key===e?k(a,b,c,d):null;case Cb:return c.key===e?n(a,b,c,d):null;case Ta:return e=c._init,r(a,b,e(c._payload),d)}if(cc(c)||ac(c))return null!==e?null:l(a,b,c,d,null);qd(a,c)}return null}function p(a,b,c,d,e){if("string"===typeof d&&""!==d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case sd:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,
|
||||
e);case Cb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e);case Ta:var f=d._init;return p(a,b,c,f(d._payload),e)}if(cc(d)||ac(d))return a=a.get(c)||null,l(b,a,d,e,null);qd(b,d)}return null}function x(e,g,h,k){for(var n=null,m=null,l=g,t=g=0,q=null;null!==l&&t<h.length;t++){l.index>t?(q=l,l=null):q=l.sibling;var A=r(e,l,h[t],k);if(null===A){null===l&&(l=q);break}a&&l&&null===A.alternate&&b(e,l);g=f(A,g,t);null===m?n=A:m.sibling=A;m=A;l=q}if(t===h.length)return c(e,l),D&&qb(e,t),n;if(null===l){for(;t<
|
||||
h.length;t++)l=u(e,h[t],k),null!==l&&(g=f(l,g,t),null===m?n=l:m.sibling=l,m=l);D&&qb(e,t);return n}for(l=d(e,l);t<h.length;t++)q=p(l,e,t,h[t],k),null!==q&&(a&&null!==q.alternate&&l.delete(null===q.key?t:q.key),g=f(q,g,t),null===m?n=q:m.sibling=q,m=q);a&&l.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function I(e,g,h,k){var n=ac(h);if("function"!==typeof n)throw Error(m(150));h=n.call(h);if(null==h)throw Error(m(151));for(var l=n=null,q=g,t=g=0,A=null,w=h.next();null!==q&&!w.done;t++,w=
|
||||
h.next()){q.index>t?(A=q,q=null):A=q.sibling;var x=r(e,q,w.value,k);if(null===x){null===q&&(q=A);break}a&&q&&null===x.alternate&&b(e,q);g=f(x,g,t);null===l?n=x:l.sibling=x;l=x;q=A}if(w.done)return c(e,q),D&&qb(e,t),n;if(null===q){for(;!w.done;t++,w=h.next())w=u(e,w.value,k),null!==w&&(g=f(w,g,t),null===l?n=w:l.sibling=w,l=w);D&&qb(e,t);return n}for(q=d(e,q);!w.done;t++,w=h.next())w=p(q,e,t,w.value,k),null!==w&&(a&&null!==w.alternate&&q.delete(null===w.key?t:w.key),g=f(w,g,t),null===l?n=w:l.sibling=
|
||||
w,l=w);a&&q.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function v(a,d,f,h){"object"===typeof f&&null!==f&&f.type===Bb&&null===f.key&&(f=f.props.children);if("object"===typeof f&&null!==f){switch(f.$$typeof){case sd:a:{for(var k=f.key,n=d;null!==n;){if(n.key===k){k=f.type;if(k===Bb){if(7===n.tag){c(a,n.sibling);d=e(n,f.props.children);d.return=a;a=d;break a}}else if(n.elementType===k||"object"===typeof k&&null!==k&&k.$$typeof===Ta&&Ch(k)===n.type){c(a,n.sibling);d=e(n,f.props);d.ref=vc(a,
|
||||
n,f);d.return=a;a=d;break a}c(a,n);break}else b(a,n);n=n.sibling}f.type===Bb?(d=sb(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=rd(f.type,f.key,f.props,null,a.mode,h),h.ref=vc(a,d,f),h.return=a,a=h)}return g(a);case Cb:a:{for(n=f.key;null!==d;){if(d.key===n)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=$e(f,a.mode,h);d.return=a;
|
||||
a=d}return g(a);case Ta:return n=f._init,v(a,d,n(f._payload),h)}if(cc(f))return x(a,d,f,h);if(ac(f))return I(a,d,f,h);qd(a,f)}return"string"===typeof f&&""!==f||"number"===typeof f?(f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Ze(f,a.mode,h),d.return=a,a=d),g(a)):c(a,d)}return v}function af(){bf=Rb=td=null}function cf(a,b){b=ud.current;v(ud);a._currentValue=b}function df(a,b,c){for(;null!==a;){var d=a.alternate;(a.childLanes&b)!==b?(a.childLanes|=b,null!==d&&(d.childLanes|=
|
||||
b)):null!==d&&(d.childLanes&b)!==b&&(d.childLanes|=b);if(a===c)break;a=a.return}}function Sb(a,b){td=a;bf=Rb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(0!==(a.lanes&b)&&(ha=!0),a.firstContext=null)}function qa(a){var b=a._currentValue;if(bf!==a)if(a={context:a,memoizedValue:b,next:null},null===Rb){if(null===td)throw Error(m(308));Rb=a;td.dependencies={lanes:0,firstContext:a}}else Rb=Rb.next=a;return b}function ef(a){null===tb?tb=[a]:tb.push(a)}function Eh(a,b,c,d){var e=b.interleaved;
|
||||
null===e?(c.next=c,ef(b)):(c.next=e.next,e.next=c);b.interleaved=c;return Oa(a,d)}function Oa(a,b){a.lanes|=b;var c=a.alternate;null!==c&&(c.lanes|=b);c=a;for(a=a.return;null!==a;)a.childLanes|=b,c=a.alternate,null!==c&&(c.childLanes|=b),c=a,a=a.return;return 3===c.tag?c.stateNode:null}function ff(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Fh(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue=
|
||||
{baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function Pa(a,b){return{eventTime:a,lane:b,tag:0,payload:null,callback:null,next:null}}function fb(a,b,c){var d=a.updateQueue;if(null===d)return null;d=d.shared;if(0!==(p&2)){var e=d.pending;null===e?b.next=b:(b.next=e.next,e.next=b);d.pending=b;return kk(a,c)}e=d.interleaved;null===e?(b.next=b,ef(d)):(b.next=e.next,e.next=b);d.interleaved=b;return Oa(a,c)}function vd(a,b,c){b=
|
||||
b.updateQueue;if(null!==b&&(b=b.shared,0!==(c&4194240))){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Gh(a,b){var c=a.updateQueue,d=a.alternate;if(null!==d&&(d=d.updateQueue,c===d)){var e=null,f=null;c=c.firstBaseUpdate;if(null!==c){do{var g={eventTime:c.eventTime,lane:c.lane,tag:c.tag,payload:c.payload,callback:c.callback,next:null};null===f?e=f=g:f=f.next=g;c=c.next}while(null!==c);null===f?e=f=b:f=f.next=b}else e=f=b;c={baseState:d.baseState,firstBaseUpdate:e,lastBaseUpdate:f,
|
||||
shared:d.shared,effects:d.effects};a.updateQueue=c;return}a=c.lastBaseUpdate;null===a?c.firstBaseUpdate=b:a.next=b;c.lastBaseUpdate=b}function wd(a,b,c,d){var e=a.updateQueue;gb=!1;var f=e.firstBaseUpdate,g=e.lastBaseUpdate,h=e.shared.pending;if(null!==h){e.shared.pending=null;var k=h,n=k.next;k.next=null;null===g?f=n:g.next=n;g=k;var l=a.alternate;null!==l&&(l=l.updateQueue,h=l.lastBaseUpdate,h!==g&&(null===h?l.firstBaseUpdate=n:h.next=n,l.lastBaseUpdate=k))}if(null!==f){var m=e.baseState;g=0;l=
|
||||
n=k=null;h=f;do{var r=h.lane,p=h.eventTime;if((d&r)===r){null!==l&&(l=l.next={eventTime:p,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});a:{var x=a,v=h;r=b;p=c;switch(v.tag){case 1:x=v.payload;if("function"===typeof x){m=x.call(p,m,r);break a}m=x;break a;case 3:x.flags=x.flags&-65537|128;case 0:x=v.payload;r="function"===typeof x?x.call(p,m,r):x;if(null===r||void 0===r)break a;m=E({},m,r);break a;case 2:gb=!0}}null!==h.callback&&0!==h.lane&&(a.flags|=64,r=e.effects,null===r?e.effects=
|
||||
[h]:r.push(h))}else p={eventTime:p,lane:r,tag:h.tag,payload:h.payload,callback:h.callback,next:null},null===l?(n=l=p,k=m):l=l.next=p,g|=r;h=h.next;if(null===h)if(h=e.shared.pending,null===h)break;else r=h,h=r.next,r.next=null,e.lastBaseUpdate=r,e.shared.pending=null}while(1);null===l&&(k=m);e.baseState=k;e.firstBaseUpdate=n;e.lastBaseUpdate=l;b=e.shared.interleaved;if(null!==b){e=b;do g|=e.lane,e=e.next;while(e!==b)}else null===f&&(e.shared.lanes=0);ra|=g;a.lanes=g;a.memoizedState=m}}function Hh(a,
|
||||
b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=c;if("function"!==typeof e)throw Error(m(191,e));e.call(d)}}}function ub(a){if(a===wc)throw Error(m(174));return a}function gf(a,b){y(xc,b);y(yc,a);y(Ea,wc);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:oe(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=oe(b,a)}v(Ea);y(Ea,b)}function Tb(a){v(Ea);v(yc);v(xc)}function Ih(a){ub(xc.current);
|
||||
var b=ub(Ea.current);var c=oe(b,a.type);b!==c&&(y(yc,a),y(Ea,c))}function hf(a){yc.current===a&&(v(Ea),v(yc))}function xd(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||"$?"===c.data||"$!"===c.data))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.flags&128))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=
|
||||
b.return;b=b.sibling}return null}function jf(){for(var a=0;a<kf.length;a++)kf[a]._workInProgressVersionPrimary=null;kf.length=0}function V(){throw Error(m(321));}function lf(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!ua(a[c],b[c]))return!1;return!0}function mf(a,b,c,d,e,f){vb=f;C=b;b.memoizedState=null;b.updateQueue=null;b.lanes=0;yd.current=null===a||null===a.memoizedState?lk:mk;a=c(d,e);if(zc){f=0;do{zc=!1;Ac=0;if(25<=f)throw Error(m(301));f+=1;N=K=null;b.updateQueue=null;
|
||||
yd.current=nk;a=c(d,e)}while(zc)}yd.current=zd;b=null!==K&&null!==K.next;vb=0;N=K=C=null;Ad=!1;if(b)throw Error(m(300));return a}function nf(){var a=0!==Ac;Ac=0;return a}function Fa(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===N?C.memoizedState=N=a:N=N.next=a;return N}function sa(){if(null===K){var a=C.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===N?C.memoizedState:N.next;if(null!==b)N=b,K=a;else{if(null===a)throw Error(m(310));K=a;
|
||||
a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===N?C.memoizedState=N=a:N=N.next=a}return N}function Bc(a,b){return"function"===typeof b?b(a):b}function of(a,b,c){b=sa();c=b.queue;if(null===c)throw Error(m(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){f=e.next;d=d.baseState;var h=g=null,k=null,n=f;do{var l=n.lane;if((vb&
|
||||
l)===l)null!==k&&(k=k.next={lane:0,action:n.action,hasEagerState:n.hasEagerState,eagerState:n.eagerState,next:null}),d=n.hasEagerState?n.eagerState:a(d,n.action);else{var u={lane:l,action:n.action,hasEagerState:n.hasEagerState,eagerState:n.eagerState,next:null};null===k?(h=k=u,g=d):k=k.next=u;C.lanes|=l;ra|=l}n=n.next}while(null!==n&&n!==f);null===k?g=d:k.next=h;ua(d,b.memoizedState)||(ha=!0);b.memoizedState=d;b.baseState=g;b.baseQueue=k;c.lastRenderedState=d}a=c.interleaved;if(null!==a){e=a;do f=
|
||||
e.lane,C.lanes|=f,ra|=f,e=e.next;while(e!==a)}else null===e&&(c.lanes=0);return[b.memoizedState,c.dispatch]}function pf(a,b,c){b=sa();c=b.queue;if(null===c)throw Error(m(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);ua(f,b.memoizedState)||(ha=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function Jh(a,b,c){}function Kh(a,b,c){c=C;var d=sa(),
|
||||
e=b(),f=!ua(d.memoizedState,e);f&&(d.memoizedState=e,ha=!0);d=d.queue;qf(Lh.bind(null,c,d,a),[a]);if(d.getSnapshot!==b||f||null!==N&&N.memoizedState.tag&1){c.flags|=2048;Cc(9,Mh.bind(null,c,d,e,b),void 0,null);if(null===O)throw Error(m(349));0!==(vb&30)||Nh(c,b,e)}return e}function Nh(a,b,c){a.flags|=16384;a={getSnapshot:b,value:c};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.stores=[a]):(c=b.stores,null===c?b.stores=[a]:c.push(a))}function Mh(a,b,c,d){b.value=c;b.getSnapshot=
|
||||
d;Oh(b)&&Ph(a)}function Lh(a,b,c){return c(function(){Oh(b)&&Ph(a)})}function Oh(a){var b=a.getSnapshot;a=a.value;try{var c=b();return!ua(a,c)}catch(d){return!0}}function Ph(a){var b=Oa(a,1);null!==b&&xa(b,a,1,-1)}function Qh(a){var b=Fa();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:Bc,lastRenderedState:a};b.queue=a;a=a.dispatch=ok.bind(null,C,a);return[b.memoizedState,a]}function Cc(a,b,c,d){a={tag:a,create:b,
|
||||
destroy:c,deps:d,next:null};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function Rh(a){return sa().memoizedState}function Bd(a,b,c,d){var e=Fa();C.flags|=a;e.memoizedState=Cc(1|b,c,void 0,void 0===d?null:d)}function Cd(a,b,c,d){var e=sa();d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&lf(d,g.deps)){e.memoizedState=
|
||||
Cc(b,c,f,d);return}}C.flags|=a;e.memoizedState=Cc(1|b,c,f,d)}function Sh(a,b){return Bd(8390656,8,a,b)}function qf(a,b){return Cd(2048,8,a,b)}function Th(a,b){return Cd(4,2,a,b)}function Uh(a,b){return Cd(4,4,a,b)}function Vh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function Wh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Cd(4,4,Vh.bind(null,b,a),c)}function rf(a,b){}function Xh(a,b){var c=
|
||||
sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&lf(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function Yh(a,b){var c=sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&lf(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Zh(a,b,c){if(0===(vb&21))return a.baseState&&(a.baseState=!1,ha=!0),a.memoizedState=c;ua(c,b)||(c=Dg(),C.lanes|=c,ra|=c,a.baseState=!0);return b}function pk(a,b,c){c=z;z=0!==c&&4>c?c:4;a(!0);var d=sf.transition;sf.transition=
|
||||
{};try{a(!1),b()}finally{z=c,sf.transition=d}}function $h(){return sa().memoizedState}function qk(a,b,c){var d=hb(a);c={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,c);else if(c=Eh(a,b,c,d),null!==c){var e=Z();xa(c,a,d,e);ci(c,b,d)}}function ok(a,b,c){var d=hb(a),e={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,e);else{var f=a.alternate;if(0===a.lanes&&(null===f||0===f.lanes)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
|
||||
h=f(g,c);e.hasEagerState=!0;e.eagerState=h;if(ua(h,g)){var k=b.interleaved;null===k?(e.next=e,ef(b)):(e.next=k.next,k.next=e);b.interleaved=e;return}}catch(n){}finally{}c=Eh(a,b,e,d);null!==c&&(e=Z(),xa(c,a,d,e),ci(c,b,d))}}function ai(a){var b=a.alternate;return a===C||null!==b&&b===C}function bi(a,b){zc=Ad=!0;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}function ci(a,b,c){if(0!==(c&4194240)){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function ya(a,b){if(a&&
|
||||
a.defaultProps){b=E({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return b}function tf(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:E({},b,c);a.memoizedState=c;0===a.lanes&&(a.updateQueue.baseState=c)}function di(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!qc(c,d)||!qc(e,f):!0}function ei(a,b,c){var d=!1,e=cb;var f=b.contextType;"object"===typeof f&&
|
||||
null!==f?f=qa(f):(e=ea(b)?pb:J.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?Nb(a,e):cb);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Dd;a.stateNode=b;b._reactInternals=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function fi(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&
|
||||
b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Dd.enqueueReplaceState(b,b.state,null)}function uf(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs={};ff(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=qa(f):(f=ea(b)?pb:J.current,e.context=Nb(a,f));e.state=a.memoizedState;f=b.getDerivedStateFromProps;"function"===typeof f&&(tf(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==
|
||||
typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Dd.enqueueReplaceState(e,e.state,null),wd(a,c,e,d),e.state=a.memoizedState);"function"===typeof e.componentDidMount&&(a.flags|=4194308)}function Ub(a,b){try{var c="",d=b;do c+=fj(d),d=d.return;while(d);var e=c}catch(f){e="\nError generating stack: "+f.message+
|
||||
"\n"+f.stack}return{value:a,source:b,stack:e,digest:null}}function vf(a,b,c){return{value:a,source:null,stack:null!=c?c:null,digest:null!=b?b:null}}function wf(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}function gi(a,b,c){c=Pa(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Ed||(Ed=!0,xf=d);wf(a,b)};return c}function hi(a,b,c){c=Pa(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)};
|
||||
c.callback=function(){wf(a,b)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){wf(a,b);"function"!==typeof d&&(null===ib?ib=new Set([this]):ib.add(this));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ii(a,b,c){var d=a.pingCache;if(null===d){d=a.pingCache=new rk;var e=new Set;d.set(b,e)}else e=d.get(b),void 0===e&&(e=new Set,d.set(b,e));e.has(c)||(e.add(c),a=sk.bind(null,a,b,c),b.then(a,a))}function ji(a){do{var b;
|
||||
if(b=13===a.tag)b=a.memoizedState,b=null!==b?null!==b.dehydrated?!0:!1:!0;if(b)return a;a=a.return}while(null!==a);return null}function ki(a,b,c,d,e){if(0===(a.mode&1))return a===b?a.flags|=65536:(a.flags|=128,c.flags|=131072,c.flags&=-52805,1===c.tag&&(null===c.alternate?c.tag=17:(b=Pa(-1,1),b.tag=2,fb(c,b,1))),c.lanes|=1),a;a.flags|=65536;a.lanes=e;return a}function aa(a,b,c,d){b.child=null===a?li(b,null,c,d):Vb(b,a.child,c,d)}function mi(a,b,c,d,e){c=c.render;var f=b.ref;Sb(b,e);d=mf(a,b,c,d,f,
|
||||
e);c=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&c&&Ue(b);b.flags|=1;aa(a,b,d,e);return b.child}function ni(a,b,c,d,e){if(null===a){var f=c.type;if("function"===typeof f&&!yf(f)&&void 0===f.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=f,oi(a,b,f,d,e);a=rd(c.type,null,d,b,b.mode,e);a.ref=b.ref;a.return=b;return b.child=a}f=a.child;if(0===(a.lanes&e)){var g=f.memoizedProps;c=c.compare;c=null!==c?c:qc;if(c(g,d)&&a.ref===
|
||||
b.ref)return Qa(a,b,e)}b.flags|=1;a=eb(f,d);a.ref=b.ref;a.return=b;return b.child=a}function oi(a,b,c,d,e){if(null!==a){var f=a.memoizedProps;if(qc(f,d)&&a.ref===b.ref)if(ha=!1,b.pendingProps=d=f,0!==(a.lanes&e))0!==(a.flags&131072)&&(ha=!0);else return b.lanes=a.lanes,Qa(a,b,e)}return zf(a,b,c,d,e)}function pi(a,b,c){var d=b.pendingProps,e=d.children,f=null!==a?a.memoizedState:null;if("hidden"===d.mode)if(0===(b.mode&1))b.memoizedState={baseLanes:0,cachePool:null,transitions:null},y(Ga,ba),ba|=c;
|
||||
else{if(0===(c&1073741824))return a=null!==f?f.baseLanes|c:c,b.lanes=b.childLanes=1073741824,b.memoizedState={baseLanes:a,cachePool:null,transitions:null},b.updateQueue=null,y(Ga,ba),ba|=a,null;b.memoizedState={baseLanes:0,cachePool:null,transitions:null};d=null!==f?f.baseLanes:c;y(Ga,ba);ba|=d}else null!==f?(d=f.baseLanes|c,b.memoizedState=null):d=c,y(Ga,ba),ba|=d;aa(a,b,e,c);return b.child}function qi(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.flags|=512,b.flags|=2097152}function zf(a,
|
||||
b,c,d,e){var f=ea(c)?pb:J.current;f=Nb(b,f);Sb(b,e);c=mf(a,b,c,d,f,e);d=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&d&&Ue(b);b.flags|=1;aa(a,b,c,e);return b.child}function ri(a,b,c,d,e){if(ea(c)){var f=!0;ld(b)}else f=!1;Sb(b,e);if(null===b.stateNode)Fd(a,b),ei(b,c,d),uf(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var k=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=qa(n):(n=ea(c)?pb:J.current,n=Nb(b,
|
||||
n));var l=c.getDerivedStateFromProps,m="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate;m||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||k!==n)&&fi(b,g,d,n);gb=!1;var r=b.memoizedState;g.state=r;wd(b,d,g,e);k=b.memoizedState;h!==d||r!==k||S.current||gb?("function"===typeof l&&(tf(b,c,l,d),k=b.memoizedState),(h=gb||di(b,c,h,d,r,k,n))?(m||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||
|
||||
("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.flags|=4194308)):("function"===typeof g.componentDidMount&&(b.flags|=4194308),b.memoizedProps=d,b.memoizedState=k),g.props=d,g.state=k,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.flags|=4194308),d=!1)}else{g=b.stateNode;Fh(a,b);h=b.memoizedProps;n=b.type===b.elementType?h:ya(b.type,h);g.props=
|
||||
n;m=b.pendingProps;r=g.context;k=c.contextType;"object"===typeof k&&null!==k?k=qa(k):(k=ea(c)?pb:J.current,k=Nb(b,k));var p=c.getDerivedStateFromProps;(l="function"===typeof p||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==m||r!==k)&&fi(b,g,d,k);gb=!1;r=b.memoizedState;g.state=r;wd(b,d,g,e);var x=b.memoizedState;h!==m||r!==x||S.current||gb?("function"===typeof p&&(tf(b,c,p,d),x=b.memoizedState),
|
||||
(n=gb||di(b,c,n,d,r,x,k)||!1)?(l||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,x,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,x,k)),"function"===typeof g.componentDidUpdate&&(b.flags|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.flags|=1024)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=
|
||||
4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),b.memoizedProps=d,b.memoizedState=x),g.props=d,g.state=x,g.context=k,d=n):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),d=!1)}return Af(a,b,c,d,f,e)}function Af(a,b,c,d,e,f){qi(a,b);var g=0!==(b.flags&128);if(!d&&!g)return e&&vh(b,c,!1),
|
||||
Qa(a,b,f);d=b.stateNode;tk.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.flags|=1;null!==a&&g?(b.child=Vb(b,a.child,null,f),b.child=Vb(b,null,h,f)):aa(a,b,h,f);b.memoizedState=d.state;e&&vh(b,c,!0);return b.child}function si(a){var b=a.stateNode;b.pendingContext?th(a,b.pendingContext,b.pendingContext!==b.context):b.context&&th(a,b.context,!1);gf(a,b.containerInfo)}function ti(a,b,c,d,e){Qb();Ye(e);b.flags|=256;aa(a,b,c,d);return b.child}function Bf(a){return{baseLanes:a,
|
||||
cachePool:null,transitions:null}}function ui(a,b,c){var d=b.pendingProps,e=F.current,f=!1,g=0!==(b.flags&128),h;(h=g)||(h=null!==a&&null===a.memoizedState?!1:0!==(e&2));if(h)f=!0,b.flags&=-129;else if(null===a||null!==a.memoizedState)e|=1;y(F,e&1);if(null===a){Xe(b);a=b.memoizedState;if(null!==a&&(a=a.dehydrated,null!==a))return 0===(b.mode&1)?b.lanes=1:"$!"===a.data?b.lanes=8:b.lanes=1073741824,null;g=d.children;a=d.fallback;return f?(d=b.mode,f=b.child,g={mode:"hidden",children:g},0===(d&1)&&null!==
|
||||
f?(f.childLanes=0,f.pendingProps=g):f=Gd(g,d,0,null),a=sb(a,d,c,null),f.return=b,a.return=b,f.sibling=a,b.child=f,b.child.memoizedState=Bf(c),b.memoizedState=Cf,a):Df(b,g)}e=a.memoizedState;if(null!==e&&(h=e.dehydrated,null!==h))return uk(a,b,g,d,h,e,c);if(f){f=d.fallback;g=b.mode;e=a.child;h=e.sibling;var k={mode:"hidden",children:d.children};0===(g&1)&&b.child!==e?(d=b.child,d.childLanes=0,d.pendingProps=k,b.deletions=null):(d=eb(e,k),d.subtreeFlags=e.subtreeFlags&14680064);null!==h?f=eb(h,f):(f=
|
||||
sb(f,g,c,null),f.flags|=2);f.return=b;d.return=b;d.sibling=f;b.child=d;d=f;f=b.child;g=a.child.memoizedState;g=null===g?Bf(c):{baseLanes:g.baseLanes|c,cachePool:null,transitions:g.transitions};f.memoizedState=g;f.childLanes=a.childLanes&~c;b.memoizedState=Cf;return d}f=a.child;a=f.sibling;d=eb(f,{mode:"visible",children:d.children});0===(b.mode&1)&&(d.lanes=c);d.return=b;d.sibling=null;null!==a&&(c=b.deletions,null===c?(b.deletions=[a],b.flags|=16):c.push(a));b.child=d;b.memoizedState=null;return d}
|
||||
function Df(a,b,c){b=Gd({mode:"visible",children:b},a.mode,0,null);b.return=a;return a.child=b}function Hd(a,b,c,d){null!==d&&Ye(d);Vb(b,a.child,null,c);a=Df(b,b.pendingProps.children);a.flags|=2;b.memoizedState=null;return a}function uk(a,b,c,d,e,f,g){if(c){if(b.flags&256)return b.flags&=-257,d=vf(Error(m(422))),Hd(a,b,g,d);if(null!==b.memoizedState)return b.child=a.child,b.flags|=128,null;f=d.fallback;e=b.mode;d=Gd({mode:"visible",children:d.children},e,0,null);f=sb(f,e,g,null);f.flags|=2;d.return=
|
||||
b;f.return=b;d.sibling=f;b.child=d;0!==(b.mode&1)&&Vb(b,a.child,null,g);b.child.memoizedState=Bf(g);b.memoizedState=Cf;return f}if(0===(b.mode&1))return Hd(a,b,g,null);if("$!"===e.data){d=e.nextSibling&&e.nextSibling.dataset;if(d)var h=d.dgst;d=h;f=Error(m(419));d=vf(f,d,void 0);return Hd(a,b,g,d)}h=0!==(g&a.childLanes);if(ha||h){d=O;if(null!==d){switch(g&-g){case 4:e=2;break;case 16:e=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:e=
|
||||
32;break;case 536870912:e=268435456;break;default:e=0}e=0!==(e&(d.suspendedLanes|g))?0:e;0!==e&&e!==f.retryLane&&(f.retryLane=e,Oa(a,e),xa(d,a,e,-1))}Ef();d=vf(Error(m(421)));return Hd(a,b,g,d)}if("$?"===e.data)return b.flags|=128,b.child=a.child,b=vk.bind(null,a),e._reactRetry=b,null;a=f.treeContext;fa=Ka(e.nextSibling);la=b;D=!0;wa=null;null!==a&&(na[oa++]=Ma,na[oa++]=Na,na[oa++]=rb,Ma=a.id,Na=a.overflow,rb=b);b=Df(b,d.children);b.flags|=4096;return b}function vi(a,b,c){a.lanes|=b;var d=a.alternate;
|
||||
null!==d&&(d.lanes|=b);df(a.return,b,c)}function Ff(a,b,c,d,e){var f=a.memoizedState;null===f?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailMode:e}:(f.isBackwards=b,f.rendering=null,f.renderingStartTime=0,f.last=d,f.tail=c,f.tailMode=e)}function wi(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;aa(a,b,d.children,c);d=F.current;if(0!==(d&2))d=d&1|2,b.flags|=128;else{if(null!==a&&0!==(a.flags&128))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&
|
||||
vi(a,c,b);else if(19===a.tag)vi(a,c,b);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(F,d);if(0===(b.mode&1))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===xd(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ff(b,!1,e,c,f);break;case "backwards":c=
|
||||
null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===xd(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ff(b,!0,c,null,f);break;case "together":Ff(b,!1,null,null,void 0);break;default:b.memoizedState=null}return b.child}function Fd(a,b){0===(b.mode&1)&&null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2)}function Qa(a,b,c){null!==a&&(b.dependencies=a.dependencies);ra|=b.lanes;if(0===(c&b.childLanes))return null;if(null!==a&&b.child!==a.child)throw Error(m(153));if(null!==
|
||||
b.child){a=b.child;c=eb(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=eb(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}function wk(a,b,c){switch(b.tag){case 3:si(b);Qb();break;case 5:Ih(b);break;case 1:ea(b.type)&&ld(b);break;case 4:gf(b,b.stateNode.containerInfo);break;case 10:var d=b.type._context,e=b.memoizedProps.value;y(ud,d._currentValue);d._currentValue=e;break;case 13:d=b.memoizedState;if(null!==d){if(null!==d.dehydrated)return y(F,F.current&
|
||||
1),b.flags|=128,null;if(0!==(c&b.child.childLanes))return ui(a,b,c);y(F,F.current&1);a=Qa(a,b,c);return null!==a?a.sibling:null}y(F,F.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&128)){if(d)return wi(a,b,c);b.flags|=128}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);y(F,F.current);if(d)break;else return null;case 22:case 23:return b.lanes=0,pi(a,b,c)}return Qa(a,b,c)}function Dc(a,b){if(!D)switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==
|
||||
b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function W(a){var b=null!==a.alternate&&a.alternate.child===a.child,c=0,d=0;if(b)for(var e=a.child;null!==e;)c|=e.lanes|e.childLanes,d|=e.subtreeFlags&14680064,d|=e.flags&14680064,e.return=a,e=e.sibling;else for(e=a.child;null!==e;)c|=e.lanes|e.childLanes,
|
||||
d|=e.subtreeFlags,d|=e.flags,e.return=a,e=e.sibling;a.subtreeFlags|=d;a.childLanes=c;return b}function xk(a,b,c){var d=b.pendingProps;Ve(b);switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return W(b),null;case 1:return ea(b.type)&&(v(S),v(J)),W(b),null;case 3:d=b.stateNode;Tb();v(S);v(J);jf();d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null);if(null===a||null===a.child)pd(b)?b.flags|=4:null===a||a.memoizedState.isDehydrated&&0===(b.flags&
|
||||
256)||(b.flags|=1024,null!==wa&&(Gf(wa),wa=null));xi(a,b);W(b);return null;case 5:hf(b);var e=ub(xc.current);c=b.type;if(null!==a&&null!=b.stateNode)yk(a,b,c,d,e),a.ref!==b.ref&&(b.flags|=512,b.flags|=2097152);else{if(!d){if(null===b.stateNode)throw Error(m(166));W(b);return null}a=ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.type;var f=b.memoizedProps;d[Da]=b;d[uc]=f;a=0!==(b.mode&1);switch(c){case "dialog":B("cancel",d);B("close",d);break;case "iframe":case "object":case "embed":B("load",d);break;
|
||||
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],d);break;case "source":B("error",d);break;case "img":case "image":case "link":B("error",d);B("load",d);break;case "details":B("toggle",d);break;case "input":kg(d,f);B("invalid",d);break;case "select":d._wrapperState={wasMultiple:!!f.multiple};B("invalid",d);break;case "textarea":ng(d,f),B("invalid",d)}pe(c,f);e=null;for(var g in f)if(f.hasOwnProperty(g)){var h=f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(!0!==f.suppressHydrationWarning&&
|
||||
jd(d.textContent,h,a),e=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(!0!==f.suppressHydrationWarning&&jd(d.textContent,h,a),e=["children",""+h]):$b.hasOwnProperty(g)&&null!=h&&"onScroll"===g&&B("scroll",d)}switch(c){case "input":Pc(d);mg(d,f,!0);break;case "textarea":Pc(d);pg(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=kd)}d=e;b.updateQueue=d;null!==d&&(b.flags|=4)}else{g=9===e.nodeType?e:e.ownerDocument;"http://www.w3.org/1999/xhtml"===
|
||||
a&&(a=qg(c));"http://www.w3.org/1999/xhtml"===a?"script"===c?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),"select"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[Da]=b;a[uc]=d;zk(a,b,!1,!1);b.stateNode=a;a:{g=qe(c,d);switch(c){case "dialog":B("cancel",a);B("close",a);e=d;break;case "iframe":case "object":case "embed":B("load",a);e=d;break;
|
||||
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],a);e=d;break;case "source":B("error",a);e=d;break;case "img":case "image":case "link":B("error",a);B("load",a);e=d;break;case "details":B("toggle",a);e=d;break;case "input":kg(a,d);e=ke(a,d);B("invalid",a);break;case "option":e=d;break;case "select":a._wrapperState={wasMultiple:!!d.multiple};e=E({},d,{value:void 0});B("invalid",a);break;case "textarea":ng(a,d);e=ne(a,d);B("invalid",a);break;default:e=d}pe(c,e);h=e;for(f in h)if(h.hasOwnProperty(f)){var k=
|
||||
h[f];"style"===f?sg(a,k):"dangerouslySetInnerHTML"===f?(k=k?k.__html:void 0,null!=k&&yi(a,k)):"children"===f?"string"===typeof k?("textarea"!==c||""!==k)&&Fc(a,k):"number"===typeof k&&Fc(a,""+k):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&($b.hasOwnProperty(f)?null!=k&&"onScroll"===f&&B("scroll",a):null!=k&&$d(a,f,k,g))}switch(c){case "input":Pc(a);mg(a,d,!1);break;case "textarea":Pc(a);pg(a);break;case "option":null!=d.value&&a.setAttribute("value",""+Ua(d.value));
|
||||
break;case "select":a.multiple=!!d.multiple;f=d.value;null!=f?Db(a,!!d.multiple,f,!1):null!=d.defaultValue&&Db(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof e.onClick&&(a.onclick=kd)}switch(c){case "button":case "input":case "select":case "textarea":d=!!d.autoFocus;break a;case "img":d=!0;break a;default:d=!1}}d&&(b.flags|=4)}null!==b.ref&&(b.flags|=512,b.flags|=2097152)}W(b);return null;case 6:if(a&&null!=b.stateNode)Ak(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===
|
||||
b.stateNode)throw Error(m(166));c=ub(xc.current);ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.memoizedProps;d[Da]=b;if(f=d.nodeValue!==c)if(a=la,null!==a)switch(a.tag){case 3:jd(d.nodeValue,c,0!==(a.mode&1));break;case 5:!0!==a.memoizedProps.suppressHydrationWarning&&jd(d.nodeValue,c,0!==(a.mode&1))}f&&(b.flags|=4)}else d=(9===c.nodeType?c:c.ownerDocument).createTextNode(d),d[Da]=b,b.stateNode=d}W(b);return null;case 13:v(F);d=b.memoizedState;if(null===a||null!==a.memoizedState&&null!==a.memoizedState.dehydrated){if(D&&
|
||||
null!==fa&&0!==(b.mode&1)&&0===(b.flags&128)){for(f=fa;f;)f=Ka(f.nextSibling);Qb();b.flags|=98560;f=!1}else if(f=pd(b),null!==d&&null!==d.dehydrated){if(null===a){if(!f)throw Error(m(318));f=b.memoizedState;f=null!==f?f.dehydrated:null;if(!f)throw Error(m(317));f[Da]=b}else Qb(),0===(b.flags&128)&&(b.memoizedState=null),b.flags|=4;W(b);f=!1}else null!==wa&&(Gf(wa),wa=null),f=!0;if(!f)return b.flags&65536?b:null}if(0!==(b.flags&128))return b.lanes=c,b;d=null!==d;d!==(null!==a&&null!==a.memoizedState)&&
|
||||
d&&(b.child.flags|=8192,0!==(b.mode&1)&&(null===a||0!==(F.current&1)?0===L&&(L=3):Ef()));null!==b.updateQueue&&(b.flags|=4);W(b);return null;case 4:return Tb(),xi(a,b),null===a&&sc(b.stateNode.containerInfo),W(b),null;case 10:return cf(b.type._context),W(b),null;case 17:return ea(b.type)&&(v(S),v(J)),W(b),null;case 19:v(F);f=b.memoizedState;if(null===f)return W(b),null;d=0!==(b.flags&128);g=f.rendering;if(null===g)if(d)Dc(f,!1);else{if(0!==L||null!==a&&0!==(a.flags&128))for(a=b.child;null!==a;){g=
|
||||
xd(a);if(null!==g){b.flags|=128;Dc(f,!1);d=g.updateQueue;null!==d&&(b.updateQueue=d,b.flags|=4);b.subtreeFlags=0;d=c;for(c=b.child;null!==c;)f=c,a=d,f.flags&=14680066,g=f.alternate,null===g?(f.childLanes=0,f.lanes=a,f.child=null,f.subtreeFlags=0,f.memoizedProps=null,f.memoizedState=null,f.updateQueue=null,f.dependencies=null,f.stateNode=null):(f.childLanes=g.childLanes,f.lanes=g.lanes,f.child=g.child,f.subtreeFlags=0,f.deletions=null,f.memoizedProps=g.memoizedProps,f.memoizedState=g.memoizedState,
|
||||
f.updateQueue=g.updateQueue,f.type=g.type,a=g.dependencies,f.dependencies=null===a?null:{lanes:a.lanes,firstContext:a.firstContext}),c=c.sibling;y(F,F.current&1|2);return b.child}a=a.sibling}null!==f.tail&&P()>Hf&&(b.flags|=128,d=!0,Dc(f,!1),b.lanes=4194304)}else{if(!d)if(a=xd(g),null!==a){if(b.flags|=128,d=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Dc(f,!0),null===f.tail&&"hidden"===f.tailMode&&!g.alternate&&!D)return W(b),null}else 2*P()-f.renderingStartTime>Hf&&1073741824!==c&&(b.flags|=
|
||||
128,d=!0,Dc(f,!1),b.lanes=4194304);f.isBackwards?(g.sibling=b.child,b.child=g):(c=f.last,null!==c?c.sibling=g:b.child=g,f.last=g)}if(null!==f.tail)return b=f.tail,f.rendering=b,f.tail=b.sibling,f.renderingStartTime=P(),b.sibling=null,c=F.current,y(F,d?c&1|2:c&1),b;W(b);return null;case 22:case 23:return ba=Ga.current,v(Ga),d=null!==b.memoizedState,null!==a&&null!==a.memoizedState!==d&&(b.flags|=8192),d&&0!==(b.mode&1)?0!==(ba&1073741824)&&(W(b),b.subtreeFlags&6&&(b.flags|=8192)):W(b),null;case 24:return null;
|
||||
case 25:return null}throw Error(m(156,b.tag));}function Bk(a,b,c){Ve(b);switch(b.tag){case 1:return ea(b.type)&&(v(S),v(J)),a=b.flags,a&65536?(b.flags=a&-65537|128,b):null;case 3:return Tb(),v(S),v(J),jf(),a=b.flags,0!==(a&65536)&&0===(a&128)?(b.flags=a&-65537|128,b):null;case 5:return hf(b),null;case 13:v(F);a=b.memoizedState;if(null!==a&&null!==a.dehydrated){if(null===b.alternate)throw Error(m(340));Qb()}a=b.flags;return a&65536?(b.flags=a&-65537|128,b):null;case 19:return v(F),null;case 4:return Tb(),
|
||||
null;case 10:return cf(b.type._context),null;case 22:case 23:return ba=Ga.current,v(Ga),null;case 24:return null;default:return null}}function Wb(a,b){var c=a.ref;if(null!==c)if("function"===typeof c)try{c(null)}catch(d){G(a,b,d)}else c.current=null}function If(a,b,c){try{c()}catch(d){G(a,b,d)}}function Ck(a,b){Jf=Zc;a=ch();if(Ie(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else a:{c=(c=a.ownerDocument)&&c.defaultView||window;var d=c.getSelection&&c.getSelection();
|
||||
if(d&&0!==d.rangeCount){c=d.anchorNode;var e=d.anchorOffset,f=d.focusNode;d=d.focusOffset;try{c.nodeType,f.nodeType}catch(M){c=null;break a}var g=0,h=-1,k=-1,n=0,q=0,u=a,r=null;b:for(;;){for(var p;;){u!==c||0!==e&&3!==u.nodeType||(h=g+e);u!==f||0!==d&&3!==u.nodeType||(k=g+d);3===u.nodeType&&(g+=u.nodeValue.length);if(null===(p=u.firstChild))break;r=u;u=p}for(;;){if(u===a)break b;r===c&&++n===e&&(h=g);r===f&&++q===d&&(k=g);if(null!==(p=u.nextSibling))break;u=r;r=u.parentNode}u=p}c=-1===h||-1===k?null:
|
||||
{start:h,end:k}}else c=null}c=c||{start:0,end:0}}else c=null;Kf={focusedElem:a,selectionRange:c};Zc=!1;for(l=b;null!==l;)if(b=l,a=b.child,0!==(b.subtreeFlags&1028)&&null!==a)a.return=b,l=a;else for(;null!==l;){b=l;try{var x=b.alternate;if(0!==(b.flags&1024))switch(b.tag){case 0:case 11:case 15:break;case 1:if(null!==x){var v=x.memoizedProps,z=x.memoizedState,w=b.stateNode,A=w.getSnapshotBeforeUpdate(b.elementType===b.type?v:ya(b.type,v),z);w.__reactInternalSnapshotBeforeUpdate=A}break;case 3:var t=
|
||||
b.stateNode.containerInfo;1===t.nodeType?t.textContent="":9===t.nodeType&&t.documentElement&&t.removeChild(t.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163));}}catch(M){G(b,b.return,M)}a=b.sibling;if(null!==a){a.return=b.return;l=a;break}l=b.return}x=zi;zi=!1;return x}function Gc(a,b,c){var d=b.updateQueue;d=null!==d?d.lastEffect:null;if(null!==d){var e=d=d.next;do{if((e.tag&a)===a){var f=e.destroy;e.destroy=void 0;void 0!==f&&If(b,c,f)}e=e.next}while(e!==d)}}
|
||||
function Id(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function Lf(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:a=c;break;default:a=c}"function"===typeof b?b(a):b.current=a}}function Ai(a){var b=a.alternate;null!==b&&(a.alternate=null,Ai(b));a.child=null;a.deletions=null;a.sibling=null;5===a.tag&&(b=a.stateNode,null!==b&&(delete b[Da],delete b[uc],delete b[Me],delete b[Dk],
|
||||
delete b[Ek]));a.stateNode=null;a.return=null;a.dependencies=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.stateNode=null;a.updateQueue=null}function Bi(a){return 5===a.tag||3===a.tag||4===a.tag}function Ci(a){a:for(;;){for(;null===a.sibling;){if(null===a.return||Bi(a.return))return null;a=a.return}a.sibling.return=a.return;for(a=a.sibling;5!==a.tag&&6!==a.tag&&18!==a.tag;){if(a.flags&2)continue a;if(null===a.child||4===a.tag)continue a;else a.child.return=a,a=a.child}if(!(a.flags&
|
||||
2))return a.stateNode}}function Mf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=kd));else if(4!==d&&(a=a.child,null!==a))for(Mf(a,b,c),a=a.sibling;null!==a;)Mf(a,b,c),a=a.sibling}function Nf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?c.insertBefore(a,b):c.appendChild(a);
|
||||
else if(4!==d&&(a=a.child,null!==a))for(Nf(a,b,c),a=a.sibling;null!==a;)Nf(a,b,c),a=a.sibling}function jb(a,b,c){for(c=c.child;null!==c;)Di(a,b,c),c=c.sibling}function Di(a,b,c){if(Ca&&"function"===typeof Ca.onCommitFiberUnmount)try{Ca.onCommitFiberUnmount(Uc,c)}catch(h){}switch(c.tag){case 5:X||Wb(c,b);case 6:var d=T,e=za;T=null;jb(a,b,c);T=d;za=e;null!==T&&(za?(a=T,c=c.stateNode,8===a.nodeType?a.parentNode.removeChild(c):a.removeChild(c)):T.removeChild(c.stateNode));break;case 18:null!==T&&(za?
|
||||
(a=T,c=c.stateNode,8===a.nodeType?Re(a.parentNode,c):1===a.nodeType&&Re(a,c),nc(a)):Re(T,c.stateNode));break;case 4:d=T;e=za;T=c.stateNode.containerInfo;za=!0;jb(a,b,c);T=d;za=e;break;case 0:case 11:case 14:case 15:if(!X&&(d=c.updateQueue,null!==d&&(d=d.lastEffect,null!==d))){e=d=d.next;do{var f=e,g=f.destroy;f=f.tag;void 0!==g&&(0!==(f&2)?If(c,b,g):0!==(f&4)&&If(c,b,g));e=e.next}while(e!==d)}jb(a,b,c);break;case 1:if(!X&&(Wb(c,b),d=c.stateNode,"function"===typeof d.componentWillUnmount))try{d.props=
|
||||
c.memoizedProps,d.state=c.memoizedState,d.componentWillUnmount()}catch(h){G(c,b,h)}jb(a,b,c);break;case 21:jb(a,b,c);break;case 22:c.mode&1?(X=(d=X)||null!==c.memoizedState,jb(a,b,c),X=d):jb(a,b,c);break;default:jb(a,b,c)}}function Ei(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=new Fk);b.forEach(function(b){var d=Gk.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Aa(a,b,c){c=b.deletions;if(null!==c)for(var d=0;d<c.length;d++){var e=
|
||||
c[d];try{var f=a,g=b,h=g;a:for(;null!==h;){switch(h.tag){case 5:T=h.stateNode;za=!1;break a;case 3:T=h.stateNode.containerInfo;za=!0;break a;case 4:T=h.stateNode.containerInfo;za=!0;break a}h=h.return}if(null===T)throw Error(m(160));Di(f,g,e);T=null;za=!1;var k=e.alternate;null!==k&&(k.return=null);e.return=null}catch(n){G(e,b,n)}}if(b.subtreeFlags&12854)for(b=b.child;null!==b;)Fi(b,a),b=b.sibling}function Fi(a,b,c){var d=a.alternate;c=a.flags;switch(a.tag){case 0:case 11:case 14:case 15:Aa(b,a);
|
||||
Ha(a);if(c&4){try{Gc(3,a,a.return),Id(3,a)}catch(I){G(a,a.return,I)}try{Gc(5,a,a.return)}catch(I){G(a,a.return,I)}}break;case 1:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);break;case 5:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);if(a.flags&32){var e=a.stateNode;try{Fc(e,"")}catch(I){G(a,a.return,I)}}if(c&4&&(e=a.stateNode,null!=e)){var f=a.memoizedProps,g=null!==d?d.memoizedProps:f,h=a.type,k=a.updateQueue;a.updateQueue=null;if(null!==k)try{"input"===h&&"radio"===f.type&&null!=f.name&&lg(e,f);
|
||||
qe(h,g);var n=qe(h,f);for(g=0;g<k.length;g+=2){var q=k[g],u=k[g+1];"style"===q?sg(e,u):"dangerouslySetInnerHTML"===q?yi(e,u):"children"===q?Fc(e,u):$d(e,q,u,n)}switch(h){case "input":le(e,f);break;case "textarea":og(e,f);break;case "select":var r=e._wrapperState.wasMultiple;e._wrapperState.wasMultiple=!!f.multiple;var p=f.value;null!=p?Db(e,!!f.multiple,p,!1):r!==!!f.multiple&&(null!=f.defaultValue?Db(e,!!f.multiple,f.defaultValue,!0):Db(e,!!f.multiple,f.multiple?[]:"",!1))}e[uc]=f}catch(I){G(a,a.return,
|
||||
I)}}break;case 6:Aa(b,a);Ha(a);if(c&4){if(null===a.stateNode)throw Error(m(162));e=a.stateNode;f=a.memoizedProps;try{e.nodeValue=f}catch(I){G(a,a.return,I)}}break;case 3:Aa(b,a);Ha(a);if(c&4&&null!==d&&d.memoizedState.isDehydrated)try{nc(b.containerInfo)}catch(I){G(a,a.return,I)}break;case 4:Aa(b,a);Ha(a);break;case 13:Aa(b,a);Ha(a);e=a.child;e.flags&8192&&(f=null!==e.memoizedState,e.stateNode.isHidden=f,!f||null!==e.alternate&&null!==e.alternate.memoizedState||(Of=P()));c&4&&Ei(a);break;case 22:q=
|
||||
null!==d&&null!==d.memoizedState;a.mode&1?(X=(n=X)||q,Aa(b,a),X=n):Aa(b,a);Ha(a);if(c&8192){n=null!==a.memoizedState;if((a.stateNode.isHidden=n)&&!q&&0!==(a.mode&1))for(l=a,q=a.child;null!==q;){for(u=l=q;null!==l;){r=l;p=r.child;switch(r.tag){case 0:case 11:case 14:case 15:Gc(4,r,r.return);break;case 1:Wb(r,r.return);var x=r.stateNode;if("function"===typeof x.componentWillUnmount){c=r;b=r.return;try{d=c,x.props=d.memoizedProps,x.state=d.memoizedState,x.componentWillUnmount()}catch(I){G(c,b,I)}}break;
|
||||
case 5:Wb(r,r.return);break;case 22:if(null!==r.memoizedState){Gi(u);continue}}null!==p?(p.return=r,l=p):Gi(u)}q=q.sibling}a:for(q=null,u=a;;){if(5===u.tag){if(null===q){q=u;try{e=u.stateNode,n?(f=e.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(h=u.stateNode,k=u.memoizedProps.style,g=void 0!==k&&null!==k&&k.hasOwnProperty("display")?k.display:null,h.style.display=rg("display",g))}catch(I){G(a,a.return,I)}}}else if(6===u.tag){if(null===q)try{u.stateNode.nodeValue=
|
||||
n?"":u.memoizedProps}catch(I){G(a,a.return,I)}}else if((22!==u.tag&&23!==u.tag||null===u.memoizedState||u===a)&&null!==u.child){u.child.return=u;u=u.child;continue}if(u===a)break a;for(;null===u.sibling;){if(null===u.return||u.return===a)break a;q===u&&(q=null);u=u.return}q===u&&(q=null);u.sibling.return=u.return;u=u.sibling}}break;case 19:Aa(b,a);Ha(a);c&4&&Ei(a);break;case 21:break;default:Aa(b,a),Ha(a)}}function Ha(a){var b=a.flags;if(b&2){try{a:{for(var c=a.return;null!==c;){if(Bi(c)){var d=c;
|
||||
break a}c=c.return}throw Error(m(160));}switch(d.tag){case 5:var e=d.stateNode;d.flags&32&&(Fc(e,""),d.flags&=-33);var f=Ci(a);Nf(a,f,e);break;case 3:case 4:var g=d.stateNode.containerInfo,h=Ci(a);Mf(a,h,g);break;default:throw Error(m(161));}}catch(k){G(a,a.return,k)}a.flags&=-3}b&4096&&(a.flags&=-4097)}function Hk(a,b,c){l=a;Hi(a,b,c)}function Hi(a,b,c){for(var d=0!==(a.mode&1);null!==l;){var e=l,f=e.child;if(22===e.tag&&d){var g=null!==e.memoizedState||Jd;if(!g){var h=e.alternate,k=null!==h&&null!==
|
||||
h.memoizedState||X;h=Jd;var n=X;Jd=g;if((X=k)&&!n)for(l=e;null!==l;)g=l,k=g.child,22===g.tag&&null!==g.memoizedState?Ii(e):null!==k?(k.return=g,l=k):Ii(e);for(;null!==f;)l=f,Hi(f,b,c),f=f.sibling;l=e;Jd=h;X=n}Ji(a,b,c)}else 0!==(e.subtreeFlags&8772)&&null!==f?(f.return=e,l=f):Ji(a,b,c)}}function Ji(a,b,c){for(;null!==l;){b=l;if(0!==(b.flags&8772)){c=b.alternate;try{if(0!==(b.flags&8772))switch(b.tag){case 0:case 11:case 15:X||Id(5,b);break;case 1:var d=b.stateNode;if(b.flags&4&&!X)if(null===c)d.componentDidMount();
|
||||
else{var e=b.elementType===b.type?c.memoizedProps:ya(b.type,c.memoizedProps);d.componentDidUpdate(e,c.memoizedState,d.__reactInternalSnapshotBeforeUpdate)}var f=b.updateQueue;null!==f&&Hh(b,f,d);break;case 3:var g=b.updateQueue;if(null!==g){c=null;if(null!==b.child)switch(b.child.tag){case 5:c=b.child.stateNode;break;case 1:c=b.child.stateNode}Hh(b,g,c)}break;case 5:var h=b.stateNode;if(null===c&&b.flags&4){c=h;var k=b.memoizedProps;switch(b.type){case "button":case "input":case "select":case "textarea":k.autoFocus&&
|
||||
c.focus();break;case "img":k.src&&(c.src=k.src)}}break;case 6:break;case 4:break;case 12:break;case 13:if(null===b.memoizedState){var n=b.alternate;if(null!==n){var q=n.memoizedState;if(null!==q){var p=q.dehydrated;null!==p&&nc(p)}}}break;case 19:case 17:case 21:case 22:case 23:case 25:break;default:throw Error(m(163));}X||b.flags&512&&Lf(b)}catch(r){G(b,b.return,r)}}if(b===a){l=null;break}c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Gi(a){for(;null!==l;){var b=l;if(b===
|
||||
a){l=null;break}var c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Ii(a){for(;null!==l;){var b=l;try{switch(b.tag){case 0:case 11:case 15:var c=b.return;try{Id(4,b)}catch(k){G(b,c,k)}break;case 1:var d=b.stateNode;if("function"===typeof d.componentDidMount){var e=b.return;try{d.componentDidMount()}catch(k){G(b,e,k)}}var f=b.return;try{Lf(b)}catch(k){G(b,f,k)}break;case 5:var g=b.return;try{Lf(b)}catch(k){G(b,g,k)}}}catch(k){G(b,b.return,k)}if(b===a){l=null;break}var h=b.sibling;
|
||||
if(null!==h){h.return=b.return;l=h;break}l=b.return}}function Hc(){Hf=P()+500}function Z(){return 0!==(p&6)?P():-1!==Kd?Kd:Kd=P()}function hb(a){if(0===(a.mode&1))return 1;if(0!==(p&2)&&0!==U)return U&-U;if(null!==Ik.transition)return 0===Ld&&(Ld=Dg()),Ld;a=z;if(0!==a)return a;a=window.event;a=void 0===a?16:Lg(a.type);return a}function xa(a,b,c,d){if(50<Ic)throw Ic=0,Pf=null,Error(m(185));ic(a,c,d);if(0===(p&2)||a!==O)a===O&&(0===(p&2)&&(Md|=c),4===L&&kb(a,U)),ia(a,d),1===c&&0===p&&0===(b.mode&1)&&
|
||||
(Hc(),md&&db())}function ia(a,b){var c=a.callbackNode;tj(a,b);var d=Vc(a,a===O?U:0);if(0===d)null!==c&&Ki(c),a.callbackNode=null,a.callbackPriority=0;else if(b=d&-d,a.callbackPriority!==b){null!=c&&Ki(c);if(1===b)0===a.tag?jk(Li.bind(null,a)):wh(Li.bind(null,a)),Jk(function(){0===(p&6)&&db()}),c=null;else{switch(Eg(d)){case 1:c=De;break;case 4:c=Mg;break;case 16:c=ad;break;case 536870912:c=Ng;break;default:c=ad}c=Mi(c,Ni.bind(null,a))}a.callbackPriority=b;a.callbackNode=c}}function Ni(a,b){Kd=-1;
|
||||
Ld=0;if(0!==(p&6))throw Error(m(327));var c=a.callbackNode;if(Xb()&&a.callbackNode!==c)return null;var d=Vc(a,a===O?U:0);if(0===d)return null;if(0!==(d&30)||0!==(d&a.expiredLanes)||b)b=Nd(a,d);else{b=d;var e=p;p|=2;var f=Oi();if(O!==a||U!==b)Ra=null,Hc(),wb(a,b);do try{Kk();break}catch(h){Pi(a,h)}while(1);af();Od.current=f;p=e;null!==H?b=0:(O=null,U=0,b=L)}if(0!==b){2===b&&(e=ve(a),0!==e&&(d=e,b=Qf(a,e)));if(1===b)throw c=Jc,wb(a,0),kb(a,d),ia(a,P()),c;if(6===b)kb(a,d);else{e=a.current.alternate;
|
||||
if(0===(d&30)&&!Lk(e)&&(b=Nd(a,d),2===b&&(f=ve(a),0!==f&&(d=f,b=Qf(a,f))),1===b))throw c=Jc,wb(a,0),kb(a,d),ia(a,P()),c;a.finishedWork=e;a.finishedLanes=d;switch(b){case 0:case 1:throw Error(m(345));case 2:xb(a,ja,Ra);break;case 3:kb(a,d);if((d&130023424)===d&&(b=Of+500-P(),10<b)){if(0!==Vc(a,0))break;e=a.suspendedLanes;if((e&d)!==d){Z();a.pingedLanes|=a.suspendedLanes&e;break}a.timeoutHandle=Rf(xb.bind(null,a,ja,Ra),b);break}xb(a,ja,Ra);break;case 4:kb(a,d);if((d&4194240)===d)break;b=a.eventTimes;
|
||||
for(e=-1;0<d;){var g=31-ta(d);f=1<<g;g=b[g];g>e&&(e=g);d&=~f}d=e;d=P()-d;d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*Mk(d/1960))-d;if(10<d){a.timeoutHandle=Rf(xb.bind(null,a,ja,Ra),d);break}xb(a,ja,Ra);break;case 5:xb(a,ja,Ra);break;default:throw Error(m(329));}}}ia(a,P());return a.callbackNode===c?Ni.bind(null,a):null}function Qf(a,b){var c=Kc;a.current.memoizedState.isDehydrated&&(wb(a,b).flags|=256);a=Nd(a,b);2!==a&&(b=ja,ja=c,null!==b&&Gf(b));return a}function Gf(a){null===
|
||||
ja?ja=a:ja.push.apply(ja,a)}function Lk(a){for(var b=a;;){if(b.flags&16384){var c=b.updateQueue;if(null!==c&&(c=c.stores,null!==c))for(var d=0;d<c.length;d++){var e=c[d],f=e.getSnapshot;e=e.value;try{if(!ua(f(),e))return!1}catch(g){return!1}}}c=b.child;if(b.subtreeFlags&16384&&null!==c)c.return=b,b=c;else{if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return!0;b=b.return}b.sibling.return=b.return;b=b.sibling}}return!0}function kb(a,b){b&=~Sf;b&=~Md;a.suspendedLanes|=b;a.pingedLanes&=
|
||||
~b;for(a=a.expirationTimes;0<b;){var c=31-ta(b),d=1<<c;a[c]=-1;b&=~d}}function Li(a){if(0!==(p&6))throw Error(m(327));Xb();var b=Vc(a,0);if(0===(b&1))return ia(a,P()),null;var c=Nd(a,b);if(0!==a.tag&&2===c){var d=ve(a);0!==d&&(b=d,c=Qf(a,d))}if(1===c)throw c=Jc,wb(a,0),kb(a,b),ia(a,P()),c;if(6===c)throw Error(m(345));a.finishedWork=a.current.alternate;a.finishedLanes=b;xb(a,ja,Ra);ia(a,P());return null}function Tf(a,b){var c=p;p|=1;try{return a(b)}finally{p=c,0===p&&(Hc(),md&&db())}}function yb(a){null!==
|
||||
lb&&0===lb.tag&&0===(p&6)&&Xb();var b=p;p|=1;var c=ca.transition,d=z;try{if(ca.transition=null,z=1,a)return a()}finally{z=d,ca.transition=c,p=b,0===(p&6)&&db()}}function wb(a,b){a.finishedWork=null;a.finishedLanes=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,Nk(c));if(null!==H)for(c=H.return;null!==c;){var d=c;Ve(d);switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(v(S),v(J));break;case 3:Tb();v(S);v(J);jf();break;case 5:hf(d);break;case 4:Tb();break;case 13:v(F);break;
|
||||
case 19:v(F);break;case 10:cf(d.type._context);break;case 22:case 23:ba=Ga.current,v(Ga)}c=c.return}O=a;H=a=eb(a.current,null);U=ba=b;L=0;Jc=null;Sf=Md=ra=0;ja=Kc=null;if(null!==tb){for(b=0;b<tb.length;b++)if(c=tb[b],d=c.interleaved,null!==d){c.interleaved=null;var e=d.next,f=c.pending;if(null!==f){var g=f.next;f.next=e;d.next=g}c.pending=d}tb=null}return a}function Pi(a,b){do{var c=H;try{af();yd.current=zd;if(Ad){for(var d=C.memoizedState;null!==d;){var e=d.queue;null!==e&&(e.pending=null);d=d.next}Ad=
|
||||
!1}vb=0;N=K=C=null;zc=!1;Ac=0;Uf.current=null;if(null===c||null===c.return){L=1;Jc=b;H=null;break}a:{var f=a,g=c.return,h=c,k=b;b=U;h.flags|=32768;if(null!==k&&"object"===typeof k&&"function"===typeof k.then){var n=k,l=h,p=l.tag;if(0===(l.mode&1)&&(0===p||11===p||15===p)){var r=l.alternate;r?(l.updateQueue=r.updateQueue,l.memoizedState=r.memoizedState,l.lanes=r.lanes):(l.updateQueue=null,l.memoizedState=null)}var v=ji(g);if(null!==v){v.flags&=-257;ki(v,g,h,f,b);v.mode&1&&ii(f,n,b);b=v;k=n;var x=b.updateQueue;
|
||||
if(null===x){var z=new Set;z.add(k);b.updateQueue=z}else x.add(k);break a}else{if(0===(b&1)){ii(f,n,b);Ef();break a}k=Error(m(426))}}else if(D&&h.mode&1){var y=ji(g);if(null!==y){0===(y.flags&65536)&&(y.flags|=256);ki(y,g,h,f,b);Ye(Ub(k,h));break a}}f=k=Ub(k,h);4!==L&&(L=2);null===Kc?Kc=[f]:Kc.push(f);f=g;do{switch(f.tag){case 3:f.flags|=65536;b&=-b;f.lanes|=b;var w=gi(f,k,b);Gh(f,w);break a;case 1:h=k;var A=f.type,t=f.stateNode;if(0===(f.flags&128)&&("function"===typeof A.getDerivedStateFromError||
|
||||
null!==t&&"function"===typeof t.componentDidCatch&&(null===ib||!ib.has(t)))){f.flags|=65536;b&=-b;f.lanes|=b;var B=hi(f,h,b);Gh(f,B);break a}}f=f.return}while(null!==f)}Qi(c)}catch(ma){b=ma;H===c&&null!==c&&(H=c=c.return);continue}break}while(1)}function Oi(){var a=Od.current;Od.current=zd;return null===a?zd:a}function Ef(){if(0===L||3===L||2===L)L=4;null===O||0===(ra&268435455)&&0===(Md&268435455)||kb(O,U)}function Nd(a,b){var c=p;p|=2;var d=Oi();if(O!==a||U!==b)Ra=null,wb(a,b);do try{Ok();break}catch(e){Pi(a,
|
||||
e)}while(1);af();p=c;Od.current=d;if(null!==H)throw Error(m(261));O=null;U=0;return L}function Ok(){for(;null!==H;)Ri(H)}function Kk(){for(;null!==H&&!Pk();)Ri(H)}function Ri(a){var b=Qk(a.alternate,a,ba);a.memoizedProps=a.pendingProps;null===b?Qi(a):H=b;Uf.current=null}function Qi(a){var b=a;do{var c=b.alternate;a=b.return;if(0===(b.flags&32768)){if(c=xk(c,b,ba),null!==c){H=c;return}}else{c=Bk(c,b);if(null!==c){c.flags&=32767;H=c;return}if(null!==a)a.flags|=32768,a.subtreeFlags=0,a.deletions=null;
|
||||
else{L=6;H=null;return}}b=b.sibling;if(null!==b){H=b;return}H=b=a}while(null!==b);0===L&&(L=5)}function xb(a,b,c){var d=z,e=ca.transition;try{ca.transition=null,z=1,Rk(a,b,c,d)}finally{ca.transition=e,z=d}return null}function Rk(a,b,c,d){do Xb();while(null!==lb);if(0!==(p&6))throw Error(m(327));c=a.finishedWork;var e=a.finishedLanes;if(null===c)return null;a.finishedWork=null;a.finishedLanes=0;if(c===a.current)throw Error(m(177));a.callbackNode=null;a.callbackPriority=0;var f=c.lanes|c.childLanes;
|
||||
uj(a,f);a===O&&(H=O=null,U=0);0===(c.subtreeFlags&2064)&&0===(c.flags&2064)||Pd||(Pd=!0,Mi(ad,function(){Xb();return null}));f=0!==(c.flags&15990);if(0!==(c.subtreeFlags&15990)||f){f=ca.transition;ca.transition=null;var g=z;z=1;var h=p;p|=4;Uf.current=null;Ck(a,c);Fi(c,a);Tj(Kf);Zc=!!Jf;Kf=Jf=null;a.current=c;Hk(c,a,e);Sk();p=h;z=g;ca.transition=f}else a.current=c;Pd&&(Pd=!1,lb=a,Qd=e);f=a.pendingLanes;0===f&&(ib=null);oj(c.stateNode,d);ia(a,P());if(null!==b)for(d=a.onRecoverableError,c=0;c<b.length;c++)e=
|
||||
b[c],d(e.value,{componentStack:e.stack,digest:e.digest});if(Ed)throw Ed=!1,a=xf,xf=null,a;0!==(Qd&1)&&0!==a.tag&&Xb();f=a.pendingLanes;0!==(f&1)?a===Pf?Ic++:(Ic=0,Pf=a):Ic=0;db();return null}function Xb(){if(null!==lb){var a=Eg(Qd),b=ca.transition,c=z;try{ca.transition=null;z=16>a?16:a;if(null===lb)var d=!1;else{a=lb;lb=null;Qd=0;if(0!==(p&6))throw Error(m(331));var e=p;p|=4;for(l=a.current;null!==l;){var f=l,g=f.child;if(0!==(l.flags&16)){var h=f.deletions;if(null!==h){for(var k=0;k<h.length;k++){var n=
|
||||
h[k];for(l=n;null!==l;){var q=l;switch(q.tag){case 0:case 11:case 15:Gc(8,q,f)}var u=q.child;if(null!==u)u.return=q,l=u;else for(;null!==l;){q=l;var r=q.sibling,v=q.return;Ai(q);if(q===n){l=null;break}if(null!==r){r.return=v;l=r;break}l=v}}}var x=f.alternate;if(null!==x){var y=x.child;if(null!==y){x.child=null;do{var C=y.sibling;y.sibling=null;y=C}while(null!==y)}}l=f}}if(0!==(f.subtreeFlags&2064)&&null!==g)g.return=f,l=g;else b:for(;null!==l;){f=l;if(0!==(f.flags&2048))switch(f.tag){case 0:case 11:case 15:Gc(9,
|
||||
f,f.return)}var w=f.sibling;if(null!==w){w.return=f.return;l=w;break b}l=f.return}}var A=a.current;for(l=A;null!==l;){g=l;var t=g.child;if(0!==(g.subtreeFlags&2064)&&null!==t)t.return=g,l=t;else b:for(g=A;null!==l;){h=l;if(0!==(h.flags&2048))try{switch(h.tag){case 0:case 11:case 15:Id(9,h)}}catch(ma){G(h,h.return,ma)}if(h===g){l=null;break b}var B=h.sibling;if(null!==B){B.return=h.return;l=B;break b}l=h.return}}p=e;db();if(Ca&&"function"===typeof Ca.onPostCommitFiberRoot)try{Ca.onPostCommitFiberRoot(Uc,
|
||||
a)}catch(ma){}d=!0}return d}finally{z=c,ca.transition=b}}return!1}function Si(a,b,c){b=Ub(c,b);b=gi(a,b,1);a=fb(a,b,1);b=Z();null!==a&&(ic(a,1,b),ia(a,b))}function G(a,b,c){if(3===a.tag)Si(a,a,c);else for(;null!==b;){if(3===b.tag){Si(b,a,c);break}else if(1===b.tag){var d=b.stateNode;if("function"===typeof b.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===ib||!ib.has(d))){a=Ub(c,a);a=hi(b,a,1);b=fb(b,a,1);a=Z();null!==b&&(ic(b,1,a),ia(b,a));break}}b=b.return}}function sk(a,
|
||||
b,c){var d=a.pingCache;null!==d&&d.delete(b);b=Z();a.pingedLanes|=a.suspendedLanes&c;O===a&&(U&c)===c&&(4===L||3===L&&(U&130023424)===U&&500>P()-Of?wb(a,0):Sf|=c);ia(a,b)}function Ti(a,b){0===b&&(0===(a.mode&1)?b=1:(b=Rd,Rd<<=1,0===(Rd&130023424)&&(Rd=4194304)));var c=Z();a=Oa(a,b);null!==a&&(ic(a,b,c),ia(a,c))}function vk(a){var b=a.memoizedState,c=0;null!==b&&(c=b.retryLane);Ti(a,c)}function Gk(a,b){var c=0;switch(a.tag){case 13:var d=a.stateNode;var e=a.memoizedState;null!==e&&(c=e.retryLane);
|
||||
break;case 19:d=a.stateNode;break;default:throw Error(m(314));}null!==d&&d.delete(b);Ti(a,c)}function Mi(a,b){return xh(a,b)}function Tk(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.subtreeFlags=this.flags=0;this.deletions=null;this.childLanes=this.lanes=0;this.alternate=null}function yf(a){a=
|
||||
a.prototype;return!(!a||!a.isReactComponent)}function Uk(a){if("function"===typeof a)return yf(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===ie)return 11;if(a===je)return 14}return 2}function eb(a,b){var c=a.alternate;null===c?(c=pa(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.type=a.type,c.flags=0,c.subtreeFlags=0,c.deletions=null);c.flags=a.flags&14680064;c.childLanes=a.childLanes;c.lanes=a.lanes;c.child=
|
||||
a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{lanes:b.lanes,firstContext:b.firstContext};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function rd(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)yf(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case Bb:return sb(c.children,e,f,b);case fe:g=8;e|=8;break;case ee:return a=pa(12,c,b,e|2),a.elementType=ee,a.lanes=f,a;case ge:return a=
|
||||
pa(13,c,b,e),a.elementType=ge,a.lanes=f,a;case he:return a=pa(19,c,b,e),a.elementType=he,a.lanes=f,a;case Ui:return Gd(c,e,f,b);default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case hg:g=10;break a;case gg:g=9;break a;case ie:g=11;break a;case je:g=14;break a;case Ta:g=16;d=null;break a}throw Error(m(130,null==a?a:typeof a,""));}b=pa(g,c,b,e);b.elementType=a;b.type=d;b.lanes=f;return b}function sb(a,b,c,d){a=pa(7,a,d,b);a.lanes=c;return a}function Gd(a,b,c,d){a=pa(22,a,d,b);a.elementType=
|
||||
Ui;a.lanes=c;a.stateNode={isHidden:!1};return a}function Ze(a,b,c){a=pa(6,a,null,b);a.lanes=c;return a}function $e(a,b,c){b=pa(4,null!==a.children?a.children:[],a.key,b);b.lanes=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Vk(a,b,c,d,e){this.tag=b;this.containerInfo=a;this.finishedWork=this.pingCache=this.current=this.pendingChildren=null;this.timeoutHandle=-1;this.callbackNode=this.pendingContext=this.context=null;this.callbackPriority=
|
||||
0;this.eventTimes=we(0);this.expirationTimes=we(-1);this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0;this.entanglements=we(0);this.identifierPrefix=d;this.onRecoverableError=e;this.mutableSourceEagerHydrationData=null}function Vf(a,b,c,d,e,f,g,h,k,l){a=new Vk(a,b,c,h,k);1===b?(b=1,!0===f&&(b|=8)):b=0;f=pa(3,null,null,b);a.current=f;f.stateNode=a;f.memoizedState={element:d,isDehydrated:c,cache:null,transitions:null,
|
||||
pendingSuspenseBoundaries:null};ff(f);return a}function Wk(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Cb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Vi(a){if(!a)return cb;a=a._reactInternals;a:{if(nb(a)!==a||1!==a.tag)throw Error(m(170));var b=a;do{switch(b.tag){case 3:b=b.stateNode.context;break a;case 1:if(ea(b.type)){b=b.stateNode.__reactInternalMemoizedMergedChildContext;break a}}b=b.return}while(null!==b);throw Error(m(171));
|
||||
}if(1===a.tag){var c=a.type;if(ea(c))return uh(a,c,b)}return b}function Wi(a,b,c,d,e,f,g,h,k,l){a=Vf(c,d,!0,a,e,f,g,h,k);a.context=Vi(null);c=a.current;d=Z();e=hb(c);f=Pa(d,e);f.callback=void 0!==b&&null!==b?b:null;fb(c,f,e);a.current.lanes=e;ic(a,e,d);ia(a,d);return a}function Sd(a,b,c,d){var e=b.current,f=Z(),g=hb(e);c=Vi(c);null===b.context?b.context=c:b.pendingContext=c;b=Pa(f,g);b.payload={element:a};d=void 0===d?null:d;null!==d&&(b.callback=d);a=fb(e,b,g);null!==a&&(xa(a,e,g,f),vd(a,e,g));return g}
|
||||
function Td(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Xi(a,b){a=a.memoizedState;if(null!==a&&null!==a.dehydrated){var c=a.retryLane;a.retryLane=0!==c&&c<b?c:b}}function Wf(a,b){Xi(a,b);(a=a.alternate)&&Xi(a,b)}function Xk(a){a=Bg(a);return null===a?null:a.stateNode}function Yk(a){return null}function Xf(a){this._internalRoot=a}function Ud(a){this._internalRoot=a}function Yf(a){return!(!a||1!==a.nodeType&&9!==
|
||||
a.nodeType&&11!==a.nodeType)}function Vd(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Yi(){}function Zk(a,b,c,d,e){if(e){if("function"===typeof d){var f=d;d=function(){var a=Td(g);f.call(a)}}var g=Wi(b,d,a,0,null,!1,!1,"",Yi);a._reactRootContainer=g;a[Ja]=g.current;sc(8===a.nodeType?a.parentNode:a);yb();return g}for(;e=a.lastChild;)a.removeChild(e);if("function"===typeof d){var h=d;d=function(){var a=Td(k);
|
||||
h.call(a)}}var k=Vf(a,0,!1,null,null,!1,!1,"",Yi);a._reactRootContainer=k;a[Ja]=k.current;sc(8===a.nodeType?a.parentNode:a);yb(function(){Sd(b,k,c,d)});return k}function Wd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f;if("function"===typeof e){var h=e;e=function(){var a=Td(g);h.call(a)}}Sd(b,g,a,e)}else g=Zk(c,b,a,e,d);return Td(g)}var cg=new Set,$b={},Ia=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),Zd=Object.prototype.hasOwnProperty,
|
||||
cj=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,eg={},dg={},R={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){R[a]=
|
||||
new Y(a,0,!1,a,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];R[b]=new Y(b,1,!1,a[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){R[a]=new Y(a,2,!1,a.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){R[a]=new Y(a,2,!1,a,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){R[a]=
|
||||
new Y(a,3,!1,a.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(a){R[a]=new Y(a,3,!0,a,null,!1,!1)});["capture","download"].forEach(function(a){R[a]=new Y(a,4,!1,a,null,!1,!1)});["cols","rows","size","span"].forEach(function(a){R[a]=new Y(a,6,!1,a,null,!1,!1)});["rowSpan","start"].forEach(function(a){R[a]=new Y(a,5,!1,a.toLowerCase(),null,!1,!1)});var Zf=/[\-:]([a-z])/g,$f=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
|
||||
a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!1,!1)});R.xlinkHref=new Y("xlinkHref",
|
||||
1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!0,!0)});var Sa=zb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,sd=Symbol.for("react.element"),Cb=Symbol.for("react.portal"),Bb=Symbol.for("react.fragment"),fe=Symbol.for("react.strict_mode"),ee=Symbol.for("react.profiler"),hg=Symbol.for("react.provider"),gg=Symbol.for("react.context"),ie=Symbol.for("react.forward_ref"),ge=Symbol.for("react.suspense"),
|
||||
he=Symbol.for("react.suspense_list"),je=Symbol.for("react.memo"),Ta=Symbol.for("react.lazy");Symbol.for("react.scope");Symbol.for("react.debug_trace_mode");var Ui=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden");Symbol.for("react.cache");Symbol.for("react.tracing_marker");var fg=Symbol.iterator,E=Object.assign,ae,ce=!1,cc=Array.isArray,Xd,yi=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,
|
||||
c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{Xd=Xd||document.createElement("div");Xd.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=Xd.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Fc=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},dc={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,
|
||||
borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,
|
||||
strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},$k=["Webkit","ms","Moz","O"];Object.keys(dc).forEach(function(a){$k.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);dc[b]=dc[a]})});var ij=E({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ze=null,se=null,Eb=null,Fb=null,xg=function(a,b){return a(b)},yg=function(){},te=!1,Oe=!1;if(Ia)try{var Lc={};Object.defineProperty(Lc,
|
||||
"passive",{get:function(){Oe=!0}});window.addEventListener("test",Lc,Lc);window.removeEventListener("test",Lc,Lc)}catch(a){Oe=!1}var kj=function(a,b,c,d,e,f,g,h,k){var l=Array.prototype.slice.call(arguments,3);try{b.apply(c,l)}catch(q){this.onError(q)}},gc=!1,Sc=null,Tc=!1,ue=null,lj={onError:function(a){gc=!0;Sc=a}},Ba=zb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Jg=Ba.unstable_scheduleCallback,Kg=Ba.unstable_NormalPriority,xh=Jg,Ki=Ba.unstable_cancelCallback,Pk=Ba.unstable_shouldYield,
|
||||
Sk=Ba.unstable_requestPaint,P=Ba.unstable_now,Dj=Ba.unstable_getCurrentPriorityLevel,De=Ba.unstable_ImmediatePriority,Mg=Ba.unstable_UserBlockingPriority,ad=Kg,Ej=Ba.unstable_LowPriority,Ng=Ba.unstable_IdlePriority,Uc=null,Ca=null,ta=Math.clz32?Math.clz32:pj,qj=Math.log,rj=Math.LN2,Wc=64,Rd=4194304,z=0,Ae=!1,Yc=[],Va=null,Wa=null,Xa=null,jc=new Map,kc=new Map,Ya=[],Bj="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit".split(" "),
|
||||
Gb=Sa.ReactCurrentBatchConfig,Zc=!0,$c=null,Za=null,Ee=null,bd=null,Yb={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},He=ka(Yb),Mc=E({},Yb,{view:0,detail:0}),ak=ka(Mc),ag,bg,Nc,Yd=E({},Mc,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:Fe,button:0,buttons:0,relatedTarget:function(a){return void 0===a.relatedTarget?a.fromElement===a.srcElement?a.toElement:a.fromElement:
|
||||
a.relatedTarget},movementX:function(a){if("movementX"in a)return a.movementX;a!==Nc&&(Nc&&"mousemove"===a.type?(ag=a.screenX-Nc.screenX,bg=a.screenY-Nc.screenY):bg=ag=0,Nc=a);return ag},movementY:function(a){return"movementY"in a?a.movementY:bg}}),ih=ka(Yd),al=E({},Yd,{dataTransfer:0}),Wj=ka(al),bl=E({},Mc,{relatedTarget:0}),Pe=ka(bl),cl=E({},Yb,{animationName:0,elapsedTime:0,pseudoElement:0}),Yj=ka(cl),dl=E({},Yb,{clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),
|
||||
ck=ka(dl),el=E({},Yb,{data:0}),qh=ka(el),fk=qh,fl={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},gl={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",
|
||||
112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Gj={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},hl=E({},Mc,{key:function(a){if(a.key){var b=fl[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=cd(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?gl[a.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,
|
||||
metaKey:0,repeat:0,locale:0,getModifierState:Fe,charCode:function(a){return"keypress"===a.type?cd(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===a.type?cd(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),Vj=ka(hl),il=E({},Yd,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0}),nh=ka(il),jl=E({},Mc,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,
|
||||
ctrlKey:0,shiftKey:0,getModifierState:Fe}),Xj=ka(jl),kl=E({},Yb,{propertyName:0,elapsedTime:0,pseudoElement:0}),Zj=ka(kl),ll=E({},Yd,{deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?-a.wheelDelta:0},deltaZ:0,deltaMode:0}),bk=ka(ll),Hj=[9,13,27,32],Ge=Ia&&"CompositionEvent"in window,Oc=null;Ia&&"documentMode"in document&&(Oc=document.documentMode);var ek=Ia&&"TextEvent"in
|
||||
window&&!Oc,Ug=Ia&&(!Ge||Oc&&8<Oc&&11>=Oc),Tg=String.fromCharCode(32),Sg=!1,Hb=!1,Kj={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},oc=null,pc=null,ph=!1;Ia&&(ph=Lj("input")&&(!document.documentMode||9<document.documentMode));var ua="function"===typeof Object.is?Object.is:Sj,dk=Ia&&"documentMode"in document&&11>=document.documentMode,Jb=null,Ke=null,rc=null,Je=!1,Kb={animationend:gd("Animation","AnimationEnd"),
|
||||
animationiteration:gd("Animation","AnimationIteration"),animationstart:gd("Animation","AnimationStart"),transitionend:gd("Transition","TransitionEnd")},Le={},eh={};Ia&&(eh=document.createElement("div").style,"AnimationEvent"in window||(delete Kb.animationend.animation,delete Kb.animationiteration.animation,delete Kb.animationstart.animation),"TransitionEvent"in window||delete Kb.transitionend.transition);var jh=hd("animationend"),kh=hd("animationiteration"),lh=hd("animationstart"),mh=hd("transitionend"),
|
||||
fh=new Map,Zi="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");
|
||||
(function(){for(var a=0;a<Zi.length;a++){var b=Zi[a],c=b.toLowerCase();b=b[0].toUpperCase()+b.slice(1);$a(c,"on"+b)}$a(jh,"onAnimationEnd");$a(kh,"onAnimationIteration");$a(lh,"onAnimationStart");$a("dblclick","onDoubleClick");$a("focusin","onFocus");$a("focusout","onBlur");$a(mh,"onTransitionEnd")})();Ab("onMouseEnter",["mouseout","mouseover"]);Ab("onMouseLeave",["mouseout","mouseover"]);Ab("onPointerEnter",["pointerout","pointerover"]);Ab("onPointerLeave",["pointerout","pointerover"]);mb("onChange",
|
||||
"change click focusin focusout input keydown keyup selectionchange".split(" "));mb("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" "));mb("onBeforeInput",["compositionend","keypress","textInput","paste"]);mb("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" "));mb("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" "));mb("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));
|
||||
var Ec="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Uj=new Set("cancel close invalid load scroll toggle".split(" ").concat(Ec)),id="_reactListening"+Math.random().toString(36).slice(2),gk=/\r\n?/g,hk=/\u0000|\uFFFD/g,Jf=null,Kf=null,Rf="function"===typeof setTimeout?setTimeout:void 0,Nk="function"===typeof clearTimeout?
|
||||
clearTimeout:void 0,$i="function"===typeof Promise?Promise:void 0,Jk="function"===typeof queueMicrotask?queueMicrotask:"undefined"!==typeof $i?function(a){return $i.resolve(null).then(a).catch(ik)}:Rf,Zb=Math.random().toString(36).slice(2),Da="__reactFiber$"+Zb,uc="__reactProps$"+Zb,Ja="__reactContainer$"+Zb,Me="__reactEvents$"+Zb,Dk="__reactListeners$"+Zb,Ek="__reactHandles$"+Zb,Se=[],Mb=-1,cb={},J=bb(cb),S=bb(!1),pb=cb,La=null,md=!1,Te=!1,Ob=[],Pb=0,od=null,nd=0,na=[],oa=0,rb=null,Ma=1,Na="",la=
|
||||
null,fa=null,D=!1,wa=null,Ik=Sa.ReactCurrentBatchConfig,Vb=Dh(!0),li=Dh(!1),ud=bb(null),td=null,Rb=null,bf=null,tb=null,kk=Oa,gb=!1,wc={},Ea=bb(wc),yc=bb(wc),xc=bb(wc),F=bb(0),kf=[],yd=Sa.ReactCurrentDispatcher,sf=Sa.ReactCurrentBatchConfig,vb=0,C=null,K=null,N=null,Ad=!1,zc=!1,Ac=0,ml=0,zd={readContext:qa,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useInsertionEffect:V,useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,
|
||||
useMutableSource:V,useSyncExternalStore:V,useId:V,unstable_isNewReconciler:!1},lk={readContext:qa,useCallback:function(a,b){Fa().memoizedState=[a,void 0===b?null:b];return a},useContext:qa,useEffect:Sh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Bd(4194308,4,Vh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Bd(4194308,4,a,b)},useInsertionEffect:function(a,b){return Bd(4,2,a,b)},useMemo:function(a,b){var c=Fa();b=void 0===b?null:b;a=a();c.memoizedState=
|
||||
[a,b];return a},useReducer:function(a,b,c){var d=Fa();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};d.queue=a;a=a.dispatch=qk.bind(null,C,a);return[d.memoizedState,a]},useRef:function(a){var b=Fa();a={current:a};return b.memoizedState=a},useState:Qh,useDebugValue:rf,useDeferredValue:function(a){return Fa().memoizedState=a},useTransition:function(){var a=Qh(!1),b=a[0];a=pk.bind(null,a[1]);Fa().memoizedState=
|
||||
a;return[b,a]},useMutableSource:function(a,b,c){},useSyncExternalStore:function(a,b,c){var d=C,e=Fa();if(D){if(void 0===c)throw Error(m(407));c=c()}else{c=b();if(null===O)throw Error(m(349));0!==(vb&30)||Nh(d,b,c)}e.memoizedState=c;var f={value:c,getSnapshot:b};e.queue=f;Sh(Lh.bind(null,d,f,a),[a]);d.flags|=2048;Cc(9,Mh.bind(null,d,f,c,b),void 0,null);return c},useId:function(){var a=Fa(),b=O.identifierPrefix;if(D){var c=Na;var d=Ma;c=(d&~(1<<32-ta(d)-1)).toString(32)+c;b=":"+b+"R"+c;c=Ac++;0<c&&
|
||||
(b+="H"+c.toString(32));b+=":"}else c=ml++,b=":"+b+"r"+c.toString(32)+":";return a.memoizedState=b},unstable_isNewReconciler:!1},mk={readContext:qa,useCallback:Xh,useContext:qa,useEffect:qf,useImperativeHandle:Wh,useInsertionEffect:Th,useLayoutEffect:Uh,useMemo:Yh,useReducer:of,useRef:Rh,useState:function(a){return of(Bc)},useDebugValue:rf,useDeferredValue:function(a){var b=sa();return Zh(b,K.memoizedState,a)},useTransition:function(){var a=of(Bc)[0],b=sa().memoizedState;return[a,b]},useMutableSource:Jh,
|
||||
useSyncExternalStore:Kh,useId:$h,unstable_isNewReconciler:!1},nk={readContext:qa,useCallback:Xh,useContext:qa,useEffect:qf,useImperativeHandle:Wh,useInsertionEffect:Th,useLayoutEffect:Uh,useMemo:Yh,useReducer:pf,useRef:Rh,useState:function(a){return pf(Bc)},useDebugValue:rf,useDeferredValue:function(a){var b=sa();return null===K?b.memoizedState=a:Zh(b,K.memoizedState,a)},useTransition:function(){var a=pf(Bc)[0],b=sa().memoizedState;return[a,b]},useMutableSource:Jh,useSyncExternalStore:Kh,useId:$h,
|
||||
unstable_isNewReconciler:!1},Dd={isMounted:function(a){return(a=a._reactInternals)?nb(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.payload=b;void 0!==c&&null!==c&&(f.callback=c);b=fb(a,f,e);null!==b&&(xa(b,a,e,d),vd(b,a,e))},enqueueReplaceState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.tag=1;f.payload=b;void 0!==c&&null!==c&&(f.callback=c);b=fb(a,f,e);null!==b&&(xa(b,a,e,d),vd(b,a,e))},enqueueForceUpdate:function(a,b){a=a._reactInternals;
|
||||
var c=Z(),d=hb(a),e=Pa(c,d);e.tag=2;void 0!==b&&null!==b&&(e.callback=b);b=fb(a,e,d);null!==b&&(xa(b,a,d,c),vd(b,a,d))}},rk="function"===typeof WeakMap?WeakMap:Map,tk=Sa.ReactCurrentOwner,ha=!1,Cf={dehydrated:null,treeContext:null,retryLane:0};var zk=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=
|
||||
c.return;c=c.sibling}};var xi=function(a,b){};var yk=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){a=b.stateNode;ub(Ea.current);e=null;switch(c){case "input":f=ke(a,f);d=ke(a,d);e=[];break;case "select":f=E({},f,{value:void 0});d=E({},d,{value:void 0});e=[];break;case "textarea":f=ne(a,f);d=ne(a,d);e=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(a.onclick=kd)}pe(c,d);var g;c=null;for(l in f)if(!d.hasOwnProperty(l)&&f.hasOwnProperty(l)&&null!=f[l])if("style"===
|
||||
l){var h=f[l];for(g in h)h.hasOwnProperty(g)&&(c||(c={}),c[g]="")}else"dangerouslySetInnerHTML"!==l&&"children"!==l&&"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&"autoFocus"!==l&&($b.hasOwnProperty(l)?e||(e=[]):(e=e||[]).push(l,null));for(l in d){var k=d[l];h=null!=f?f[l]:void 0;if(d.hasOwnProperty(l)&&k!==h&&(null!=k||null!=h))if("style"===l)if(h){for(g in h)!h.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(c||(c={}),c[g]="");for(g in k)k.hasOwnProperty(g)&&h[g]!==k[g]&&(c||
|
||||
(c={}),c[g]=k[g])}else c||(e||(e=[]),e.push(l,c)),c=k;else"dangerouslySetInnerHTML"===l?(k=k?k.__html:void 0,h=h?h.__html:void 0,null!=k&&h!==k&&(e=e||[]).push(l,k)):"children"===l?"string"!==typeof k&&"number"!==typeof k||(e=e||[]).push(l,""+k):"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&($b.hasOwnProperty(l)?(null!=k&&"onScroll"===l&&B("scroll",a),e||h===k||(e=[])):(e=e||[]).push(l,k))}c&&(e=e||[]).push("style",c);var l=e;if(b.updateQueue=l)b.flags|=4}};var Ak=function(a,
|
||||
b,c,d){c!==d&&(b.flags|=4)};var Jd=!1,X=!1,Fk="function"===typeof WeakSet?WeakSet:Set,l=null,zi=!1,T=null,za=!1,Mk=Math.ceil,Od=Sa.ReactCurrentDispatcher,Uf=Sa.ReactCurrentOwner,ca=Sa.ReactCurrentBatchConfig,p=0,O=null,H=null,U=0,ba=0,Ga=bb(0),L=0,Jc=null,ra=0,Md=0,Sf=0,Kc=null,ja=null,Of=0,Hf=Infinity,Ra=null,Ed=!1,xf=null,ib=null,Pd=!1,lb=null,Qd=0,Ic=0,Pf=null,Kd=-1,Ld=0;var Qk=function(a,b,c){if(null!==a)if(a.memoizedProps!==b.pendingProps||S.current)ha=!0;else{if(0===(a.lanes&c)&&0===(b.flags&
|
||||
128))return ha=!1,wk(a,b,c);ha=0!==(a.flags&131072)?!0:!1}else ha=!1,D&&0!==(b.flags&1048576)&&yh(b,nd,b.index);b.lanes=0;switch(b.tag){case 2:var d=b.type;Fd(a,b);a=b.pendingProps;var e=Nb(b,J.current);Sb(b,c);e=mf(null,b,d,a,e,c);var f=nf();b.flags|=1;"object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof?(b.tag=1,b.memoizedState=null,b.updateQueue=null,ea(d)?(f=!0,ld(b)):f=!1,b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null,ff(b),e.updater=Dd,b.stateNode=
|
||||
e,e._reactInternals=b,uf(b,d,a,c),b=Af(null,b,d,!0,f,c)):(b.tag=0,D&&f&&Ue(b),aa(null,b,e,c),b=b.child);return b;case 16:d=b.elementType;a:{Fd(a,b);a=b.pendingProps;e=d._init;d=e(d._payload);b.type=d;e=b.tag=Uk(d);a=ya(d,a);switch(e){case 0:b=zf(null,b,d,a,c);break a;case 1:b=ri(null,b,d,a,c);break a;case 11:b=mi(null,b,d,a,c);break a;case 14:b=ni(null,b,d,ya(d.type,a),c);break a}throw Error(m(306,d,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),zf(a,b,d,e,c);
|
||||
case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),ri(a,b,d,e,c);case 3:a:{si(b);if(null===a)throw Error(m(387));d=b.pendingProps;f=b.memoizedState;e=f.element;Fh(a,b);wd(b,d,null,c);var g=b.memoizedState;d=g.element;if(f.isDehydrated)if(f={element:d,isDehydrated:!1,cache:g.cache,pendingSuspenseBoundaries:g.pendingSuspenseBoundaries,transitions:g.transitions},b.updateQueue.baseState=f,b.memoizedState=f,b.flags&256){e=Ub(Error(m(423)),b);b=ti(a,b,d,c,e);break a}else if(d!==e){e=
|
||||
Ub(Error(m(424)),b);b=ti(a,b,d,c,e);break a}else for(fa=Ka(b.stateNode.containerInfo.firstChild),la=b,D=!0,wa=null,c=li(b,null,d,c),b.child=c;c;)c.flags=c.flags&-3|4096,c=c.sibling;else{Qb();if(d===e){b=Qa(a,b,c);break a}aa(a,b,d,c)}b=b.child}return b;case 5:return Ih(b),null===a&&Xe(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Qe(d,e)?g=null:null!==f&&Qe(d,f)&&(b.flags|=32),qi(a,b),aa(a,b,g,c),b.child;case 6:return null===a&&Xe(b),null;case 13:return ui(a,b,c);case 4:return gf(b,
|
||||
b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=Vb(b,null,d,c):aa(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),mi(a,b,d,e,c);case 7:return aa(a,b,b.pendingProps,c),b.child;case 8:return aa(a,b,b.pendingProps.children,c),b.child;case 12:return aa(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;f=b.memoizedProps;g=e.value;y(ud,d._currentValue);d._currentValue=g;if(null!==f)if(ua(f.value,g)){if(f.children===
|
||||
e.children&&!S.current){b=Qa(a,b,c);break a}}else for(f=b.child,null!==f&&(f.return=b);null!==f;){var h=f.dependencies;if(null!==h){g=f.child;for(var k=h.firstContext;null!==k;){if(k.context===d){if(1===f.tag){k=Pa(-1,c&-c);k.tag=2;var l=f.updateQueue;if(null!==l){l=l.shared;var p=l.pending;null===p?k.next=k:(k.next=p.next,p.next=k);l.pending=k}}f.lanes|=c;k=f.alternate;null!==k&&(k.lanes|=c);df(f.return,c,b);h.lanes|=c;break}k=k.next}}else if(10===f.tag)g=f.type===b.type?null:f.child;else if(18===
|
||||
f.tag){g=f.return;if(null===g)throw Error(m(341));g.lanes|=c;h=g.alternate;null!==h&&(h.lanes|=c);df(g,c,b);g=f.sibling}else g=f.child;if(null!==g)g.return=f;else for(g=f;null!==g;){if(g===b){g=null;break}f=g.sibling;if(null!==f){f.return=g.return;g=f;break}g=g.return}f=g}aa(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,d=b.pendingProps.children,Sb(b,c),e=qa(e),d=d(e),b.flags|=1,aa(a,b,d,c),b.child;case 14:return d=b.type,e=ya(d,b.pendingProps),e=ya(d.type,e),ni(a,b,d,e,c);case 15:return oi(a,
|
||||
b,b.type,b.pendingProps,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),Fd(a,b),b.tag=1,ea(d)?(a=!0,ld(b)):a=!1,Sb(b,c),ei(b,d,e),uf(b,d,e,c),Af(null,b,d,!0,a,c);case 19:return wi(a,b,c);case 22:return pi(a,b,c)}throw Error(m(156,b.tag));};var pa=function(a,b,c,d){return new Tk(a,b,c,d)},aj="function"===typeof reportError?reportError:function(a){console.error(a)};Ud.prototype.render=Xf.prototype.render=function(a){var b=this._internalRoot;if(null===b)throw Error(m(409));
|
||||
Sd(a,b,null,null)};Ud.prototype.unmount=Xf.prototype.unmount=function(){var a=this._internalRoot;if(null!==a){this._internalRoot=null;var b=a.containerInfo;yb(function(){Sd(null,a,null,null)});b[Ja]=null}};Ud.prototype.unstable_scheduleHydration=function(a){if(a){var b=nl();a={blockedOn:null,target:a,priority:b};for(var c=0;c<Ya.length&&0!==b&&b<Ya[c].priority;c++);Ya.splice(c,0,a);0===c&&Hg(a)}};var Cj=function(a){switch(a.tag){case 3:var b=a.stateNode;if(b.current.memoizedState.isDehydrated){var c=
|
||||
hc(b.pendingLanes);0!==c&&(xe(b,c|1),ia(b,P()),0===(p&6)&&(Hc(),db()))}break;case 13:yb(function(){var b=Oa(a,1);if(null!==b){var c=Z();xa(b,a,1,c)}}),Wf(a,1)}};var Gg=function(a){if(13===a.tag){var b=Oa(a,134217728);if(null!==b){var c=Z();xa(b,a,134217728,c)}Wf(a,134217728)}};var xj=function(a){if(13===a.tag){var b=hb(a),c=Oa(a,b);if(null!==c){var d=Z();xa(c,a,b,d)}Wf(a,b)}};var nl=function(){return z};var wj=function(a,b){var c=z;try{return z=a,b()}finally{z=c}};se=function(a,b,c){switch(b){case "input":le(a,
|
||||
c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=Rc(d);if(!e)throw Error(m(90));jg(d);le(d,e)}}}break;case "textarea":og(a,c);break;case "select":b=c.value,null!=b&&Db(a,!!c.multiple,b,!1)}};(function(a,b,c){xg=a;yg=c})(Tf,function(a,b,c,d,e){var f=z,g=ca.transition;try{return ca.transition=null,z=1,a(b,c,d,e)}finally{z=f,ca.transition=
|
||||
g,0===p&&Hc()}},yb);var ol={usingClientEntryPoint:!1,Events:[ec,Ib,Rc,ug,vg,Tf]};(function(a){a={bundleType:a.bundleType,version:a.version,rendererPackageName:a.rendererPackageName,rendererConfig:a.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setErrorHandler:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:Sa.ReactCurrentDispatcher,findHostInstanceByFiber:Xk,
|
||||
findFiberByHostInstance:a.findFiberByHostInstance||Yk,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null,reconcilerVersion:"18.3.1"};if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)a=!1;else{var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)a=!0;else{try{Uc=b.inject(a),Ca=b}catch(c){}a=b.checkDCE?!0:!1}}return a})({findFiberByHostInstance:ob,bundleType:0,version:"18.3.1-next-f1338f8080-20240426",
|
||||
rendererPackageName:"react-dom"});Q.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=ol;Q.createPortal=function(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!Yf(b))throw Error(m(200));return Wk(a,b,null,c)};Q.createRoot=function(a,b){if(!Yf(a))throw Error(m(299));var c=!1,d="",e=aj;null!==b&&void 0!==b&&(!0===b.unstable_strictMode&&(c=!0),void 0!==b.identifierPrefix&&(d=b.identifierPrefix),void 0!==b.onRecoverableError&&(e=b.onRecoverableError));b=Vf(a,1,!1,null,null,
|
||||
c,!1,d,e);a[Ja]=b.current;sc(8===a.nodeType?a.parentNode:a);return new Xf(b)};Q.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternals;if(void 0===b){if("function"===typeof a.render)throw Error(m(188));a=Object.keys(a).join(",");throw Error(m(268,a));}a=Bg(b);a=null===a?null:a.stateNode;return a};Q.flushSync=function(a){return yb(a)};Q.hydrate=function(a,b,c){if(!Vd(b))throw Error(m(200));return Wd(null,a,b,!0,c)};Q.hydrateRoot=function(a,b,c){if(!Yf(a))throw Error(m(405));
|
||||
var d=null!=c&&c.hydratedSources||null,e=!1,f="",g=aj;null!==c&&void 0!==c&&(!0===c.unstable_strictMode&&(e=!0),void 0!==c.identifierPrefix&&(f=c.identifierPrefix),void 0!==c.onRecoverableError&&(g=c.onRecoverableError));b=Wi(b,null,a,1,null!=c?c:null,e,!1,f,g);a[Ja]=b.current;sc(a);if(d)for(a=0;a<d.length;a++)c=d[a],e=c._getVersion,e=e(c._source),null==b.mutableSourceEagerHydrationData?b.mutableSourceEagerHydrationData=[c,e]:b.mutableSourceEagerHydrationData.push(c,e);return new Ud(b)};Q.render=
|
||||
function(a,b,c){if(!Vd(b))throw Error(m(200));return Wd(null,a,b,!1,c)};Q.unmountComponentAtNode=function(a){if(!Vd(a))throw Error(m(40));return a._reactRootContainer?(yb(function(){Wd(null,null,a,!1,function(){a._reactRootContainer=null;a[Ja]=null})}),!0):!1};Q.unstable_batchedUpdates=Tf;Q.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!Vd(c))throw Error(m(200));if(null==a||void 0===a._reactInternals)throw Error(m(38));return Wd(a,b,c,!1,d)};Q.version="18.3.1-next-f1338f8080-20240426"});
|
||||
})();
|
||||
31
public/assets/vendor/react/react.production.min.js
vendored
Normal file
31
public/assets/vendor/react/react.production.min.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
(function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b,
|
||||
e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1<l){for(var f=Array(l),k=0;k<l;k++)f[k]=arguments[k+2];d.children=f}if(a&&a.defaultProps)for(m in l=a.defaultProps,l)void 0===d[m]&&(d[m]=l[m]);return{$$typeof:y,type:a,key:c,ref:h,props:d,_owner:L.current}}function oa(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}
|
||||
function M(a){return"object"===typeof a&&null!==a&&a.$$typeof===y}function pa(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}function N(a,b){return"object"===typeof a&&null!==a&&null!=a.key?pa(""+a.key):b.toString(36)}function B(a,b,e,m,d){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var h=!1;if(null===a)h=!0;else switch(c){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case y:case qa:h=!0}}if(h)return h=a,d=d(h),a=""===m?"."+
|
||||
N(h,0):m,ca(d)?(e="",null!=a&&(e=a.replace(da,"$&/")+"/"),B(d,b,e,"",function(a){return a})):null!=d&&(M(d)&&(d=oa(d,e+(!d.key||h&&h.key===d.key?"":(""+d.key).replace(da,"$&/")+"/")+a)),b.push(d)),1;h=0;m=""===m?".":m+":";if(ca(a))for(var l=0;l<a.length;l++){c=a[l];var f=m+N(c,l);h+=B(c,b,e,f,d)}else if(f=x(a),"function"===typeof f)for(a=f.call(a),l=0;!(c=a.next()).done;)c=c.value,f=m+N(c,l++),h+=B(c,b,e,f,d);else if("object"===c)throw b=String(a),Error("Objects are not valid as a React child (found: "+
|
||||
("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}function C(a,b,e){if(null==a)return a;var c=[],d=0;B(a,c,"","",function(a){return b.call(e,a,d++)});return c}function ra(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=
|
||||
0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}function O(a,b){var e=a.length;a.push(b);a:for(;0<e;){var c=e-1>>>1,d=a[c];if(0<D(d,b))a[c]=b,a[e]=d,e=c;else break a}}function p(a){return 0===a.length?null:a[0]}function E(a){if(0===a.length)return null;var b=a[0],e=a.pop();if(e!==b){a[0]=e;a:for(var c=0,d=a.length,k=d>>>1;c<k;){var h=2*(c+1)-1,l=a[h],f=h+1,g=a[f];if(0>D(l,e))f<d&&0>D(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(f<d&&0>D(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b}
|
||||
function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null;
|
||||
k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-ha<ia?!1:!0}function R(a){G=a;H||(H=!0,I())}function T(a,b){A=ja(function(){a(v())},b)}function ka(a){throw Error("act(...) is not supported in production builds of React.");}var y=Symbol.for("react.element"),qa=Symbol.for("react.portal"),sa=Symbol.for("react.fragment"),
|
||||
ta=Symbol.for("react.strict_mode"),ua=Symbol.for("react.profiler"),va=Symbol.for("react.provider"),wa=Symbol.for("react.context"),xa=Symbol.for("react.forward_ref"),ya=Symbol.for("react.suspense"),za=Symbol.for("react.memo"),Aa=Symbol.for("react.lazy"),V=Symbol.iterator,X={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,m){},enqueueSetState:function(a,b,c,m){}},la=Object.assign,W={};w.prototype.isReactComponent={};w.prototype.setState=function(a,
|
||||
b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};Y.prototype=w.prototype;var t=K.prototype=new Y;t.constructor=K;la(t,w.prototype);t.isPureReactComponent=!0;var ca=Array.isArray,aa=Object.prototype.hasOwnProperty,L={current:null},
|
||||
ba={key:!0,ref:!0,__self:!0,__source:!0},da=/\/+/g,g={current:null},J={transition:null};if("object"===typeof performance&&"function"===typeof performance.now){var Ba=performance;var v=function(){return Ba.now()}}else{var ma=Date,Ca=ma.now();v=function(){return ma.now()-Ca}}var q=[],r=[],Da=1,n=null,k=3,F=!1,u=!1,z=!1,ja="function"===typeof setTimeout?setTimeout:null,ea="function"===typeof clearTimeout?clearTimeout:null,na="undefined"!==typeof setImmediate?setImmediate:null;"undefined"!==typeof navigator&&
|
||||
void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var H=!1,G=null,A=-1,ia=5,ha=-1,U=function(){if(null!==G){var a=v();ha=a;var b=!0;try{b=G(!0,a)}finally{b?I():(H=!1,G=null)}}else H=!1};if("function"===typeof na)var I=function(){na(U)};else if("undefined"!==typeof MessageChannel){t=new MessageChannel;var Ea=t.port2;t.port1.onmessage=U;I=function(){Ea.postMessage(null)}}else I=function(){ja(U,0)};t={ReactCurrentDispatcher:g,
|
||||
ReactCurrentOwner:L,ReactCurrentBatchConfig:J,Scheduler:{__proto__:null,unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=k;k=a;try{return b()}finally{k=c}},unstable_next:function(a){switch(k){case 1:case 2:case 3:var b=3;break;default:b=k}var c=k;k=b;try{return a()}finally{k=c}},unstable_scheduleCallback:function(a,
|
||||
b,c){var e=v();"object"===typeof c&&null!==c?(c=c.delay,c="number"===typeof c&&0<c?e+c:e):c=e;switch(a){case 1:var d=-1;break;case 2:d=250;break;case 5:d=1073741823;break;case 4:d=1E4;break;default:d=5E3}d=c+d;a={id:Da++,callback:b,priorityLevel:a,startTime:c,expirationTime:d,sortIndex:-1};c>e?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=
|
||||
k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):
|
||||
ia=0<a?Math.floor(1E3/a):5},unstable_Profiling:null}};c.Children={map:C,forEach:function(a,b,c){C(a,function(){b.apply(this,arguments)},c)},count:function(a){var b=0;C(a,function(){b++});return b},toArray:function(a){return C(a,function(a){return a})||[]},only:function(a){if(!M(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};c.Component=w;c.Fragment=sa;c.Profiler=ua;c.PureComponent=K;c.StrictMode=ta;c.Suspense=ya;c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=
|
||||
t;c.act=ka;c.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var e=la({},a.props),d=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=L.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var l=a.type.defaultProps;for(f in b)aa.call(b,f)&&!ba.hasOwnProperty(f)&&(e[f]=void 0===b[f]&&void 0!==l?l[f]:b[f])}var f=arguments.length-2;if(1===f)e.children=c;else if(1<f){l=
|
||||
Array(f);for(var g=0;g<f;g++)l[g]=arguments[g+2];e.children=l}return{$$typeof:y,type:a.type,key:d,ref:k,props:e,_owner:h}};c.createContext=function(a){a={$$typeof:wa,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:va,_context:a};return a.Consumer=a};c.createElement=Z;c.createFactory=function(a){var b=Z.bind(null,a);b.type=a;return b};c.createRef=function(){return{current:null}};c.forwardRef=function(a){return{$$typeof:xa,
|
||||
render:a}};c.isValidElement=M;c.lazy=function(a){return{$$typeof:Aa,_payload:{_status:-1,_result:a},_init:ra}};c.memo=function(a,b){return{$$typeof:za,type:a,compare:void 0===b?null:b}};c.startTransition=function(a,b){b=J.transition;J.transition={};try{a()}finally{J.transition=b}};c.unstable_act=ka;c.useCallback=function(a,b){return g.current.useCallback(a,b)};c.useContext=function(a){return g.current.useContext(a)};c.useDebugValue=function(a,b){};c.useDeferredValue=function(a){return g.current.useDeferredValue(a)};
|
||||
c.useEffect=function(a,b){return g.current.useEffect(a,b)};c.useId=function(){return g.current.useId()};c.useImperativeHandle=function(a,b,c){return g.current.useImperativeHandle(a,b,c)};c.useInsertionEffect=function(a,b){return g.current.useInsertionEffect(a,b)};c.useLayoutEffect=function(a,b){return g.current.useLayoutEffect(a,b)};c.useMemo=function(a,b){return g.current.useMemo(a,b)};c.useReducer=function(a,b,c){return g.current.useReducer(a,b,c)};c.useRef=function(a){return g.current.useRef(a)};
|
||||
c.useState=function(a){return g.current.useState(a)};c.useSyncExternalStore=function(a,b,c){return g.current.useSyncExternalStore(a,b,c)};c.useTransition=function(){return g.current.useTransition()};c.version="18.3.1"});
|
||||
})();
|
||||
389
public/index.original.php
Executable file
389
public/index.original.php
Executable file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Modules\MiningChecker\Support\ApiException as MiningApiException;
|
||||
use Modules\MiningChecker\Support\DebugState as MiningDebugState;
|
||||
|
||||
// boot application (config, autoload, services)
|
||||
require_once __DIR__ . '/../config/fileload.php';
|
||||
|
||||
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
|
||||
$uriPath = trim($uriPath, '/');
|
||||
$projectRoot = dirname(__DIR__);
|
||||
$auth = app()->auth();
|
||||
|
||||
// OIDC Auth
|
||||
$publicPaths = [
|
||||
'auth/login',
|
||||
'auth/callback',
|
||||
'auth/logout',
|
||||
'auth/keycloak/login',
|
||||
'auth/keycloak/callback',
|
||||
'auth/keycloak/logout',
|
||||
'auth/me',
|
||||
'module/pi_control/terminal_info',
|
||||
];
|
||||
$requiresGlobalAuth = in_array($uriPath, ['settings', 'settings/widgets', 'settings/search-engines', 'settings/apps', '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/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)) {
|
||||
$user = auth_user();
|
||||
if (!$user) {
|
||||
header('Location: /auth/login', true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Sicherheitscheck
|
||||
if (str_contains($uriPath, '..')) {
|
||||
http_response_code(400);
|
||||
exit('Bad request');
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/login') {
|
||||
$returnTo = (string)($_GET['return_to'] ?? '/');
|
||||
$auth->login($returnTo);
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/callback') {
|
||||
$uriPath = 'auth/callback';
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/logout') {
|
||||
$uriPath = 'auth/logout';
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/me') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'authenticated' => $auth->isAuthenticated(),
|
||||
'user' => $auth->user(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'exports/database.sql') {
|
||||
require_admin();
|
||||
$pdo = app()->basePdo() ?: app()->pdo();
|
||||
if (!$pdo instanceof PDO) {
|
||||
http_response_code(500);
|
||||
exit('Keine Datenbankverbindung fuer den Export verfuegbar.');
|
||||
}
|
||||
|
||||
$filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql';
|
||||
$sql = (new \App\SqlDataExporter())->export($pdo, 'nexus');
|
||||
header('Content-Type: application/sql; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
echo $sql;
|
||||
exit;
|
||||
}
|
||||
|
||||
if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
|
||||
$moduleName = $moduleAuthMatches[1];
|
||||
$moduleMeta = app()->modules()->get($moduleName);
|
||||
if ($moduleMeta === null) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'module_not_found'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if (!$auth->isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'auth_required'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if (!auth_is_admin()) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'forbidden'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
echo json_encode(['data' => ($moduleMeta['auth'] ?? ['required' => false, 'users' => [], 'groups' => []])], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||
$input = json_decode((string)file_get_contents('php://input'), true);
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
echo json_encode(['data' => app()->modules()->saveAuth($moduleName, $input)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'api/debug/entries') {
|
||||
require_admin();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'data' => [
|
||||
'enabled' => nexus_debug_enabled(),
|
||||
'entries' => nexus_debug_entries(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'api/debug/clear' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
require_admin();
|
||||
nexus_debug_clear();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'data' => [
|
||||
'cleared' => true,
|
||||
'entries' => [],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) {
|
||||
$moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]];
|
||||
if (!$auth->canAccessModule($moduleMeta)) {
|
||||
http_response_code($auth->isAuthenticated() ? 403 : 401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
|
||||
'login_url' => '/auth/login',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $projectRoot . '/modules/mining-checker/bootstrap.php';
|
||||
|
||||
try {
|
||||
(new Modules\MiningChecker\Api\Router($projectRoot . '/modules/mining-checker'))->handle($apiMatches[1] ?? '');
|
||||
} catch (MiningApiException $exception) {
|
||||
$debugTrace = MiningDebugState::export();
|
||||
http_response_code($exception->statusCode());
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $exception->getMessage(),
|
||||
'context' => $exception->context(),
|
||||
'debug' => $debugTrace !== [] ? $debugTrace : null,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$debugTrace = MiningDebugState::export();
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => 'Unerwarteter Mining-Checker Fehler.',
|
||||
'context' => ['message' => $exception->getMessage()],
|
||||
'debug' => $debugTrace !== [] ? $debugTrace : null,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('~^api/fx-rates(?:/(.*))?$~', $uriPath, $apiMatches)) {
|
||||
$moduleMeta = app()->modules()->get('fx-rates') ?? ['auth' => ['required' => false]];
|
||||
if (!$auth->canAccessModule($moduleMeta)) {
|
||||
http_response_code($auth->isAuthenticated() ? 403 : 401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
|
||||
'login_url' => '/auth/login',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $projectRoot . '/modules/fx-rates/bootstrap.php';
|
||||
|
||||
try {
|
||||
$service = module_fn('fx-rates', 'service');
|
||||
(new \Modules\FxRates\Api\Router($service))->handle($apiMatches[1] ?? '');
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => 'Unerwarteter FX-Module Fehler.',
|
||||
'context' => ['message' => $exception->getMessage()],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('~^module-assets/([a-zA-Z0-9_-]+)/(.*)$~', $uriPath, $assetMatches)) {
|
||||
$module = $assetMatches[1];
|
||||
$relativeAssetPath = trim($assetMatches[2], '/');
|
||||
if ($relativeAssetPath === '' || str_contains($relativeAssetPath, '..')) {
|
||||
http_response_code(400);
|
||||
exit('Bad request');
|
||||
}
|
||||
|
||||
$assetFile = $projectRoot . '/modules/' . $module . '/assets/' . $relativeAssetPath;
|
||||
if (!is_file($assetFile)) {
|
||||
http_response_code(404);
|
||||
exit('Asset not found');
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($assetFile, PATHINFO_EXTENSION));
|
||||
$contentType = match ($extension) {
|
||||
'css' => 'text/css; charset=utf-8',
|
||||
'js' => 'application/javascript; charset=utf-8',
|
||||
'json' => 'application/json; charset=utf-8',
|
||||
'png' => 'image/png',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
readfile($assetFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Basispfad fuer Landingpages
|
||||
$pagesBase = realpath(__DIR__ . '/../partials/landingpages') ?: (__DIR__ . '/../partials/landingpages');
|
||||
$page404 = $pagesBase . '/errorpages/404.php';
|
||||
|
||||
// Spezialrouten für Module
|
||||
if (str_starts_with($uriPath, 'modules/install')) {
|
||||
$target = $pagesBase . '/modules/install.php';
|
||||
} elseif (str_starts_with($uriPath, 'modules/setup/')) {
|
||||
$setupPath = trim(substr($uriPath, strlen('modules/setup/')), '/');
|
||||
$setupParts = $setupPath === '' ? [] : explode('/', $setupPath, 2);
|
||||
$_GET['module'] = trim((string) ($setupParts[0] ?? ''));
|
||||
if (isset($setupParts[1]) && trim((string) $setupParts[1]) !== '') {
|
||||
$_GET['section'] = trim((string) $setupParts[1]);
|
||||
}
|
||||
$target = $pagesBase . '/modules/setup.php';
|
||||
} elseif (str_starts_with($uriPath, 'modules/access/')) {
|
||||
$_GET['module'] = trim(substr($uriPath, strlen('modules/access/')), '/');
|
||||
$target = $pagesBase . '/modules/access.php';
|
||||
} elseif ($uriPath === 'modules/sql-import') {
|
||||
$target = $pagesBase . '/modules/sql_import.php';
|
||||
} elseif ($uriPath === 'auth/login') {
|
||||
$target = $pagesBase . '/auth/login.php';
|
||||
} elseif ($uriPath === 'auth/callback') {
|
||||
$target = $pagesBase . '/auth/callback.php';
|
||||
} elseif ($uriPath === 'auth/logout') {
|
||||
$target = $pagesBase . '/auth/logout.php';
|
||||
} elseif ($uriPath === 'settings') {
|
||||
$target = $pagesBase . '/users/settings.php';
|
||||
} elseif ($uriPath === 'settings/widgets') {
|
||||
$target = $pagesBase . '/users/settings_widgets.php';
|
||||
} elseif ($uriPath === 'settings/search-engines') {
|
||||
$target = $pagesBase . '/users/settings_search_engines.php';
|
||||
} elseif ($uriPath === 'settings/apps') {
|
||||
$target = $pagesBase . '/users/settings_apps.php';
|
||||
} elseif ($uriPath === 'users') {
|
||||
$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') {
|
||||
$target = $pagesBase . '/retool/debug.php';
|
||||
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
|
||||
$module = $m[1];
|
||||
$page = isset($m[2]) && $m[2] !== '' ? trim($m[2], '/') : 'index';
|
||||
$moduleMeta = app()->modules()->get($module);
|
||||
if ($moduleMeta !== null) {
|
||||
$auth->requireModuleAccess($moduleMeta);
|
||||
}
|
||||
$modulePage = app()->modules()->resolvePage($module, $page);
|
||||
$moduleBootstrap = $projectRoot . '/modules/' . $module . '/bootstrap.php';
|
||||
if (is_file($moduleBootstrap)) {
|
||||
require_once $moduleBootstrap;
|
||||
}
|
||||
if ($modulePage) {
|
||||
$target = $modulePage;
|
||||
} else {
|
||||
http_response_code(404);
|
||||
$target = $page404;
|
||||
}
|
||||
} elseif ($uriPath === '' || $uriPath === 'index' || $uriPath === 'index.php') {
|
||||
$target = $pagesBase . '/index.php';
|
||||
} else {
|
||||
$base = $pagesBase . '/' . $uriPath;
|
||||
// 1) Verzeichnis mit index.php
|
||||
if (is_dir($base) && is_file($base . '/index.php')) {
|
||||
$target = $base . '/index.php';
|
||||
}
|
||||
// 2) Datei
|
||||
elseif (is_file($base . '.php')) {
|
||||
|
||||
$target = $base . '.php';
|
||||
}
|
||||
// 3) 404
|
||||
elseif (is_file($base)) {
|
||||
|
||||
$target = $base;
|
||||
}
|
||||
// 3) 404
|
||||
else {
|
||||
http_response_code(404);
|
||||
$target = $page404;
|
||||
}
|
||||
}
|
||||
// ------------------------------------
|
||||
// Layout-Regel
|
||||
// ------------------------------------
|
||||
$skipLayout = false;
|
||||
$targetReal = realpath($target);
|
||||
$retoolBase = realpath($pagesBase . '/retool/raw');
|
||||
|
||||
// Beispiel: alles unter landingpages/retool/* ohne Layout
|
||||
if ($targetReal && $retoolBase && str_starts_with($targetReal, $retoolBase)) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Ausgabe
|
||||
// ------------------------------------
|
||||
// Erst Inhalt laden (ohne Ausgabe), damit Header/Redirects vor HTML funktionieren
|
||||
ob_start();
|
||||
try {
|
||||
require $target;
|
||||
$content = ob_get_clean();
|
||||
} catch (\App\ModuleConfigException $e) {
|
||||
ob_end_clean();
|
||||
http_response_code(412);
|
||||
$moduleName = $e->module();
|
||||
$module = app()->modules()->get($moduleName);
|
||||
$title = $module['title'] ?? $moduleName;
|
||||
$setupUrl = '/modules/setup/' . rawurlencode($moduleName);
|
||||
$content = '<div class="card">' .
|
||||
'<div class="pill">' . e($title) . '</div>' .
|
||||
'<h1 style="margin-top:.75rem;">Setup erforderlich</h1>' .
|
||||
'<p class="muted">' . e($e->getMessage()) . '</p>' .
|
||||
'<div style="margin-top:1rem;"><a class="nav-link" href="' . e($setupUrl) . '">Zum Setup</a></div>' .
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Wenn bereits Header gesendet wurden (z. B. eigener Redirect/Content-Type), Layout überspringen
|
||||
if (headers_sent()) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
|
||||
if (!$skipLayout) {
|
||||
tpl('layout_start', 'structure');
|
||||
}
|
||||
|
||||
echo $content;
|
||||
|
||||
if (!$skipLayout) {
|
||||
tpl('layout_end', 'structure');
|
||||
}
|
||||
22
public/index.php
Executable file → Normal file
22
public/index.php
Executable file → Normal file
@@ -24,9 +24,12 @@ $publicPaths = [
|
||||
'auth/me',
|
||||
'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', 'settings/widgets', 'settings/search-engines', 'settings/apps', '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/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)) {
|
||||
$user = auth_user();
|
||||
if (!$user) {
|
||||
@@ -272,8 +275,25 @@ if (str_starts_with($uriPath, 'modules/install')) {
|
||||
$target = $pagesBase . '/auth/logout.php';
|
||||
} elseif ($uriPath === 'settings') {
|
||||
$target = $pagesBase . '/users/settings.php';
|
||||
} elseif ($uriPath === 'settings/widgets') {
|
||||
$target = $pagesBase . '/users/settings_widgets.php';
|
||||
} elseif ($uriPath === 'settings/search-engines') {
|
||||
$target = $pagesBase . '/users/settings_search_engines.php';
|
||||
} elseif ($uriPath === 'settings/apps') {
|
||||
$target = $pagesBase . '/users/settings_apps.php';
|
||||
} elseif ($uriPath === 'users') {
|
||||
$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') {
|
||||
$target = $pagesBase . '/retool/debug.php';
|
||||
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ final class App
|
||||
private ?\PDO $basePdo;
|
||||
private ModuleManager $modules;
|
||||
private AuthService $auth;
|
||||
private NexusDashboardService $dashboards;
|
||||
|
||||
private function __construct(private Config $config)
|
||||
{
|
||||
@@ -43,6 +44,7 @@ final class App
|
||||
$this->modules = new ModuleManager($this->basePdo, $modulesPath);
|
||||
$this->modules->bootEnabled();
|
||||
$this->auth = new AuthService($this);
|
||||
$this->dashboards = new NexusDashboardService($this->basePdo);
|
||||
}
|
||||
|
||||
public static function init(Config $config): self
|
||||
@@ -71,4 +73,5 @@ final class App
|
||||
public function basePdo(): ?\PDO { return $this->basePdo; }
|
||||
public function modules(): ModuleManager { return $this->modules; }
|
||||
public function auth(): AuthService { return $this->auth; }
|
||||
public function dashboards(): NexusDashboardService { return $this->dashboards; }
|
||||
}
|
||||
|
||||
@@ -148,6 +148,136 @@ final class BaseSchema
|
||||
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)");
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
widget_type TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
config 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_search_engines (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
short_code TEXT NOT NULL,
|
||||
engine_type TEXT NOT NULL DEFAULT 'template',
|
||||
template_url TEXT NULL,
|
||||
integration_id BIGINT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
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_search_engines_owner_short_idx ON nexus_search_engines (owner_key, short_code)");
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_apps (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
app_url TEXT NOT NULL,
|
||||
icon_url TEXT NULL,
|
||||
integration_id BIGINT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function ensureSqlite(\PDO $pdo): void
|
||||
@@ -276,6 +406,136 @@ final class BaseSchema
|
||||
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)");
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
widget_type TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
config 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_search_engines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
short_code TEXT NOT NULL,
|
||||
engine_type TEXT NOT NULL DEFAULT 'template',
|
||||
template_url TEXT NULL,
|
||||
integration_id INTEGER NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
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_search_engines_owner_short_idx ON nexus_search_engines (owner_key, short_code)");
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_apps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
app_url TEXT NOT NULL,
|
||||
icon_url TEXT NULL,
|
||||
integration_id INTEGER NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'private',
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function ensureGeneric(\PDO $pdo): void
|
||||
@@ -404,6 +664,136 @@ final class BaseSchema
|
||||
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)
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_key VARCHAR(190) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
widget_type VARCHAR(64) NOT NULL,
|
||||
description TEXT NULL,
|
||||
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||
config 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_search_engines (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_key VARCHAR(190) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
short_code VARCHAR(64) NOT NULL,
|
||||
engine_type VARCHAR(32) NOT NULL DEFAULT 'template',
|
||||
template_url TEXT NULL,
|
||||
integration_id BIGINT NULL,
|
||||
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||
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_search_engines_owner_short_idx (owner_key, short_code)
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_apps (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_key VARCHAR(190) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
app_url TEXT NOT NULL,
|
||||
icon_url TEXT NULL,
|
||||
integration_id BIGINT NULL,
|
||||
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
|
||||
config TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function seedTimezones(\PDO $pdo): void
|
||||
|
||||
@@ -213,14 +213,21 @@ final class Database
|
||||
|
||||
$host = (string)($db['host'] ?? 'localhost');
|
||||
$port = (int)($db['port'] ?? 5432);
|
||||
$connectTimeout = isset($db['connect_timeout']) ? max(1, (int) $db['connect_timeout']) : null;
|
||||
|
||||
// Hinweis: charset gehört bei pgsql nicht in den DSN
|
||||
return sprintf(
|
||||
$dsn = sprintf(
|
||||
'pgsql:host=%s;port=%d;dbname=%s',
|
||||
$host,
|
||||
$port,
|
||||
(string)$db['dbname']
|
||||
);
|
||||
|
||||
if ($connectTimeout !== null) {
|
||||
$dsn .= ';connect_timeout=' . $connectTimeout;
|
||||
}
|
||||
|
||||
return $dsn;
|
||||
}
|
||||
|
||||
private static function buildSqliteDsn(array $db): string
|
||||
|
||||
1025
src/App/NexusDashboardService.php
Normal file
1025
src/App/NexusDashboardService.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,23 @@ function auth_display_name(): string
|
||||
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
|
||||
{
|
||||
$name = auth_display_name();
|
||||
@@ -188,6 +205,11 @@ function modules(): \App\ModuleManager
|
||||
return app()->modules();
|
||||
}
|
||||
|
||||
function dashboards(): \App\NexusDashboardService
|
||||
{
|
||||
return app()->dashboards();
|
||||
}
|
||||
|
||||
function module_fn(string $module, string $name, mixed ...$args): mixed
|
||||
{
|
||||
return modules()->call($module, $name, ...$args);
|
||||
|
||||
Reference in New Issue
Block a user