Compare commits

...

22 Commits

Author SHA1 Message Date
8b07d1f00c sfdsf
All checks were successful
Deploy / deploy-staging (push) Successful in 9s
Deploy / deploy-production (push) Has been skipped
2026-06-09 01:59:48 +02:00
d8892e63a9 sdsads
All checks were successful
Deploy / deploy-staging (push) Successful in 10s
Deploy / deploy-production (push) Has been skipped
2026-06-09 01:49:09 +02:00
7020ae0b1c change stage
All checks were successful
Deploy / deploy-staging (push) Successful in 11s
Deploy / deploy-production (push) Has been skipped
2026-06-07 03:14:09 +02:00
a408765e76 Repair 2
All checks were successful
Deploy / deploy-staging (push) Successful in 10s
Deploy / deploy-production (push) Has been skipped
2026-06-07 02:52:14 +02:00
72774c05aa adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 10s
Deploy / deploy-production (push) Has been skipped
2026-06-07 02:46:36 +02:00
6b0ffd7aeb repair
All checks were successful
Deploy / deploy-staging (push) Successful in 12s
Deploy / deploy-production (push) Has been skipped
2026-06-07 02:41:08 +02:00
ae837bc6df asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-06-06 01:58:57 +02:00
94a4e3616c asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-06-06 01:57:40 +02:00
c5fd8b39fb adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 8s
Deploy / deploy-production (push) Has been skipped
2026-06-06 01:56:20 +02:00
3c1cc30fe9 nexus basic 2026-06-04 22:07:25 +02:00
aa7ec1d321 ycdfdsf
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
2026-06-03 01:54:51 +02:00
7783f65b26 adsd 2026-06-03 01:27:00 +02:00
99cf5eaef2 adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-06-03 00:43:33 +02:00
66f9176ea8 sdfsf
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-06-03 00:25:33 +02:00
3f712fc09d sadasd
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-06-03 00:16:17 +02:00
4a6f23cde0 sdsd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
2026-06-03 00:10:48 +02:00
efcaa88b31 adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
2026-06-02 23:58:12 +02:00
56d85e417b yasd
All checks were successful
Deploy / deploy-staging (push) Successful in 33s
Deploy / deploy-production (push) Has been skipped
2026-06-02 23:37:49 +02:00
127a0e71e1 yXyX
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-05-20 00:54:00 +02:00
7323673158 adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
2026-05-20 00:41:20 +02:00
813dd86811 ycyxc
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
2026-05-20 00:38:05 +02:00
81d1e486e8 update
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
2026-05-20 00:32:18 +02:00
32 changed files with 3528 additions and 407 deletions

View File

@@ -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

View 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.

View 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

View 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`

View 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

View 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`.

View 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
```

View 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

View File

@@ -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))
);

View File

@@ -26,6 +26,7 @@ $pgsql = [
'host' => 'db_nexus',
'port' => 5432,
'dbname' => 'nexus_live',
'connect_timeout' => 5,
// optional: schema/search_path (commonly "public")
'schema' => 'public',

View File

@@ -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',

View File

View 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)) {

View File

@@ -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() ?>

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -23,6 +23,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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'] ?? ''));
}
@@ -61,10 +67,20 @@ if ($currentDashboard === null) {
$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';
@@ -115,7 +131,7 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
<section class="section-box">
<h2>Element hinzufügen</h2>
<p class="muted">V1 unterstützt direkte Links, iFrames und gespeicherte Seitenmodule.</p>
<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 ?>">
@@ -129,7 +145,10 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
<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>
@@ -142,6 +161,19 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
<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&#10;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">
@@ -151,6 +183,15 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
<?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">
@@ -189,6 +230,11 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
<?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 . ';';
@@ -202,10 +248,30 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
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">
@@ -224,8 +290,22 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
</form>
</div>
<?php if (($itemType === 'iframe' || ($pageModule !== null && (string) ($pageModule['module_type'] ?? '') === 'iframe')) && $targetUrl !== ''): ?>
<?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>
@@ -249,11 +329,17 @@ $GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierb
const typeSelect = document.querySelector('[data-dashboard-item-type]');
const urlField = document.querySelector('[data-dashboard-target-url]');
const pageModuleField = document.querySelector('[data-dashboard-page-module]');
if (!typeSelect || !urlField || !pageModuleField) return;
const 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 isPageModule = typeSelect.value === 'page_module';
urlField.hidden = isPageModule;
pageModuleField.hidden = !isPageModule;
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();

View File

@@ -3,202 +3,173 @@ declare(strict_types=1);
$auth = app()->auth();
$authUser = $auth->user();
$service = dashboards();
$ownerKey = auth_user_key();
$groups = auth_groups();
$service = dashboards();
$modules = array_values(array_filter(
$auth->filterModules(modules()->all()),
static fn (array $module): bool => !empty($module['enabled'])
));
$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;
}
}
$dashboardsList = [];
$pageModules = [];
$defaultDashboard = null;
$dashboardItems = [];
if ($authUser !== null && $service->available() && $ownerKey !== '') {
$defaultDashboard = $service->ensureDefaultDashboard($ownerKey, 'Mein Dashboard');
$dashboardsList = $service->listAccessibleDashboards($ownerKey, $groups);
$pageModules = $service->listPageModulesForOwner($ownerKey);
if ($defaultDashboard !== []) {
$dashboardItems = $service->listItems((int) ($defaultDashboard['id'] ?? 0));
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'] = 'Übersicht';
$GLOBALS['layout_header_text'] = $authUser === null
? 'Zentraler Einstieg in Nexus und die verfügbaren Bereiche.'
: 'Persönliche Übersicht aus Dashboards, Seitenmodulen und klassischen Modulen.';
$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 ($authUser !== null): ?>
<header class="module-hero submenu-box">
<div class="module-hero-top module-hero-top--compact">
<nav class="module-tabs" aria-label="Nexus Navigation">
<a class="module-button module-button--tab-active" href="/">Nexus Übersicht</a>
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
<?php if (auth_is_admin()): ?>
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
<?php endif; ?>
</nav>
<div class="module-hero-actions module-submenu-actions">
<a class="module-button module-button--secondary module-button--small" href="/settings">Nexus Einstellungen</a>
</div>
</div>
</header>
<?php if ($priorityModuleEntries !== []): ?>
<section class="section-box">
<h2>Mein Standard-Dashboard</h2>
<p class="muted">Der schnellste Einstieg in deine wichtigsten Nexus-Inhalte.</p>
<?php if ($defaultDashboard === null || $defaultDashboard === []): ?>
<div class="dashboard-empty">Kein Dashboard verfügbar.</div>
<?php else: ?>
<div class="nexus-quick-grid">
<article class="card-box nexus-stat-card">
<span class="module-admin-meta__label">Dashboard</span>
<strong><?= e((string) ($defaultDashboard['title'] ?? 'Mein Dashboard')) ?></strong>
<p class="muted"><?= e((string) ($defaultDashboard['description'] ?? 'Persönliches Standard-Dashboard')) ?></p>
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($defaultDashboard['id'] ?? 0) ?>">Öffnen</a>
</article>
<article class="card-box nexus-stat-card">
<span class="module-admin-meta__label">Elemente</span>
<strong><?= count($dashboardItems) ?></strong>
<p class="muted">Aktive Dashboard-Elemente im Standard-Dashboard.</p>
</article>
<article class="card-box nexus-stat-card">
<span class="module-admin-meta__label">Seitenmodule</span>
<strong><?= count($pageModules) ?></strong>
<p class="muted">Eigene, on-the-fly angelegte Seitenmodule.</p>
</article>
<article class="card-box nexus-stat-card">
<span class="module-admin-meta__label">Klassische Module</span>
<strong><?= count($modules) ?></strong>
<p class="muted">Aktuell sichtbare, klassische Nexus-Module.</p>
</article>
</div>
<?php endif; ?>
</section>
<section class="section-box">
<h2>Dashboards</h2>
<p class="muted">Eigene und freigegebene Dashboards im globalen Nexus-System.</p>
<div class="module-admin-grid module-admin-grid--compact">
<?php foreach (array_slice($dashboardsList, 0, 6) as $dashboard): ?>
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2><?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?></h2>
<p><?= e((string) ($dashboard['description'] ?? 'Flexible Dashboard-Fläche für Widgets und Seitenmodule.')) ?></p>
</div>
</div>
<div class="module-admin-meta">
<div class="module-admin-meta__item">
<span class="module-admin-meta__label">Sichtbarkeit</span>
<strong class="module-admin-badge"><?= e(ucfirst((string) ($dashboard['visibility'] ?? 'private'))) ?></strong>
</div>
</div>
<div class="module-admin-actions">
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($dashboard['id'] ?? 0) ?>">Öffnen</a>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<div class="module-admin-grid">
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Seitenmodule</h2>
<p>On-the-fly angelegte Zielseiten und eingebettete Tools.</p>
</div>
</div>
<div class="module-admin-meta">
<div class="module-admin-meta__item">
<span class="module-admin-meta__label">Anzahl</span>
<strong class="module-admin-badge module-admin-badge--success"><?= count($pageModules) ?></strong>
</div>
</div>
<div class="module-admin-actions">
<a class="module-button module-button--secondary module-button--small" href="/page-modules">Verwalten</a>
</div>
</article>
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Integrationen</h2>
<p>Zentrale Anbindungen an Home Assistant, Pi-hole, Proxmox und andere Systeme.</p>
</div>
</div>
<div class="module-admin-actions">
<a class="module-button module-button--secondary module-button--small" href="/integrations">Verwalten</a>
</div>
</article>
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Dashboards konfigurieren</h2>
<p>Eigene Dashboard-Flächen, Reihenfolge und Standard-Dashboard verwalten.</p>
</div>
</div>
<div class="module-admin-actions">
<a class="module-button module-button--secondary module-button--small" href="/dashboards">Öffnen</a>
</div>
</article>
</div>
<section class="section-box">
<h2>Klassische Module</h2>
<p class="muted">Die bisherigen Nexus-Module bleiben parallel zum neuen Grundgerüst bestehen.</p>
<div class="module-list">
<?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>
<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 else: ?>
<?php endif; ?>
<?php if ($currentDashboard === null): ?>
<section class="section-box">
<h2>Nexus Einstieg</h2>
<p class="muted">Nach der Anmeldung stehen persönliche Dashboards, Integrationen und Seitenmodule bereit.</p>
<div class="module-admin-grid">
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Persönliche Dashboards</h2>
<p>Mehrere frei konfigurierbare Übersichten pro Benutzer.</p>
</div>
</div>
</article>
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Integrationen</h2>
<p>Zentrale Anbindungen für Fremdsysteme und externe Datenquellen.</p>
</div>
</div>
</article>
<article class="card-box module-admin-card">
<div class="module-admin-card__head">
<div class="module-admin-card__title">
<h2>Seitenmodule</h2>
<p>On-the-fly angelegte Links und eingebettete Weboberflächen.</p>
</div>
</div>
</article>
</div>
<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="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; ?>
</div></div></div>

View File

@@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
require_auth();
require_admin();
$service = dashboards();
$ownerKey = auth_user_key();
$ownerKey = 'system';
$notice = null;
$error = null;
@@ -39,19 +39,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$integrations = $service->listIntegrationsForOwner($ownerKey);
$GLOBALS['layout_header_base_title'] = 'Nexus';
$GLOBALS['layout_header_title'] = 'Nexus';
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
$GLOBALS['layout_header_title'] = 'Nexus Setup';
$GLOBALS['layout_header_context'] = 'Integrationen';
$GLOBALS['layout_header_text'] = 'Zentrale Anbindungen an externe Systeme, getrennt vom klassischen Modulsystem.';
$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="Integration Navigation">
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
<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="/page-modules">Seitenmodule</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>
@@ -93,7 +95,6 @@ $GLOBALS['layout_header_text'] = 'Zentrale Anbindungen an externe Systeme, getre
<label class="setup-field muted">
<span>Sichtbarkeit</span>
<select name="visibility">
<option value="private">Privat</option>
<option value="public">Öffentlich</option>
</select>
</label>

View File

@@ -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;">

View 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>

View 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>

View 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&#10;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>

View File

@@ -831,12 +831,26 @@ body.has-modal-open {
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;

View 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"});
})();

View 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
View 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');
}

8
public/index.php Executable file → Normal file
View File

@@ -24,7 +24,7 @@ $publicPaths = [
'auth/me',
'module/pi_control/terminal_info',
];
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql', 'dashboard', 'dashboards', 'integrations', 'page-modules'], 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/')) {
@@ -275,6 +275,12 @@ 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') {

View File

@@ -230,6 +230,54 @@ final class BaseSchema
)"
);
$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
@@ -440,6 +488,54 @@ final class BaseSchema
)"
);
$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
@@ -650,6 +746,54 @@ final class BaseSchema
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

View File

@@ -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

View File

@@ -108,6 +108,64 @@ final class NexusDashboardService
return $rows;
}
public function resolveHomeDashboard(?string $ownerKey, array $groups, bool $authenticated, int $preferredId = 0): ?array
{
if (!$this->available()) {
return null;
}
if ($authenticated && $ownerKey !== null && $ownerKey !== '') {
if ($preferredId > 0) {
$preferred = $this->getDashboardById($preferredId);
if ($preferred !== null && $this->canAccessDashboard($preferred, $ownerKey, $groups)) {
return $preferred;
}
}
$dashboards = $this->listAccessibleDashboards($ownerKey, $groups);
foreach ($dashboards as $dashboard) {
if (!empty($dashboard['is_default']) && $this->canAccessDashboard($dashboard, $ownerKey, $groups)) {
return $dashboard;
}
}
foreach ($dashboards as $dashboard) {
if ($this->canAccessDashboard($dashboard, $ownerKey, $groups)) {
return $dashboard;
}
}
}
$publicId = (int) (($this->settings()['global_home_dashboard_id'] ?? 0));
if ($publicId > 0) {
$dashboard = $this->getDashboardById($publicId);
if ($dashboard !== null && (string) ($dashboard['visibility'] ?? '') === 'public') {
return $dashboard;
}
}
foreach ($this->listPublicDashboards() as $dashboard) {
return $dashboard;
}
return null;
}
public function listPublicDashboards(): array
{
if (!$this->available()) {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_dashboards
WHERE visibility = 'public'
ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC"
);
$stmt->execute();
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function getDashboardById(int $id): ?array
{
if (!$this->available() || $id <= 0) {
@@ -397,6 +455,63 @@ final class NexusDashboardService
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function listAccessiblePageModules(string $ownerKey, array $groups = []): array
{
if (!$this->available()) {
return [];
}
$rows = [];
$seen = [];
foreach ($this->listPageModulesForOwner($ownerKey) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id > 0) {
$rows[] = $module;
$seen[$id] = true;
}
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_page_modules
WHERE visibility = 'public'
ORDER BY title ASC, id ASC"
);
$stmt->execute();
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $module;
$seen[$id] = true;
}
}
foreach ($groups as $group) {
$group = trim((string) $group);
if ($group === '') {
continue;
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_page_modules
WHERE visibility = 'group'"
);
$stmt->execute();
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id <= 0 || isset($seen[$id])) {
continue;
}
if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) {
$rows[] = $module;
$seen[$id] = true;
}
}
}
return $rows;
}
public function createPageModule(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
@@ -466,6 +581,233 @@ final class NexusDashboardService
return null;
}
public function listWidgetTemplates(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
if ($clauses === []) {
return [];
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_widget_templates WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createWidgetTemplate(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_widget_templates
(owner_key, name, widget_type, description, visibility, config, created_at, updated_at)
VALUES
(:owner_key, :name, :widget_type, :description, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'Widget',
'widget_type' => $this->normalizeItemType((string) ($data['widget_type'] ?? 'link')),
'description' => trim((string) ($data['description'] ?? '')),
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteWidgetTemplate(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_widget_templates WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function listSearchEngines(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_search_engines WHERE ' . implode(' OR ', $clauses) . ' ORDER BY is_default DESC, name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createSearchEngine(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$shortCode = $this->normalizeShortCode((string) ($data['short_code'] ?? ''));
if ($shortCode === '') {
$shortCode = 's' . random_int(100, 999);
}
$shortCode = $this->uniqueSearchShortCode($ownerKey, $shortCode, 0);
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_search_engines
(owner_key, name, short_code, engine_type, template_url, integration_id, visibility, is_default, config, created_at, updated_at)
VALUES
(:owner_key, :name, :short_code, :engine_type, :template_url, :integration_id, :visibility, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'Suche',
'short_code' => $shortCode,
'engine_type' => trim((string) ($data['engine_type'] ?? 'template')) ?: 'template',
'template_url' => trim((string) ($data['template_url'] ?? '')),
'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null,
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'is_default' => !empty($data['is_default']) ? 1 : 0,
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
$id = (int) $this->pdo->lastInsertId();
if (!empty($data['is_default'])) {
$this->setDefaultSearchEngine($ownerKey, $id);
}
return $id;
}
public function setDefaultSearchEngine(string $ownerKey, int $id): void
{
if (!$this->available() || $ownerKey === '' || $id <= 0) {
return;
}
$stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key");
$stmt->execute(['owner_key' => $ownerKey]);
$stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function deleteSearchEngine(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_search_engines WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function listApps(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_apps WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createApp(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_apps
(owner_key, name, description, app_url, icon_url, integration_id, visibility, config, created_at, updated_at)
VALUES
(:owner_key, :name, :description, :app_url, :icon_url, :integration_id, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'App',
'description' => trim((string) ($data['description'] ?? '')),
'app_url' => trim((string) ($data['app_url'] ?? '')),
'icon_url' => trim((string) ($data['icon_url'] ?? '')),
'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null,
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteApp(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_apps WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
private function listSharedDashboardIds(string $ownerKey, array $groups): array
{
$result = [];
@@ -566,7 +908,7 @@ final class NexusDashboardService
private function normalizeItemType(string $value): string
{
$value = trim($value);
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link'], true) ? $value : 'link';
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link', 'app', 'widget_template'], true) ? $value : 'link';
}
private function normalizePageModuleType(string $value): string
@@ -625,4 +967,59 @@ final class NexusDashboardService
}
return (int) $value;
}
private function settings(): array
{
return function_exists('nexus_settings') ? nexus_settings() : [];
}
private function canAccessDashboard(array $dashboard, string $ownerKey, array $groups): bool
{
if ((string) ($dashboard['owner_key'] ?? '') === $ownerKey) {
return true;
}
$visibility = (string) ($dashboard['visibility'] ?? 'private');
if ($visibility === 'public') {
return true;
}
if ($visibility === 'group') {
$sharedIds = $this->listSharedDashboardIds($ownerKey, $groups);
return in_array((int) ($dashboard['id'] ?? 0), $sharedIds, true);
}
return false;
}
private function normalizeShortCode(string $value): string
{
$value = strtolower(trim($value));
$value = preg_replace('/[^a-z0-9_-]+/', '', $value) ?? '';
return substr($value, 0, 24);
}
private function uniqueSearchShortCode(string $ownerKey, string $candidate, int $ignoreId): string
{
$shortCode = $candidate;
$suffix = 1;
while ($this->searchShortCodeExists($ownerKey, $shortCode, $ignoreId)) {
$suffix++;
$shortCode = substr($candidate, 0, 20) . $suffix;
}
return $shortCode;
}
private function searchShortCodeExists(string $ownerKey, string $shortCode, int $ignoreId): bool
{
$sql = "SELECT id FROM nexus_search_engines WHERE owner_key = :owner_key AND short_code = :short_code";
if ($ignoreId > 0) {
$sql .= " AND id <> :id";
}
$sql .= " LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$params = ['owner_key' => $ownerKey, 'short_code' => $shortCode];
if ($ignoreId > 0) {
$params['id'] = $ignoreId;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
}