diff --git a/README.md b/README.md index 97cea89..b877d27 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,77 @@ -# Klangkiste 🎶📦 +# Klangkiste -Klangkiste ist eine selbstgebaute, offlinefähige Kinder-Audiobox -(Phoniebox-Prinzip, aber vollständig eigenständig). +Klangkiste ist eine DIY-Kinder-Audiobox mit serverseitiger Verwaltung und +vollstaendiger Offline-Faehigkeit. Ziel ist eine robuste, kindgerechte Box: +Tags auflegen, Inhalte starten, und Eltern verwalten alles bequem ueber eine +einfache Web-Oberflaeche. -## Ziel -- Mehrere Boxen -- Zentrale Verwaltung über Web-GUI (später) -- Jede Box arbeitet offlinefähig mit lokalem Medien-Cache +## Was die Box aktuell kann +- NFC-Workflow (simuliert): Tags auflegen, Tags erkennen, Tag-IDs in der + Datenbank speichern, Tags Medienordnern zuordnen. +- Playback-Steuerung (simuliert): Play/Pause, Next, Prev, Stop, Volume. +- Medienverwaltung: Ordnerstruktur anzeigen, Uploads, Umbenennen, Verschieben, + Loeschen. +- Box-Verwaltung: Pairing/Unpairing, Box-Status, Commands an einzelne Boxen. +- Tag-Verwaltung: Alias, Zuordnung, Sperren pro Box, Import von lokalen Tags. +- Offline-Logik (simuliert): Box arbeitet mit lokalem Cache, Server kann wieder + synchronisieren. +- Mehrere Boxen gleichzeitig (box + box2) fuer Multi-Box-Tests. + +## Was geplant ist +- Echte NFC-Hardware-Integration (Lesen/Schreiben von Tag-IDs). +- Reale Audio-Ausgabe auf der Box, nicht nur Simulation. +- Robuste Setup-Flows fuer WLAN, Updates und Fehlerdiagnose. +- Feinschliff der GUI (Mobile-Flows, Accessibility, klare Nutzerfuehrung). +- Vereinfachte Installation fuer Familien (Image/Installer). ## Status -Phase 1a – lokale Entwicklung ohne Hardware (Mocks) +Phase 1.x – lokale Entwicklung ohne Hardware (Mocks + API + GUI) -## Tech -- Python 3 -- Raspberry Pi Zero W / Zero 2 W (später) -- NFC, Buttons, Audio abstrahiert über Interfaces +## Repo-Struktur +- `box/` – Box-Software (Python, NFC/Playback/Resume, API) +- `box2/` – zweite Box-Instanz fuer Multi-Box-Tests +- `gui/backend/` – Backend (Node.js + SQLite, Box-Verwaltung, Media-API) +- `gui/frontend/` – Control-Panel (React + Vite) +- `docs/` – Dev-Playbook, API-Vertrag, Tests + +## Quickstart (lokal) +1) Alles starten: +``` +./dev.sh +``` +2) Setup-WebGUI (Box): +``` +http://127.0.0.1:9000/setup +``` +3) Backend pruefen: +``` +curl http://127.0.0.1:5001/api/boxes +``` +4) Frontend oeffnen: +``` +http://127.0.0.1:5174 +``` + +## Ports (Default) +- Backend API: `5001` +- Frontend GUI: `5174` +- Box API: `8000` +- Setup UI: `9000` +- Box2 API (optional): `8001` +- Box2 Setup (optional): `9001` + +## Entwicklungsprinzipien (Kurz) +- Box und GUI/Backend teilen keinen Code. +- Kommunikation nur ueber JSON-APIs. +- Box ist Quelle der Wahrheit fuer Playback/State. +- Server verwaltet Tags, Medien und Boxen. + +## Technisch (kurz) +- Start via `./dev.sh` (startet Frontend, Backend, Box-Simulation). +- Frontend spricht mit Backend (`/api`), Backend spricht mit Boxen (HTTP). +- NFC/Playback sind aktuell simuliert; reale Hardware folgt. + +## Doku +- `docs/README.md` – Startpunkt fuer den Dev-Playbook +- `docs/dev-workflow.md` – Start/Stop, Ports, Logs +- `docs/api.md` – API-Vertrag diff --git a/docs/README.md b/docs/README.md index a650a3c..d6e099a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ Quickstart (3-5 Minuten): ``` ./dev.sh ``` -2) Setup-WebGUI oeffnen: +2) Setup-WebGUI der Box oeffnen: ``` http://127.0.0.1:9000/setup ``` @@ -29,12 +29,18 @@ curl http://127.0.0.1:5001/api/boxes ``` http://127.0.0.1:5174 ``` +6) Tags testen: + - Tag auflegen + - Medien zuordnen + - Playback starten ## Troubleshooting - Box startet nicht: - Pruefe Abhaengigkeiten und ob Ports 8000/9000 frei sind. - GUI erreicht API nicht: - - Pruefe `VITE_BACKEND_URL` und Backend-Logs. + - Vite-Proxy: GUI ruft `/api/...` auf. + - Backend laeuft auf `http://127.0.0.1:5001`. + - Wenn Reverse-Proxy genutzt wird, `/api` auf das Backend routen. ## Was ist Mock vs. real - Mock: WLAN, IP, Spotify, Audio, Hardware. Diese Backends sind logische Simulationen. diff --git a/docs/acceptance-tests.md b/docs/acceptance-tests.md index 19498bc..86d6658 100644 --- a/docs/acceptance-tests.md +++ b/docs/acceptance-tests.md @@ -7,7 +7,11 @@ Konsistenter Testkatalog basierend auf dem aktuellen Code. Alle Tests trennen Si - Repo-Root - Python 3 - Box API fuer Simulation: `python3 box/main.py run` -- GUI optional: `cd gui && npm run dev` +- GUI optional: `cd gui/frontend && npm run dev` +- Backend optional: `cd gui/backend && npm run dev` + +Hinweis: +- Sobald die Box gepairt ist, erfordern `/command` Aufrufe den Header `X-API-Token`. ## Schritt-fuer-Schritt Die Tests koennen einzeln oder der Reihe nach ausgefuehrt werden. diff --git a/docs/api.md b/docs/api.md index 96319ff..3fba503 100644 --- a/docs/api.md +++ b/docs/api.md @@ -32,6 +32,7 @@ Felder: - `last_nfc`: - `uid`: letzte erkannte UID oder `null` - `known`: `true` wenn Tag in der Box zugeordnet ist +- `blocked_tags`: Liste gesperrter Tag-UIDs - `playback_state`: - `state`: `IDLE | PLAYING | PAUSED` - `active_uid` @@ -53,6 +54,7 @@ Beispielantwort: "connected_ssid": "HomeWiFi", "wifi_profiles_count": 1, "spotify_status": "READY", + "blocked_tags": [], "playback_state": { "state": "PLAYING", "active_uid": "UID_1", @@ -67,6 +69,14 @@ Beispielantwort: } ``` +## GET /local-tags +Nur fuer gepairte Boxen (Header: `X-API-Token`). +Zeigt lokale Tags, inkl. Dateiliste und Groesse. +``` +curl http://127.0.0.1:8000/local-tags \ + -H "X-API-Token: " +``` + ## POST /command Request Body: ``` @@ -95,10 +105,17 @@ Unterstuetzte Commands: - `unpair` - `tag_assign` `{ uid, media_path }` - `tag_remove` `{ uid }` +- `tag_block` `{ uid }` +- `tag_unblock` `{ uid }` +- `export_tag` `{ uid, target_folder }` +- `factory_reset` ## Tag-Commands - `tag_assign` `{ uid, media_path }` weist ein Tag einem Medienordner zu. - `tag_remove` `{ uid }` entfernt die Zuordnung fuer ein Tag. +- `tag_block` sperrt den Tag fuer diese Box. +- `tag_unblock` hebt die Sperre auf. +- `export_tag` uebertraegt die lokalen Medien dieses Tags an den Server. ## Auth fuer Commands - Vor Pairing sind nur diese Commands erlaubt: diff --git a/docs/debugging.md b/docs/debugging.md index 52d3936..db7b005 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -17,7 +17,9 @@ Nutze die Sektionen nach Symptomen. curl http://127.0.0.1:5001/api/boxes ``` - GUI API-Base pruefen: - - `VITE_BACKEND_URL` oder Default `http://127.0.0.1:5001` + - GUI nutzt `/api/...` und Vite-Proxy. + - `VITE_BACKEND_URL` zeigt auf das Backend (ohne `/api`). + - Reverse-Proxy: `/api` muss auf Port 5001 zeigen (ohne doppeltes `/api`). ### Playback startet nicht - Tags pruefen: @@ -32,6 +34,9 @@ ls -la gui/backend/media ``` curl http://127.0.0.1:8000/status ``` +- Tag lokal vorhanden, aber nicht in DB: + - GUI: "Tags nur auf dieser Box" pruefen. + - Import ueber "Auf Server uebertragen". ### Resume bleibt leer - State pruefen: diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index 953f98a..7d07317 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -24,7 +24,7 @@ python3 box/main.py run Frontend only: ``` cd gui/frontend -npm run dev -- --host 0.0.0.0 --port 5174 --strictPort +VITE_BACKEND_URL=http://127.0.0.1:5001 npm run dev -- --host 0.0.0.0 --port 5174 --strictPort ``` Backend only: ``` @@ -37,10 +37,12 @@ npm run dev - Frontend GUI: `http://127.0.0.1:5174` - Box API: `http://127.0.0.1:8000` - Setup UI: `http://127.0.0.1:9000/setup` -- GUI: `http://127.0.0.1:5174` +- Box2 API (optional): `http://127.0.0.1:8001` +- Box2 Setup (optional): `http://127.0.0.1:9001/setup` ## API-URL fuer die GUI -Default ist `http://:5001` (aus `hostname -I`). Override: +Die GUI nutzt relative Pfade (`/api/...`) und einen Vite-Proxy. +Der Proxy-Target wird so gesetzt: ``` KLANGKISTE_BACKEND_URL=http://:5001 ./dev.sh VITE_BACKEND_URL=http://:5001 npm run dev @@ -67,4 +69,5 @@ VITE_BACKEND_URL=http://:5001 npm run dev - CORS-Fehler: - API muss laufen und erreichbar sein. - GUI zeigt "Failed to fetch": - - `VITE_BACKEND_URL` pruefen. + - Vite-Proxy pruefen (`VITE_BACKEND_URL`). + - Reverse-Proxy: `/api` muss auf das Backend zeigen (ohne doppeltes `/api`). diff --git a/docs/gui-control-panel.md b/docs/gui-control-panel.md index b912c52..ffabd6c 100644 --- a/docs/gui-control-panel.md +++ b/docs/gui-control-panel.md @@ -14,18 +14,31 @@ Nutzung des Frontend Control Panels zur Steuerung und Beobachtung der Boxen uebe http://127.0.0.1:5174 ``` 2) In "Neue Boxen" die Box auswaehlen und "Pairen" klicken. -3) In "Gepairte Boxen" eine Box anklicken. +3) In der Sidebar unter "Aktive Sessions" eine Box aktiv setzen (gilt fuer alle Seiten). 4) Status und Commands nutzen. ## Controls (im GUI vorhanden) - Pairen (Button in "Neue Boxen") -- Statusanzeige (automatisch) +- Statusanzeige (automatisch, Polling) - Play/Pause, Next, Prev, Vol +/-, Stop -- NFC on/off (UID eingeben) +- NFC on/off (UID manuell eingeben; kein Auto-Fill) - Unpair (Button in "Gepairte Boxen") -- Medienkatalog (Explorer-Ansicht, nur Lesen) -- Neuer Tag erkannt (Panel) mit ID-Zuweisung und Zuordnung -- Tags werden nur nach NFC-Erkennung erstellt und zugeordnet. +- Medien-Explorer (Ordner/Dateien, Upload, Rename, Move, Delete) +- Neuer/Leerer Tag erkannt (Panel) + - Wenn keine UID erkannt: ID zuweisen & schreiben + - Wenn UID erkannt, `known=false`: nur Medienordnerwahl + "Nur in die DB uebernehmen" + - Medienordner zuordnen + - Importierte Tag-IDs wiederverwenden +- Tags (Datenbank) + - Alias setzen, Medium zuweisen, Tag loeschen +- Tag-Matrix (Sperren) + - Tags pro Box sperren/entsperren +- Tags nur auf dieser Box + - Lokale Tags ohne DB-Eintrag + - Dateien anzeigen + - Auf Server uebertragen (mit neuem Ordnernamen) +- Mobile: Aktive Sessions als Bottom-Sheet (Topbar-Icon) +- Hardware-UID (simuliert) wird im Tag-Panel und Live-Status angezeigt, wenn NFC on ohne UID genutzt wird ## Controls (nur Backend/API) - WiFi add/reset @@ -46,6 +59,7 @@ curl -X POST http://127.0.0.1:5001/api/boxes//command \ - ✅ GUI laedt und zeigt Backend-URL - ✅ Neue Boxen erscheinen unter "Neue Boxen" - ✅ Pairing verschiebt die Box in die gepaarte Liste +- ✅ Aktive Box ist global gesetzt (Sidebar "Aktive Sessions") - ✅ Buttons aendern den `/status` ## Troubleshooting diff --git a/docs/virtual-box.md b/docs/virtual-box.md index 6b358fe..e9f37b7 100644 --- a/docs/virtual-box.md +++ b/docs/virtual-box.md @@ -14,6 +14,8 @@ Beschreibt den vollstaendigen, hardwarefreien Box-Flow fuer die IDE. 3) Setup UI unter `http://127.0.0.1:9000/setup`. 4) Nach Submit wird `WIFI_ONLINE`. 5) API unter `http://127.0.0.1:8000`. +6) Box meldet sich beim Backend (announce). +7) Pairing ueber GUI oder API. ## Troubleshooting - Setup UI nicht erreichbar: diff --git a/gui/backend/db/klangkiste.sqlite-shm b/gui/backend/db/klangkiste.sqlite-shm index 35ff25b..8af3e2b 100644 Binary files a/gui/backend/db/klangkiste.sqlite-shm and b/gui/backend/db/klangkiste.sqlite-shm differ diff --git a/gui/backend/db/klangkiste.sqlite-wal b/gui/backend/db/klangkiste.sqlite-wal index 54ed636..f545441 100644 Binary files a/gui/backend/db/klangkiste.sqlite-wal and b/gui/backend/db/klangkiste.sqlite-wal differ diff --git a/gui/backend/db/schema.sql b/gui/backend/db/schema.sql index b57f2c1..d44cbd9 100644 --- a/gui/backend/db/schema.sql +++ b/gui/backend/db/schema.sql @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS boxes ( CREATE TABLE IF NOT EXISTS tags ( uid TEXT PRIMARY KEY, + hardware_uid TEXT, label TEXT, alias TEXT, status TEXT NOT NULL, diff --git a/gui/backend/server.js b/gui/backend/server.js index d1eab8e..dceea87 100644 --- a/gui/backend/server.js +++ b/gui/backend/server.js @@ -479,6 +479,10 @@ async function ensureTagMediaColumn() { if (!hasAlias) { await run('ALTER TABLE tags ADD COLUMN alias TEXT'); } + const hasHardwareUid = columns.some((column) => column.name === 'hardware_uid'); + if (!hasHardwareUid) { + await run('ALTER TABLE tags ADD COLUMN hardware_uid TEXT'); + } await run( `UPDATE tags SET media_path = ( @@ -1374,7 +1378,9 @@ app.post('/api/boxes/:box_id/tag-blocks', async (req, res) => { app.get('/api/tags', async (req, res) => { try { const rows = await all( - 'SELECT uid, label, alias, status, created_at, written_at, media_path FROM tags ORDER BY created_at DESC' + `SELECT uid, hardware_uid, label, alias, status, created_at, written_at, media_path + FROM tags + ORDER BY created_at DESC` ); res.json({ tags: rows }); } catch (error) { @@ -1446,7 +1452,7 @@ app.post('/api/tags/generate', async (req, res) => { }); app.post('/api/tags/claim', async (req, res) => { - const { uid: requestedUid, label } = req.body || {}; + const { uid: requestedUid, label, hardware_uid: hardwareUid } = req.body || {}; const now = nowSeconds(); let uid = ''; if (typeof requestedUid === 'string' && requestedUid) { @@ -1470,8 +1476,15 @@ app.post('/api/tags/claim', async (req, res) => { } try { await run( - 'INSERT INTO tags (uid, label, status, created_at, written_at) VALUES (?, ?, ?, ?, ?)', - [uid, typeof label === 'string' ? label : null, 'WRITTEN', now, now] + 'INSERT INTO tags (uid, hardware_uid, label, status, created_at, written_at) VALUES (?, ?, ?, ?, ?, ?)', + [ + uid, + typeof hardwareUid === 'string' && hardwareUid ? hardwareUid : null, + typeof label === 'string' ? label : null, + 'WRITTEN', + now, + now, + ] ); res.json({ uid }); } catch (error) { diff --git a/gui/frontend/package-lock.json b/gui/frontend/package-lock.json index bd3a2f0..7795a15 100644 --- a/gui/frontend/package-lock.json +++ b/gui/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "dependencies": { "@phosphor-icons/react": "2.1.7", + "primeicons": "^7.0.0", + "primereact": "^10.7.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -252,6 +254,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1156,6 +1167,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", @@ -1423,6 +1453,12 @@ "node": ">=0.10.40" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1485,6 +1521,16 @@ "node": ">=0.4.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2321,6 +2367,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -2368,6 +2423,46 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primereact": { + "version": "10.9.7", + "resolved": "https://registry.npmjs.org/primereact/-/primereact-10.9.7.tgz", + "integrity": "sha512-Ap/lg9GGaS8Pq7IIlzguuG3qlaU6PYF6E0cCRo0rnWauRw/SQGvfreSVIIxqEhtR6xqlf7OV759lyvVOvBzmsQ==", + "license": "MIT", + "dependencies": { + "@types/react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -2403,6 +2498,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -2413,6 +2514,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", diff --git a/gui/frontend/package.json b/gui/frontend/package.json index 874da08..41f66e5 100644 --- a/gui/frontend/package.json +++ b/gui/frontend/package.json @@ -10,6 +10,8 @@ }, "dependencies": { "@phosphor-icons/react": "2.1.7", + "primeicons": "^7.0.0", + "primereact": "^10.7.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/gui/frontend/src/App.jsx b/gui/frontend/src/App.jsx index b89b8c8..0521ea3 100644 --- a/gui/frontend/src/App.jsx +++ b/gui/frontend/src/App.jsx @@ -1,49 +1,63 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { - ArrowUp, ArrowRight, + ArrowUp, CaretDown, CaretRight, - FloppyDisk, FileAudio, + FloppyDisk, Folder, - List, PencilSimple, Plus, Trash, UploadSimple, } from '@phosphor-icons/react'; +import { Avatar } from 'primereact/avatar'; +import { Badge } from 'primereact/badge'; +import { Button } from 'primereact/button'; +import { Card } from 'primereact/card'; +import { Divider } from 'primereact/divider'; +import { InputText } from 'primereact/inputtext'; +import { Tag } from 'primereact/tag'; import { - getBoxes, - getStatus, - getMediaTree, - createMediaFolder, - renameMedia, - moveMedia, - deleteMedia, - uploadMedia, - getTags, - getBoxTags, - getBoxLocalTags, - getTagBlocks, - claimTag, - markTagWritten, assignTag, - unassignTag, + claimTag, + createMediaFolder, + deleteMedia, deleteTag, - setTagMedia, + getBoxes, + getBoxLocalTags, + getBoxTags, + getMediaTree, + getStatus, + getTagBlocks, + getTags, + markTagWritten, + moveMedia, + pairBox, pullTagFromBox, - setTagBlock, + renameMedia, + sendCommand, setBoxAlias, setTagAlias, - pairBox, - sendCommand, + setTagBlock, + setTagMedia, + unassignTag, unpairBox, + uploadMedia, } from './api.js'; const BOX_POLL_MS = 1500; const STATUS_POLL_MS = 1000; +const NAV_ITEMS = [ + { id: 'dashboard', label: 'Dashboard', icon: 'pi pi-home' }, + { id: 'boxes', label: 'Boxen', icon: 'pi pi-box' }, + { id: 'media', label: 'Medien', icon: 'pi pi-folder-open' }, + { id: 'tags', label: 'Tags', icon: 'pi pi-tags' }, + { id: 'settings', label: 'Einstellungen', icon: 'pi pi-cog' }, +]; + function formatTime(ts) { if (!ts) return '-'; return new Date(ts * 1000).toLocaleString(); @@ -91,7 +105,45 @@ function generateTagId() { return generated; } +function generateHardwareUid() { + const bytes = []; + for (let i = 0; i < 7; i += 1) { + bytes.push(Math.floor(Math.random() * 256)); + } + return bytes + .map((value) => value.toString(16).padStart(2, '0').toUpperCase()) + .join(':'); +} + +function SectionHeader({ title, subtitle, actions }) { + return ( +
+
+

{title}

+

{subtitle}

+
+
{actions}
+
+ ); +} + +function StatGrid({ items }) { + return ( +
+ {items.map((card) => ( + +

{card.label}

+
{card.value}
+ {card.helper} +
+ ))} +
+ ); +} + export default function App() { + const [activeSection, setActiveSection] = useState('dashboard'); + const [searchValue, setSearchValue] = useState(''); const [boxes, setBoxes] = useState([]); const [selectedId, setSelectedId] = useState(''); const [status, setStatus] = useState(null); @@ -139,6 +191,9 @@ export default function App() { const [toasts, setToasts] = useState([]); const [tagAliasDrafts, setTagAliasDrafts] = useState({}); const [boxAliasDrafts, setBoxAliasDrafts] = useState({}); + const [showSessionSheet, setShowSessionSheet] = useState(false); + const [lastHardwareUid, setLastHardwareUid] = useState({ uid: '', hardwareUid: '' }); + const [simulatedNfc, setSimulatedNfc] = useState(null); function addToast(type, message) { const id = `${Date.now()}-${toastCounter.current}`; @@ -186,7 +241,10 @@ export default function App() { } window.addEventListener('mousedown', handleOutside); window.addEventListener('mouseup', handleMouseUp); - return () => window.removeEventListener('mousedown', handleOutside); + return () => { + window.removeEventListener('mousedown', handleOutside); + window.removeEventListener('mouseup', handleMouseUp); + }; }, [activeModal]); useEffect(() => { @@ -311,22 +369,18 @@ export default function App() { useEffect(() => { const lastNfc = status?.last_nfc; - if (!lastNfc || lastNfc.known || !lastNfc.uid) { + if (!lastNfc || !lastNfc.uid) { return; } - const key = `${status?.last_nfc_at ?? ''}:${lastNfc.uid ?? ''}`; - if (lastNfcKeyRef.current === key) { - return; - } - lastNfcKeyRef.current = key; - setScanTagLabel(''); - setScanMediaPath(''); - const hasUid = Boolean(lastNfc.uid); - const existsInDb = hasUid && tags.some((tag) => tag.uid === lastNfc.uid); - if (existsInDb) { + if (!lastNfc.known) { + const key = `${status?.last_nfc_at ?? ''}:${lastNfc.uid ?? ''}`; + if (lastNfcKeyRef.current === key) { + return; + } + lastNfcKeyRef.current = key; + setScanTagLabel(''); + setScanMediaPath(''); setScanTagUid(lastNfc.uid); - } else { - setScanTagUid(generateTagId()); } }, [status, tags]); @@ -455,31 +509,90 @@ export default function App() { }; }, [mediaTree, topLevelFolders]); + const dashboardStats = useMemo(() => { + const sizeLabel = mediaTree ? formatSize(mediaTree.size) : '-'; + const freeLabel = mediaTree ? formatSize(mediaTree.free_bytes) : '-'; + return [ + { + label: 'Gepairte Boxen', + value: paired.length, + helper: `${unpaired.length} neu`, + }, + { + label: 'Tags gesamt', + value: tags.length, + helper: `${boxTags.length} auf Box`, + }, + { + label: 'Medien gesamt', + value: sizeLabel, + helper: `frei ${freeLabel}`, + }, + { + label: 'Letzter NFC', + value: status?.last_nfc?.uid || '-', + helper: status?.last_nfc_at ? formatTime(status.last_nfc_at) : '-', + }, + ]; + }, [paired.length, unpaired.length, tags.length, boxTags.length, mediaTree, status]); + async function handlePair(boxId) { const response = await pairBox(boxId); if (!response.ok) { - setError(response.data.detail || 'Pairing fehlgeschlagen.'); - addToast('error', response.data.detail || 'Pairing fehlgeschlagen.'); + setError(response.data.detail || 'Box pairing fehlgeschlagen.'); + addToast('error', response.data.detail || 'Box pairing fehlgeschlagen.'); return; } - setError(''); - addToast('success', `Box ${boxId} gepairt.`); + addToast('success', 'Box gepairt.'); + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); + } } async function handleCommand(command, payload = {}) { if (!selectedId) { - setError('Bitte zuerst eine gepairte Box auswaehlen.'); - addToast('error', 'Bitte zuerst eine gepairte Box auswaehlen.'); + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); return; } - const response = await sendCommand(selectedId, command, payload); + let nextPayload = payload; + if (command === 'nfc_on' || command === 'nfc_off') { + const rawUid = typeof payload?.uid === 'string' ? payload.uid.trim() : ''; + let uid = rawUid; + if (!uid) { + if (command === 'nfc_on') { + setSimulatedNfc({ + uid: '', + known: false, + hardwareUid: generateHardwareUid(), + at: Date.now(), + }); + setScanTagLabel(''); + setScanMediaPath(''); + if (!scanTagUid) { + setScanTagUid(generateTagId()); + } + return; + } + setError('Bitte eine UID angeben.'); + addToast('error', 'Bitte eine UID angeben.'); + return; + } + nextPayload = { ...payload, uid }; + if (command === 'nfc_on') { + setLastHardwareUid({ uid, hardwareUid: generateHardwareUid() }); + setSimulatedNfc(null); + } + } + const response = await sendCommand(selectedId, command, nextPayload); if (!response.ok) { setError(response.data.detail || 'Command fehlgeschlagen.'); addToast('error', response.data.detail || 'Command fehlgeschlagen.'); return; } setError(''); - addToast('success', `Command ausgefuehrt: ${command}`); + addToast('success', 'Command gesendet.'); } async function handleUnpair(boxId) { @@ -489,25 +602,22 @@ export default function App() { addToast('error', response.data.detail || 'Unpair fehlgeschlagen.'); return; } - if (selectedId === boxId) { - setSelectedId(''); - setStatus(null); + addToast('success', 'Box unpaired.'); + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); } - setError(''); - addToast('success', `Box ${boxId} entkoppelt.`); } async function handleMediaRefresh() { const response = await getMediaTree(); if (!response.ok) { setMediaError(response.data.detail || 'Medien nicht verfuegbar.'); - addToast('error', response.data.detail || 'Medien nicht verfuegbar.'); setMediaTree(null); return; } setMediaTree(response.data); setMediaError(''); - addToast('success', 'Medienliste aktualisiert.'); } function getNodeByPath(node, targetPath) { @@ -522,240 +632,187 @@ export default function App() { } function listChildren(node) { - if (!node || !Array.isArray(node.children)) return []; - const folders = node.children.filter((child) => child.type === 'folder'); - const files = node.children.filter((child) => child.type === 'file'); - folders.sort((a, b) => a.name.localeCompare(b.name)); - files.sort((a, b) => a.name.localeCompare(b.name)); - return [...folders, ...files]; + if (!node) return []; + if (!Array.isArray(node.children)) return []; + return node.children; } function buildBreadcrumb(pathValue) { if (!pathValue) return []; const parts = pathValue.split('/').filter(Boolean); - const crumbs = []; - let acc = ''; - parts.forEach((part) => { - acc = acc ? `${acc}/${part}` : part; - crumbs.push({ name: part, path: acc }); - }); - return crumbs; + return parts.map((part, index) => ({ + name: part, + path: parts.slice(0, index + 1).join('/'), + })); } function collectTopLevelFolders(node) { if (!node || !Array.isArray(node.children)) return []; return node.children - .filter((child) => child.type === 'folder' && child.path) + .filter((child) => child.type === 'folder') .map((child) => child.path); } function collectFolderPaths(node) { if (!node || node.type !== 'folder') return []; - const paths = []; - if (node.path) paths.push(node.path); - if (!Array.isArray(node.children)) return paths; + const entries = [node.path || '']; + if (!Array.isArray(node.children)) return entries; node.children.forEach((child) => { - if (child.type === 'folder') { - paths.push(...collectFolderPaths(child)); - } + if (child.type !== 'folder') return; + entries.push(...collectFolderPaths(child)); }); - return paths; + return entries; } function isSelected(pathValue) { - return selectedPaths.includes(pathValue); + return selectedPaths.includes(pathValue || ''); } async function handleCreateFolder() { - if (!uploadAfterCreate && !newFolderName.trim()) { - addToast('error', 'Ordnername fehlt.'); - return; - } - if (uploadAfterCreate && pendingUploadFiles.length === 0) { - addToast('error', 'Bitte Audiodateien auswaehlen.'); - return; - } - const trimmedName = newFolderName.trim(); - const needsFolder = uploadAfterCreate && !trimmedName && !currentPath; - if (needsFolder) { - addToast('error', 'Bitte zuerst einen Ordner anlegen.'); - return; - } - - let targetPath = currentPath; - if (trimmedName) { - const response = await createMediaFolder(currentPath, trimmedName); - if (!response.ok) { - addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.'); - return; - } - targetPath = currentPath ? `${currentPath}/${trimmedName}` : trimmedName; - addToast('success', 'Ordner angelegt.'); - await handleMediaRefresh(); - } - - setNewFolderName(''); - if (uploadAfterCreate) { - setActiveUploadLabel(`Upload: ${pendingUploadFiles.length} Datei(en)`); + if (uploadAfterCreate && pendingUploadFiles.length > 0) { + const targetPath = trimmedName ? `${currentPath}/${trimmedName}` : currentPath; setUploadInProgress(true); - setUploadProgress(0); + setActiveUploadLabel( + pendingUploadFiles.length ? `Upload: ${pendingUploadFiles.length} Datei(en)` : '' + ); const uploadResponse = await uploadMedia( targetPath, pendingUploadFiles, (percent) => setUploadProgress(percent) ); - if (!uploadResponse.ok) { - addToast('error', uploadResponse.data.detail || 'Upload fehlgeschlagen.'); - setUploadInProgress(false); - return; - } setUploadInProgress(false); - setActiveUploadLabel(''); - setUploadAfterCreate(false); - setPendingUploadFiles([]); - if (targetPath) { - setCurrentPath(targetPath); + setUploadProgress(0); + if (!uploadResponse.ok) { + setMediaError(uploadResponse.data.detail || 'Upload fehlgeschlagen.'); + addToast('error', uploadResponse.data.detail || 'Upload fehlgeschlagen.'); + return; } addToast('success', 'Upload abgeschlossen.'); await handleMediaRefresh(); setActiveModal(''); + setUploadAfterCreate(false); + setPendingUploadFiles([]); + setNewFolderName(''); return; } + if (!trimmedName && !uploadAfterCreate) { + setMediaError('Bitte einen Ordnernamen angeben.'); + return; + } + + const response = await createMediaFolder(currentPath, trimmedName); + if (!response.ok) { + setMediaError(response.data.detail || 'Ordner anlegen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.'); + return; + } + addToast('success', 'Ordner angelegt.'); + setNewFolderName(''); setActiveModal(''); + await handleMediaRefresh(); } async function handleRename() { - if (selectedPaths.length !== 1) { - addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); - return; - } - if (!renameName.trim()) { - addToast('error', 'Neuer Name fehlt.'); - return; - } + if (!selectedPaths.length) return; const response = await renameMedia(selectedPaths[0], renameName.trim()); if (!response.ok) { + setMediaError(response.data.detail || 'Umbenennen fehlgeschlagen.'); addToast('error', response.data.detail || 'Umbenennen fehlgeschlagen.'); return; } + addToast('success', 'Eintrag umbenannt.'); + setRenameName(''); setActiveModal(''); - addToast('success', 'Umbenannt.'); await handleMediaRefresh(); } async function handleDeleteSelected() { - if (selectedPaths.length === 0) { - addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); - return; - } + if (!selectedPaths.length) return; for (const pathValue of selectedPaths) { const response = await deleteMedia(pathValue); if (!response.ok) { + setMediaError(response.data.detail || 'Loeschen fehlgeschlagen.'); addToast('error', response.data.detail || 'Loeschen fehlgeschlagen.'); return; } } + addToast('success', 'Eintraege geloescht.'); setSelectedPaths([]); - setRenameName(''); setActiveModal(''); - addToast('success', 'Geloescht.'); await handleMediaRefresh(); } async function handleMoveSelected() { - if (selectedPaths.length === 0) { - addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); + if (!selectedPaths.length) return; + const pathValue = selectedPaths[0]; + const isRootTarget = moveTarget === '__root__' || moveTarget === ''; + const response = await moveMedia(pathValue, isRootTarget ? '' : moveTarget); + if (!response.ok) { + setMediaError(response.data.detail || 'Verschieben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Verschieben fehlgeschlagen.'); return; } - if (!moveTarget) { - addToast('error', 'Bitte Zielordner waehlen.'); - return; - } - const isRootTarget = moveTarget === '__root__'; - if (isRootTarget) { - const hasFile = selectedPaths.some((pathValue) => { - const node = getNodeByPath(mediaTree, pathValue); - return node && node.type === 'file'; - }); - if (hasFile) { - addToast('error', 'Dateien duerfen nicht in den Root-Ordner.'); - return; - } - } - for (const pathValue of selectedPaths) { - const response = await moveMedia(pathValue, isRootTarget ? '' : moveTarget); - if (!response.ok) { - addToast('error', response.data.detail || 'Verschieben fehlgeschlagen.'); - return; - } - } + addToast('success', 'Eintrag verschoben.'); + setMoveTarget(''); + setSelectedPaths([]); setActiveModal(''); - addToast('success', 'Verschoben.'); await handleMediaRefresh(); } async function handleUpload(event) { const files = Array.from(event.target.files || []); - if (!currentPath) { - addToast('error', 'Bitte zuerst einen Ordner anlegen.'); - event.target.value = ''; - return; - } - if (!files.length) return; const audioFiles = files.filter((file) => file.type.startsWith('audio/')); - if (!audioFiles.length) { - addToast('error', 'Nur Audiodateien erlaubt.'); - return; - } - setActiveUploadLabel(`Upload: ${audioFiles.length} Datei(en)`); + if (!audioFiles.length) return; setUploadInProgress(true); - setUploadProgress(0); + setActiveUploadLabel( + audioFiles.length ? `Upload: ${audioFiles.length} Datei(en)` : '' + ); const response = await uploadMedia(currentPath, audioFiles, (percent) => setUploadProgress(percent) ); + setUploadInProgress(false); + setUploadProgress(0); if (!response.ok) { + setMediaError(response.data.detail || 'Upload fehlgeschlagen.'); addToast('error', response.data.detail || 'Upload fehlgeschlagen.'); - setUploadInProgress(false); - setActiveUploadLabel(''); return; } - setUploadInProgress(false); - setActiveUploadLabel(''); addToast('success', 'Upload abgeschlossen.'); await handleMediaRefresh(); } function handleSelect(item, event) { const itemPath = item.path || ''; - if (Date.now() - lastDblClickRef.current < 250) { - return; - } - if (event && event.shiftKey && lastAnchorRef.current) { - const node = getNodeByPath(mediaTree, currentPath); - const visible = listChildren(node).map((child) => child.path || ''); - const start = visible.indexOf(lastAnchorRef.current); - const end = visible.indexOf(itemPath); - if (start !== -1 && end !== -1) { - const [from, to] = start < end ? [start, end] : [end, start]; - setSelectedPaths(visible.slice(from, to + 1)); - lastAnchorRef.current = itemPath; + if (event.shiftKey && lastAnchorRef.current) { + const items = listChildren(getNodeByPath(mediaTree, currentPath)); + const anchorIndex = items.findIndex((entry) => entry.path === lastAnchorRef.current); + const currentIndex = items.findIndex((entry) => entry.path === itemPath); + if (anchorIndex !== -1 && currentIndex !== -1) { + const [start, end] = anchorIndex < currentIndex + ? [anchorIndex, currentIndex] + : [currentIndex, anchorIndex]; + const range = items.slice(start, end + 1).map((entry) => entry.path || ''); + setSelectedPaths(Array.from(new Set([...selectedPaths, ...range]))); return; } } - if (event && (event.metaKey || event.ctrlKey)) { + if (event.metaKey || event.ctrlKey) { setSelectedPaths((prev) => prev.includes(itemPath) - ? prev.filter((p) => p !== itemPath) + ? prev.filter((pathValue) => pathValue !== itemPath) : [...prev, itemPath] ); lastAnchorRef.current = itemPath; return; } - setSelectedPaths([itemPath]); - setRenameName(item.name || ''); - lastAnchorRef.current = itemPath; + const now = Date.now(); + const isDoubleClick = now - lastDblClickRef.current < 250; + if (!isDoubleClick) { + setSelectedPaths([itemPath]); + lastAnchorRef.current = itemPath; + } } function handleDragSelect(item) { @@ -936,7 +993,6 @@ export default function App() { addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); return; } - setError(''); addToast('success', 'Tag geschrieben und zugeordnet.'); const updated = await getTags(); if (updated.ok) { @@ -957,7 +1013,6 @@ export default function App() { addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); return; } - setError(''); addToast('success', 'Tag geschrieben.'); const updated = await getTags(); if (updated.ok) { @@ -965,15 +1020,34 @@ export default function App() { } } - async function handleAssignFromScan() { - if (!selectedId) { - setError('Bitte zuerst eine Box auswaehlen.'); - addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + async function handleStoreTagOnly() { + if (!activeNfc?.uid) { + setError('Keine UID erkannt.'); + addToast('error', 'Keine UID erkannt.'); return; } + const existing = tags.find((tag) => tag.uid === activeNfc.uid); + if (existing) { + addToast('success', 'Tag ist bereits in der Datenbank.'); + return; + } + const response = await claimTag(activeNfc.uid, ''); + if (!response.ok) { + setError(response.data.detail || 'Tag speichern fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag speichern fehlgeschlagen.'); + return; + } + addToast('success', 'Tag in der Datenbank gespeichert.'); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + } + + async function handleAssignFromScan() { if (!scanMediaPath) { - setError('Bitte Medienordner waehlen.'); - addToast('error', 'Bitte Medienordner waehlen.'); + setError('Bitte zuerst einen Medienordner waehlen.'); + addToast('error', 'Bitte zuerst einen Medienordner waehlen.'); return; } const uid = scanTagUid.trim(); @@ -982,13 +1056,12 @@ export default function App() { addToast('error', 'Bitte zuerst eine Tag-ID schreiben.'); return; } - const existing = tags.find((tag) => tag.uid === uid); - if (!existing) { + const matching = tags.find((tag) => tag.uid === uid); + if (!matching) { setError('Tag-ID existiert nicht. Bitte zuerst schreiben.'); addToast('error', 'Tag-ID existiert nicht. Bitte zuerst schreiben.'); return; } - const mediaSet = await setTagMedia(uid, scanMediaPath); if (!mediaSet.ok) { setError(mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); @@ -1001,8 +1074,6 @@ export default function App() { addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); return; } - - setError(''); addToast('success', 'Tag zugeordnet.'); setScanTagUid(''); setScanTagLabel(''); @@ -1028,45 +1099,6 @@ export default function App() { addToast('error', 'Kein Tag ausgewaehlt.'); return; } - if (!importTargetFolder.trim()) { - setError('Bitte Ordnernamen eingeben.'); - addToast('error', 'Bitte Ordnernamen eingeben.'); - return; - } - if (uploadInProgress) { - addToast('error', 'Es laeuft bereits ein Upload/Transfer.'); - return; - } - const startTransferProgress = () => { - setActiveUploadLabel(`Uebertragung: ${importTargetFolder.trim()}`); - setUploadInProgress(true); - setUploadProgress(5); - if (transferTimerRef.current) { - clearInterval(transferTimerRef.current); - } - transferTimerRef.current = setInterval(() => { - setUploadProgress((prev) => (prev < 90 ? prev + 5 : prev)); - }, 400); - }; - const stopTransferProgress = (success) => { - if (transferTimerRef.current) { - clearInterval(transferTimerRef.current); - transferTimerRef.current = null; - } - if (success) { - setUploadProgress(100); - setTimeout(() => { - setUploadInProgress(false); - setUploadProgress(0); - setActiveUploadLabel(''); - }, 600); - return; - } - setUploadInProgress(false); - setUploadProgress(0); - setActiveUploadLabel(''); - }; - startTransferProgress(); const response = await pullTagFromBox( selectedId, importTargetUid, @@ -1075,17 +1107,15 @@ export default function App() { if (!response.ok) { setError(response.data.detail || 'Import fehlgeschlagen.'); addToast('error', response.data.detail || 'Import fehlgeschlagen.'); - stopTransferProgress(false); return; } addToast('success', 'Medien vom Box-Tag uebertragen.'); - setImportTargetFolder(''); setImportTargetUid(''); + setImportTargetFolder(''); setActiveModal(''); - stopTransferProgress(true); - const updatedTags = await getTags(); - if (updatedTags.ok) { - setTags(updatedTags.data.tags || []); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); } const updatedLocal = await getBoxLocalTags(selectedId); if (updatedLocal.ok) { @@ -1095,15 +1125,13 @@ export default function App() { } async function handleUnassignTag(uid) { - if (!selectedId) return; const response = await unassignTag(uid, selectedId); if (!response.ok) { - setError(response.data.detail || 'Zuordnung loeschen fehlgeschlagen.'); - addToast('error', response.data.detail || 'Zuordnung loeschen fehlgeschlagen.'); + setError(response.data.detail || 'Tag loesen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag loesen fehlgeschlagen.'); return; } - setError(''); - addToast('success', 'Zuordnung entfernt.'); + addToast('success', 'Tag getrennt.'); const updated = await getBoxTags(selectedId); if (updated.ok) { setBoxTags(updated.data.tags || []); @@ -1117,7 +1145,6 @@ export default function App() { addToast('error', response.data.detail || 'Tag loeschen fehlgeschlagen.'); return; } - setError(''); addToast('success', 'Tag geloescht.'); const updatedTags = await getTags(); if (updatedTags.ok) { @@ -1154,25 +1181,18 @@ export default function App() { addToast('error', response.data.detail || 'Medium setzen fehlgeschlagen.'); return; } - let assigned = null; const lastNfcUid = status?.last_nfc?.uid || ''; const shouldAssign = selectedId && status?.last_nfc?.known === false && - ((lastNfcUid && lastNfcUid === uid) || - (!lastNfcUid && scanTagUid && scanTagUid === uid)); + (lastNfcUid === uid || (!lastNfcUid && scanTagUid && scanTagUid === uid)); if (shouldAssign) { - assigned = await assignTag(uid, selectedId); + const assigned = await assignTag(uid, selectedId); if (!assigned.ok) { setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); return; } - } - setError(''); - if (!mediaPath) { - addToast('success', 'Medium entfernt.'); - } else if (assigned) { addToast('success', 'Medium gespeichert und Tag zugeordnet.'); } else { addToast('success', 'Medium gespeichert.'); @@ -1297,11 +1317,633 @@ export default function App() { const mediaBytes = mediaTree?.size ?? null; const freeBytes = mediaTree?.free_bytes ?? null; + const activeMeta = useMemo( + () => NAV_ITEMS.find((item) => item.id === activeSection), + [activeSection] + ); + const activeNfc = useMemo(() => { + if (status?.last_nfc && status.last_nfc.known === false) { + return { ...status.last_nfc, at: status.last_nfc_at }; + } + return simulatedNfc; + }, [status, simulatedNfc]); + const statusWithHardware = useMemo(() => { + if (!status) return null; + const hardwareUid = + lastHardwareUid.uid === status?.last_nfc?.uid && lastHardwareUid.hardwareUid + ? lastHardwareUid.hardwareUid + : activeNfc?.hardwareUid || null; + if (!status.last_nfc) return status; + return { + ...status, + last_nfc: { + ...status.last_nfc, + hardware_uid: hardwareUid, + }, + }; + }, [status, lastHardwareUid, activeNfc]); + + function renderSection() { + if (activeSection === 'dashboard') { + return ( +
+ +
+ +

Live-Status

+ {!selectedId &&

Waehle eine gepairte Box aus.

} + {selectedId && !status &&

Status wird geladen...

} + {status && status.error &&

{status.error}

} + {status && !status.error && ( +
{JSON.stringify(statusWithHardware, null, 2)}
+ )} +
+ +

Commands

+
+ + + + + + +
+
+ setNfcUid(event.target.value)} + placeholder="UID_1" + /> + + +
+

+ Steuerung ist nur moeglich, wenn die Box gepairt ist. +

+
+
+
+ ); + } + + if (activeSection === 'boxes') { + return ( +
+
+ +

Neue Boxen

+ {unpaired.length === 0 &&

Keine neuen Boxen.

} + {unpaired.map((box) => ( +
+
+ {box.alias || box.box_id} +
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Firmware: {box.firmware_version}
+
ID: {box.box_id}
+
+ +
+ ))} +
+ +

Gepairte Boxen

+ {paired.length === 0 &&

Noch keine gepairten Boxen.

} + {paired.map((box) => ( +
setSelectedId(box.box_id)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setSelectedId(box.box_id); + }} + > +
+ {box.alias || box.box_id} +
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Capabilities: {parseCapabilities(box.capabilities_json)}
+
ID: {box.box_id}
+
+
+ event.stopPropagation()} + onChange={(event) => { + event.stopPropagation(); + setBoxAliasDrafts((prev) => ({ + ...prev, + [box.box_id]: event.target.value, + })); + }} + /> + + {box.state} + +
+
+ ))} +
+
+
+ ); + } + + if (activeSection === 'media') { + return ( +
+ +
+

Medien Explorer

+ +
+ {uploadInProgress && ( +
+ {activeUploadLabel || 'Upload laeuft...'} +
+
+
+
+ )} +

Ordnerverwaltung, Upload und Datei-Listen im Server-Medienordner.

+ {mediaError &&

{mediaError}

} + {!mediaError && !mediaTree &&

Medienliste wird geladen...

} + {!mediaError && mediaTree && ( +
+
+
+ setSidebarQuery(event.target.value)} + aria-label="Ordner suchen" + /> +
+
+ {topLevelFolders.length === 0 && ( +
Keine Ordner gefunden.
+ )} + {filteredTree && renderFolderTree(filteredTree, 0)} +
+
+
+
+ +
+ setCurrentPath('')} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setCurrentPath(''); + }} + > + media + + {buildBreadcrumb(currentPath).map((crumb) => ( + setCurrentPath(crumb.path)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setCurrentPath(crumb.path); + }} + > + / {crumb.name} + + ))} +
+
+ + + + + +
+
+
+
+ Name + {showMeta ? ( + <> + Interpret + Titel + Laenge + Groesse + + ) : ( + <> + Typ + Groesse + + )} +
+ {currentItems.map((item) => ( +
{ + if (event.button !== 0) return; + if (event.metaKey || event.ctrlKey || event.shiftKey) { + return; + } + dragSelectRef.current = true; + handleSelect(item, event); + }} + onMouseEnter={() => handleDragSelect(item)} + onClick={(event) => handleSelect(item, event)} + onDoubleClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + lastDblClickRef.current = Date.now(); + handleOpen(item); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') handleOpen(item); + }} + > + + + {item.type === 'folder' ? ( + + ) : ( + + )} + + {item.name} + + {showMeta ? ( + <> + {item.type === 'folder' ? '-' : item.artist || '-'} + {item.type === 'folder' ? '-' : item.title || '-'} + + {item.type === 'folder' ? '-' : formatDuration(item.duration)} + + {formatSize(item.size)} + + ) : ( + <> + {item.type === 'folder' ? 'Ordner' : 'Datei'} + {formatSize(item.size)} + + )} +
+ ))} + {selectedPaths.length > 0 && ( +
+ + {showMeta ? ( + <> + + + + + {selectedPaths.length} ausgewaehlt + + + ) : ( + <> + + + {selectedPaths.length} ausgewaehlt + + + )} +
+ )} +
+
+
+ + Medienordner: {formatSize(mediaBytes)} · Verfuegbar:{' '} + {formatSize(freeBytes)} + +
+
+ )} + +
+ ); + } + + if (activeSection === 'tags') { + return ( +
+ +
+

Tags nur auf dieser Box

+
+ {localBoxError &&

{localBoxError}

} + {!localBoxError && localBoxTags.length === 0 && ( +

Keine lokalen Tags ohne Server-Zuordnung.

+ )} + {localBoxTags.map((tag) => ( +
+
+ {tag.uid} +
+ Dateien: {tag.file_count} · {formatSize(tag.total_size)} +
+ {tag.media_exists && tag.files?.length > 0 && ( +
    + {tag.files.map((file) => ( +
  • {file}
  • + ))} +
+ )} + {!tag.media_exists && ( +

Keine Dateien im Box-Ordner gefunden.

+ )} +
+
+ +
+
+ ))} +
+ + +

Tags (Datenbank)

+ {tags.length === 0 &&

Keine Tags vorhanden.

} + {tags.map((tag) => ( +
+
+
+ + {tag.alias ? `${tag.alias} (${tag.uid})` : tag.uid} + +
Status: {tag.status}
+
Medium: {tag.media_path || '-'}
+ {tag.label ?
Label: {tag.label}
: null} +
+
+ + setTagAliasDrafts((prev) => ({ + ...prev, + [tag.uid]: event.target.value, + })) + } + /> + + + {dbTagMedia[tag.uid] !== undefined && + dbTagMedia[tag.uid] !== (tag.media_path ?? '') ? ( + + ) : null} + +
+
+
+ ))} +
+ + +

Tag-Matrix (Sperren)

+ {paired.length === 0 && ( +

Keine gepairten Boxen vorhanden.

+ )} + {tags.length === 0 &&

Keine Tags vorhanden.

} + {paired.length > 0 && tags.length > 0 && ( +
+
+ Tag + {paired.map((box) => ( + + {box.alias || box.box_id} + + ))} +
+ {tags.map((tag) => ( +
+ {tag.alias || tag.uid} + {paired.map((box) => { + const blocked = (blockedByBox[box.box_id] || []).includes(tag.uid); + return ( + + ); + })} +
+ ))} +
+ )} +
+
+ ); + } + + return ( +
+ +

System

+

Backend: {import.meta.env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'}

+
+ Gepairte Boxen + {paired.length} +
+
+ Tags + {tags.length} +
+
+
+ ); + } + return ( -
-
-

Klangkiste Control Panel

-

Backend: {import.meta.env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'}

+
+
+
+
K
+
+ Klangkiste + PrimeReact GUI +
+
+
+
+
+ + + setSearchValue(event.target.value)} + placeholder="Suche nach Boxen, Tags oder Medien" + /> + +
+
+ +
@@ -1312,132 +1954,234 @@ export default function App() { ))}
- {error &&
{error}
} - -
-
-

Neue Boxen

- {unpaired.length === 0 &&

Keine neuen Boxen.

} - {unpaired.map((box) => ( -
-
- {box.alias || box.box_id} -
Zuletzt gesehen: {formatTime(box.last_seen)}
-
Firmware: {box.firmware_version}
-
ID: {box.box_id}
-
- -
- ))} -
- -
-

Gepairte Boxen

- {paired.length === 0 &&

Noch keine gepairten Boxen.

} - {paired.map((box) => ( -
setSelectedId(box.box_id)} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === 'Enter') setSelectedId(box.box_id); - }} - > -
- {box.alias || box.box_id} -
Zuletzt gesehen: {formatTime(box.last_seen)}
-
Capabilities: {parseCapabilities(box.capabilities_json)}
-
ID: {box.box_id}
-
-
- event.stopPropagation()} - onChange={(event) => { - event.stopPropagation(); - setBoxAliasDrafts((prev) => ({ - ...prev, - [box.box_id]: event.target.value, - })); - }} +
+
-
+
+ +
+

Quick Actions

+
+
+
+ +
+

Aktive Sessions

+ {paired.length === 0 && ( +

Keine gepairten Boxen.

+ )} +
+ {paired.map((box) => { + const isActive = selectedId === box.box_id; + const label = (box.alias || box.box_id || '').slice(0, 2).toUpperCase(); + return ( + + ); + })} +
+
+ -
-
-

Live-Status

- {!selectedId &&

Waehle eine gepairte Box aus.

} - {selectedId && !status &&

Status wird geladen...

} - {status && status.error &&

{status.error}

} - {status && !status.error && ( -
{JSON.stringify(status, null, 2)}
+
+ + +
+ ) : null} + {tags.some((tag) => tag.status === 'IMPORTED') && ( +
+ + +
+ )} +
+ + + {activeNfc.uid && activeNfc.known === false && ( + + )} +
+ +
)} -
+ {renderSection()} + -
-

Commands

-
- - - - - - -
-
- setNfcUid(event.target.value)} - placeholder="UID_1" - /> - - -
-

- Steuerung ist nur moeglich, wenn die Box gepairt ist. -

-
- + + - {activeModal && ( + + + {activeModal === 'new-folder' && (
{ @@ -1452,228 +2196,246 @@ export default function App() { onClick={(event) => event.stopPropagation()} ref={modalRef} > - {activeModal === 'new-folder' && ( +

{uploadAfterCreate ? 'Upload vorbereiten' : 'Neuen Ordner anlegen'}

+ setNewFolderName(event.target.value)} + placeholder={ + uploadAfterCreate + ? 'Optionaler Ordnername (leer = aktueller Ordner)' + : 'Ordnername' + } + /> + {uploadAfterCreate && ( <> -

{uploadAfterCreate ? 'Upload vorbereiten' : 'Neuen Ordner anlegen'}

- setNewFolderName(event.target.value)} - placeholder={ - uploadAfterCreate - ? 'Optionaler Ordnername (leer = aktueller Ordner)' - : 'Ordnername' - } - /> - {uploadAfterCreate && ( - <> -
{ - if (uploadInProgress) return; - uploadDropInputRef.current?.click(); - }} - onKeyDown={(event) => { - if (event.key === 'Enter') uploadDropInputRef.current?.click(); - }} - onDragOver={(event) => { - event.preventDefault(); - }} - onDrop={(event) => { - event.preventDefault(); - if (uploadInProgress) return; - const files = Array.from(event.dataTransfer.files || []); - const audioFiles = files.filter((file) => - file.type.startsWith('audio/') - ); - setPendingUploadFiles(audioFiles); - setActiveUploadLabel( - audioFiles.length - ? `Upload: ${audioFiles.length} Datei(en)` - : '' - ); - }} - > -
Audiodateien auswaehlen
-
- Datei hier ablegen oder klicken, um auszuwaehlen -
- {pendingUploadFiles.length > 0 && ( -
- {pendingUploadFiles.map((file) => ( -
{file.name}
- ))} -
- )} - { - const files = Array.from(event.target.files || []); - const audioFiles = files.filter((file) => - file.type.startsWith('audio/') - ); - setPendingUploadFiles(audioFiles); - setActiveUploadLabel( - audioFiles.length - ? `Upload: ${audioFiles.length} Datei(en)` - : '' - ); - }} - /> -
-

- Upload startet direkt. Ohne Ordnername landet er im aktuellen Ordner. -

- {uploadInProgress && ( -
-
-
- )} - - )} -
- - -
- - )} - {activeModal === 'rename' && ( - <> -

Umbenennen

- setRenameName(event.target.value)} - placeholder="Neuer Name" - /> -
- - -
- - )} - {activeModal === 'delete' && ( - <> -

Loeschen

-

- {selectedPaths.length} Eintrag(e) wirklich loeschen? -

-
- - -
- - )} - {activeModal === 'move' && ( - <> -

Verschieben

- {selectedPaths.length > 0 && - selectedPaths.every((pathValue) => { - const node = getNodeByPath(mediaTree, pathValue); - return node && node.type === 'file'; - }) && ( -

- Dateien duerfen nicht in den Root-Ordner verschoben werden. -

- )} - -
- - +
Audiodateien auswaehlen
+
+ Datei hier ablegen oder klicken, um auszuwaehlen +
+ {pendingUploadFiles.length > 0 && ( +

+ {pendingUploadFiles.length} Datei(en) bereit. +

+ )} + { + const files = Array.from(event.target.files || []); + const audioFiles = files.filter((file) => + file.type.startsWith('audio/') + ); + setPendingUploadFiles(audioFiles); + setActiveUploadLabel( + audioFiles.length + ? `Upload: ${audioFiles.length} Datei(en)` + : '' + ); + }} + /> )} +
+ + +
+
+
+ )} + + {activeModal === 'rename' && ( +
{ + setActiveModal(''); + setRenameName(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Umbenennen

+ setRenameName(event.target.value)} + placeholder="Neuer Name" + /> +
+ + +
+
+
+ )} + + {activeModal === 'delete' && ( +
{ + setActiveModal(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Eintraege loeschen

+

{selectedPaths.length} Eintraege werden geloescht.

+
+ + +
+
+
+ )} + + {activeModal === 'move' && ( +
{ + setActiveModal(''); + setMoveTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Verschieben

+ +
+ + +
+
+
+ )} + {activeModal === 'tag-delete' && ( - <> -

Tag entfernen

-

- Was moechtest du entfernen? -

-
- - - -
- - )} +
{ + setActiveModal(''); + setTagDeleteTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Tag entfernen

+

Der Tag wird komplett geloescht.

+
+ + +
)} @@ -1720,488 +2482,57 @@ export default function App() { )} - {status?.last_nfc && status.last_nfc.known === false && status.last_nfc.uid && ( -
-

- {tags.find((tag) => tag.uid === status.last_nfc.uid && !tag.media_path) - ? 'Leerer Tag erkannt' - : 'Neuer Tag erkannt'} -

-

- UID erkannt: {status.last_nfc.uid || '-'} -

- {tags.find((tag) => tag.uid === status.last_nfc.uid) && ( -

- Dieser Tag ist bekannt, aber noch nicht zugewiesen. -

- )} - {!tags.find((tag) => tag.uid === status.last_nfc.uid && !tag.media_path) && ( -
- - setScanTagLabel(event.target.value)} - placeholder="Label (optional)" - /> -
- )} - {tags.some((tag) => tag.status === 'IMPORTED') && ( -
- - + {paired.length === 0 && ( +

Keine gepairten Boxen.

+ )} +
+ {paired.map((box) => { + const isActive = selectedId === box.box_id; + const label = (box.alias || box.box_id || '').slice(0, 2).toUpperCase(); + return ( + + ); + })} +
+ +
+
- )} -
- -
-
+ )} - -
-
-

Medien Explorer

- -
- {uploadInProgress && ( -
- {activeUploadLabel || 'Upload laeuft...'} -
-
-
-
- )} -

Ordnerverwaltung, Upload und Datei-Listen im Server-Medienordner.

- {mediaError &&

{mediaError}

} - {!mediaError && !mediaTree &&

Medienliste wird geladen...

} - {!mediaError && mediaTree && ( -
-
-
- setSidebarQuery(event.target.value)} - aria-label="Ordner suchen" - /> -
-
- {topLevelFolders.length === 0 && ( -
Keine Ordner gefunden.
- )} - {filteredTree && renderFolderTree(filteredTree, 0)} -
-
-
-
- -
- setCurrentPath('')} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === 'Enter') setCurrentPath(''); - }} - > - media - - {buildBreadcrumb(currentPath).map((crumb) => ( - setCurrentPath(crumb.path)} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === 'Enter') setCurrentPath(crumb.path); - }} - > - / {crumb.name} - - ))} -
-
- - - - - -
-
-
-
- Name - {showMeta ? ( - <> - Interpret - Titel - Länge - Größe - - ) : ( - <> - Typ - Größe - - )} -
- {currentItems.map((item) => ( -
{ - if (event.button !== 0) return; - if (event.metaKey || event.ctrlKey || event.shiftKey) { - return; - } - dragSelectRef.current = true; - handleSelect(item, event); - }} - onMouseEnter={() => handleDragSelect(item)} - onClick={(event) => handleSelect(item, event)} - onDoubleClick={(event) => { - event.preventDefault(); - event.stopPropagation(); - lastDblClickRef.current = Date.now(); - handleOpen(item); - }} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === 'Enter') handleOpen(item); - }} - > - - - {item.type === 'folder' ? ( - - ) : ( - - )} - - {item.name} - - {showMeta ? ( - <> - {item.type === 'folder' ? '-' : item.artist || '-'} - {item.type === 'folder' ? '-' : item.title || '-'} - - {item.type === 'folder' ? '-' : formatDuration(item.duration)} - - {formatSize(item.size)} - - ) : ( - <> - {item.type === 'folder' ? 'Ordner' : 'Datei'} - {formatSize(item.size)} - - )} -
- ))} - {selectedPaths.length > 0 && ( -
- - {showMeta ? ( - <> - - - - - {selectedPaths.length} ausgewaehlt - - - ) : ( - <> - - - {selectedPaths.length} ausgewaehlt - - - )} -
- )} -
-
-
- - Medienordner: {formatSize(mediaBytes)} · Verfügbar:{' '} - {formatSize(freeBytes)} - -
-
- )} -
- -
-
-

Tags nur auf dieser Box

-
- {localBoxError &&

{localBoxError}

} - {!localBoxError && localBoxTags.length === 0 && ( -

Keine lokalen Tags ohne Server-Zuordnung.

- )} - {localBoxTags.map((tag) => ( -
-
- {tag.uid} -
- Dateien: {tag.file_count} · {formatSize(tag.total_size)} -
- {tag.media_exists && tag.files?.length > 0 && ( -
    - {tag.files.map((file) => ( -
  • {file}
  • - ))} -
- )} - {!tag.media_exists && ( -

Keine Dateien im Box-Ordner gefunden.

- )} -
-
- -
-
- ))} -
- -
-
-

Tags (Datenbank)

- {tags.length === 0 &&

Keine Tags vorhanden.

} - {tags.map((tag) => ( -
-
-
- - {tag.alias ? `${tag.alias} (${tag.uid})` : tag.uid} - -
Status: {tag.status}
-
Medium: {tag.media_path || '-'}
- {tag.label ?
Label: {tag.label}
: null} -
-
- - setTagAliasDrafts((prev) => ({ - ...prev, - [tag.uid]: event.target.value, - })) - } - /> - - - {dbTagMedia[tag.uid] !== undefined && - dbTagMedia[tag.uid] !== (tag.media_path ?? '') ? ( - - ) : null} - -
-
-
- ))} -
-
- -
-

Tag-Matrix (Sperren)

- {paired.length === 0 && ( -

Keine gepairten Boxen vorhanden.

- )} - {tags.length === 0 &&

Keine Tags vorhanden.

} - {paired.length > 0 && tags.length > 0 && ( -
-
- Tag - {paired.map((box) => ( - - {box.alias || box.box_id} - - ))} -
- {tags.map((tag) => ( -
- {tag.alias || tag.uid} - {paired.map((box) => { - const blocked = (blockedByBox[box.box_id] || []).includes(tag.uid); - return ( - - ); - })} -
- ))} -
- )} -
); } diff --git a/gui/frontend/src/main.jsx b/gui/frontend/src/main.jsx index d78ecd4..f7903cf 100644 --- a/gui/frontend/src/main.jsx +++ b/gui/frontend/src/main.jsx @@ -1,5 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import 'primereact/resources/themes/lara-dark-teal/theme.css'; +import 'primereact/resources/primereact.min.css'; +import 'primeicons/primeicons.css'; import App from './App.jsx'; import './styles.css'; diff --git a/gui/frontend/src/styles.css b/gui/frontend/src/styles.css index 0c0271d..58a462c 100644 --- a/gui/frontend/src/styles.css +++ b/gui/frontend/src/styles.css @@ -1,8 +1,10 @@ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); + :root { - color-scheme: light; - font-family: "IBM Plex Sans", "Segoe UI", sans-serif; - color: #131522; - background: #f4f2ef; + color-scheme: dark; + font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif; + background-color: #0b0f14; + color: #e6edf6; } * { @@ -11,7 +13,539 @@ body { margin: 0; - padding: 0; + min-height: 100vh; + background: + radial-gradient(circle at 20% 10%, rgba(50, 112, 124, 0.35), transparent 45%), + radial-gradient(circle at 80% 0%, rgba(24, 45, 67, 0.6), transparent 40%), + linear-gradient(130deg, #0b0f14 0%, #111826 45%, #0d1220 100%); +} + +#root { + min-height: 100vh; +} + +.app-shell { + min-height: 100vh; + padding-bottom: 80px; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + display: grid; + grid-template-columns: auto minmax(240px, 1fr) auto; + gap: 16px; + align-items: center; + padding: 18px 24px; + background: rgba(11, 15, 20, 0.92); + backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 12px; + color: rgba(230, 237, 246, 0.7); +} + +.brand strong { + display: block; + font-size: 14px; + color: #e6edf6; +} + +.brand span { + display: block; + margin-top: 2px; +} + +.brand-mark { + width: 36px; + height: 36px; + border-radius: 12px; + display: grid; + place-items: center; + background: linear-gradient(130deg, #19a4a1, #0b6f84); + color: #061018; + font-weight: 700; +} + +.topbar-search .p-inputtext { + width: 100%; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); + color: #e6edf6; +} + +.topbar-search .p-inputtext::placeholder { + color: rgba(230, 237, 246, 0.5); +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.mobile-actions { + display: none; +} + +.user-avatar { + background: #1c2735; + color: #e6edf6; +} + +.layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr) 280px; + gap: 24px; + padding: 24px; +} + +.sidebar, +.right-panel { + position: sticky; + top: 96px; + align-self: start; + height: fit-content; +} + +.sidebar { + padding: 16px; + border-radius: 20px; + background: rgba(10, 17, 26, 0.85); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.sidebar-title { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + color: rgba(230, 237, 246, 0.55); + margin: 0 0 12px; +} + +.sidebar-section { + margin-bottom: 16px; +} + +.nav-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.nav-button { + justify-content: flex-start; + color: rgba(230, 237, 246, 0.7); +} + +.nav-button.active { + background: rgba(25, 164, 161, 0.15); + color: #e6edf6; + border-radius: 12px; +} + +.button-stack { + display: flex; + flex-direction: column; + gap: 8px; +} + +.session-card { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px; + align-items: center; + padding: 12px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid transparent; + text-align: left; +} + +.session-card span { + display: block; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.session-list { + display: grid; + gap: 8px; +} + +.session-card.active { + border-color: rgba(25, 164, 161, 0.5); + background: rgba(25, 164, 161, 0.12); +} + +.session-card:focus-visible { + outline: 2px solid rgba(25, 164, 161, 0.6); + outline-offset: 2px; +} + +.content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.section-header h1 { + margin: 0 0 4px; + font-size: 28px; +} + +.section-header p { + margin: 0; + color: rgba(230, 237, 246, 0.6); +} + +.section-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.section-stack { + display: flex; + flex-direction: column; + gap: 20px; +} + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +.stat-card { + background: rgba(14, 24, 35, 0.9); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.stat-label { + margin: 0; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.stat-value { + font-size: 24px; + font-weight: 600; + margin: 8px 0; +} + +.stat-helper { + font-size: 12px; + color: rgba(25, 164, 161, 0.9); +} + +.split-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; +} + +.wide-card, +.content-card, +.panel-card { + background: rgba(14, 24, 35, 0.92); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.card-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.progress-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 12px; + align-items: center; + margin-top: 12px; + font-size: 12px; + color: rgba(230, 237, 246, 0.7); +} + +.activity-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + +.activity-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.activity-item span { + display: block; + font-size: 11px; + color: rgba(230, 237, 246, 0.55); +} + +.content-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.content-card h3 { + margin-top: 0; +} + +.placeholder-block { + height: 120px; + border-radius: 14px; + background: linear-gradient(130deg, rgba(25, 164, 161, 0.2), rgba(23, 45, 64, 0.9)); +} + +.placeholder-table { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; +} + +.table-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 12px; + align-items: center; + padding: 8px 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.table-row.header { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(230, 237, 246, 0.5); +} + +.placeholder-tree { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + font-size: 13px; + color: rgba(230, 237, 246, 0.75); +} + +.placeholder-tree .nested { + margin-left: 16px; +} + +.queue-item { + display: grid; + gap: 8px; + margin-top: 12px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.queue-item span { + display: block; + font-size: 11px; + color: rgba(230, 237, 246, 0.5); +} + +.tab-shell .p-tabview-nav { + background: transparent; + border-color: rgba(255, 255, 255, 0.08); +} + +.tag-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-top: 12px; +} + +.tag-card { + background: rgba(255, 255, 255, 0.03); +} + +.tag-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.settings-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; +} + +.token-chip { + display: inline-flex; + align-items: center; + padding: 6px 12px; + border-radius: 999px; + margin-right: 8px; + margin-top: 8px; + background: rgba(25, 164, 161, 0.18); + font-size: 12px; +} + +.right-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.panel-card h3 { + margin-top: 0; +} + +.status-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; + font-size: 13px; +} + +.panel-pill { + padding: 10px 12px; + border-radius: 12px; + margin-top: 8px; + background: rgba(255, 255, 255, 0.04); + font-size: 13px; +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: none; + gap: 6px; + padding: 10px 12px 16px; + background: rgba(11, 15, 20, 0.95); + border-top: 1px solid rgba(255, 255, 255, 0.08); + z-index: 20; +} + +.bottom-nav button { + flex: 1; + display: grid; + place-items: center; + gap: 4px; + background: transparent; + border: none; + color: rgba(230, 237, 246, 0.6); + font-size: 11px; +} + +.bottom-nav button.active { + color: #19a4a1; +} + +@media (max-width: 1100px) { + .layout { + grid-template-columns: 220px minmax(0, 1fr); + } + + .right-panel { + display: none; + } +} + +@media (max-width: 900px) { + .topbar { + grid-template-columns: auto auto 1fr; + } + + .topbar-actions { + display: none; + } + + .mobile-actions { + display: flex; + align-items: center; + } +} + +@media (max-width: 760px) { + .layout { + grid-template-columns: minmax(0, 1fr); + padding: 16px; + } + + .sidebar { + display: none; + } + + .topbar-search { + display: none; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + } + + .bottom-nav { + display: grid; + grid-template-columns: repeat(5, 1fr); + } +} + +.sheet-backdrop { + position: fixed; + inset: 0; + background: rgba(5, 8, 12, 0.6); + display: grid; + align-items: end; + z-index: 50; +} + +.sheet-card { + background: #0f1622; + border-radius: 20px 20px 0 0; + padding: 20px 18px 28px; + border: 1px solid rgba(255, 255, 255, 0.08); + max-height: 80vh; + overflow-y: auto; +} + +.sheet-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; } .toast-container { @@ -32,107 +566,84 @@ body { font-size: 13px; line-height: 1.4; min-height: 38px; - box-shadow: 0 8px 20px rgba(13, 16, 31, 0.12); - background: #ffffff; - border: 1px solid #e4e0dd; + box-shadow: 0 8px 20px rgba(5, 8, 14, 0.4); + background: rgba(18, 26, 36, 0.95); + border: 1px solid rgba(255, 255, 255, 0.08); } .toast.success { - border-color: #b9d9b9; - background: #eef7ee; - color: #205b20; + border-color: rgba(77, 186, 135, 0.4); + background: rgba(21, 46, 36, 0.95); + color: #9de4c3; } .toast.error { - border-color: #f2c9c9; - background: #ffefef; - color: #9a2a2a; + border-color: rgba(242, 201, 201, 0.4); + background: rgba(60, 20, 24, 0.95); + color: #f4a7a7; } -.page { - max-width: 1200px; - margin: 0 auto; - padding: 32px 24px 48px; +.error { + background: rgba(80, 24, 32, 0.6); + color: #f4a7a7; + border: 1px solid rgba(242, 201, 201, 0.4); + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 16px; } -header { - margin-bottom: 24px; +.muted { + color: rgba(230, 237, 246, 0.6); } -header h1 { - margin: 0 0 6px; - font-size: 28px; +.status { + background: #0f1520; + color: #e6edf6; + padding: 12px; + border-radius: 12px; + font-size: 12px; + overflow: auto; + border: 1px solid rgba(255, 255, 255, 0.08); } -header p { - margin: 0; - color: #525466; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; - margin-bottom: 0; -} - -.panel { - background: #ffffff; - border-radius: 16px; - padding: 16px; - box-shadow: 0 8px 24px rgba(13, 16, 31, 0.08); -} - -.page > section { - margin-bottom: 24px; -} - -.panel-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} - -.subheading { - margin: 16px 0 8px; - font-size: 14px; - color: #3b3f46; -} - -.card { +.legacy .card { display: flex; justify-content: space-between; align-items: center; gap: 12px; - border: 1px solid #e4e0dd; + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 12px; margin-top: 12px; cursor: pointer; + background: rgba(10, 17, 26, 0.7); } -.card.selected { - border-color: #d28735; - box-shadow: 0 0 0 2px rgba(210, 135, 53, 0.2); +.legacy .card.selected { + border-color: rgba(25, 164, 161, 0.6); + box-shadow: 0 0 0 2px rgba(25, 164, 161, 0.15); } -.stack { +.legacy .card.compact { + padding: 10px; + margin-top: 10px; +} + +.legacy .stack { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; } -.stack-inline { +.legacy .stack-inline { flex-direction: row; align-items: center; justify-content: flex-start; flex-wrap: wrap; } -.tag-row { +.legacy .tag-row { display: flex; align-items: center; justify-content: space-between; @@ -140,72 +651,81 @@ header p { width: 100%; } -.box-tag-row { +.legacy .box-tag-row { display: flex; justify-content: space-between; gap: 16px; padding: 12px; - border: 1px solid #e4e0dd; + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; margin-top: 12px; + background: rgba(10, 17, 26, 0.7); } -.box-tag-actions { +.legacy .box-tag-actions { display: flex; align-items: flex-start; } -.file-list { +.legacy .file-list { margin: 8px 0 0; padding-left: 18px; - color: #3b3f46; + color: rgba(230, 237, 246, 0.7); font-size: 13px; } -.file-list li { +.legacy .file-list li { margin-bottom: 4px; } -.tag-info { +.legacy .tag-info { display: flex; flex-direction: column; gap: 4px; } -.alias-input { - border: 1px solid #dde0e6; +.legacy .alias-input, +.legacy input, +.modal-card input { + border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 8px; padding: 6px 8px; font-size: 12px; min-width: 140px; + background: rgba(255, 255, 255, 0.04); + color: #e6edf6; } -.alias-input:focus { - outline: none; - border-color: #cfd4dc; - box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08); +.legacy select, +.modal-card select { + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 8px 10px; + min-width: 180px; + background: rgba(255, 255, 255, 0.04); + color: #e6edf6; } -.toggle { +.legacy .toggle { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; - color: #3b3f46; + color: rgba(230, 237, 246, 0.8); } -.toggle input { +.legacy .toggle input { width: 16px; height: 16px; } -.tag-matrix { +.legacy .tag-matrix { display: grid; gap: 8px; overflow-x: auto; } -.matrix-row { +.legacy .matrix-row { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(120px, 1fr); @@ -213,265 +733,79 @@ header p { align-items: center; } -.matrix-row.header { +.legacy .matrix-row.header { font-weight: 600; - color: #3b3f46; + color: rgba(230, 237, 246, 0.8); } -.matrix-cell { +.legacy .matrix-cell { display: flex; align-items: center; gap: 8px; padding: 8px 10px; - border: 1px solid #e6e8ec; + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 10px; - background: #fbfbfc; + background: rgba(255, 255, 255, 0.03); font-size: 12px; } -.matrix-cell.label { +.legacy .matrix-cell.label { font-weight: 600; - background: #ffffff; + background: rgba(10, 17, 26, 0.8); } -.meta { +.legacy .meta { font-size: 12px; - color: #6a6c7a; + color: rgba(230, 237, 246, 0.55); } -.pill { - background: #f0e2d4; - color: #7a4b17; +.legacy .pill { + background: rgba(25, 164, 161, 0.2); + color: #8fe3df; padding: 4px 10px; border-radius: 999px; font-size: 12px; } -.controls { +.legacy .controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } -button { - border: none; - background: #1f243d; - color: #ffffff; +.legacy button, +.modal-card button { + border: 1px solid rgba(255, 255, 255, 0.1); + background: #1b2736; + color: #e6edf6; padding: 8px 12px; border-radius: 8px; cursor: pointer; font-size: 14px; } -button:hover { - background: #2c3252; +.legacy button:hover, +.modal-card button:hover { + background: #233144; } -select { - border-radius: 8px; - border: 1px solid #d6d1cc; - padding: 8px 10px; - min-width: 180px; - background: #ffffff; -} - -.button-ghost { +.legacy .button-ghost, +.modal-card .button-ghost { background: transparent; - color: #1f243d; - border: 1px solid #d6d1cc; + color: rgba(230, 237, 246, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); } -.button-ghost:hover { - background: #f6f2ee; -} - -.card.compact { - padding: 10px; - margin-top: 10px; -} - -input { - border-radius: 8px; - border: 1px solid #d6d1cc; - padding: 8px 10px; - min-width: 140px; -} - -.status { - background: #111522; - color: #e8e8ef; - padding: 12px; - border-radius: 12px; - font-size: 12px; - overflow: auto; -} - -.error { - background: #ffefef; - color: #9a2a2a; - border: 1px solid #f2c9c9; - border-radius: 10px; - padding: 10px 12px; - margin-bottom: 16px; -} - -.muted { - color: #6a6c7a; -} - -.tree { - border: 1px solid #e6e1dc; - border-radius: 12px; - padding: 12px; - background: #faf7f4; -} - -.explorer { - display: grid; - grid-template-columns: 260px 1fr; - grid-template-rows: 1fr auto; - gap: 0; - border: 1px solid #e2e4e8; - border-radius: 12px; - overflow-x: auto; - overflow-y: hidden; - background: #ffffff; - min-height: 200px; - align-items: stretch; -} - -.explorer-sidebar { - grid-row: 1; - padding: 12px; - display: flex; - flex-direction: column; - gap: 10px; - background: #ffffff; - height: 100%; -} - -.sidebar-title { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.04em; - color: #8a919d; - margin: 0; -} - -.sidebar-toolbar { - display: flex; - align-items: center; - justify-content: flex-start; - border: 1px solid #e6e8ec; - background: #fbfbfc; - padding: 10px 12px; - min-height: 44px; - gap: 10px; -} - -.sidebar-tree { - border: 1px solid #e6e8ec; - border-radius: 10px; - background: #ffffff; - padding: 8px; - overflow: auto; -} - -.sidebar-search { - flex: 1; - width: 100%; - border: 1px solid #dde0e6; - background: #ffffff; - border-radius: 8px; - padding: 6px 10px; - font-size: 12px; - color: #2f3442; - min-width: 0; -} - -.sidebar-search:focus { - outline: none; - border-color: #cfd4dc; - box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08); -} - -.explorer-toolbar, -.sidebar-toolbar { - height: 44px; - min-height: 44px; - box-shadow: 0 1px 2px rgba(16, 18, 22, 0.06); - align-items: center; -} - -.explorer-actions.footer { - padding-top: 6px; -} - -.explorer-main { - grid-row: 1; - display: flex; - flex-direction: column; - gap: 10px; - padding: 12px 14px 14px; - background: #ffffff; - height: 100%; -} - -.explorer-toolbar { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - padding: 10px 12px; - border: 1px solid #e6e8ec; - border-radius: 10px; - background: #fbfbfc; -} - -.explorer-path { - display: flex; - flex-wrap: wrap; - gap: 6px; - font-size: 12px; - color: #6a7280; - align-items: center; -} - -.breadcrumb-root { - font-weight: 600; - color: #3b3f46; -} - -.path-link { - background: transparent; - border: none; - color: #3b3f46; - cursor: pointer; - padding: 0; - font-size: 12px; - font-weight: 600; -} - -.path-link:hover, -.path-link:focus { - background: transparent; - color: #3b3f46; - outline: none; -} - - -.toolbar-actions { - margin-left: auto; - display: flex; - align-items: center; - gap: 6px; +.legacy .button-ghost:hover, +.modal-card .button-ghost:hover { + background: rgba(255, 255, 255, 0.06); } .icon-button { - border: 1px solid #dde0e6; - background: #ffffff; - color: #1f243d; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(10, 17, 26, 0.7); + color: #e6edf6; padding: 6px 8px; border-radius: 8px; cursor: pointer; @@ -483,12 +817,12 @@ input { } .icon-button:hover { - background: #f3f5f8; + background: rgba(255, 255, 255, 0.08); } .icon-button.danger { - border-color: #f2c9c9; - color: #9a2a2a; + border-color: rgba(242, 201, 201, 0.4); + color: #f4a7a7; } .icon-button.upload { @@ -508,319 +842,350 @@ input { cursor: pointer; } -.explorer-actions { +.panel-header { display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.explorer-list { - border: 1px solid #e6e8ec; - border-radius: 10px; - background: #ffffff; - overflow: auto; - max-height: 420px; - user-select: none; -} - -.explorer-row { - display: grid; - grid-template-columns: 1fr 120px 90px; + align-items: center; + justify-content: space-between; gap: 12px; - padding: 10px 14px; - border-bottom: 1px solid #f4f5f7; - cursor: pointer; + margin-bottom: 12px; } -.explorer-list.has-meta .explorer-row { - grid-template-columns: 1fr 160px 200px 80px 90px; +.explorer { + display: grid; + grid-template-columns: 260px 1fr; + grid-template-rows: 1fr auto; + gap: 0; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + overflow-x: auto; + overflow-y: hidden; + background: rgba(8, 12, 18, 0.7); + min-height: 200px; + align-items: stretch; } -.explorer-footer { - grid-row: 2; - grid-column: 1 / -1; - margin: 0 12px 12px; - padding: 10px 12px; - border: 1px solid #e6e8ec; - border-radius: 10px; - background: #fbfbfc; - font-size: 12px; - color: #3b3f46; +.explorer-sidebar { + grid-row: 1; + padding: 12px; display: flex; - justify-content: flex-end; + flex-direction: column; + gap: 10px; + background: rgba(10, 17, 26, 0.85); + height: 100%; +} + +.sidebar-toolbar { + display: flex; + align-items: center; + justify-content: flex-start; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + padding: 10px 12px; + min-height: 44px; + gap: 10px; } .sidebar-tree { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(10, 17, 26, 0.8); + padding: 8px; + overflow: auto; +} + +.sidebar-search { flex: 1; -} - -.explorer-row.header { - background: #f6f7f9; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - cursor: default; -} - -.explorer-row.footer { - background: #fbfbfc; + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.04); + border-radius: 8px; + padding: 6px 10px; font-size: 12px; - cursor: default; + color: #e6edf6; + min-width: 0; } -.footer-count { - text-align: right; - color: #6a7280; +.sidebar-search:focus { + outline: none; + border-color: rgba(25, 164, 161, 0.6); + box-shadow: 0 0 0 2px rgba(25, 164, 161, 0.2); } -.explorer-row.selected { - background: #eef3ff; +.explorer-toolbar, +.sidebar-toolbar { + height: 44px; + min-height: 44px; + box-shadow: 0 1px 2px rgba(16, 18, 22, 0.2); + align-items: center; } -.tree-row { +.explorer-actions.footer { + padding-top: 6px; +} + +.explorer-main { + grid-row: 1; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px 14px; + background: rgba(10, 17, 26, 0.7); + height: 100%; +} + +.explorer-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); +} + +.explorer-path { + display: flex; + flex-wrap: wrap; + gap: 6px; + font-size: 12px; + color: rgba(230, 237, 246, 0.7); + align-items: center; +} + +.breadcrumb-root { + font-weight: 600; + color: #e6edf6; +} + +.path-link { background: transparent; - padding: 4px 6px; - border-radius: 6px; + border: none; + color: #e6edf6; + cursor: pointer; + padding: 0; + font-size: 12px; + font-weight: 600; +} + +.path-link:hover, +.path-link:focus { + background: transparent; + color: #e6edf6; + outline: none; +} + +.toolbar-actions { + margin-left: auto; display: flex; align-items: center; gap: 6px; } -.tree-row.folder { - background: transparent; - font-weight: 500; - color: #3b3f46; +.explorer-list { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(10, 17, 26, 0.8); + overflow: hidden; } -.tree-row.folder:hover { - background: transparent; -} - -.tree-row.active { - background: #e7edf7; -} - -.tree-tag-count { - font-size: 11px; - color: #6a7280; - background: #eef1f5; - border-radius: 999px; - padding: 0 6px; - display: inline-flex; +.explorer-row { + display: grid; + grid-template-columns: 2fr repeat(2, minmax(80px, 1fr)); + gap: 12px; align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - line-height: 18px; + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + font-size: 13px; } -.media-tag-count { - margin-left: 6px; - font-size: 11px; - color: #6a7280; - background: #eef1f5; - border-radius: 999px; - padding: 2px 6px; +.explorer-row.has-meta, +.explorer-list.has-meta .explorer-row { + grid-template-columns: 2fr repeat(4, minmax(100px, 1fr)); } -.tree-caret { - background: transparent; - border: none; - padding: 0; - margin-right: 6px; - color: #8a919d; - cursor: pointer; +.explorer-row.header { font-size: 12px; - width: 16px; - display: inline-flex; - align-items: center; - justify-content: center; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(230, 237, 246, 0.5); + background: rgba(255, 255, 255, 0.02); } -.tree-caret:hover, -.tree-caret:focus { - background: transparent; - color: #8a919d; - outline: none; +.explorer-row.footer { + background: rgba(255, 255, 255, 0.02); } -.tree-caret.disabled { - opacity: 0.35; - cursor: default; -} - -.tree-icon { - margin-right: 6px; - color: #8a919d; - font-size: 11px; -} - -.tree-icon.folder { - color: #5a7bb6; - margin-right: 8px; +.explorer-row.selected { + background: rgba(25, 164, 161, 0.15); } .row-name { - display: inline-flex; + display: flex; align-items: center; gap: 8px; } .row-icon { - width: 18px; display: inline-flex; - justify-content: center; - color: #5a7bb6; + align-items: center; +} + +.footer-count { + font-weight: 600; + color: rgba(230, 237, 246, 0.9); +} + +.explorer-footer { + grid-column: 1 / -1; + padding: 8px 12px; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(10, 17, 26, 0.8); +} + +.tree-node { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tree-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; + color: rgba(230, 237, 246, 0.75); +} + +.tree-row.active { + background: rgba(25, 164, 161, 0.18); + color: #e6edf6; +} + +.tree-caret { + border: none; + background: transparent; + color: rgba(230, 237, 246, 0.7); + padding: 0; + display: inline-flex; +} + +.tree-caret.disabled { + width: 18px; +} + +.tree-icon.folder { + color: rgba(25, 164, 161, 0.9); +} + +.tree-label { + flex: 1; +} + +.tree-tag-count { + background: rgba(25, 164, 161, 0.2); + padding: 2px 6px; + border-radius: 999px; + font-size: 11px; +} + +.upload-status { + display: grid; + gap: 8px; + padding: 10px 12px; + margin-bottom: 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.04); +} + +.upload-progress { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 999px; + overflow: hidden; +} + +.upload-progress div { + height: 100%; + background: linear-gradient(120deg, #19a4a1, #11709c); +} + +.upload-dropzone { + border: 1px dashed rgba(255, 255, 255, 0.2); + padding: 16px; + border-radius: 12px; + text-align: center; + cursor: pointer; + margin-top: 12px; + color: rgba(230, 237, 246, 0.7); +} + +.upload-dropzone.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.dropzone-title { + font-weight: 600; + margin-bottom: 4px; +} + +.dropzone-hint { + font-size: 12px; + color: rgba(230, 237, 246, 0.6); } .modal-backdrop { position: fixed; inset: 0; - background: rgba(16, 18, 22, 0.35); - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; + background: rgba(5, 8, 12, 0.7); + display: grid; + place-items: center; + z-index: 40; } .modal-card { - background: #ffffff; - border-radius: 12px; - padding: 16px; - width: min(420px, 90vw); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18); - display: flex; - flex-direction: column; + background: #0f1622; + border-radius: 14px; + padding: 20px; + min-width: min(420px, 90vw); + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; gap: 12px; } -.modal-card h3 { - margin: 0; - font-size: 16px; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.modal-body { - display: flex; - flex-direction: column; - gap: 8px; -} - -.modal-label { - font-size: 12px; - color: #5a5f6b; -} - -.modal-card input { - width: 100%; -} - .modal-actions { display: flex; justify-content: flex-end; gap: 8px; + margin-top: 6px; } -.upload-dropzone { - margin-top: 12px; - border: 1px dashed #d5d9e0; - border-radius: 12px; - padding: 14px; - background: #fbfbfc; - display: flex; - flex-direction: column; - gap: 6px; - cursor: pointer; +.modal-header h3 { + margin: 0; +} + +.modal-body { + display: grid; + gap: 10px; +} + +.modal-label { font-size: 12px; - color: #6a7280; + color: rgba(230, 237, 246, 0.6); } -.upload-dropzone input[type='file'] { - display: none; -} +@media (max-width: 960px) { + .explorer { + grid-template-columns: 1fr; + } -.upload-dropzone.disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.dropzone-title { - font-size: 13px; - color: #2f3442; - font-weight: 600; -} - -.dropzone-hint { - font-size: 12px; - color: #6a7280; -} - -.dropzone-files { - font-size: 11px; - color: #3b3f46; - word-break: break-word; -} - -.upload-progress { - height: 6px; - border-radius: 999px; - background: #eef1f5; - overflow: hidden; - margin-top: 10px; -} - -.upload-progress > div { - height: 100%; - background: #5b7cfa; - width: 0%; - transition: width 0.2s ease; -} - -.upload-status { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 10px; - padding: 10px 12px; - border: 1px solid #e6e8ec; - border-radius: 10px; - background: #fbfbfc; - font-size: 12px; - color: #3b3f46; -} - -.tree-node { - margin-left: 0; -} - -.tree-label { - display: inline-block; - user-select: none; - cursor: pointer; -} - -.depth-1 .tree-row { - margin-left: 12px; -} - -.depth-2 .tree-row { - margin-left: 24px; -} - -.depth-3 .tree-row { - margin-left: 36px; -} - -.depth-4 .tree-row { - margin-left: 48px; -} - -.depth-5 .tree-row { - margin-left: 60px; + .explorer-sidebar { + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } } diff --git a/gui/frontend_old/index.html b/gui/frontend_old/index.html new file mode 100644 index 0000000..7df8f21 --- /dev/null +++ b/gui/frontend_old/index.html @@ -0,0 +1,12 @@ + + + + + + Klangkiste Control Panel + + +
+ + + diff --git a/gui/frontend_old/package-lock.json b/gui/frontend_old/package-lock.json new file mode 100644 index 0000000..bd3a2f0 --- /dev/null +++ b/gui/frontend_old/package-lock.json @@ -0,0 +1,2743 @@ +{ + "name": "klangkiste-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "klangkiste-frontend", + "version": "0.1.0", + "dependencies": { + "@phosphor-icons/react": "2.1.7", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "4.3.1", + "vite": "5.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", + "integrity": "sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha512-KbiZEa9/vofNcVJXGwdWWn25reQ3V3dHBWbS07FTF3/TOehLnm9GEhJV4T6ZvGPkShRpmUqYwnaCrkj0mRnP6Q==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha512-FFN5KwpvvQTTS5hWPxrU8/QE4kQUc6uwZcrnlMBN82t1MgAtq8mnoDwINBly9Tdr02seeIIhtdF+UH1feBYGog==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==", + "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + }, + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)", + "optional": true + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=0.12" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "2.81.0", + "source-map": "^0.5.3" + } + }, + "node_modules/less/node_modules/ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/less/node_modules/assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha512-u1L0ZLywRziOVjUhRxI0Qg9G+4RnFB9H/Rq40YWn0dieDgO7vAYeJz6jKAO6t/aruzlDFLAPkQTT87e+f8Imaw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/less/node_modules/aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha512-JnJpAS0p9RmixkOvW2XwDxxzs1bd4/VAGIl6Q0EC5YOo+p+hqIhtDhn/nmFnB/xUNXbLkpE2mOjgVIBRKD4xYw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/less/node_modules/form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha512-8HWGSLAPr+AG0hBpsqi5Ob8HrLStN/LWeqhpFl14d7FJgHK48TmgLoALPz69XSUR65YJzDfLUX/BM8+MLJLghQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/less/node_modules/har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha512-f8xf2GOR6Rgwc9FPTLNzgwB+JQ2/zMauYXSWmX5YV5acex6VomT0ocSuwR7BfXo5MpHi+jL+saaux2fwsGJDKQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha512-5Gbp6RAftMYYV3UEI4c4Vv3+a4dQ7taVyvHt+/L6kRt+f4HX1GweAk5UDWN0SvdVnRBzGQ6OG89pGaD9uSFnVw==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha512-iUn0NcRULlDGtqNLN1Jxmzayk8ogm7NToldASyZBpM2qggbphjXzNOiw3piN8tgz+e/DRs6X5gAzFwTI6BCRcg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/less/node_modules/oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha512-VlF07iu3VV3+BTXj43Nmp6Irt/G7j/NgEctUS6IweH1RGhURjjCc2NWtzXFPXXWWfc7hgbXQdtiQu2LGp6MxUg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/less/node_modules/performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/less/node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/less/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/less/node_modules/qs": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.1.tgz", + "integrity": "sha512-LQy1Q1fcva/UsnP/6Iaa4lVeM49WiOitu2T4hZCyA/elLKu37L99qcBJk4VCCk+rdLvnMzfKyiN3SZTqdAZGSQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/less/node_modules/request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha512-IZnsR7voF0miGSu29EXPRgPTuEsI/+aibNSBbN1pplrfartF5wDYGADz3iD9vmBVf2r00rckWZf8BtS5kk7Niw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/less/node_modules/tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha512-7bgVOAnPj3XjrKY577S+puCKGCRlUrcrEdsMeRXlg9Ghf5df/xNi6sONUa43WrHUd3TjJBF7O04jYoiY0FVa0A==", + "deprecated": "This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dev": true, + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense", + "optional": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/vite": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", + "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/gui/frontend_old/package.json b/gui/frontend_old/package.json new file mode 100644 index 0000000..874da08 --- /dev/null +++ b/gui/frontend_old/package.json @@ -0,0 +1,20 @@ +{ + "name": "klangkiste-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@phosphor-icons/react": "2.1.7", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "4.3.1", + "vite": "5.4.3" + } +} diff --git a/gui/frontend_old/src/App.jsx b/gui/frontend_old/src/App.jsx new file mode 100644 index 0000000..b89b8c8 --- /dev/null +++ b/gui/frontend_old/src/App.jsx @@ -0,0 +1,2207 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { + ArrowUp, + ArrowRight, + CaretDown, + CaretRight, + FloppyDisk, + FileAudio, + Folder, + List, + PencilSimple, + Plus, + Trash, + UploadSimple, +} from '@phosphor-icons/react'; +import { + getBoxes, + getStatus, + getMediaTree, + createMediaFolder, + renameMedia, + moveMedia, + deleteMedia, + uploadMedia, + getTags, + getBoxTags, + getBoxLocalTags, + getTagBlocks, + claimTag, + markTagWritten, + assignTag, + unassignTag, + deleteTag, + setTagMedia, + pullTagFromBox, + setTagBlock, + setBoxAlias, + setTagAlias, + pairBox, + sendCommand, + unpairBox, +} from './api.js'; + +const BOX_POLL_MS = 1500; +const STATUS_POLL_MS = 1000; + +function formatTime(ts) { + if (!ts) return '-'; + return new Date(ts * 1000).toLocaleString(); +} + +function parseCapabilities(raw) { + if (!raw) return '-'; + try { + const data = JSON.parse(raw); + return Object.entries(data) + .map(([key, value]) => `${key}:${value ? '1' : '0'}`) + .join(' '); + } catch (error) { + return '-'; + } +} + +function formatSize(bytes) { + if (bytes === 0) return '0 B'; + if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return '-'; + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let idx = 0; + while (size >= 1024 && idx < units.length - 1) { + size /= 1024; + idx += 1; + } + return `${size.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; +} + +function formatDuration(seconds) { + if (seconds === null || seconds === undefined || Number.isNaN(seconds)) return '-'; + const total = Math.max(0, Math.floor(seconds)); + const mins = Math.floor(total / 60); + const secs = total % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function generateTagId() { + const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let generated = ''; + for (let i = 0; i < 10; i += 1) { + generated += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return generated; +} + +export default function App() { + const [boxes, setBoxes] = useState([]); + const [selectedId, setSelectedId] = useState(''); + const [status, setStatus] = useState(null); + const [error, setError] = useState(''); + const [nfcUid, setNfcUid] = useState('UID_1'); + const [mediaTree, setMediaTree] = useState(null); + const [mediaError, setMediaError] = useState(''); + const [currentPath, setCurrentPath] = useState(''); + const [selectedPaths, setSelectedPaths] = useState([]); + const [newFolderName, setNewFolderName] = useState(''); + const [renameName, setRenameName] = useState(''); + const [moveTarget, setMoveTarget] = useState(''); + const [activeModal, setActiveModal] = useState(''); + const [tagDeleteTarget, setTagDeleteTarget] = useState(''); + const [expandedFolders, setExpandedFolders] = useState(() => new Set(['__root__'])); + const [sidebarQuery, setSidebarQuery] = useState(''); + const [uploadAfterCreate, setUploadAfterCreate] = useState(false); + const [pendingUploadFiles, setPendingUploadFiles] = useState([]); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadInProgress, setUploadInProgress] = useState(false); + const [activeUploadLabel, setActiveUploadLabel] = useState(''); + const transferTimerRef = useRef(null); + const explorerRef = useRef(null); + const modalRef = useRef(null); + const uploadDropInputRef = useRef(null); + const dragSelectRef = useRef(false); + const lastDblClickRef = useRef(0); + const lastAnchorRef = useRef(''); + const explorerInitRef = useRef(false); + const expandedInitRef = useRef(false); + const [tags, setTags] = useState([]); + const [boxTags, setBoxTags] = useState([]); + const [blockedByBox, setBlockedByBox] = useState({}); + const [dbTagMedia, setDbTagMedia] = useState({}); + const [localBoxTags, setLocalBoxTags] = useState([]); + const [localBoxError, setLocalBoxError] = useState(''); + const [importTargetFolder, setImportTargetFolder] = useState(''); + const [importTargetUid, setImportTargetUid] = useState(''); + const [scanTagUid, setScanTagUid] = useState(''); + const [scanTagLabel, setScanTagLabel] = useState(''); + const [scanMediaPath, setScanMediaPath] = useState(''); + const [reuseTagUid, setReuseTagUid] = useState(''); + const lastNfcKeyRef = useRef(''); + const toastCounter = useRef(0); + const [toasts, setToasts] = useState([]); + const [tagAliasDrafts, setTagAliasDrafts] = useState({}); + const [boxAliasDrafts, setBoxAliasDrafts] = useState({}); + + function addToast(type, message) { + const id = `${Date.now()}-${toastCounter.current}`; + toastCounter.current += 1; + setToasts((prev) => [...prev, { id, type, message }]); + setTimeout(() => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, 4000); + } + + useEffect(() => { + let active = true; + + async function refreshBoxes() { + const response = await getBoxes(); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Fehler beim Laden der Boxen.'); + return; + } + setBoxes(response.data.boxes || []); + setError(''); + } + + refreshBoxes(); + const handle = setInterval(refreshBoxes, BOX_POLL_MS); + return () => { + active = false; + clearInterval(handle); + }; + }, []); + + useEffect(() => { + function handleOutside(event) { + if (activeModal) return; + if (!explorerRef.current) return; + if (modalRef.current && modalRef.current.contains(event.target)) return; + if (!explorerRef.current.contains(event.target)) { + setSelectedPaths([]); + setRenameName(''); + } + } + function handleMouseUp() { + dragSelectRef.current = false; + } + window.addEventListener('mousedown', handleOutside); + window.addEventListener('mouseup', handleMouseUp); + return () => window.removeEventListener('mousedown', handleOutside); + }, [activeModal]); + + useEffect(() => { + if (!selectedId) { + setStatus(null); + setMediaTree(null); + setMediaError(''); + setSelectedPaths([]); + setNewFolderName(''); + setRenameName(''); + setActiveModal(''); + setBoxTags([]); + setDbTagMedia({}); + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + setReuseTagUid(''); + setLocalBoxTags([]); + setLocalBoxError(''); + return; + } + + let active = true; + + async function refreshStatus() { + const response = await getStatus(selectedId); + if (!active) return; + if (!response.ok) { + setStatus({ error: response.data.detail || 'Status nicht verfuegbar.' }); + return; + } + setStatus(response.data); + } + + refreshStatus(); + const handle = setInterval(refreshStatus, STATUS_POLL_MS); + return () => { + active = false; + clearInterval(handle); + }; + }, [selectedId]); + + useEffect(() => { + let active = true; + + async function refreshMedia() { + const response = await getMediaTree(); + if (!active) return; + if (!response.ok) { + setMediaError(response.data.detail || 'Medien nicht verfuegbar.'); + setMediaTree(null); + return; + } + setMediaTree(response.data); + setMediaError(''); + } + + refreshMedia(); + return () => { + active = false; + }; + }, []); + + function findPathChain(node, target, chain = []) { + if (!node) return null; + const currentPath = node.path || ''; + const nextChain = [...chain, currentPath]; + if (currentPath === target) { + return nextChain; + } + if (!Array.isArray(node.children)) return null; + for (const child of node.children) { + if (child.type !== 'folder') continue; + const result = findPathChain(child, target, nextChain); + if (result) return result; + } + return null; + } + + useEffect(() => { + if (!mediaTree) return; + const saved = localStorage.getItem('klangkiste_explorer_path'); + if (saved === null) return; + const chain = findPathChain(mediaTree, saved); + if (!chain) return; + setCurrentPath(saved); + if (!expandedInitRef.current) { + const savedExpandedRaw = localStorage.getItem('klangkiste_explorer_expanded'); + let savedExpanded = []; + if (savedExpandedRaw) { + try { + const parsed = JSON.parse(savedExpandedRaw); + if (Array.isArray(parsed)) { + savedExpanded = parsed; + } + } catch (error) { + savedExpanded = []; + } + } + const merged = new Set([ + ...savedExpanded, + ...chain.map((p) => (p ? p : '__root__')), + ]); + setExpandedFolders(merged); + expandedInitRef.current = true; + } + explorerInitRef.current = true; + }, [mediaTree]); + + useEffect(() => { + if (!explorerInitRef.current) return; + localStorage.setItem('klangkiste_explorer_path', currentPath || ''); + }, [currentPath]); + + useEffect(() => { + if (!expandedInitRef.current) return; + localStorage.setItem( + 'klangkiste_explorer_expanded', + JSON.stringify(Array.from(expandedFolders)) + ); + }, [expandedFolders]); + + useEffect(() => { + const lastNfc = status?.last_nfc; + if (!lastNfc || lastNfc.known || !lastNfc.uid) { + return; + } + const key = `${status?.last_nfc_at ?? ''}:${lastNfc.uid ?? ''}`; + if (lastNfcKeyRef.current === key) { + return; + } + lastNfcKeyRef.current = key; + setScanTagLabel(''); + setScanMediaPath(''); + const hasUid = Boolean(lastNfc.uid); + const existsInDb = hasUid && tags.some((tag) => tag.uid === lastNfc.uid); + if (existsInDb) { + setScanTagUid(lastNfc.uid); + } else { + setScanTagUid(generateTagId()); + } + }, [status, tags]); + + useEffect(() => { + let active = true; + + async function refreshTags() { + const response = await getTags(); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Tags nicht verfuegbar.'); + return; + } + setTags(response.data.tags || []); + } + + refreshTags(); + return () => { + active = false; + }; + }, []); + + useEffect(() => { + if (!selectedId) return; + let active = true; + + async function refreshBoxTags() { + const response = await getBoxTags(selectedId); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Box-Tags nicht verfuegbar.'); + return; + } + setBoxTags(response.data.tags || []); + } + + refreshBoxTags(); + return () => { + active = false; + }; + }, [selectedId]); + + useEffect(() => { + if (!selectedId) return; + let active = true; + + async function refreshLocalTags() { + const response = await getBoxLocalTags(selectedId); + if (!active) return; + if (!response.ok) { + setLocalBoxError(response.data.detail || 'Lokale Box-Tags nicht verfuegbar.'); + return; + } + setLocalBoxTags(response.data.tags || []); + setLocalBoxError(''); + } + + refreshLocalTags(); + return () => { + active = false; + }; + }, [selectedId]); + + useEffect(() => { + let active = true; + + async function refreshBlocked() { + if (!boxes.length) { + setBlockedByBox({}); + return; + } + const results = await Promise.all( + boxes.map(async (box) => { + const response = await getTagBlocks(box.box_id); + return { boxId: box.box_id, response }; + }) + ); + if (!active) return; + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + + refreshBlocked(); + return () => { + active = false; + }; + }, [boxes]); + + const unpaired = useMemo( + () => boxes.filter((box) => box.state === 'UNPAIRED'), + [boxes] + ); + const paired = useMemo( + () => boxes.filter((box) => box.state === 'PAIRED'), + [boxes] + ); + + const mediaTagCounts = useMemo(() => { + const counts = {}; + tags.forEach((tag) => { + const mediaPath = tag.media_path || ''; + if (!mediaPath) return; + counts[mediaPath] = (counts[mediaPath] || 0) + 1; + }); + return counts; + }, [tags]); + + const topLevelFolders = useMemo(() => { + if (!mediaTree || !Array.isArray(mediaTree.children)) return []; + const folders = mediaTree.children.filter((child) => child.type === 'folder'); + const query = sidebarQuery.trim().toLowerCase(); + if (!query) return folders; + return folders.filter((folder) => (folder.name || '').toLowerCase().includes(query)); + }, [mediaTree, sidebarQuery]); + + const filteredTree = useMemo(() => { + if (!mediaTree) return null; + return { + ...mediaTree, + children: topLevelFolders, + }; + }, [mediaTree, topLevelFolders]); + + async function handlePair(boxId) { + const response = await pairBox(boxId); + if (!response.ok) { + setError(response.data.detail || 'Pairing fehlgeschlagen.'); + addToast('error', response.data.detail || 'Pairing fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', `Box ${boxId} gepairt.`); + } + + async function handleCommand(command, payload = {}) { + if (!selectedId) { + setError('Bitte zuerst eine gepairte Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine gepairte Box auswaehlen.'); + return; + } + const response = await sendCommand(selectedId, command, payload); + if (!response.ok) { + setError(response.data.detail || 'Command fehlgeschlagen.'); + addToast('error', response.data.detail || 'Command fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', `Command ausgefuehrt: ${command}`); + } + + async function handleUnpair(boxId) { + const response = await unpairBox(boxId); + if (!response.ok) { + setError(response.data.detail || 'Unpair fehlgeschlagen.'); + addToast('error', response.data.detail || 'Unpair fehlgeschlagen.'); + return; + } + if (selectedId === boxId) { + setSelectedId(''); + setStatus(null); + } + setError(''); + addToast('success', `Box ${boxId} entkoppelt.`); + } + + async function handleMediaRefresh() { + const response = await getMediaTree(); + if (!response.ok) { + setMediaError(response.data.detail || 'Medien nicht verfuegbar.'); + addToast('error', response.data.detail || 'Medien nicht verfuegbar.'); + setMediaTree(null); + return; + } + setMediaTree(response.data); + setMediaError(''); + addToast('success', 'Medienliste aktualisiert.'); + } + + function getNodeByPath(node, targetPath) { + if (!node) return null; + if ((node.path || '') === targetPath) return node; + if (!Array.isArray(node.children)) return null; + for (const child of node.children) { + const found = getNodeByPath(child, targetPath); + if (found) return found; + } + return null; + } + + function listChildren(node) { + if (!node || !Array.isArray(node.children)) return []; + const folders = node.children.filter((child) => child.type === 'folder'); + const files = node.children.filter((child) => child.type === 'file'); + folders.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + return [...folders, ...files]; + } + + function buildBreadcrumb(pathValue) { + if (!pathValue) return []; + const parts = pathValue.split('/').filter(Boolean); + const crumbs = []; + let acc = ''; + parts.forEach((part) => { + acc = acc ? `${acc}/${part}` : part; + crumbs.push({ name: part, path: acc }); + }); + return crumbs; + } + + function collectTopLevelFolders(node) { + if (!node || !Array.isArray(node.children)) return []; + return node.children + .filter((child) => child.type === 'folder' && child.path) + .map((child) => child.path); + } + + function collectFolderPaths(node) { + if (!node || node.type !== 'folder') return []; + const paths = []; + if (node.path) paths.push(node.path); + if (!Array.isArray(node.children)) return paths; + node.children.forEach((child) => { + if (child.type === 'folder') { + paths.push(...collectFolderPaths(child)); + } + }); + return paths; + } + + function isSelected(pathValue) { + return selectedPaths.includes(pathValue); + } + + async function handleCreateFolder() { + if (!uploadAfterCreate && !newFolderName.trim()) { + addToast('error', 'Ordnername fehlt.'); + return; + } + if (uploadAfterCreate && pendingUploadFiles.length === 0) { + addToast('error', 'Bitte Audiodateien auswaehlen.'); + return; + } + + const trimmedName = newFolderName.trim(); + const needsFolder = uploadAfterCreate && !trimmedName && !currentPath; + if (needsFolder) { + addToast('error', 'Bitte zuerst einen Ordner anlegen.'); + return; + } + + let targetPath = currentPath; + if (trimmedName) { + const response = await createMediaFolder(currentPath, trimmedName); + if (!response.ok) { + addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.'); + return; + } + targetPath = currentPath ? `${currentPath}/${trimmedName}` : trimmedName; + addToast('success', 'Ordner angelegt.'); + await handleMediaRefresh(); + } + + setNewFolderName(''); + if (uploadAfterCreate) { + setActiveUploadLabel(`Upload: ${pendingUploadFiles.length} Datei(en)`); + setUploadInProgress(true); + setUploadProgress(0); + const uploadResponse = await uploadMedia( + targetPath, + pendingUploadFiles, + (percent) => setUploadProgress(percent) + ); + if (!uploadResponse.ok) { + addToast('error', uploadResponse.data.detail || 'Upload fehlgeschlagen.'); + setUploadInProgress(false); + return; + } + setUploadInProgress(false); + setActiveUploadLabel(''); + setUploadAfterCreate(false); + setPendingUploadFiles([]); + if (targetPath) { + setCurrentPath(targetPath); + } + addToast('success', 'Upload abgeschlossen.'); + await handleMediaRefresh(); + setActiveModal(''); + return; + } + + setActiveModal(''); + } + + async function handleRename() { + if (selectedPaths.length !== 1) { + addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); + return; + } + if (!renameName.trim()) { + addToast('error', 'Neuer Name fehlt.'); + return; + } + const response = await renameMedia(selectedPaths[0], renameName.trim()); + if (!response.ok) { + addToast('error', response.data.detail || 'Umbenennen fehlgeschlagen.'); + return; + } + setActiveModal(''); + addToast('success', 'Umbenannt.'); + await handleMediaRefresh(); + } + + async function handleDeleteSelected() { + if (selectedPaths.length === 0) { + addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); + return; + } + for (const pathValue of selectedPaths) { + const response = await deleteMedia(pathValue); + if (!response.ok) { + addToast('error', response.data.detail || 'Loeschen fehlgeschlagen.'); + return; + } + } + setSelectedPaths([]); + setRenameName(''); + setActiveModal(''); + addToast('success', 'Geloescht.'); + await handleMediaRefresh(); + } + + async function handleMoveSelected() { + if (selectedPaths.length === 0) { + addToast('error', 'Bitte zuerst einen Eintrag waehlen.'); + return; + } + if (!moveTarget) { + addToast('error', 'Bitte Zielordner waehlen.'); + return; + } + const isRootTarget = moveTarget === '__root__'; + if (isRootTarget) { + const hasFile = selectedPaths.some((pathValue) => { + const node = getNodeByPath(mediaTree, pathValue); + return node && node.type === 'file'; + }); + if (hasFile) { + addToast('error', 'Dateien duerfen nicht in den Root-Ordner.'); + return; + } + } + for (const pathValue of selectedPaths) { + const response = await moveMedia(pathValue, isRootTarget ? '' : moveTarget); + if (!response.ok) { + addToast('error', response.data.detail || 'Verschieben fehlgeschlagen.'); + return; + } + } + setActiveModal(''); + addToast('success', 'Verschoben.'); + await handleMediaRefresh(); + } + + async function handleUpload(event) { + const files = Array.from(event.target.files || []); + if (!currentPath) { + addToast('error', 'Bitte zuerst einen Ordner anlegen.'); + event.target.value = ''; + return; + } + if (!files.length) return; + const audioFiles = files.filter((file) => file.type.startsWith('audio/')); + if (!audioFiles.length) { + addToast('error', 'Nur Audiodateien erlaubt.'); + return; + } + setActiveUploadLabel(`Upload: ${audioFiles.length} Datei(en)`); + setUploadInProgress(true); + setUploadProgress(0); + const response = await uploadMedia(currentPath, audioFiles, (percent) => + setUploadProgress(percent) + ); + if (!response.ok) { + addToast('error', response.data.detail || 'Upload fehlgeschlagen.'); + setUploadInProgress(false); + setActiveUploadLabel(''); + return; + } + setUploadInProgress(false); + setActiveUploadLabel(''); + addToast('success', 'Upload abgeschlossen.'); + await handleMediaRefresh(); + } + + function handleSelect(item, event) { + const itemPath = item.path || ''; + if (Date.now() - lastDblClickRef.current < 250) { + return; + } + if (event && event.shiftKey && lastAnchorRef.current) { + const node = getNodeByPath(mediaTree, currentPath); + const visible = listChildren(node).map((child) => child.path || ''); + const start = visible.indexOf(lastAnchorRef.current); + const end = visible.indexOf(itemPath); + if (start !== -1 && end !== -1) { + const [from, to] = start < end ? [start, end] : [end, start]; + setSelectedPaths(visible.slice(from, to + 1)); + lastAnchorRef.current = itemPath; + return; + } + } + if (event && (event.metaKey || event.ctrlKey)) { + setSelectedPaths((prev) => + prev.includes(itemPath) + ? prev.filter((p) => p !== itemPath) + : [...prev, itemPath] + ); + lastAnchorRef.current = itemPath; + return; + } + setSelectedPaths([itemPath]); + setRenameName(item.name || ''); + lastAnchorRef.current = itemPath; + } + + function handleDragSelect(item) { + if (!dragSelectRef.current) return; + const itemPath = item.path || ''; + setSelectedPaths((prev) => + prev.includes(itemPath) ? prev : [...prev, itemPath] + ); + } + + function handleOpen(item) { + if (item.type !== 'folder') return; + setCurrentPath(item.path || ''); + setSelectedPaths([]); + setRenameName(''); + const key = item.path || '__root__'; + setExpandedFolders((prev) => new Set(prev).add(key)); + } + + function handleGoUp() { + if (!currentPath) return; + const parts = currentPath.split('/').filter(Boolean); + parts.pop(); + const next = parts.join('/'); + setCurrentPath(next); + setSelectedPaths([]); + setRenameName(''); + } + + function toggleFolder(pathValue) { + const key = pathValue || '__root__'; + setExpandedFolders((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + } + + function renderFolderTree(node, depth = 0) { + if (!node || node.type !== 'folder') return null; + const isActive = (node.path || '') === currentPath; + const key = node.path || '__root__'; + const isExpanded = expandedFolders.has(key); + const childFolders = + Array.isArray(node.children) && node.children.filter((child) => child.type === 'folder'); + const hasChildren = childFolders && childFolders.length > 0; + const tagCount = mediaTagCounts[node.path || ''] || 0; + return ( +
+
handleOpen(node)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') handleOpen(node); + }} + > + {hasChildren ? ( + + ) : ( +
+ {isExpanded && + hasChildren && + childFolders.map((child) => renderFolderTree(child, depth + 1))} +
+ ); + } + + async function handleClaimTagForScan() { + const currentUid = scanTagUid.trim(); + const existing = tags.find((tag) => tag.uid === currentUid); + if (existing) { + if (existing.status === 'NEW') { + const written = await markTagWritten(currentUid); + if (!written.ok) { + setError(written.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', written.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + } + } else { + const response = await claimTag(currentUid, scanTagLabel.trim()); + if (!response.ok) { + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + setScanTagUid(response.data.uid || ''); + } + setError(''); + addToast('success', 'Tag geschrieben.'); + const shouldAssign = + selectedId && status?.last_nfc?.known === false && scanMediaPath; + if (shouldAssign) { + const mediaSet = await setTagMedia(currentUid, scanMediaPath); + if (!mediaSet.ok) { + setError(mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + const assigned = await assignTag(currentUid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + addToast('success', 'Tag zugeordnet.'); + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + } else if (selectedId && status?.last_nfc?.known === false) { + addToast('success', 'Tag gespeichert. Medium fehlt noch.'); + } + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + } + + async function handleReuseImportedTag() { + if (!reuseTagUid) { + setError('Bitte eine gespeicherte Tag-ID waehlen.'); + addToast('error', 'Bitte eine gespeicherte Tag-ID waehlen.'); + return; + } + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + const tag = tags.find((entry) => entry.uid === reuseTagUid); + if (!tag || !tag.media_path) { + setError('Tag hat keine Medienzuordnung.'); + addToast('error', 'Tag hat keine Medienzuordnung.'); + return; + } + const response = await markTagWritten(reuseTagUid); + if (!response.ok) { + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + const assigned = await assignTag(reuseTagUid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Tag geschrieben und zugeordnet.'); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + setScanTagUid(reuseTagUid); + setReuseTagUid(''); + } + + async function handleWriteTag(uid) { + const response = await markTagWritten(uid); + if (!response.ok) { + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Tag geschrieben.'); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + } + + async function handleAssignFromScan() { + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + if (!scanMediaPath) { + setError('Bitte Medienordner waehlen.'); + addToast('error', 'Bitte Medienordner waehlen.'); + return; + } + const uid = scanTagUid.trim(); + if (!uid) { + setError('Bitte zuerst eine Tag-ID schreiben.'); + addToast('error', 'Bitte zuerst eine Tag-ID schreiben.'); + return; + } + const existing = tags.find((tag) => tag.uid === uid); + if (!existing) { + setError('Tag-ID existiert nicht. Bitte zuerst schreiben.'); + addToast('error', 'Tag-ID existiert nicht. Bitte zuerst schreiben.'); + return; + } + + const mediaSet = await setTagMedia(uid, scanMediaPath); + if (!mediaSet.ok) { + setError(mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + const assigned = await assignTag(uid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + + setError(''); + addToast('success', 'Tag zugeordnet.'); + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + + async function handlePullTagFromBox() { + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + if (!importTargetUid) { + setError('Kein Tag ausgewaehlt.'); + addToast('error', 'Kein Tag ausgewaehlt.'); + return; + } + if (!importTargetFolder.trim()) { + setError('Bitte Ordnernamen eingeben.'); + addToast('error', 'Bitte Ordnernamen eingeben.'); + return; + } + if (uploadInProgress) { + addToast('error', 'Es laeuft bereits ein Upload/Transfer.'); + return; + } + const startTransferProgress = () => { + setActiveUploadLabel(`Uebertragung: ${importTargetFolder.trim()}`); + setUploadInProgress(true); + setUploadProgress(5); + if (transferTimerRef.current) { + clearInterval(transferTimerRef.current); + } + transferTimerRef.current = setInterval(() => { + setUploadProgress((prev) => (prev < 90 ? prev + 5 : prev)); + }, 400); + }; + const stopTransferProgress = (success) => { + if (transferTimerRef.current) { + clearInterval(transferTimerRef.current); + transferTimerRef.current = null; + } + if (success) { + setUploadProgress(100); + setTimeout(() => { + setUploadInProgress(false); + setUploadProgress(0); + setActiveUploadLabel(''); + }, 600); + return; + } + setUploadInProgress(false); + setUploadProgress(0); + setActiveUploadLabel(''); + }; + startTransferProgress(); + const response = await pullTagFromBox( + selectedId, + importTargetUid, + importTargetFolder.trim() + ); + if (!response.ok) { + setError(response.data.detail || 'Import fehlgeschlagen.'); + addToast('error', response.data.detail || 'Import fehlgeschlagen.'); + stopTransferProgress(false); + return; + } + addToast('success', 'Medien vom Box-Tag uebertragen.'); + setImportTargetFolder(''); + setImportTargetUid(''); + setActiveModal(''); + stopTransferProgress(true); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + const updatedLocal = await getBoxLocalTags(selectedId); + if (updatedLocal.ok) { + setLocalBoxTags(updatedLocal.data.tags || []); + } + await handleMediaRefresh(); + } + + async function handleUnassignTag(uid) { + if (!selectedId) return; + const response = await unassignTag(uid, selectedId); + if (!response.ok) { + setError(response.data.detail || 'Zuordnung loeschen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Zuordnung loeschen fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Zuordnung entfernt.'); + const updated = await getBoxTags(selectedId); + if (updated.ok) { + setBoxTags(updated.data.tags || []); + } + } + + async function handleDeleteTag(uid) { + const response = await deleteTag(uid); + if (!response.ok) { + setError(response.data.detail || 'Tag loeschen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag loeschen fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Tag geloescht.'); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleSetTagMedia(uid) { + const mediaPath = dbTagMedia[uid] || ''; + const response = await setTagMedia(uid, mediaPath); + if (!response.ok) { + setError(response.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + let assigned = null; + const lastNfcUid = status?.last_nfc?.uid || ''; + const shouldAssign = + selectedId && + status?.last_nfc?.known === false && + ((lastNfcUid && lastNfcUid === uid) || + (!lastNfcUid && scanTagUid && scanTagUid === uid)); + if (shouldAssign) { + assigned = await assignTag(uid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + } + setError(''); + if (!mediaPath) { + addToast('success', 'Medium entfernt.'); + } else if (assigned) { + addToast('success', 'Medium gespeichert und Tag zugeordnet.'); + } else { + addToast('success', 'Medium gespeichert.'); + } + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleClearTagMedia(uid) { + const response = await setTagMedia(uid, ''); + if (!response.ok) { + setError(response.data.detail || 'Medium entfernen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Medium entfernen fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Medium entfernt.'); + setDbTagMedia((prev) => ({ ...prev, [uid]: '' })); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleToggleTagBlock(boxId, uid, nextBlocked) { + const response = await setTagBlock(boxId, uid, nextBlocked); + if (!response.ok) { + addToast('error', response.data.detail || 'Tag-Sperre fehlgeschlagen.'); + return; + } + setBlockedByBox((prev) => { + const current = new Set(prev[boxId] || []); + if (nextBlocked) { + current.add(uid); + } else { + current.delete(uid); + } + return { ...prev, [boxId]: Array.from(current) }; + }); + addToast( + 'success', + nextBlocked ? 'Tag gesperrt.' : 'Tag freigegeben.' + ); + } + + async function handleSaveTagAlias(uid) { + const value = (tagAliasDrafts[uid] ?? '').trim(); + const response = await setTagAlias(uid, value || null); + if (!response.ok) { + addToast('error', response.data.detail || 'Tag-Alias speichern fehlgeschlagen.'); + return; + } + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + addToast('success', 'Tag-Alias gespeichert.'); + } + + async function handleSaveBoxAlias(boxId) { + const value = (boxAliasDrafts[boxId] ?? '').trim(); + const response = await setBoxAlias(boxId, value || null); + if (!response.ok) { + addToast('error', response.data.detail || 'Box-Alias speichern fehlgeschlagen.'); + return; + } + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); + } + addToast('success', 'Box-Alias gespeichert.'); + } + + const currentItems = mediaTree + ? listChildren(getNodeByPath(mediaTree, currentPath)) + : []; + const showMeta = currentItems.some((item) => item.type === 'file'); + const mediaBytes = mediaTree?.size ?? null; + const freeBytes = mediaTree?.free_bytes ?? null; + + return ( +
+
+

Klangkiste Control Panel

+

Backend: {import.meta.env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'}

+
+ +
+ {toasts.map((toast) => ( +
+ {toast.message} +
+ ))} +
+ + {error &&
{error}
} + +
+
+

Neue Boxen

+ {unpaired.length === 0 &&

Keine neuen Boxen.

} + {unpaired.map((box) => ( +
+
+ {box.alias || box.box_id} +
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Firmware: {box.firmware_version}
+
ID: {box.box_id}
+
+ +
+ ))} +
+ +
+

Gepairte Boxen

+ {paired.length === 0 &&

Noch keine gepairten Boxen.

} + {paired.map((box) => ( +
setSelectedId(box.box_id)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setSelectedId(box.box_id); + }} + > +
+ {box.alias || box.box_id} +
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Capabilities: {parseCapabilities(box.capabilities_json)}
+
ID: {box.box_id}
+
+
+ event.stopPropagation()} + onChange={(event) => { + event.stopPropagation(); + setBoxAliasDrafts((prev) => ({ + ...prev, + [box.box_id]: event.target.value, + })); + }} + /> + + {box.state} + +
+
+ ))} +
+
+ +
+
+

Live-Status

+ {!selectedId &&

Waehle eine gepairte Box aus.

} + {selectedId && !status &&

Status wird geladen...

} + {status && status.error &&

{status.error}

} + {status && !status.error && ( +
{JSON.stringify(status, null, 2)}
+ )} +
+ +
+

Commands

+
+ + + + + + +
+
+ setNfcUid(event.target.value)} + placeholder="UID_1" + /> + + +
+

+ Steuerung ist nur moeglich, wenn die Box gepairt ist. +

+
+
+ + {activeModal && ( +
{ + setActiveModal(''); + setUploadAfterCreate(false); + setPendingUploadFiles([]); + setTagDeleteTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > + {activeModal === 'new-folder' && ( + <> +

{uploadAfterCreate ? 'Upload vorbereiten' : 'Neuen Ordner anlegen'}

+ setNewFolderName(event.target.value)} + placeholder={ + uploadAfterCreate + ? 'Optionaler Ordnername (leer = aktueller Ordner)' + : 'Ordnername' + } + /> + {uploadAfterCreate && ( + <> +
{ + if (uploadInProgress) return; + uploadDropInputRef.current?.click(); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') uploadDropInputRef.current?.click(); + }} + onDragOver={(event) => { + event.preventDefault(); + }} + onDrop={(event) => { + event.preventDefault(); + if (uploadInProgress) return; + const files = Array.from(event.dataTransfer.files || []); + const audioFiles = files.filter((file) => + file.type.startsWith('audio/') + ); + setPendingUploadFiles(audioFiles); + setActiveUploadLabel( + audioFiles.length + ? `Upload: ${audioFiles.length} Datei(en)` + : '' + ); + }} + > +
Audiodateien auswaehlen
+
+ Datei hier ablegen oder klicken, um auszuwaehlen +
+ {pendingUploadFiles.length > 0 && ( +
+ {pendingUploadFiles.map((file) => ( +
{file.name}
+ ))} +
+ )} + { + const files = Array.from(event.target.files || []); + const audioFiles = files.filter((file) => + file.type.startsWith('audio/') + ); + setPendingUploadFiles(audioFiles); + setActiveUploadLabel( + audioFiles.length + ? `Upload: ${audioFiles.length} Datei(en)` + : '' + ); + }} + /> +
+

+ Upload startet direkt. Ohne Ordnername landet er im aktuellen Ordner. +

+ {uploadInProgress && ( +
+
+
+ )} + + )} +
+ + +
+ + )} + {activeModal === 'rename' && ( + <> +

Umbenennen

+ setRenameName(event.target.value)} + placeholder="Neuer Name" + /> +
+ + +
+ + )} + {activeModal === 'delete' && ( + <> +

Loeschen

+

+ {selectedPaths.length} Eintrag(e) wirklich loeschen? +

+
+ + +
+ + )} + {activeModal === 'move' && ( + <> +

Verschieben

+ {selectedPaths.length > 0 && + selectedPaths.every((pathValue) => { + const node = getNodeByPath(mediaTree, pathValue); + return node && node.type === 'file'; + }) && ( +

+ Dateien duerfen nicht in den Root-Ordner verschoben werden. +

+ )} + +
+ + +
+ + )} + {activeModal === 'tag-delete' && ( + <> +

Tag entfernen

+

+ Was moechtest du entfernen? +

+
+ + + +
+ + )} +
+
+ )} + + {activeModal === 'import-tag' && ( +
+
+
+

Tag vom Box-Speicher uebertragen

+
+
+ + setImportTargetFolder(event.target.value)} + placeholder="z. B. grimm_volume_3" + /> + {uploadInProgress && ( +
+
+
+ )} +
+
+ + +
+
+
+ )} + + {status?.last_nfc && status.last_nfc.known === false && status.last_nfc.uid && ( +
+

+ {tags.find((tag) => tag.uid === status.last_nfc.uid && !tag.media_path) + ? 'Leerer Tag erkannt' + : 'Neuer Tag erkannt'} +

+

+ UID erkannt: {status.last_nfc.uid || '-'} +

+ {tags.find((tag) => tag.uid === status.last_nfc.uid) && ( +

+ Dieser Tag ist bekannt, aber noch nicht zugewiesen. +

+ )} + {!tags.find((tag) => tag.uid === status.last_nfc.uid && !tag.media_path) && ( +
+ + setScanTagLabel(event.target.value)} + placeholder="Label (optional)" + /> + +
+ )} + {tags.some((tag) => tag.status === 'IMPORTED') && ( +
+ + +
+ )} +
+ + +
+
+ )} + +
+
+

Medien Explorer

+ +
+ {uploadInProgress && ( +
+ {activeUploadLabel || 'Upload laeuft...'} +
+
+
+
+ )} +

Ordnerverwaltung, Upload und Datei-Listen im Server-Medienordner.

+ {mediaError &&

{mediaError}

} + {!mediaError && !mediaTree &&

Medienliste wird geladen...

} + {!mediaError && mediaTree && ( +
+
+
+ setSidebarQuery(event.target.value)} + aria-label="Ordner suchen" + /> +
+
+ {topLevelFolders.length === 0 && ( +
Keine Ordner gefunden.
+ )} + {filteredTree && renderFolderTree(filteredTree, 0)} +
+
+
+
+ +
+ setCurrentPath('')} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setCurrentPath(''); + }} + > + media + + {buildBreadcrumb(currentPath).map((crumb) => ( + setCurrentPath(crumb.path)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setCurrentPath(crumb.path); + }} + > + / {crumb.name} + + ))} +
+
+ + + + + +
+
+
+
+ Name + {showMeta ? ( + <> + Interpret + Titel + Länge + Größe + + ) : ( + <> + Typ + Größe + + )} +
+ {currentItems.map((item) => ( +
{ + if (event.button !== 0) return; + if (event.metaKey || event.ctrlKey || event.shiftKey) { + return; + } + dragSelectRef.current = true; + handleSelect(item, event); + }} + onMouseEnter={() => handleDragSelect(item)} + onClick={(event) => handleSelect(item, event)} + onDoubleClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + lastDblClickRef.current = Date.now(); + handleOpen(item); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') handleOpen(item); + }} + > + + + {item.type === 'folder' ? ( + + ) : ( + + )} + + {item.name} + + {showMeta ? ( + <> + {item.type === 'folder' ? '-' : item.artist || '-'} + {item.type === 'folder' ? '-' : item.title || '-'} + + {item.type === 'folder' ? '-' : formatDuration(item.duration)} + + {formatSize(item.size)} + + ) : ( + <> + {item.type === 'folder' ? 'Ordner' : 'Datei'} + {formatSize(item.size)} + + )} +
+ ))} + {selectedPaths.length > 0 && ( +
+ + {showMeta ? ( + <> + + + + + {selectedPaths.length} ausgewaehlt + + + ) : ( + <> + + + {selectedPaths.length} ausgewaehlt + + + )} +
+ )} +
+
+
+ + Medienordner: {formatSize(mediaBytes)} · Verfügbar:{' '} + {formatSize(freeBytes)} + +
+
+ )} +
+ +
+
+

Tags nur auf dieser Box

+
+ {localBoxError &&

{localBoxError}

} + {!localBoxError && localBoxTags.length === 0 && ( +

Keine lokalen Tags ohne Server-Zuordnung.

+ )} + {localBoxTags.map((tag) => ( +
+
+ {tag.uid} +
+ Dateien: {tag.file_count} · {formatSize(tag.total_size)} +
+ {tag.media_exists && tag.files?.length > 0 && ( +
    + {tag.files.map((file) => ( +
  • {file}
  • + ))} +
+ )} + {!tag.media_exists && ( +

Keine Dateien im Box-Ordner gefunden.

+ )} +
+
+ +
+
+ ))} +
+ +
+
+

Tags (Datenbank)

+ {tags.length === 0 &&

Keine Tags vorhanden.

} + {tags.map((tag) => ( +
+
+
+ + {tag.alias ? `${tag.alias} (${tag.uid})` : tag.uid} + +
Status: {tag.status}
+
Medium: {tag.media_path || '-'}
+ {tag.label ?
Label: {tag.label}
: null} +
+
+ + setTagAliasDrafts((prev) => ({ + ...prev, + [tag.uid]: event.target.value, + })) + } + /> + + + {dbTagMedia[tag.uid] !== undefined && + dbTagMedia[tag.uid] !== (tag.media_path ?? '') ? ( + + ) : null} + +
+
+
+ ))} +
+
+ +
+

Tag-Matrix (Sperren)

+ {paired.length === 0 && ( +

Keine gepairten Boxen vorhanden.

+ )} + {tags.length === 0 &&

Keine Tags vorhanden.

} + {paired.length > 0 && tags.length > 0 && ( +
+
+ Tag + {paired.map((box) => ( + + {box.alias || box.box_id} + + ))} +
+ {tags.map((tag) => ( +
+ {tag.alias || tag.uid} + {paired.map((box) => { + const blocked = (blockedByBox[box.box_id] || []).includes(tag.uid); + return ( + + ); + })} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/gui/frontend_old/src/api.js b/gui/frontend_old/src/api.js new file mode 100644 index 0000000..666a89d --- /dev/null +++ b/gui/frontend_old/src/api.js @@ -0,0 +1,239 @@ +const defaultHost = + typeof window !== 'undefined' && window.location ? window.location.hostname : '127.0.0.1'; +const API_BASE = '/api'; + +async function readJson(response) { + const text = await response.text(); + try { + return { ok: response.ok, status: response.status, data: JSON.parse(text) }; + } catch (error) { + return { ok: response.ok, status: response.status, data: { detail: text } }; + } +} + +export async function getBoxes() { + const response = await fetch(`${API_BASE}/boxes`); + return readJson(response); +} + +export async function pairBox(boxId) { + const response = await fetch(`${API_BASE}/boxes/pair`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function getStatus(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/status`); + return readJson(response); +} + +export async function getMediaTree() { + const response = await fetch(`${API_BASE}/media-tree`); + return readJson(response); +} + +export async function createMediaFolder(parentPath, name) { + const response = await fetch(`${API_BASE}/media/folder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ parent_path: parentPath, name }), + }); + return readJson(response); +} + +export async function renameMedia(path, name) { + const response = await fetch(`${API_BASE}/media/rename`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, name }), + }); + return readJson(response); +} + +export async function moveMedia(path, targetParent) { + const response = await fetch(`${API_BASE}/media/move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, target_parent: targetParent }), + }); + return readJson(response); +} + +export async function deleteMedia(path) { + const response = await fetch(`${API_BASE}/media/delete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + return readJson(response); +} + +export function uploadMedia(targetPath, files, onProgress) { + const formData = new FormData(); + formData.append('target_path', targetPath); + files.forEach((file) => { + formData.append('files', file); + }); + + return new Promise((resolve) => { + const request = new XMLHttpRequest(); + request.open('POST', `${API_BASE}/media/upload`); + request.upload.onprogress = (event) => { + if (!event.lengthComputable || !onProgress) return; + const percent = Math.round((event.loaded / event.total) * 100); + onProgress(percent); + }; + request.onload = () => { + try { + const data = JSON.parse(request.responseText || '{}'); + resolve({ ok: request.status >= 200 && request.status < 300, status: request.status, data }); + } catch (error) { + resolve({ + ok: request.status >= 200 && request.status < 300, + status: request.status, + data: { detail: request.responseText || '' }, + }); + } + }; + request.onerror = () => { + resolve({ ok: false, status: 0, data: { detail: 'network error' } }); + }; + request.send(formData); + }); +} + +export async function getTags() { + const response = await fetch(`${API_BASE}/tags`); + return readJson(response); +} + +export async function generateTag(label) { + const response = await fetch(`${API_BASE}/tags/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ label }), + }); + return readJson(response); +} + +export async function claimTag(uid, label) { + const response = await fetch(`${API_BASE}/tags/claim`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, label }), + }); + return readJson(response); +} + +export async function markTagWritten(uid) { + const response = await fetch(`${API_BASE}/tags/${uid}/write`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + return readJson(response); +} + +export async function getBoxTags(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tags`); + return readJson(response); +} + +export async function getBoxLocalTags(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/local-tags`); + return readJson(response); +} + +export async function getTagBlocks(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`); + return readJson(response); +} + +export async function setTagBlock(boxId, uid, blocked) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, blocked }), + }); + return readJson(response); +} + +export async function setBoxAlias(boxId, alias) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/alias`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ alias }), + }); + return readJson(response); +} + +export async function setTagAlias(uid, alias) { + const response = await fetch(`${API_BASE}/tags/${uid}/alias`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ alias }), + }); + return readJson(response); +} + +export async function assignTag(uid, boxId) { + const response = await fetch(`${API_BASE}/tags/${uid}/assign`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function unassignTag(uid, boxId) { + const response = await fetch(`${API_BASE}/tags/${uid}/unassign`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function deleteTag(uid) { + const response = await fetch(`${API_BASE}/tags/${uid}`, { + method: 'DELETE', + }); + return readJson(response); +} + +export async function setTagMedia(uid, mediaPath) { + const response = await fetch(`${API_BASE}/tags/${uid}/media`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ media_path: mediaPath }), + }); + return readJson(response); +} + +export async function pullTagFromBox(boxId, uid, targetFolder) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/pull-tag`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, target_folder: targetFolder }), + }); + return readJson(response); +} + +export async function sendCommand(boxId, command, payload = {}) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/command`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command, payload }), + }); + return readJson(response); +} + +export async function unpairBox(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/unpair`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + return readJson(response); +} diff --git a/gui/frontend_old/src/main.jsx b/gui/frontend_old/src/main.jsx new file mode 100644 index 0000000..d78ecd4 --- /dev/null +++ b/gui/frontend_old/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/gui/frontend_old/src/styles.css b/gui/frontend_old/src/styles.css new file mode 100644 index 0000000..0c0271d --- /dev/null +++ b/gui/frontend_old/src/styles.css @@ -0,0 +1,826 @@ +:root { + color-scheme: light; + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; + color: #131522; + background: #f4f2ef; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; +} + +.toast-container { + position: fixed; + top: 16px; + right: 16px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} + +.toast { + display: flex; + align-items: center; + padding: 10px 14px; + border-radius: 10px; + font-size: 13px; + line-height: 1.4; + min-height: 38px; + box-shadow: 0 8px 20px rgba(13, 16, 31, 0.12); + background: #ffffff; + border: 1px solid #e4e0dd; +} + +.toast.success { + border-color: #b9d9b9; + background: #eef7ee; + color: #205b20; +} + +.toast.error { + border-color: #f2c9c9; + background: #ffefef; + color: #9a2a2a; +} + +.page { + max-width: 1200px; + margin: 0 auto; + padding: 32px 24px 48px; +} + +header { + margin-bottom: 24px; +} + +header h1 { + margin: 0 0 6px; + font-size: 28px; +} + +header p { + margin: 0; + color: #525466; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 0; +} + +.panel { + background: #ffffff; + border-radius: 16px; + padding: 16px; + box-shadow: 0 8px 24px rgba(13, 16, 31, 0.08); +} + +.page > section { + margin-bottom: 24px; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.subheading { + margin: 16px 0 8px; + font-size: 14px; + color: #3b3f46; +} + +.card { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + border: 1px solid #e4e0dd; + border-radius: 12px; + padding: 12px; + margin-top: 12px; + cursor: pointer; +} + +.card.selected { + border-color: #d28735; + box-shadow: 0 0 0 2px rgba(210, 135, 53, 0.2); +} + +.stack { + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; +} + +.stack-inline { + flex-direction: row; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; +} + +.tag-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; +} + +.box-tag-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 12px; + border: 1px solid #e4e0dd; + border-radius: 12px; + margin-top: 12px; +} + +.box-tag-actions { + display: flex; + align-items: flex-start; +} + +.file-list { + margin: 8px 0 0; + padding-left: 18px; + color: #3b3f46; + font-size: 13px; +} + +.file-list li { + margin-bottom: 4px; +} + +.tag-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.alias-input { + border: 1px solid #dde0e6; + border-radius: 8px; + padding: 6px 8px; + font-size: 12px; + min-width: 140px; +} + +.alias-input:focus { + outline: none; + border-color: #cfd4dc; + box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08); +} + +.toggle { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #3b3f46; +} + +.toggle input { + width: 16px; + height: 16px; +} + +.tag-matrix { + display: grid; + gap: 8px; + overflow-x: auto; +} + +.matrix-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(120px, 1fr); + gap: 8px; + align-items: center; +} + +.matrix-row.header { + font-weight: 600; + color: #3b3f46; +} + +.matrix-cell { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #fbfbfc; + font-size: 12px; +} + +.matrix-cell.label { + font-weight: 600; + background: #ffffff; +} + +.meta { + font-size: 12px; + color: #6a6c7a; +} + +.pill { + background: #f0e2d4; + color: #7a4b17; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} + +button { + border: none; + background: #1f243d; + color: #ffffff; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; +} + +button:hover { + background: #2c3252; +} + +select { + border-radius: 8px; + border: 1px solid #d6d1cc; + padding: 8px 10px; + min-width: 180px; + background: #ffffff; +} + +.button-ghost { + background: transparent; + color: #1f243d; + border: 1px solid #d6d1cc; +} + +.button-ghost:hover { + background: #f6f2ee; +} + +.card.compact { + padding: 10px; + margin-top: 10px; +} + +input { + border-radius: 8px; + border: 1px solid #d6d1cc; + padding: 8px 10px; + min-width: 140px; +} + +.status { + background: #111522; + color: #e8e8ef; + padding: 12px; + border-radius: 12px; + font-size: 12px; + overflow: auto; +} + +.error { + background: #ffefef; + color: #9a2a2a; + border: 1px solid #f2c9c9; + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 16px; +} + +.muted { + color: #6a6c7a; +} + +.tree { + border: 1px solid #e6e1dc; + border-radius: 12px; + padding: 12px; + background: #faf7f4; +} + +.explorer { + display: grid; + grid-template-columns: 260px 1fr; + grid-template-rows: 1fr auto; + gap: 0; + border: 1px solid #e2e4e8; + border-radius: 12px; + overflow-x: auto; + overflow-y: hidden; + background: #ffffff; + min-height: 200px; + align-items: stretch; +} + +.explorer-sidebar { + grid-row: 1; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; + background: #ffffff; + height: 100%; +} + +.sidebar-title { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #8a919d; + margin: 0; +} + +.sidebar-toolbar { + display: flex; + align-items: center; + justify-content: flex-start; + border: 1px solid #e6e8ec; + background: #fbfbfc; + padding: 10px 12px; + min-height: 44px; + gap: 10px; +} + +.sidebar-tree { + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #ffffff; + padding: 8px; + overflow: auto; +} + +.sidebar-search { + flex: 1; + width: 100%; + border: 1px solid #dde0e6; + background: #ffffff; + border-radius: 8px; + padding: 6px 10px; + font-size: 12px; + color: #2f3442; + min-width: 0; +} + +.sidebar-search:focus { + outline: none; + border-color: #cfd4dc; + box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08); +} + +.explorer-toolbar, +.sidebar-toolbar { + height: 44px; + min-height: 44px; + box-shadow: 0 1px 2px rgba(16, 18, 22, 0.06); + align-items: center; +} + +.explorer-actions.footer { + padding-top: 6px; +} + +.explorer-main { + grid-row: 1; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px 14px; + background: #ffffff; + height: 100%; +} + +.explorer-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 10px 12px; + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #fbfbfc; +} + +.explorer-path { + display: flex; + flex-wrap: wrap; + gap: 6px; + font-size: 12px; + color: #6a7280; + align-items: center; +} + +.breadcrumb-root { + font-weight: 600; + color: #3b3f46; +} + +.path-link { + background: transparent; + border: none; + color: #3b3f46; + cursor: pointer; + padding: 0; + font-size: 12px; + font-weight: 600; +} + +.path-link:hover, +.path-link:focus { + background: transparent; + color: #3b3f46; + outline: none; +} + + +.toolbar-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 6px; +} + +.icon-button { + border: 1px solid #dde0e6; + background: #ffffff; + color: #1f243d; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.icon-button:hover { + background: #f3f5f8; +} + +.icon-button.danger { + border-color: #f2c9c9; + color: #9a2a2a; +} + +.icon-button.upload { + position: relative; + overflow: hidden; +} + +.icon-button:disabled { + cursor: not-allowed; + opacity: 0.4; +} + +.icon-button.upload input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.explorer-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.explorer-list { + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #ffffff; + overflow: auto; + max-height: 420px; + user-select: none; +} + +.explorer-row { + display: grid; + grid-template-columns: 1fr 120px 90px; + gap: 12px; + padding: 10px 14px; + border-bottom: 1px solid #f4f5f7; + cursor: pointer; +} + +.explorer-list.has-meta .explorer-row { + grid-template-columns: 1fr 160px 200px 80px 90px; +} + +.explorer-footer { + grid-row: 2; + grid-column: 1 / -1; + margin: 0 12px 12px; + padding: 10px 12px; + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #fbfbfc; + font-size: 12px; + color: #3b3f46; + display: flex; + justify-content: flex-end; +} + +.sidebar-tree { + flex: 1; +} + +.explorer-row.header { + background: #f6f7f9; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + cursor: default; +} + +.explorer-row.footer { + background: #fbfbfc; + font-size: 12px; + cursor: default; +} + +.footer-count { + text-align: right; + color: #6a7280; +} + +.explorer-row.selected { + background: #eef3ff; +} + +.tree-row { + background: transparent; + padding: 4px 6px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 6px; +} + +.tree-row.folder { + background: transparent; + font-weight: 500; + color: #3b3f46; +} + +.tree-row.folder:hover { + background: transparent; +} + +.tree-row.active { + background: #e7edf7; +} + +.tree-tag-count { + font-size: 11px; + color: #6a7280; + background: #eef1f5; + border-radius: 999px; + padding: 0 6px; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + line-height: 18px; +} + +.media-tag-count { + margin-left: 6px; + font-size: 11px; + color: #6a7280; + background: #eef1f5; + border-radius: 999px; + padding: 2px 6px; +} + +.tree-caret { + background: transparent; + border: none; + padding: 0; + margin-right: 6px; + color: #8a919d; + cursor: pointer; + font-size: 12px; + width: 16px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.tree-caret:hover, +.tree-caret:focus { + background: transparent; + color: #8a919d; + outline: none; +} + +.tree-caret.disabled { + opacity: 0.35; + cursor: default; +} + +.tree-icon { + margin-right: 6px; + color: #8a919d; + font-size: 11px; +} + +.tree-icon.folder { + color: #5a7bb6; + margin-right: 8px; +} + +.row-name { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.row-icon { + width: 18px; + display: inline-flex; + justify-content: center; + color: #5a7bb6; +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(16, 18, 22, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.modal-card { + background: #ffffff; + border-radius: 12px; + padding: 16px; + width: min(420px, 90vw); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + gap: 12px; +} + +.modal-card h3 { + margin: 0; + font-size: 16px; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-body { + display: flex; + flex-direction: column; + gap: 8px; +} + +.modal-label { + font-size: 12px; + color: #5a5f6b; +} + +.modal-card input { + width: 100%; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.upload-dropzone { + margin-top: 12px; + border: 1px dashed #d5d9e0; + border-radius: 12px; + padding: 14px; + background: #fbfbfc; + display: flex; + flex-direction: column; + gap: 6px; + cursor: pointer; + font-size: 12px; + color: #6a7280; +} + +.upload-dropzone input[type='file'] { + display: none; +} + +.upload-dropzone.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.dropzone-title { + font-size: 13px; + color: #2f3442; + font-weight: 600; +} + +.dropzone-hint { + font-size: 12px; + color: #6a7280; +} + +.dropzone-files { + font-size: 11px; + color: #3b3f46; + word-break: break-word; +} + +.upload-progress { + height: 6px; + border-radius: 999px; + background: #eef1f5; + overflow: hidden; + margin-top: 10px; +} + +.upload-progress > div { + height: 100%; + background: #5b7cfa; + width: 0%; + transition: width 0.2s ease; +} + +.upload-status { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; + padding: 10px 12px; + border: 1px solid #e6e8ec; + border-radius: 10px; + background: #fbfbfc; + font-size: 12px; + color: #3b3f46; +} + +.tree-node { + margin-left: 0; +} + +.tree-label { + display: inline-block; + user-select: none; + cursor: pointer; +} + +.depth-1 .tree-row { + margin-left: 12px; +} + +.depth-2 .tree-row { + margin-left: 24px; +} + +.depth-3 .tree-row { + margin-left: 36px; +} + +.depth-4 .tree-row { + margin-left: 48px; +} + +.depth-5 .tree-row { + margin-left: 60px; +} diff --git a/gui/frontend_old/vite.config.js b/gui/frontend_old/vite.config.js new file mode 100644 index 0000000..ee322b1 --- /dev/null +++ b/gui/frontend_old/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + const rawTarget = env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'; + const apiTarget = rawTarget.replace(/\/api\/?$/, ''); + + return { + plugins: [react()], + server: { + host: true, + port: 5174, + strictPort: true, + proxy: { + '/api': { + target: apiTarget, + changeOrigin: true, + }, + }, + }, + }; +});