diff --git a/.gitignore b/.gitignore index 7401cee..01f41e6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,19 @@ build/ .claude/ Screenshots/ screenshots/ + +# Planning/thought documents +*_SUMMARY.md +*_COMPLETE.md +DEVLOG.md +backend/DEVLOG.md +frontend/DEVLOG.md +BUG_FIXES_*.md +SESSION_SUMMARY_*.md +TEST_PLAN.md +TEST_RESULTS.md +DEPLOYMENT_MATRIX.md +DEPLOYMENT_PI.md +*_QUICKSTART.md +PROXMOX_*.md +memory/ diff --git a/BACKEND_COMPLETE.md b/BACKEND_COMPLETE.md deleted file mode 100644 index 0abf84e..0000000 --- a/BACKEND_COMPLETE.md +++ /dev/null @@ -1,230 +0,0 @@ -# ZMB Webui Backend – KOMPLETT ✅ - -## Übersicht - -Vollständiges **Cockpit-Ersatz-Backend** mit allen Funktionen: -- ✅ ZFS Pool/Dataset/Snapshot Management -- ✅ File Manager (Browse, Upload, Download) -- ✅ User/Group Management (Linux System Users) -- ✅ Samba & NFS Share Management -- ✅ System Info (Hostname, CPU, Memory, Uptime, Updates, Reboot/Shutdown) -- ✅ JWT Authentication + User Management CLI -- ✅ Production-ready Systemd Service - -## Code-Struktur - -``` -backend/ -├── main.py FastAPI App (alle Router eingebunden) -├── requirements.txt Python Dependencies -├── install.sh Auto-Installation für Pi -├── manage_users.py User Management CLI -├── README.md API Documentation -│ -├── services/ -│ ├── zfs_runner.py (401 Lines) ZFS Wrapper + Caching -│ ├── auth.py (104 Lines) JWT + Passwort-Hashing -│ ├── file_manager.py (313 Lines) File Browser + Upload/Download -│ ├── system_users.py (250 Lines) System Users/Groups Management -│ ├── shares.py (220 Lines) Samba & NFS Shares -│ └── system_info.py (270 Lines) System Information -│ -├── routers/ -│ ├── auth.py (38 Lines) Authentication -│ ├── pools.py (59 Lines) ZFS Pools -│ ├── datasets.py (61 Lines) ZFS Datasets -│ ├── snapshots.py (71 Lines) ZFS Snapshots + Rollback -│ ├── files.py (188 Lines) File Manager -│ ├── identities.py (140 Lines) Users & Groups -│ ├── shares.py (95 Lines) Samba & NFS Shares -│ └── system.py (130 Lines) System Management -│ -├── models/ -│ ├── pool.py, dataset.py, snapshot.py, auth.py -│ -└── config/ - └── users.json Default Admin User -``` - -**Gesamt: ~2250+ Lines Python Code** - -## API Endpoints (Complete) - -### 🔐 Authentication -``` -POST /api/auth/login # Login (no auth needed) -POST /api/auth/verify # Verify token -``` - -### 📦 ZFS Pools -``` -GET /api/pools # List pools -GET /api/pools/{name} # Pool status -POST /api/pools/{name}/scrub # Start scrub -``` - -### 📁 ZFS Datasets -``` -GET /api/datasets # List datasets -POST /api/datasets # Create dataset -DELETE /api/datasets/{name} # Delete dataset -``` - -### 📸 ZFS Snapshots -``` -GET /api/snapshots # List snapshots -POST /api/snapshots # Create snapshot -DELETE /api/snapshots/{name} # Delete snapshot -POST /api/snapshots/rollback # Rollback -``` - -### 📂 File Manager (cockpit-files) -``` -GET /api/files/browse # Browse directory -GET /api/files/read # Read text file -GET /api/files/download # Download file -POST /api/files/upload # Upload file -POST /api/files/create # Create file -POST /api/files/mkdir # Create directory -POST /api/files/rename # Rename file -DELETE /api/files/delete # Delete file/directory -GET /api/files/space # Get space usage -``` - -### 👥 Users & Groups (cockpit-identities) -``` -GET /api/identities/users # List system users -GET /api/identities/users/{user} # Get user details -POST /api/identities/users # Create user -DELETE /api/identities/users/{user} # Delete user - -GET /api/identities/groups # List system groups -GET /api/identities/groups/{group} # Get group details -POST /api/identities/groups # Create group -DELETE /api/identities/groups/{group} # Delete group - -POST /api/identities/users/{user}/groups/{group} # Add user to group -``` - -### 🔗 Shares (cockpit-file-sharing) -``` -GET /api/shares/samba # List Samba shares -POST /api/shares/samba # Create Samba share -DELETE /api/shares/samba/{name} # Delete Samba share - -GET /api/shares/nfs # List NFS shares -POST /api/shares/nfs # Create NFS share -DELETE /api/shares/nfs # Delete NFS share -``` - -### 🖥️ System (cockpit-system) -``` -GET /api/system/info # System information -GET /api/system/hostname # Get hostname -POST /api/system/hostname # Set hostname -GET /api/system/uptime # Get uptime -GET /api/system/memory # Memory usage -GET /api/system/cpu # CPU info -GET /api/system/time # Get time -POST /api/system/time # Set time -GET /api/system/updates # Check updates -POST /api/system/reboot # Reboot system -POST /api/system/shutdown # Shutdown system -``` - -## Installation - -```bash -# 1. Backend auf den Pi kopieren -scp -r backend root@10.66.120.3:/tmp/zmb-webui-backend - -# 2. Installation -ssh root@10.66.120.3 -cd /tmp/zmb-webui-backend -sudo bash install.sh - -# 3. Service starten -sudo systemctl start zmb-webui-backend -sudo systemctl enable zmb-webui-backend - -# 4. Passwort ändern (wichtig!) -sudo python3 /opt/zmb-webui/backend/manage_users.py change-password admin -``` - -## Default Credentials - -- Username: `admin` -- Password: `admin123` -- ⚠️ **SOFORT ÄNDERN!** - -## Login & API Usage - -```bash -# 1. Login -TOKEN=$(curl -s -X POST http://10.66.120.3:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"newpassword"}' | jq -r .access_token) - -# 2. Use token für alle API calls -curl http://10.66.120.3:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" - -# 3. Get all shares -curl http://10.66.120.3:8000/api/shares/samba \ - -H "Authorization: Bearer $TOKEN" - -# 4. List system users -curl http://10.66.120.3:8000/api/identities/users \ - -H "Authorization: Bearer $TOKEN" - -# 5. File browser -curl "http://10.66.120.3:8000/api/files/browse?path=/" \ - -H "Authorization: Bearer $TOKEN" -``` - -## Performance (4GB RAM Pi) - -- **gunicorn**: 2 Worker -- **Memory**: 512M soft / 768M hard -- **Caching**: 30-120s TTL (ZFS queries) -- **Timeouts**: 30s request, 5s subprocess - -## Sicherheit - -- ✅ JWT Token-basierte Auth (kein Session) -- ✅ bcrypt Password Hashing -- ✅ Path Traversal Prevention (File Manager) -- ✅ Subprocess Timeout (ZFS Commands) -- ✅ Resource Limits (Systemd) - -## Nächste Schritte - -1. **Phase 2**: Next.js Frontend bauen (Dashboard, File Browser UI, etc.) -2. **Phase 3**: WebSocket für Live-Updates -3. **Phase 4**: Alerts, Monitoring, Full Deployment - -## Testing - -Alle Module compilieren erfolgreich: -```bash -python3 -m py_compile main.py models/*.py routers/*.py services/*.py -# ✓ All files compile -``` - -## Production Deployment - -Systemd Service läuft als root, Port 8000: -- CORS enabled (für Frontend) -- Logging zu journalctl -- Auto-Restart bei Crash -- Memory/CPU Limits gesetzt - -Reverse Proxy (nginx) würde auf Port 9090 von vorne Listen und zu :8000 weiterleiten. - ---- - -**Status: Phase 1 KOMPLETT ✅** - -Das Backend ist **production-ready** und bietet **vollständige Cockpit-Funktionalität**! - -Nächste: Phase 2 – Next.js Frontend diff --git a/BUG_FIXES_2026-04-18.md b/BUG_FIXES_2026-04-18.md deleted file mode 100644 index 157aba4..0000000 --- a/BUG_FIXES_2026-04-18.md +++ /dev/null @@ -1,178 +0,0 @@ -# Bug Fixes – April 18, 2026 - -## Summary -✅ All reported bugs fixed and verified - ---- - -## Bug #1: Recent Logins Missing Usernames - -**Reported**: "Recent Logins hat keine user name drin!" - -**Root Cause**: -- Frontend was looking for `login.user` field -- Backend API returns `login.username` field (different name) - -**Fix Applied**: -- Updated `frontend/app/identities/page.tsx` line 559 -- Changed to: `(login as any).username || (login as any).user` -- Added fallback for compatibility - -**Verification**: -``` -Backend Response: {"username":"administrator","login_str":"Wed Apr 15 22:45 2026"} -Frontend Now Shows: → administrator (Wed Apr 15 22:45 2026) -Status: ✅ FIXED -``` - ---- - -## Bug #2: File Properties Owner – No Name Autocomplete - -**Reported**: "File Properties Owner macht keine vervollständigung der namen" - -**Root Cause**: -- Owner input field was plain `` -- No autocomplete or data suggestions -- Users had to type UID numbers or guess names - -**Fix Applied**: -1. Added state variables to track available users/groups: - ```typescript - const [usernames, setUsernames] = useState([]) - const [groupnames, setGroupnames] = useState([]) - ``` - -2. Added `loadUsersAndGroups()` function to fetch from API: - - Calls `/api/identities/users` on component mount - - Calls `/api/identities/groups` on component mount - - Extracts username and groupname arrays - -3. Updated Owner input field: - ```html - - - {usernames.map(name => - ``` - -4. Added same for Group field - -**Verification**: -``` -Available Users: root, administrator, testuser, wsdd2, nobody -Available Groups: root, sudo, administrator, tape, ... -Owner Dropdown: ✅ NOW shows autocomplete -Group Dropdown: ✅ NOW shows autocomplete -Status: ✅ FIXED -``` - ---- - -## Bug #3: File Properties Group – Same Autocomplete Issue - -**Reported**: Same as #2, applies to both Owner and Group - -**Status**: ✅ **FIXED** (both handled in same code change) - ---- - -## Positive Feedback - -**Comment**: "Samba Users gefällt mir so gut!" - -**Response**: ✅ **Feature Noted and Appreciated** -- Samba Users feature is working well -- Provides easy visibility into Samba-configured users -- Allows password management for Samba accounts -- Consider expanding with more Samba-specific features in Phase 3 - ---- - -## Files Modified - -1. `frontend/app/identities/page.tsx` - - Fixed login history username display - - Lines 556-567: Updated field mapping - -2. `frontend/app/files/page.tsx` - - Added user/group loading on mount - - Added state variables for usernames/groupnames - - Updated Owner input with datalist - - Updated Group input with datalist - - Lines 91-93: New state variables - - Lines 113-131: New `loadUsersAndGroups()` function - - Lines 1005-1033: Updated input fields with autocomplete - ---- - -## Build & Deployment - -``` -Frontend Build: ✅ SUCCESS - - Identities page: 4.49 kB (no change) - - Files page: 8 kB → 8.17 kB (minimal increase) - - Total bundle: 130 kB - -Deployment: ✅ SUCCESS - - All files copied to 192.168.1.179:/opt/zmb-webui/backend/static/ -``` - ---- - -## Testing Results - -### Login History -``` -✅ Field Names Match: username (was user) -✅ Data Displays: "administrator" visible -✅ Time Format: "Wed Apr 15 22:45 2026" displays correctly -``` - -### File Properties Autocomplete -``` -✅ Owner Field: Dropdown shows all 5 users -✅ Group Field: Dropdown shows available groups -✅ Type-as-you-filter: Native HTML5 datalist filtering -✅ Smooth UX: No lag, built-in browser behavior -``` - ---- - -## How to Use New Features - -### Login History - View Users -1. Go to **Identities → History** -2. See login records with usernames displayed - -### File Properties - Owner/Group Autocomplete -1. Go to **Files → Select File → Edit Mode** -2. Click Owner field → dropdown appears with suggestions -3. Start typing → browser filters suggestions -4. Click to select → sets owner/group automatically - ---- - -## Browser Compatibility - -HTML5 `` element supported in: -- ✅ Chrome/Edge 17+ -- ✅ Firefox 4+ -- ✅ Safari 12.1+ -- ✅ All modern browsers - -Fallback for older browsers: manual text input still works - ---- - -## Next Phase Improvements - -Consider for Phase 3: -- [ ] Add UID/GID fallback display in autocomplete -- [ ] Add search/filter UI above autocomplete -- [ ] Add "new user" quick-create option in dropdown -- [ ] Add recent users history cache (browser localStorage) - ---- - -**All bugs fixed, tested, and ready for production.** diff --git a/DEPLOYMENT_MATRIX.md b/DEPLOYMENT_MATRIX.md deleted file mode 100644 index a24bf82..0000000 --- a/DEPLOYMENT_MATRIX.md +++ /dev/null @@ -1,267 +0,0 @@ -# Deployment Matrix – Alle Umgebungen - -ZMB Webui läuft auf **allen Plattformen**: - -## ✅ Unterstützte Architekturen & Umgebungen - -``` -┌──────────────────────┬──────────┬────────┬─────────────────────────┐ -│ Platform │ Arch │ Test │ Notes │ -├──────────────────────┼──────────┼────────┼─────────────────────────┤ -│ Raspberry Pi │ ARM64 │ ✓ │ Primär, optimiert │ -│ Debian (x86_64) │ AMD64 │ ✓ │ Full support │ -│ Ubuntu (x86_64) │ AMD64 │ ✓ │ Full support │ -│ Debian (i686) │ x86 32bit│ ✓ │ Supported, slower │ -│ LXC Container │ any │ ✓ │ Privilegiert, ZFS native│ -│ Proxmox LXC │ any │ ✓ │ Auf Proxmox Host mit ZFS│ -│ Docker │ any │ ⚠️ │ Kein Docker (kein Plan) │ -└──────────────────────┴──────────┴────────┴─────────────────────────┘ -``` - -## Installation Quickstart - -### 1️⃣ Raspberry Pi / ARM64 Debian - -```bash -scp -r backend root@:/tmp/zmb-webui-backend -ssh root@ -cd /tmp/zmb-webui-backend -sudo bash check_system.sh # Prüfe Kompatibilität -sudo bash install.sh # Auto-Installation -sudo systemctl status zmb-webui-backend -``` - -### 2️⃣ x86/AMD64 Debian/Ubuntu - -```bash -scp -r backend root@:/tmp/zmb-webui-backend -ssh root@ -cd /tmp/zmb-webui-backend -sudo bash check_system.sh -sudo bash install.sh -sudo systemctl status zmb-webui-backend -``` - -### 3️⃣ LXC Container Standalone (Privilegiert – für ZFS Management) - -```bash -# Host-Seite: Container mit privilegiertem Mode -lxc launch images:debian/bookworm zmb-webui \ - --config security.privileged=true \ - --config security.nesting=true - -# Port-Mapping -lxc config device add zmb-webui http proxy \ - listen=tcp:0.0.0.0:9090 \ - connect=tcp:127.0.0.1:8000 - -# Container-Seite -lxc exec zmb-webui -- bash -apt update && apt install -y python3 python3-pip python3-venv -cd /opt && git clone zmb-webui && cd zmb-webui/backend -bash install.sh -systemctl start zmb-webui-backend - -# ZFS wird automatisch sichtbar im Container! -lxc exec zmb-webui -- zpool list # zeigt Host-Pools -``` - -### 4️⃣ Proxmox VM (wie bare metal) - -```bash -# VM mit Debian/Ubuntu erstellen -# Dann wie x86/AMD64 Installation -bash check_system.sh -bash install.sh -``` - -## Frontend Build - -### Auf stärkerem Host bauen - -```bash -# Build auf x86/AMD64 (schneller) -cd frontend -npm install -npm run build # 2-5 min -npm run export # Static export - -# Oder auf Pi (langsamer, aber funktioniert) -npm install # 20-30 min -npm run build # 20-30 min -npm run export # 5-10 min -``` - -### Deploy überall gleich - -```bash -# Der Build-Output ist überall identisch (HTML/JS/CSS) -scp -r frontend/.next/out root@10.66.120.3:/opt/zmb-webui/frontend -# Dann nginx oder Next.js Server starten -``` - -## Architektur-Spezifische Gotchas - -### ARM64 (Raspberry Pi) - -✅ **Alles funktioniert** -- Python: ✓ -- FastAPI: ✓ -- ZFS Tools: ✓ -- systemd: ✓ - -⚠️ **Langsam** -- npm install/build: 20-30 min (nicht auf Pi bauen!) -- Subprocess Timeout: 5s ist OK - -### x86/AMD64 - -✅ **Schnell** -- npm build: 2-5 min -- Python: ✓ -- ZFS Tools: ✓ - -✓ **Alles optimal** - -### x86 32-bit - -✓ **Funktioniert** -- Python 32-bit: OK -- aber RAM-limitiert (max ~2GB pro Prozess) - -⚠️ **Nicht empfohlen** für Production - -### LXC Container (Privilegiert) - -✅ **Container-agnostisch** -- Funktioniert auf ARM64, x86, AMD64 Host - -✓ **ZFS funktioniert nativ** -- Privilegierter Container hat `/dev/zfs` Zugriff -- `zpool` und `zfs` Commands arbeiten direkt -- Snapshots, Scrub, alles im Container möglich - -✓ **File Manager funktioniert** -- ZFS Datasets sind sichtbar im Container -- Read/Write auf `/tank/share` etc. - -## Requirements per Architektur - -``` -┌─────────────────┬──────────┬─────────┬──────────┬──────────┐ -│ Requirement │ ARM64 │ AMD64 │ x86_32 │ LXC │ -├─────────────────┼──────────┼─────────┼──────────┼──────────┤ -│ Python 3.8+ │ ✓ │ ✓ │ ✓ │ ✓ │ -│ pip │ ✓ │ ✓ │ ✓ │ ✓ │ -│ ZFS Tools │ ✓ │ ✓ │ ✓ │ mounted │ -│ systemd │ ✓ │ ✓ │ ✓ │ ✓ │ -│ 512MB+ RAM │ ✓ │ ✓ │ ⚠️ │ ✓ │ -│ 500MB+ Disk │ ✓ │ ✓ │ ✓ │ ✓ │ -│ Internet │ ✓ │ ✓ │ ✓ │ host fw │ -└─────────────────┴──────────┴─────────┴──────────┴──────────┘ -``` - -## Performance-Vergleich - -``` -┌─────────────────┬──────────┬──────────┬─────────┐ -│ Operation │ Pi/ARM64 │ x86/AMD64│ LXC │ -├─────────────────┼──────────┼──────────┼─────────┤ -│ /api/pools │ 50-100ms │ 10-20ms │ 20-50ms │ -│ /api/files │ 200ms │ 50ms │ 100ms │ -│ Snapshot create │ 2-3s │ 0.5-1s │ 1-2s │ -│ npm build │ 20-30min │ 2-5 min │ ↑ host │ -└─────────────────┴──────────┴──────────┴─────────┘ -``` - -## Checklist vor Production - -### Pre-Installation - -- [ ] `bash check_system.sh` erfolgreich -- [ ] Python 3.8+ installiert -- [ ] ZFS Tools installiert (wenn nötig) -- [ ] ≥512MB RAM verfügbar (besser 1-2GB) -- [ ] ≥500MB Disk verfügbar -- [ ] Internet-Konnektivität (für apt) -- [ ] Falls LXC: Privilegierter Container (`security.privileged=true`) - -### Installation - -- [ ] `bash install.sh` erfolgreich -- [ ] `systemctl status zmb-webui-backend` → active -- [ ] `curl http://localhost:8000/health` → 200 OK - -### Post-Installation - -- [ ] Admin-Passwort geändert -- [ ] `zpool list` funktioniert (als root) -- [ ] `/tank/share` ist readwrite -- [ ] Firewall Port 9090 (wenn via nginx) oder 8000 (direkt) -- [ ] Backups konfiguriert - -## Troubleshooting Multi-Arch - -### Python Fehler -```bash -# Check architecture: -python3 -c "import struct; print(struct.calcsize('P') * 8)" -# 32 = 32-bit, 64 = 64-bit - -# Check Python arch: -file $(which python3) -# x86-64, ARM aarch64, Intel 80386, etc. -``` - -### ZFS Fehler -```bash -# Check ZFS verfügbar: -zpool list 2>&1 - -# Falls "command not found": -apt install zfsutils-linux zfs-auto-snapshot -``` - -### systemd Fehler -```bash -# Nur auf Linux mit systemd: -systemctl --version - -# Falls fehlt: Manuell via supervisor/runit einrichten -``` - -## Multi-Arch CI/CD - -### Build Strategy -``` -Architect Publish - ↓ -┌─────────────────────────────┐ -│ Build auf x86/AMD64 (schnell)│ -│ • Backend: python wheels │ -│ • Frontend: static export │ -└─────────────────────────────┘ - ↓ -Artifacts (universal) - ↓ -Deploy auf: -├── ARM64 Pi -├── x86/AMD64 Server -├── LXC Container -└── Proxmox VM -``` - -**Alle nutzen die gleichen Artifacts – kein re-build nötig!** - -## Summary - -| Plattform | Kompatibilität | Performance | Empfehlung | -|-----------|---|---|---| -| Raspberry Pi 4/5 | ✓ Vollständig | ⭐⭐ Ausreichend | ✓ Primary | -| Debian/Ubuntu x86 | ✓ Vollständig | ⭐⭐⭐⭐ Sehr gut | ✓ Production | -| LXC Container | ✓ Vollständig | ⭐⭐⭐ Gut | ✓ Enterprise | -| x86 32-bit | ✓ Unterstützt | ⭐ Langsam | ⚠️ Fallback | - ---- - -**Backend läuft überall – eine Codebase für alle Plattformen!** 🎯 diff --git a/DEVLOG.md b/DEVLOG.md deleted file mode 100644 index 0c587c5..0000000 --- a/DEVLOG.md +++ /dev/null @@ -1,1279 +0,0 @@ -# cockpit_new – Dev Log - -## 2026-04-14 17:33 – 21:45 (4h 12m) -**Beschreibung:** Claude Code Session -**Projekt:** spesenapp - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 21:49 – 21:51 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 21:51 – 21:53 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 21:55 – 22:01 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:46 – 23:09 (23m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 23:10 – 23:13 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:51 – 09:53 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:58 – 09:59 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 10:01 – 10:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 10:03 – 10:19 (16m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 10:19 – 10:24 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 10:25 – 10:25 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 10:31 – 10:35 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:24 – 16:26 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:27 – 16:29 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:33 – 16:34 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:34 – 16:35 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:36 – 16:37 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:39 – 16:40 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:41 – 16:53 (11m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:54 – 17:03 (9m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 17:04 – 17:05 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 17:06 – 17:06 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 17:08 – 17:12 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 17:16 – 17:17 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:21 – 22:23 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:25 – 22:26 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:28 – 22:39 (11m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:41 – 22:47 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:48 – 22:48 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:30 – 23:32 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:34 – 23:36 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:37 – 23:37 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:39 – 23:39 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:42 – 23:44 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:44 – 23:50 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:53 – 23:55 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 00:01 – 00:01 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 00:02 – 00:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 00:02 – 00:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:02 – 20:04 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:10 – 20:10 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:14 – 20:15 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:16 – 20:16 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:18 – 20:18 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:18 – 20:19 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:20 – 20:20 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:20 – 20:21 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 15:19 – 15:19 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 22:25 – 22:26 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 22:28 – 22:30 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 22:35 – 22:36 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 22:44 – 22:45 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 22:46 – 22:47 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:34 – 23:35 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:35 – 23:38 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:43 – 23:43 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:44 – 23:44 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:07 – 00:07 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:17 – 00:17 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:22 – 00:22 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:25 – 00:26 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 5f78e88 Initial commit: ZMB Webui Phase 3a - Dashboard, Backend APIs, Frontend with ISR - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:27 – 00:29 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:30 – 00:32 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 87063ee Add Gitea-based updater scripts for automated deployments - -### Geänderte Dateien -- deploy/update-from-gitea.sh | 195 ++++++++++++++++++++++++++++++++++++++++++++ -- update-pi.sh | 3 + -- update-test.sh | 3 + - ---- -## 2026-04-22 00:33 – 00:37 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 7226905 Improve updater: better SSH handling and file existence checks -- 17e073b Fix Next.js 15 export: use build output instead of removed 'npm run export' - -### Geänderte Dateien -- deploy/update-from-gitea.sh | 20 +++++++++++--------- - ---- -## 2026-04-22 00:38 – 00:38 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- deploy/update-from-gitea.sh | 20 +++++++++++--------- - ---- -## 2026-04-22 00:39 – 00:40 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 36da798 Rebrand: Replace 'ZMB Webui' with 'ZMB Webui' throughout project - -### Geänderte Dateien -- BACKEND_COMPLETE.md | 2 +- -- CLAUDE.md | 4 +-- -- DEPLOYMENT.md | 2 +- -- DEPLOYMENT_MATRIX.md | 2 +- -- DEPLOYMENT_PI.md | 2 +- -- DEVLOG.md | 58 ++++++++++++++++++++++++++++++++++++++++ -- INSTALL_GUIDE.md | 2 +- -- LXC_QUICKSTART.md | 2 +- -- PHASE1_SUMMARY.md | 2 +- -- PHASE_3A_COMPLETE.md | 2 +- -- PROXMOX_LXC_SETUP.md | 4 +-- -- PROXMOX_QUICKSTART.md | 2 +- -- SESSION_SUMMARY_2026-04-18.md | 2 +- -- TEST_PLAN.md | 2 +- -- TEST_RESULTS.md | 2 +- -- backend/README.md | 2 +- -- backend/check_system.sh | 4 +-- -- backend/install.sh | 4 +-- -- backend/main.py | 8 +++--- -- backend/main_aiohttp.py | 10 +++---- -- backend/manage_users.py | 2 +- -- backend/routers_aiohttp.py | 2 +- -- deploy/deploy-frontend-static.sh | 4 +-- -- deploy/deploy-frontend.sh | 4 +-- -- deploy/deploy.sh | 2 +- -- deploy/lxc-setup.md | 2 +- -- deploy/update-from-gitea.sh | 4 +-- -- frontend/README.md | 2 +- -- frontend/app/layout.tsx | 2 +- -- frontend/app/login/page.tsx | 6 ++--- -- frontend/components/Header.tsx | 2 +- -- update-179.sh | 4 +-- - ---- -## 2026-04-22 00:41 – 00:43 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 92bed20 ZMB Webui: Complete Project – Rebrand & Initial Clean Commit - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:44 – 00:45 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:46 – 00:51 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:57 – 00:58 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:58 – 00:58 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 00:59 – 01:03 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 52b9c02 Improve Samba Global Configuration display in WebUI -- 8dbf0e4 .env.local~ gelöscht - -### Geänderte Dateien -- backend/services/shares.py | 23 +++++++++++++------- -- frontend/app/shares/page.tsx | 52 ++++++++++++++++++++++++++++++++++++++++++-- -- frontend/lib/api.ts | 5 +++++ - ---- -## 2026-04-22 01:03 – 01:04 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- backend/services/shares.py | 23 +++++++++++++------- -- frontend/app/shares/page.tsx | 52 ++++++++++++++++++++++++++++++++++++++++++-- -- frontend/lib/api.ts | 5 +++++ - ---- -## 2026-04-22 01:07 – 01:09 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- afa74d4 Add Samba registry setup script for initial configuration -- 0504b5b Read Samba config from registry instead of smb.conf - -### Geänderte Dateien -- deploy/setup-samba-registry.sh | 65 ++++++++++++++++++++++++++++++++++++++++++ - ---- -## 2026-04-22 01:10 – 01:11 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- deploy/setup-samba-registry.sh | 65 ++++++++++++++++++++++++++++++++++++++++++ - ---- -## 2026-04-22 01:12 – 01:14 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- e5afa47 Fix systemd service paths for correct backend location - -### Geänderte Dateien -- deploy/zfs-manager-backend.service | 32 ++++++-------------------------- - ---- -## 2026-04-22 01:14 – 01:20 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 87e2dec Update service file to use /opt/zmb-webui paths - -### Geänderte Dateien -- deploy/zfs-manager-backend.service | 8 ++++---- - ---- -## 2026-04-22 01:21 – 01:22 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- a373378 Fix: Use full path for 'net' command in Samba config reading - -### Geänderte Dateien -- backend/services/shares.py | 2 +- - ---- -## 2026-04-22 01:26 – 01:26 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- backend/services/shares.py | 2 +- - ---- -## 2026-04-22 01:30 – 01:31 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 07cf45a Add editable Samba global configuration with net conf setparm - -### Geänderte Dateien -- backend/routers/shares.py | 6 ++--- -- backend/services/shares.py | 49 +++++++++++-------------------------- -- frontend/app/shares/page.tsx | 58 ++++++++++++++++++++++++++++++++++++++++++-- -- frontend/lib/api.ts | 5 ++++ - ---- -## 2026-04-22 01:33 – 01:33 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- backend/routers/shares.py | 6 ++--- -- backend/services/shares.py | 49 +++++++++++-------------------------- -- frontend/app/shares/page.tsx | 58 ++++++++++++++++++++++++++++++++++++++++++-- -- frontend/lib/api.ts | 5 ++++ - ---- -## 2026-04-22 01:34 – 01:36 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 71164dd Update /file-sharing page to support new Samba parameters - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- -## 2026-04-22 01:37 – 01:37 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- -## 2026-04-22 01:38 – 01:43 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- -## 2026-04-22 01:44 – 01:44 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- -## 2026-04-22 01:46 – 01:47 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- -## 2026-04-22 01:58 – 02:00 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -- ea6b250 Hinzufügen: INSTALLATION.md mit umfassender Installationsanleitung - -### Geänderte Dateien -- INSTALLATION.md | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ---- -## 2026-04-22 10:26 – 10:26 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- INSTALLATION.md | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ---- -## 2026-04-22 10:28 – 10:29 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- INSTALLATION.md | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ---- -## 2026-04-22 10:31 – 10:34 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 90d62fa Dokumentation: Data Persistence für System- und Samba-User - -### Geänderte Dateien -- CLAUDE.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------- - ---- -## 2026-04-22 12:48 – 12:49 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- CLAUDE.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------- - ---- -## 2026-04-22 12:49 – 12:50 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 6401027 Fix: Navigator Permissions - absolute Pfade für chmod/chown - -### Geänderte Dateien -- backend/services/file_manager.py | 6 +++--- - ---- -## 2026-04-22 12:51 – 12:53 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 78f41b9 Fix: Identities Group Management - bessere Fehlermeldungen - -### Geänderte Dateien -- DEVLOG.md | 298 +++++++++++++++++++++++++++++++++++++++++ -- backend/routers/identities.py | 12 +- -- backend/routers/shares.py | 23 ++++ -- backend/services/identities.py | 16 ++- -- backend/services/shares.py | 44 +++++- -- frontend/DEVLOG.md | 11 ++ -- frontend/app/shares/page.tsx | 138 ++++++++++++++++--- -- frontend/lib/api.ts | 5 + - ---- -## 2026-04-22 12:53 – 13:03 (10m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 110e07b Add: next.config.ts für Static Export - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 13:04 – 13:09 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 13:10 – 13:10 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 13:11 – 13:11 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 13:11 – 13:13 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 13:15 – 13:15 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 21:55 – 21:55 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 21:55 – 21:56 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 22:33 – 22:33 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 22:38 – 22:38 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 22:38 – 22:38 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 22:41 – 22:41 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-04-22 22:42 – 22:43 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-06-03 09:19 – 09:19 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-06-03 09:20 – 09:21 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-06-03 09:28 – 09:29 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -- cd7105b Konfigurierbares CORS per ZMB_CORS_ORIGINS + dynamische Frontend-URL - -### Geänderte Dateien -- CLAUDE.md | 2 +- -- backend/main.py | 24 +++++++++++++++++++----- -- deploy/zfs-manager-backend.service | 4 ++++ - ---- -## 2026-06-03 09:31 – 23:42 (14h 11m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -- 7b9775f Dokumentation: DEVLOG-Updates auf aktuellen Stand - -### Geänderte Dateien -- DEVLOG.md | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -- backend/DEVLOG.md | 22 ++++++++++++++++++++ - ---- diff --git a/LXC_QUICKSTART.md b/LXC_QUICKSTART.md deleted file mode 100644 index 227dbc8..0000000 --- a/LXC_QUICKSTART.md +++ /dev/null @@ -1,145 +0,0 @@ -# LXC Container Quick Start - -ZMB Webui läuft in **privilegiertem LXC Container** mit vollständigem ZFS Management. - -## One-Liner Setup - -```bash -# 1. Container erstellen (privilégiiert!) -lxc launch images:debian/bookworm zmb-webui \ - --config security.privileged=true \ - --config security.nesting=true - -# 2. Port-Mapping -lxc config device add zmb-webui http proxy \ - listen=tcp:0.0.0.0:9090 \ - connect=tcp:127.0.0.1:8000 - -# 3. Shell in Container -lxc exec zmb-webui -- bash - -# 4. Im Container: -apt update && apt install -y python3 python3-pip python3-venv git -git clone /opt/zmb-webui -cd /opt/zmb-webui/backend -bash check_system.sh -bash install.sh - -# 5. Service starten -systemctl start zmb-webui-backend -systemctl status zmb-webui-backend - -# 6. Test -curl http://localhost:8000/health - -# 7. Login & Change Password -TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' | jq -r .access_token) - -python3 manage_users.py change-password admin -``` - -## Verify ZFS im Container - -```bash -# Alle diese Commands funktionieren im privilegierten Container: - -lxc exec zmb-webui -- zpool list -# → Zeigt tank pool vom Host - -lxc exec zmb-webui -- zfs list -# → Alle Datasets - -lxc exec zmb-webui -- zpool status tank -# → VDEV-Status - -lxc exec zmb-webui -- zfs list -t snapshot | head -# → Snapshots - -# Backend kann ZFS direkt managen: -TOKEN=$(...) # siehe oben -curl "http://localhost:9090/api/pools" \ - -H "Authorization: Bearer $TOKEN" -``` - -## Container-Management - -```bash -# Container Info -lxc info zmb-webui - -# Resources begrenzen -lxc config set zmb-webui limits.memory 2GB -lxc config set zmb-webui limits.cpu 2 - -# Container neustarten -lxc restart zmb-webui - -# Shell zugriff (jederzeit) -lxc exec zmb-webui -- bash - -# Logs anschauen -lxc exec zmb-webui -- journalctl -u zmb-webui-backend -f - -# Files transferieren -lxc file push ./local-file zmb-webui/root/ -lxc file pull zmb-webui/root/remote-file ./ - -# Container Snapshot -lxc snapshot zmb-webui backup-2026-04-14 - -# Restore -lxc restore zmb-webui backup-2026-04-14 -``` - -## Networking - -```bash -# Container IP -lxc exec zmb-webui -- ip addr - -# Extern vom Host zugreifen: -curl http://localhost:9090/health - -# Vom anderen Host (wenn freigegeben): -curl http://:9090/health -``` - -## Performance im Container - -``` -Pool-Query: 20-50ms (vs 10-20ms bare metal) -Snapshots: 1-2s -File Upload: 100-500ms - -⚠️ Overhead: ~50% (normal für virtualisierte Umgebung) -``` - -## Security Notes - -⚠️ **Privilegierter Container:** -- Hat Root-ähnliche Zugriffe -- Kann Host-Disks direkt zugreifen -- ZFS Management im Container möglich -- **Use Case:** All-in-one Server auf Proxmox/LXD - -✅ **Mitigations:** -- Memory/CPU Limits setzen -- Firewall auf Host -- Regelmäßige Backups (`lxc snapshot`) - -## Cleanup - -```bash -# Container stoppen & löschen -lxc stop zmb-webui -lxc delete zmb-webui - -# All snapshots entfernen -lxc delete zmb-webui/backup-2026-04-14 -``` - ---- - -**Das war's!** Backend läuft im Container und kann ZFS vollständig managen. 🚀 diff --git a/PHASE1_SUMMARY.md b/PHASE1_SUMMARY.md deleted file mode 100644 index 45b64ee..0000000 --- a/PHASE1_SUMMARY.md +++ /dev/null @@ -1,219 +0,0 @@ -# Phase 1: FastAPI Backend – Abgeschlossen ✓ - -## Was wurde gebaut - -Komplettes FastAPI-Backend für ZMB Webui mit: -- ✅ Alle ZFS-Operationen (Pool, Dataset, Snapshot) -- ✅ File Manager (Browse, Upload, Download) wie cockpit-files -- ✅ JWT Authentication mit bcrypt -- ✅ Caching (30s-300s TTL) -- ✅ Production-ready Systemd Service -- ✅ User Management CLI Tool -- ✅ Comprehensive Documentation - -## Dateistruktur - -``` -backend/ -├── main.py # FastAPI App (1-Click Run) -├── requirements.txt # Python Dependencies -├── install.sh # Auto-Installation für Pi -├── manage_users.py # User Management CLI -├── README.md # API Documentation -│ -├── services/ -│ ├── zfs_runner.py # ZFS Subprocess Wrapper + Caching -│ ├── auth.py # JWT Token Generation/Verification -│ └── file_manager.py # File Browser, Upload, Download -│ -├── routers/ -│ ├── auth.py # POST /api/auth/login -│ ├── pools.py # GET/POST Pool Operations -│ ├── datasets.py # GET/POST/DELETE Dataset CRUD -│ ├── snapshots.py # GET/POST/DELETE Snapshot CRUD -│ └── files.py # GET/POST/DELETE File Manager -│ -├── models/ -│ ├── pool.py # Pool, PoolStatus, PoolHealth -│ ├── dataset.py # Dataset, DatasetType -│ ├── snapshot.py # Snapshot -│ └── auth.py # User, Token, TokenData -│ -└── config/ - └── users.json # Default Admin User - -deploy/ -└── zmb-webui-backend.service # Systemd Service (2 Workers, 512MB RAM limit) -``` - -## API Endpoints - -### Authentication -``` -POST /api/auth/login # Login mit Username/Passwort -POST /api/auth/verify # Token Verifikation -``` - -### Pools -``` -GET /api/pools # List aller Pools -GET /api/pools/{name} # Pool Status + VDEV-Baum -POST /api/pools/{name}/scrub # Start Scrub -``` - -### Datasets -``` -GET /api/datasets # List Datasets (max depth 2) -POST /api/datasets # Create Dataset -DELETE /api/datasets/{name} # Delete Dataset -``` - -### Snapshots -``` -GET /api/snapshots # List Snapshots (limit 50) -POST /api/snapshots # Create Snapshot -DELETE /api/snapshots/{name} # Delete Snapshot -POST /api/snapshots/rollback # Rollback to Snapshot -``` - -## Performance-Features (für 4GB RAM Pi) - -✅ **Caching Layer:** -- Pool Status: 30s TTL -- Snapshots/Datasets: 60-120s TTL -- In-Memory Cache (kein Redis nötig) - -✅ **Resource Limits:** -- gunicorn: 2 Worker -- Memory: 512M soft / 768M hard -- Timeout: 30s -- Max Requests per Worker: 500 (Memory-Leak Prevention) - -✅ **ZFS Optimization:** -- Queries mit Depth-Limit (max 2 Ebenen) -- Snapshots limitiert auf 50 (konfigurierbar) -- Subprocess Timeout: 5s -- Lazy Parsing (streaming output) - -## Installation auf dem Pi - -```bash -# 1. Backend kopieren -scp -r backend root@10.66.120.3:/tmp/zmb-webui-backend - -# 2. Installation durchführen -ssh root@10.66.120.3 -cd /tmp/zmb-webui-backend -sudo bash install.sh - -# 3. Service starten -sudo systemctl start zmb-webui-backend -sudo systemctl enable zmb-webui-backend - -# 4. Test -curl http://10.66.120.3:8000/health -``` - -## Default Credentials - -**Admin User (nach Installation):** -- Username: `admin` -- Password: `admin123` -- ⚠️ SOFORT ÄNDERN! - -```bash -# Passwort ändern auf dem Pi: -python3 /opt/zmb-webui/backend/manage_users.py change-password admin -``` - -## Login-Flow - -```bash -# 1. Login -curl -X POST http://10.66.120.3:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# Response: -# {"access_token":"eyJhbGc...","token_type":"bearer"} - -# 2. API Request mit Token -TOKEN="eyJhbGc..." -curl http://10.66.120.3:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" -``` - -## Testing - -### Lokal testen (ohne ZFS nötig) -```bash -# Nur Python-Syntax checken -python3 -m py_compile main.py models/*.py routers/*.py services/*.py -``` - -### Auf dem Pi testen -```bash -# Health Check -curl http://10.66.120.3:8000/health - -# Login -curl -X POST http://10.66.120.3:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# List Pools -TOKEN="" -curl http://10.66.120.3:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" - -# List Snapshots -curl "http://10.66.120.3:8000/api/snapshots?limit=10" \ - -H "Authorization: Bearer $TOKEN" -``` - -## Troubleshooting - -### Backend startet nicht -```bash -# Logs anschauen -journalctl -u zmb-webui-backend -f - -# Service Status -systemctl status zmb-webui-backend -``` - -### Memory-Probleme -```bash -# Memory Usage checken -ps aux | grep uvicorn - -# Service neustarten -systemctl restart zmb-webui-backend - -# Oder: Cron Job für tägliche Restart (wenn nötig) -0 3 * * * systemctl restart zmb-webui-backend -``` - -### Snapshots dauern zu lange -```bash -# Cache leeren -curl -X POST http://10.66.120.3:8000/api/pools/clear-cache \ - -H "Authorization: Bearer $TOKEN" - -# Oder: Limit in URL erhöhen -curl "http://10.66.120.3:8000/api/snapshots?limit=100" -``` - -## Next Steps - -- ✅ Phase 1: Backend fertig -- ⏳ Phase 2: Next.js Frontend bauen -- ⏳ Phase 3: WebSocket + Advanced Features -- ⏳ Phase 4: Alerts + Full Deployment - -## Notes - -- **No Docker!** (wie gewünscht) – Direkter systemd deployment -- **Performance-optimiert** für ARM64 Raspberry Pi -- **JWT Auth** statt Session-basiert (für API-Nutzung ideal) -- **Minimal Dependencies** (nur FastAPI, Pydantic, python-jose, passlib) diff --git a/PHASE2_SUMMARY.md b/PHASE2_SUMMARY.md deleted file mode 100644 index 5cc0e17..0000000 --- a/PHASE2_SUMMARY.md +++ /dev/null @@ -1,350 +0,0 @@ -# Phase 2: Next.js Frontend - Complete ✓ - -## Summary - -Phase 2 is now complete! A production-ready Next.js 15 frontend has been built with full TypeScript support, Tailwind CSS styling, component architecture, and integration with the FastAPI backend. - -## What Was Built - -### Core Framework -- **Next.js 15** with App Router (latest stable) -- **TypeScript** with strict mode for type safety -- **Tailwind CSS 3.4** for utility-first styling -- **PostCSS** with autoprefixer for cross-browser support - -### Configuration Files -``` -frontend/ -├── package.json ✓ All dependencies locked -├── tsconfig.json ✓ Strict TypeScript config -├── tailwind.config.ts ✓ Dark mode + custom colors -├── next.config.ts ✓ ISR + compression optimizations -├── postcss.config.js ✓ Tailwind + autoprefixer -├── .eslintrc.json ✓ Next.js linting rules -├── next-env.d.ts ✓ TypeScript definitions -├── .env.example ✓ Template for API URL -└── .env.local ✓ Local dev configuration -``` - -### Pages (3 Complete + 1 Placeholder) - -1. **`/` (Dashboard)** - - Real-time pool list with refresh (every 30s) - - Pool cards showing: - - Health status (ONLINE/DEGRADED/FAULTED) - - Capacity bar with color coding - - Total/Used/Free space - - Fragmentation percentage - - Click pools to view details (routing ready) - - Loading states and error handling - - Auto-refresh with visual feedback - -2. **`/login`** - - JWT authentication - - Username/password input - - Error messages - - Session persistence (localStorage) - - Auto-redirect if already authenticated - - Default credentials display - -3. **`/snapshots`** - - Snapshot list table with: - - Name, Dataset, Created timestamp, Used space - - Delete functionality with confirmation - - Refresh button - - Paginated for large datasets - - Error handling - -4. **`/files` (Placeholder)** - - Coming soon message - - Planned features listed - -### Components - -#### UI Components (Atomic) -- `Card` - Container with border and shadow -- `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` -- `Button` - Variants: default, secondary, destructive, outline, ghost -- `Badge` - Status indicators (success, warning, destructive) -- `Progress` - Capacity bars with auto-color selection - -#### Feature Components -- **`PoolCard`** - Individual pool display - - Health badge with color coding - - Capacity progress bar (auto-colors at 75%/90%) - - Space breakdown (Total/Used/Free) - - Clickable for pool details - -- **`Header`** - Navigation + Logout - - Logo with icon - - Navigation links (Dashboard, Snapshots, Files) - - Mobile hamburger menu - - Logout button - - Active link highlighting - -### Libraries & Utilities - -**`lib/api.ts`** - TypeScript API client with: -- Axios HTTP client -- Full type definitions for all endpoints -- Authentication (login/logout/verify) -- Pool operations (list, status, scrub) -- Dataset management (list, create, delete) -- Snapshot management (CRUD, rollback) -- File operations (browse, upload, download) -- System info queries -- Auto token refresh on 401 errors -- localStorage persistence - -**`lib/utils.ts`** - Helper functions: -- `formatBytes()` - Convert bytes to KB/MB/GB/TB -- `formatPercent()` - Used/total percentages -- `formatUptime()` - Human-readable uptime -- `formatDate()` - Timestamp formatting -- `getPoolHealthColor()` - Color codes for health status -- `cn()` - Safe classname concatenation - -### Styling - -- **Tailwind CSS** with custom color scheme -- **Dark mode support** (class-based) -- **CSS Variables** for theming -- **Responsive design** (mobile-first) -- **Smooth transitions** on interactions -- **Color palette**: - - Primary: Black/White - - Accent: Red (error emphasis) - - Status: Green (online), Yellow (degraded), Red (faulted) - -### Performance Optimizations - -#### ISR (Incremental Static Regeneration) -- Dashboard: revalidate every 30s -- Snapshots: revalidate every 60s -- Login: no caching -- Static assets: 1 hour cache - -#### Bundle Optimizations -- Tree-shaking enabled -- Next.js built-in compression -- Image optimization ready (disabled for static export) -- Dynamic imports for heavy libraries - -#### Memory Efficiency -- Minimal client-side state -- API responses cached via axios -- No heavy libraries in bundle -- ISR reduces runtime computation - -### File Structure - -``` -frontend/ -├── app/ -│ ├── layout.tsx Root layout with metadata -│ ├── page.tsx Dashboard (ISR: 30s) -│ ├── globals.css Tailwind directives + colors -│ ├── login/page.tsx Authentication page -│ ├── snapshots/page.tsx Snapshot management (ISR: 60s) -│ └── files/page.tsx File browser placeholder -│ -├── components/ -│ ├── Header.tsx Navigation + logout -│ ├── PoolCard.tsx Individual pool display -│ └── ui/ Reusable UI components -│ ├── button.tsx -│ ├── card.tsx -│ ├── badge.tsx -│ └── progress.tsx -│ -├── lib/ -│ ├── api.ts FastAPI client (40+ methods) -│ └── utils.ts Helper functions -│ -├── package.json 19 dependencies total -├── tsconfig.json Strict TypeScript config -├── tailwind.config.ts Theme customization -├── next.config.ts ISR + compression -├── postcss.config.js CSS processing -├── .eslintrc.json Linting rules -│ -├── .env.example Template -├── .env.local Local dev config -├── .gitignore Git exclusions -├── README.md Full documentation -└── [.next/] (generated on build) -``` - -## Development Commands - -```bash -# Install dependencies -npm install - -# Start dev server (with hot reload) -npm run dev -# → http://localhost:3000 - -# Build for production -npm run build - -# Start production server -npm start - -# Export to static HTML (for nginx) -npm run export - -# Lint code -npm run lint -``` - -## Key Features Implemented - -✅ **Authentication** -- JWT tokens with 8h lifetime -- Secure password hashing (bcrypt) -- Session persistence -- Automatic logout on invalid token - -✅ **Real-time Updates** -- Auto-refresh every 30s (dashboard) -- Refresh button for manual updates -- Last update timestamp -- Loading spinners - -✅ **Responsive Design** -- Mobile-first approach -- Desktop navigation menu -- Mobile hamburger menu -- Touch-friendly buttons - -✅ **Error Handling** -- Network error display -- User-friendly error messages -- Retry mechanisms -- Fallback states - -✅ **Performance** -- ISR caching strategy -- Bundle ~120KB (gzipped) -- Fast page loads -- Minimal JavaScript - -✅ **Type Safety** -- Full TypeScript coverage -- Strict mode enabled -- API types auto-generated -- Zero `any` types - -## Integration with Backend - -The frontend is fully integrated with the FastAPI backend: - -- `POST /api/auth/login` → Login page -- `GET /api/pools/` → Dashboard -- `GET /api/snapshots` → Snapshots page -- `GET /api/health` → Connection verification - -All endpoints use JWT Bearer tokens from the authentication service. - -## Next Steps (Phase 3) - -Phase 3 will add: - -1. **Snapshot Management UI** - - Create snapshots with custom names - - Rollback functionality - - Clone snapshots - - Retention policies - -2. **Dataset Management** - - Create/delete datasets - - Quota and compression settings - - Mount point management - -3. **Share Management** - - NFS share configuration - - Samba share setup - - Permission management - -4. **File Manager** - - Directory browsing - - Upload/download - - File preview - - Drag-and-drop - -5. **WebSocket Updates** - - Real-time notifications - - Alert system - - Live event streaming - -6. **Advanced Features** - - Pool scrub monitoring - - SMART disk info - - Email alerts - - Webhook notifications - -## Deployment Ready - -The frontend is ready to deploy: - -```bash -# Build -npm run build - -# Copy to production server -scp -r .next root@zmb-webui:/opt/zmb-webui/frontend/ - -# Start with systemd service -systemctl start zmb-webui-frontend -``` - -Or use the static export: -```bash -npm run export -# Serve with nginx or any static file server -``` - -## Testing the Frontend - -Test on the container with backend: - -```bash -# Terminal 1: Start backend (already running on 192.168.1.179:8000) - -# Terminal 2: Start frontend -cd frontend -npm install -npm run dev -# Open http://localhost:3000 - -# Test flow: -# 1. Go to /login -# 2. Enter: admin / testpass123 -# 3. Dashboard should show pool "tank" (if on Pi) -# 4. Or empty list if no pools on container -# 5. Navigate to Snapshots -# 6. Test Logout -``` - -## Statistics - -- **Lines of Code**: ~2000 (frontend) -- **Components**: 12 (5 UI + 7 feature) -- **Pages**: 4 (login, dashboard, snapshots, files) -- **API Methods**: 40+ -- **TypeScript Coverage**: 100% -- **Bundle Size**: ~120KB gzipped -- **Development Time**: ~2 hours - -## Summary - -Phase 2 is complete with a **production-ready**, **type-safe**, **performant** Next.js frontend that connects seamlessly to the FastAPI backend. The codebase is clean, well-organized, and ready for Phase 3 feature expansion. - -All files are in `/home/sysops/Dokumente/Scripte/cockpit_new/frontend/` and ready to deploy or develop locally. - ---- - -**Status**: ✅ Phase 2 Complete -**Ready for**: Phase 3 (Advanced Features) -**Estimated Timeline**: 2-3 hours for Phase 3 development diff --git a/PHASE_3A_COMPLETE.md b/PHASE_3A_COMPLETE.md deleted file mode 100644 index 85eba0c..0000000 --- a/PHASE_3A_COMPLETE.md +++ /dev/null @@ -1,177 +0,0 @@ -# Phase 3a Complete – Dashboard Quick Stats ✅ - -**Date**: 2026-04-18 -**Status**: ✅ **COMPLETE AND DEPLOYED** - ---- - -## What Was Implemented - -### Dashboard Quick Stats Cards (4 Cards) - -``` -┌──────────────┬──────────────┬──────────────┬──────────────┐ -│ SYSTEM │ CPU │ MEMORY │ STORAGE │ -├──────────────┼──────────────┼──────────────┼──────────────┤ -│ zmbfamilie │ 8.5% │ 4.2% │ ZFS Status │ -│ Uptime: │ ████░ │ █░░░ │ Available │ -│ 3d 22h 59m │ Load: 1.9 │ 172.6/4GB │ or N/A │ -│ Kernel: │ │ │ │ -│ 6.17.13 │ │ │ │ -└──────────────┴──────────────┴──────────────┴──────────────┘ -``` - -### Features Added - -1. **System Card** - - Hostname - - Uptime (days, hours, minutes) - - Kernel version - -2. **CPU Card** - - CPU usage percentage - - Progress bar (green < 50%, yellow 50-75%, red > 75%) - - Load average (1 min) - -3. **Memory Card** - - Memory usage percentage - - Progress bar with color coding - - Used / Total RAM (in GB/MB) - -4. **Storage Card** - - Shows "ZFS" if available - - Shows "N/A" if ZFS not available - -### Backend APIs Used - -✅ All endpoints already existed, no backend changes needed: -- `GET /api/system/info` → System information -- `GET /api/system/memory` → Memory usage -- `GET /api/system/cpu` → CPU metrics -- `GET /api/system/uptime` → Uptime information - -### Frontend Changes - -**`frontend/lib/api.ts`**: -- Added `getMemory()` method -- Added `getCpuInfo()` method -- Added `getUptime()` method - -**`frontend/app/page.tsx`**: -- Added state variables for system stats -- Added `loadSystemStats()` function -- Added `getUsageColor()` helper for progress bar colors -- Added `formatBytes()` helper for data formatting -- Added 4-column grid layout with stat cards -- Updated ZFS message to be less prominent (blue instead of yellow) - -### Testing - -All APIs verified working: -``` -✅ System: hostname "zmbfamilie", kernel "6.17.13-2-pve" -✅ Memory: 4.0 GB total, 172.6 MB used (4.2% usage) -✅ CPU: 16 cores, 8.5% usage, Load 1.9 -✅ Uptime: 3 days, 22 hours, 59 minutes -``` - ---- - -## What It Looks Like - -### Dashboard Layout - -``` -Dashboard -Last updated: [time] [Refresh Button] - -┌─────────────────────────────────────────────────────────────┐ -│ Quick Stats (4-column grid, responsive) │ -├─────────────────────────────────────────────────────────────┤ -│ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────┐ -│ │ ⚡ SYSTEM │ │ 🖥️ CPU │ │ 💾 MEMORY │ │ 📀 │ -│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │STOR. │ -│ │ zmbfamilie │ │ 8.5% │ │ 4.2% │ │ │ -│ │ Uptime: │ │ ████░░░░░░ │ │ █░░░░░░░░░ │ │ ZFS │ -│ │ 3d 22h 59m │ │ Load: 1.9 │ │ 172MB/4GB │ │ Avail│ -│ │ 6.17.13 │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ └──────┘ -│ -├─────────────────────────────────────────────────────────────┤ -│ ZFS Not Available │ -│ ZFS is not installed on this system. Files and Identities │ -│ features are available. │ -└─────────────────────────────────────────────────────────────┘ - -(If ZFS available, shows Storage Pools cards below) -``` - ---- - -## Responsive Behavior - -- **Mobile (< 768px)**: 1 column -- **Tablet (768px - 1024px)**: 2 columns -- **Desktop (> 1024px)**: 4 columns - ---- - -## Color Coding - -Progress bars use system color scheme: -- 🟢 **Green** (< 50% usage) - Healthy -- 🟡 **Yellow** (50-75% usage) - Warning -- 🔴 **Red** (> 75% usage) - Critical - ---- - -## Performance - -- Stats loaded once on mount with `loadSystemStats()` -- Pool refresh still happens every 30 seconds -- Stats are separate, don't block pool loading -- All calls are non-blocking (Promise.all with catch) - ---- - -## File Changes - -### Build Impact -``` -Dashboard page: 2.81 kB → 3.63 kB (minimal increase) -Total bundle: 125 kB (shared across all pages) -``` - -### Deployment -``` -✅ Frontend built successfully -✅ All files deployed to 192.168.1.179 -✅ Static export ready for production -``` - ---- - -## Next Phase (3b) - -What to add next: -- [ ] Real-time graphs (CPU/Memory over time) -- [ ] Service status (sshd, samba, etc.) -- [ ] Network interfaces and IPs -- [ ] Recent system logs -- [ ] System reboot/shutdown buttons - ---- - -## Success Metrics - -✅ Dashboard now shows system health at a glance -✅ Replaces Cockpit dashboard functionality -✅ Mobile-responsive design -✅ Color-coded progress bars -✅ No performance impact -✅ Consistent with ZMB Webui design - ---- - -**Ready for Phase 3b when you are!** 🚀 diff --git a/PROXMOX_LXC_SETUP.md b/PROXMOX_LXC_SETUP.md deleted file mode 100644 index dbe7c57..0000000 --- a/PROXMOX_LXC_SETUP.md +++ /dev/null @@ -1,417 +0,0 @@ -# Proxmox LXC Setup für ZMB Webui - -ZMB Webui läuft in **Proxmox LXC Container** mit direktem Zugriff auf Proxmox Host ZFS Pools. - -## Voraussetzungen - -- ✅ Proxmox Host mit ZFS (z.B. pool "tank") -- ✅ LXC Container Support -- ✅ Netzwerk-Zugriff zum Container - -## 1. Container im Proxmox erstellen - -### Via Proxmox Web UI - -1. **Datacenter → Nodes → → Create CT** - - Hostname: `zmb-webui` - - CT ID: z.B. `100` - - Unprivileged: **NEIN** ← Muss Privilegiert sein! - - Template: `debian-12-standard` - - Storage: Proxmox-Default OK - - Memory: 2048 MB (2GB) mindestens - - Cores: 2 (für Pi Kompatibilität reicht auch 1) - - Disk: 20-30GB - -2. **Features aktivieren:** - - [x] Nesting (für systemd, etc.) - - [x] Keyctl (für systemd-homed) - - [x] Mknod (für Devices) - -### Via CLI (pveam) - -```bash -# Auf Proxmox Host: - -# Template herunterladen (falls nicht vorhanden) -pveam download local debian-12-standard_12.2-1_amd64.tar.zst - -# Container erstellen -pct create 100 local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst \ - --hostname zmb-webui \ - --memory 2048 \ - --cores 2 \ - --storage local-lvm \ - --net0 name=eth0,bridge=vmbr0 \ - --onboot 1 \ - --features nesting=1,keyctl=1,mknod=1 \ - --privileged 1 - -# Starten -pct start 100 - -# Shell Zugriff -pct enter 100 -``` - -## 2. ZFS Mounting im Container - -### A. Host-Bindung (Recommended) - -```bash -# Auf dem Proxmox Host: - -# ZFS-Mountpoint für Container zugänglich machen -# (Proxmox macht das nicht automatisch!) - -# Option 1: Via /etc/pct/lxc/100/config - -pct set 100 -mp0 /tank/share,mp=/tank/share - -# oder direkt in config editieren: -nano /etc/pve/lxc/100.conf - -# Hinzufügen: -mp0: /tank/share,mp=/tank/share - -# Container neustarten: -pct reboot 100 -``` - -### B. ZFS im Container - Kernel Module - -```bash -# Proxmox Host muss ZFS Kernel Module haben: -lsmod | grep zfs -# Falls leer: apt install zfsutils-linux - -# Im privilegierten Container wird das Kernel-Modul vom Host sichtbar: -pct enter 100 -# Im Container: -lsmod | grep zfs # Sollte auch sichtbar sein! -zpool list # Sollte Host-Pools zeigen -``` - -## 3. Backend Installation im Container - -```bash -# Auf dem Proxmox Host: -pct enter 100 - -# Im Container: -apt update && apt upgrade -y -apt install -y python3 python3-pip python3-venv git curl - -# Backend klonen (oder kopieren) -git clone /opt/zmb-webui -cd /opt/zmb-webui/backend - -# System check -bash check_system.sh -# Sollte zeigen: -# ✓ Debian -# ✓ Privileged Container (erkannt!) -# ✓ ZFS Tools verfügbar -# ✓ /tank/share gemountet - -# Installation -bash install.sh - -# Service starten -systemctl start zmb-webui-backend -systemctl status zmb-webui-backend - -# Test -curl http://localhost:8000/health -``` - -## 4. Network Access vom Host/External - -### Container IP finden - -```bash -# Im Container: -ip addr show eth0 - -# Oder vom Host: -pct exec 100 ip addr show eth0 -# z.B.: 192.168.100.150 -``` - -### Zugriff vom Host - -```bash -# SSH vom Host zum Container -ssh root@ - -# oder direkt via pct: -pct enter 100 - -# Über Proxmox Firewall (falls aktiviert): -# Neue Regel: Port 8000 (Backend) erlauben -``` - -### Zugriff von External (Outside Proxmox) - -```bash -# Option A: Port Forward auf Proxmox Host -# (In Proxmox Firewall oder iptables) - -# Option B: Reverse Proxy auf Host -# nginx auf Proxmox Host → 9090 → Container :8000 - -sudo nano /etc/nginx/sites-available/zmb-webui -# Inhalt: -server { - listen 9090 ssl http2; - server_name _; - - ssl_certificate /etc/pve/nodes//pve-ssl.pem; - ssl_certificate_key /etc/pve/nodes//pve-ssl.key; - - location / { - proxy_pass http://:8000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} - -sudo systemctl restart nginx -# Dann: https://:9090 -``` - -## 5. ZFS Management im Container - -### Test ZFS Funktionalität - -```bash -# Im Container: - -# Pool-Liste (vom Proxmox Host!) -zpool list -# tank 364G 189G 175G - - 0% 52% 1.00x ONLINE - - -# Datasets anschauen -zfs list - -# Snapshots erstellen -zfs snapshot tank/share@test-2026-04-14 - -# Scrub starten -zpool scrub tank - -# Backend API Test -TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' | jq -r .access_token) - -curl http://localhost:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" -# → Zeigt [{"name":"tank", ...}] -``` - -## 6. Container Backup/Restore - -### Proxmox Native Backup - -```bash -# Auf dem Host: - -# Container Backup erstellen -vzdump 100 --storage local --notes "zmb-webui vor update" - -# Backup anschauen -ls -lh /var/lib/vz/dump/ - -# Restore -pct restore 101 /var/lib/vz/dump/vzdump-lxc-100-2026_04_14-12_30_45.tar.zst -pct start 101 -``` - -### Snapshot im Container - -```bash -# Im Container: -systemctl stop zmb-webui-backend - -# ZFS Snapshot des / Filesystems -zfs snapshot rpool/data/containers/100@backup-2026-04-14 - -# Oder: Proxmox Snapshot -systemctl start zmb-webui-backend -``` - -## 7. Monitoring & Logging - -```bash -# Proxmox Web UI → CT 100 → Logs - -# oder SSH: -pct enter 100 -journalctl -u zmb-webui-backend -f - -# Memory/CPU im Container -top -free -h -df -h / - -# Proxmox Monitoring -# Web UI → Nodes → → Resources -``` - -## 8. Performance Tuning - -### Memory im Container - -```bash -# Proxmox Host – Container config anpassen -pct set 100 --memory 2048 -pct set 100 --swap 512 # Swap auch gut - -# Vom Container-Zustand her: -free -h -# Total sollte ~2GB sein -``` - -### CPU Zuordnung - -```bash -# Alle Cores des Proxmox Hosts nutzen -pct set 100 --cores 2 # oder mehr, je nach Host - -# CPU-Limit setzen (optional) -# pct set 100 --cpulimit 2 # Max 2 CPU cores -``` - -### Disk Performance - -```bash -# Wenn Container auf lokallvm läuft: -# Default OK, aber SSD ist besser - -# Wenn auf ZFS läuft (Proxmox Storage): -# ZFS selbst managed das -``` - -## 9. Proxmox-spezifische Gotchas - -### Issue: ZFS im Container nicht sichtbar - -```bash -# Problem: zfs commands geben "command not found" -# Lösung: - -# 1. Im Container installieren -apt install -y zfsutils-linux - -# 2. Host-Kernel-Module müssen geladen sein -pct enter 100 -modprobe zfs -lsmod | grep zfs - -# 3. Privilegiert-Mode checken -# /etc/pve/lxc/100.conf sollte haben: -features: nesting=1 -``` - -### Issue: /tank/share nicht gemountet im Container - -```bash -# Problem: ls /tank/share → Permission denied -# Lösung: - -# /etc/pve/lxc/100.conf checken: -cat /etc/pve/lxc/100.conf | grep mp0 - -# Falls nicht vorhanden, hinzufügen: -pct set 100 -mp0 /tank/share,mp=/tank/share - -# Container neustarten: -pct reboot 100 - -# Oder manuell in config: -nano /etc/pve/lxc/100.conf -# mp0: /tank/share,mp=/tank/share -``` - -### Issue: Port 8000/9090 nicht erreichbar - -```bash -# Proxmox Firewall prüfen -# Web UI → Firewall - -# oder CLI: -pve-firewall status -pve-firewall enable - -# Port erlauben: -# Datacenter → Firewall → Add Rule -# Action: ACCEPT -# Direction: IN -# Protocol: TCP -# Destination Port: 8000 (oder 9090) - -# Dann im Container prüfen: -netstat -tlnp | grep 8000 -``` - -## 10. Security - -### Unprivilegiert vs Privilegiert - -``` -⚠️ Container ist PRIVILEGIERT! - -Risiken: -- Root im Container ≈ Root auf Host -- Zugriff auf Host-Filesystems - -Mitigationen: -- Firewall (Proxmox + System) -- regelmäßige Updates -- Backup-Strategy -- Monitoring -``` - -### Firewall Rules (Proxmox) - -```bash -# Nur lokales Netzwerk zulassen -Datacenter → Firewall: -- Allow FROM 192.168.x.0/24 → Port 8000 - -# oder SSH Tunnel statt direkter Zugriff -ssh -L 9090:localhost:8000 root@proxmox-host -curl http://localhost:9090/health -``` - -## 11. Production Checklist - -- [ ] Container erstellt & Started -- [ ] ZFS Mount funktioniert (`ls /tank/share`) -- [ ] Backend installiert & Running -- [ ] `curl /health` → 200 OK -- [ ] Admin-Passwort geändert -- [ ] Firewall konfiguriert -- [ ] Network/SSH Zugriff funktioniert -- [ ] Backup-Strategy definiert -- [ ] Monitoring konfiguriert -- [ ] Logs prüfbar - -## Zusammenfassung - -``` -Proxmox Host -├── ZFS Pool: tank -├── LXC Container: 100 (zmb-webui, privilegiert) -│ ├── /tank/share (gemountet) -│ ├── FastAPI :8000 -│ └── systemd service: zmb-webui-backend -└── Firewall: Port 8000/9090 allowed -``` - ---- - -**Backend läuft im Proxmox LXC Container mit vollständigem ZFS Management!** 🚀 diff --git a/PROXMOX_QUICKSTART.md b/PROXMOX_QUICKSTART.md deleted file mode 100644 index ea7d66a..0000000 --- a/PROXMOX_QUICKSTART.md +++ /dev/null @@ -1,232 +0,0 @@ -# Proxmox LXC Quick Start (5 Minutes) - -ZMB Webui auf Proxmox LXC – schnell & einfach! - -## One-Liner Setup - -```bash -# === AUF PROXMOX HOST === - -# 1. Container erstellen -pct create 100 local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst \ - --hostname zmb-webui \ - --memory 2048 \ - --cores 2 \ - --privileged 1 \ - --features nesting=1,keyctl=1 \ - --net0 name=eth0,bridge=vmbr0 \ - --onboot 1 - -# 2. ZFS Mount -pct set 100 -mp0 /tank/share,mp=/tank/share - -# 3. Starten -pct start 100 - -# 4. Shell -pct enter 100 - -# === IM CONTAINER === - -# 5. Update -apt update && apt upgrade -y - -# 6. Dependencies -apt install -y python3 python3-pip python3-venv git curl - -# 7. Backend -git clone /opt/zmb-webui -cd /opt/zmb-webui/backend - -# 8. Check -bash check_system.sh -# Sollte zeigen: ✓ Debian, ✓ Privileged, ✓ ZFS - -# 9. Install -bash install.sh - -# 10. Start -systemctl start zmb-webui-backend - -# 11. Test -curl http://localhost:8000/health -# → {"status":"healthy"} - -# 12. ZFS Test -zpool list -# → tank pool sichtbar! - -# 13. Password -python3 manage_users.py change-password admin -``` - -## Container IP & Access - -```bash -# Im Container oder vom Host: -pct exec 100 ip addr show eth0 -# z.B.: 192.168.100.150 - -# SSH vom Host: -ssh root@192.168.100.150 - -# Oder direkt: -pct enter 100 - -# API Test: -curl http://192.168.100.150:8000/health -``` - -## Verify ZFS Management - -```bash -# Im Container: - -# ZFS Pools -zpool list -# → Zeigt Proxmox Host Pools (z.B. tank) - -# Datasets -zfs list -# → Alle Datasets vom Host - -# Snapshots -zfs list -t snapshot | head -# → Snapshots sind sichtbar - -# Backend API -TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"newpass"}' | jq -r .access_token) - -curl http://localhost:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" -# → [{"name":"tank","health":"ONLINE",...}] -``` - -## Access vom Proxmox Host - -```bash -# Shell zum Container -pct enter 100 - -# SSH zum Container -pct enter 100 # oder -ssh root@ - -# curl von Host -curl http://:8000/health -``` - -## Performance im Proxmox LXC - -- Pool Queries: 20-50ms (vs 10-20ms bare metal) -- ZFS funktioniert native (Host-Kernel-Module) -- ~50% Performance-Overhead normal (akzeptabel) - -## Container Config anpassen - -```bash -# Memory -pct set 100 --memory 2048 -pct set 100 --swap 512 - -# Cores -pct set 100 --cores 2 - -# Disk (falls nötig) -pct set 100 --rootfs local-lvm:vm-100-disk-0,size=30G - -# Neustarten -pct reboot 100 -``` - -## Proxmox Firewall (Optional) - -```bash -# Wenn Firewall active: -# Web UI → Firewall → Add Rule -# -# In: TCP, Destination Port 8000 -# Out: TCP, Allow all (default) - -# oder CLI (nicht empfohlen) -``` - -## Troubleshooting - -### ZFS nicht sichtbar - -```bash -# Im Container: -apt install -y zfsutils-linux -modprobe zfs -zpool list # Sollte funktionieren -``` - -### /tank/share nicht gemountet - -```bash -# Auf Proxmox Host: -pct set 100 -mp0 /tank/share,mp=/tank/share -pct reboot 100 - -# Im Container: -ls -la /tank/share # Sollte funktionieren -``` - -### Port 8000 nicht erreichbar - -```bash -# Im Container: -netstat -tlnp | grep 8000 -# Sollte zeigen: LISTEN ... 8000 - -# Proxmox Firewall prüfen -# Web UI → Firewall → Status - -# SSH Tunnel als Workaround: -# ssh -L 9090:localhost:8000 root@proxmox-host -# curl http://localhost:9090/health -``` - -## Backup & Restore - -```bash -# Backup -vzdump 100 --storage local - -# Restore -pct restore 101 /var/lib/vz/dump/vzdump-lxc-100-*.tar.zst -pct start 101 -``` - -## Logs - -```bash -# Im Container: -journalctl -u zmb-webui-backend -f - -# Von Proxmox Host: -pct exec 100 journalctl -u zmb-webui-backend -f -``` - -## Summary - -``` -Proxmox Host (mit ZFS Pool "tank") -└── LXC Container 100 (zmb-webui, privilegiert) - ├── /tank/share (gemountet) - ├── FastAPI :8000 running - ├── systemd service enabled - └── ZFS Management funktioniert! - -Access: -├── Local: pct enter 100 -├── SSH: ssh root@ -└── API: curl http://:8000/health -``` - ---- - -**Das war's!** Backend läuft auf Proxmox LXC mit vollständigem ZFS Management. 🚀 diff --git a/SESSION_SUMMARY_2026-04-18.md b/SESSION_SUMMARY_2026-04-18.md deleted file mode 100644 index a712ad0..0000000 --- a/SESSION_SUMMARY_2026-04-18.md +++ /dev/null @@ -1,178 +0,0 @@ -# Session Summary – ZMB Webui – April 18, 2026 - -**Session Duration**: Continued from previous session -**Primary Objective**: Implement and verify Samba password management, resolve SPA routing issues -**Status**: ✅ **ALL OBJECTIVES COMPLETED AND VERIFIED** - ---- - -## What Was Accomplished - -### 1. Fixed SPA Routing & Frontend Deployment ✅ - -**Issue**: `/identities`, `/files`, and other SPA routes were returning 404 instead of serving index.html - -**Root Cause**: `main.py` catch-all route was checking wrong path for static files (looking for `/frontend/out/` instead of `/backend/static/`) - -**Solution**: -- Modified `backend/main.py` lines 174-191 to check `/static/` directory first -- Fixed path priority: static (production) before frontend/out (dev) -- Deployed fix to container and restarted backend - -**Result**: ✅ All SPA routes now return 200 OK with correct HTML - -### 2. Fixed Frontend API Configuration ✅ - -**Issue**: `.env.local` was empty, frontend didn't know where API was located - -**Solution**: -- Set `NEXT_PUBLIC_API_URL=http://192.168.1.179:8000` in `frontend/.env.local` -- This variable is baked into JavaScript at build time for static export -- Rebuilt frontend and redeployed to container - -**Result**: ✅ Frontend can now communicate with backend API - -### 3. Verified System Users Display ✅ - -**Status**: System users are now correctly displayed in Identities page - -**Test Results**: -- GET `/api/identities/users` returns 5 system users - - root, administrator, testuser, wsdd2, nobody -- Frontend routes all return HTML (200) -- Identities page correctly loads and displays users - -### 4. Implemented Samba Password Management ✅ - -**Features Added**: - -**Backend** (`backend/services/identities.py`): -- New method: `set_samba_password(username, password)` -- Uses `smbpasswd -a -s` command -- Sends password via stdin with error handling - -**API** (`backend/routers/identities.py`): -- New endpoint: `POST /api/identities/users/{username}/samba-password` -- Returns: `{"status": "updated", "username": "...", "type": "samba"}` -- Protected with JWT authentication - -**Frontend** (`frontend/lib/api.ts`): -- New method: `setSambaPassword(username, password)` - -**Frontend UI** (`frontend/app/identities/page.tsx`): -- New dialog for setting Samba password -- Button added to Linux users table (dimmed Key icon) -- Handler with error management -- State variables for dialog management - -**Build Impact**: -- Identities page: 4.38 kB → 4.49 kB (minimal increase) -- Total bundle size unchanged - ---- - -## Comprehensive Test Results - -All systems verified working correctly: - -| Test | Result | Details | -|------|--------|---------| -| Frontend serving | ✅ PASS | GET / returns HTML (200) | -| API health | ✅ PASS | `/health` returns healthy status | -| ZFS detection | ✅ PASS | `/api/status` correctly reports ZFS unavailable | -| Authentication | ✅ PASS | Login generates valid JWT token | -| System users | ✅ PASS | Returns 5 users from PAM | -| Linux password | ✅ PASS | POST `/users/{user}/password` works | -| **Samba password** | ✅ **PASS** | **POST `/users/{user}/samba-password` works** | -| SPA routing | ✅ PASS | All routes (/, /files, /identities, /login, /dashboard) return 200 | - ---- - -## Files Modified - -### Backend -- `backend/main.py` – Fixed SPA catch-all route path logic -- `backend/services/identities.py` – Added `set_samba_password()` method -- `backend/routers/identities.py` – Added `/users/{username}/samba-password` endpoint - -### Frontend -- `frontend/.env.local` – Set API URL (already fixed in previous session) -- `frontend/lib/api.ts` – Added `setSambaPassword()` method -- `frontend/app/identities/page.tsx` – Added Samba password UI, dialog, and handlers - -### Documentation -- Created `memory/samba_password_feature.md` – Feature documentation -- Updated `memory/feature_system_users_display.md` – Marked Samba password as complete -- Updated `memory/MEMORY.md` – Added index entries - ---- - -## How to Use Samba Password Feature - -1. Navigate to **Identities → Users → Linux Users** -2. Find user in table -3. Click the dimmed **Key** icon (Samba password button) -4. Enter new Samba password -5. Click **"Set Password"** -6. Dialog closes and user list reloads - ---- - -## Ready for Production - -### Current Deployment Status -- **Test Container**: 192.168.1.179 ✅ Fully functional -- **Production Pi**: 10.66.120.3 ⏳ Ready to deploy - -### Next Steps for Production -1. Build frontend with production environment variables -2. Deploy to Pi at 10.66.120.3:9090 (adjust `.env.local` accordingly) -3. Verify ZFS pool detection works (Pi has actual ZFS) -4. Test all features with real ZFS pools and snapshots - -### Configuration for Production -```bash -# In frontend/.env.local: -NEXT_PUBLIC_API_URL=http://10.66.120.3:8000 -# OR if using domain/HTTPS: -NEXT_PUBLIC_API_URL=https://zmb-webui.example.com -``` - ---- - -## Session Metrics - -- **Features Completed**: 1 major (Samba password management) -- **Bugs Fixed**: 1 critical (SPA routing) -- **Endpoints Added**: 1 new API endpoint -- **Frontend Components Updated**: 1 page -- **Test Coverage**: All routes tested, all endpoints verified -- **Code Quality**: Type-safe TypeScript, proper error handling - ---- - -## Key Takeaways - -1. **Environment Variables**: Next.js static export requires `NEXT_PUBLIC_` variables to be set at build time and compiled into bundles -2. **SPA Routing**: Catch-all route must serve index.html from correct location (static dir in production) -3. **Samba Integration**: `smbpasswd -a -s` accepts password via stdin for automation -4. **Backend API Consistency**: New endpoints follow existing patterns (same auth, request models, response format) - ---- - -## Verified Functionality - -✅ System users display correctly -✅ PAM authentication working -✅ JWT token generation functional -✅ API endpoints protected with auth -✅ Frontend-backend communication working -✅ SPA routing functional -✅ Samba password setting implemented and tested -✅ Static export builds successfully -✅ All pages load correctly -✅ Error handling in place - ---- - -**Ready for Next Phase**: Phase 3a Quick Wins (Snapshot Create, Snapshot Rollback, Dark Mode Toggle) diff --git a/TEST_PLAN.md b/TEST_PLAN.md deleted file mode 100644 index c81dc9b..0000000 --- a/TEST_PLAN.md +++ /dev/null @@ -1,207 +0,0 @@ -# Testing Plan – ZMB Webui auf 192.168.1.179 - -**Date**: 2026-04-18 -**Target**: Test-LXC Container at 192.168.1.179 -**Backend**: Should be running on :8000 -**Frontend**: Will test via npm dev or static export - ---- - -## Pre-Test Checklist - -- [ ] Backend still running? `curl http://192.168.1.179:8000/health` -- [ ] SSH access available? `ssh root@192.168.1.179` - ---- - -## Test Scenario 1: Frontend Dev Server (Slow, Good for Debugging) - -```bash -# On your local machine -cd frontend -npm install # If not done -npm run dev # Starts on http://localhost:3000 - -# Visit http://localhost:3000 -# Browser will call API at: http://192.168.1.179:8000 (via .env.local) -``` - -**Test Flow**: -1. Go to `/login` → Enter `admin / ` -2. Dashboard → Should show pools (or empty if no ZFS) -3. **Check menu**: Snapshots + Datasets visible? (Only if ZFS available) -4. Click each page to verify it loads - ---- - -## Test Scenario 2: Static Export (Faster, Production-like) - -```bash -# On local machine -cd frontend -npm run build -npm run export # Creates ./out/ with static HTML - -# Copy to test-container -scp -r out/* root@192.168.1.179:/opt/zmb-webui/backend/static/ - -# Or just test locally with nginx -python3 -m http.server 3000 --directory out/ -# Visit http://localhost:3000 -``` - ---- - -## Test Checklist – All Pages - -### Login Page -- [ ] Username/password input visible -- [ ] Login button works -- [ ] After login → redirects to Dashboard -- [ ] Token saved in localStorage - -### Dashboard -- [ ] Header loads with logo -- [ ] Pool cards visible (if ZFS on container) -- [ ] Capacity bar shows -- [ ] Auto-refresh every 30s (check network tab) -- [ ] Health badge color correct (ONLINE green, DEGRADED yellow, etc.) - -### Menu Visibility (ZFS-Conditional) -``` -✅ Always visible: - - Dashboard - - Files - - Identities - -❓ Conditional (only if ZFS available on container): - - Snapshots - - Datasets -``` -- [ ] Check browser console for `/api/status` call -- [ ] Verify `zfs_available: true/false` response - -### Snapshots Page -- [ ] Page loads (if ZFS available) -- [ ] Table shows header: Name, Dataset, Created, Used -- [ ] Refresh button works -- [ ] Delete button triggers dialog - -### Datasets Page -- [ ] Tab navigation works (Datasets ↔ Shares) -- [ ] Datasets tab: List visible, Create button clickable -- [ ] Shares tab: Samba + NFS subtabs work -- [ ] Create dialogs open/close properly - -### Files Page -- [ ] Loads and shows current directory -- [ ] Breadcrumb navigation works -- [ ] Can navigate up and into directories -- [ ] Upload button works (try small file) -- [ ] Create Folder dialog works -- [ ] View toggle (List ↔ Grid) works -- [ ] Search box works -- [ ] File selection and multi-select works -- [ ] Delete dialog confirms action - -### Identities Page -- [ ] Users tab shows Linux users table -- [ ] Samba subtab shows or empty message -- [ ] Groups tab lists groups -- [ ] Login History shows recent logins -- [ ] Create User dialog fields present -- [ ] Create Group dialog present - ---- - -## API Endpoint Tests (curl) - -```bash -# Get token -TOKEN=$(curl -s -X POST http://192.168.1.179:8000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":""}' | jq -r '.access_token') - -echo "Token: $TOKEN" - -# Check ZFS availability -curl -s http://192.168.1.179:8000/api/status | jq . - -# List pools (should work even on container with no ZFS) -curl -s http://192.168.1.179:8000/api/pools \ - -H "Authorization: Bearer $TOKEN" | jq . - -# List users -curl -s http://192.168.1.179:8000/api/identities/users \ - -H "Authorization: Bearer $TOKEN" | jq . - -# List datasets (may fail if no ZFS) -curl -s http://192.168.1.179:8000/api/datasets \ - -H "Authorization: Bearer $TOKEN" | jq . - -# List shares -curl -s http://192.168.1.179:8000/api/shares/samba \ - -H "Authorization: Bearer $TOKEN" | jq . -``` - ---- - -## Common Issues & Debugging - -### "Cannot GET /" or 404 -- Backend might not be serving static files -- Check: `ls -la /opt/zmb-webui/backend/static/` -- If empty, need to do `npm run export` and `scp` - -### CORS errors in browser console -- Backend allow_origins set to `["*"]` — should work -- Check backend logs: `journalctl -u zmb-webui-backend -f` - -### API call fails with 401 -- Token expired or invalid -- Logout (clear localStorage) → Login again -- Check token in localStorage: `localStorage.getItem('access_token')` - -### Snapshots/Datasets menu hidden -- `/api/status` returned `zfs_available: false` -- This is expected on container without ZFS -- Test on actual Pi if need to test ZFS features - -### Files not uploading -- Check file size (< some limit?) -- Check `/tank/share` exists and is writable -- Check backend logs for upload errors - ---- - -## Success Criteria - -- [ ] All pages load without errors -- [ ] Navigation menu works -- [ ] Login/logout works -- [ ] ZFS-conditional menu works (menu items hide/show correctly) -- [ ] File manager can browse and upload -- [ ] No console errors (check F12) -- [ ] No 401/403 errors -- [ ] All dialogs open/close properly - ---- - -## After Testing - -If all ✅: -```bash -# Build final static export -cd frontend -npm run build -npm run export - -# Deploy to Pi -scp -r out/* root@10.66.120.3:/opt/zmb-webui/backend/static/ -``` - -If issues ❌: -- Document error messages -- Check browser console (F12 → Console tab) -- Run curl tests to isolate backend vs frontend -- Check backend logs on container diff --git a/TEST_RESULTS.md b/TEST_RESULTS.md deleted file mode 100644 index a406a84..0000000 --- a/TEST_RESULTS.md +++ /dev/null @@ -1,200 +0,0 @@ -# Test Results – ZMB Webui WebUI on 192.168.1.179 - -**Date**: 2026-04-18 -**Tester**: Claude Code -**Target**: Test-LXC Container at 192.168.1.179 -**Result**: ✅ **PASSING** — All features working as expected - ---- - -## Summary - -Frontend deployment successful. All pages load correctly. **ZFS-Conditional Menu feature working perfectly** — Snapshots + Datasets menu items are hidden on container (no ZFS), shown only on Pi with ZFS. - ---- - -## Deployment Status - -✅ **Frontend Built**: `out/` directory with static HTML -✅ **Frontend Deployed**: Copied to `/opt/zmb-webui/backend/static/` -✅ **Backend Running**: Port 8000, healthy -✅ **Frontend Accessible**: http://192.168.1.179/ serving index.html - ---- - -## Functional Tests - -### API Status Endpoint -``` -GET /api/status -Response: {"status":"healthy","zfs_available":false,"version":"1.0.0"} -Result: ✅ PASS -``` - -**Significance**: `zfs_available: false` triggers conditional menu logic in frontend. - ---- - -### Authentication -``` -POST /api/auth/login -Username: testuser -Password: testpass123 -Response: JWT token generated -Result: ✅ PASS -``` - -Token format: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` - ---- - -### Protected API Endpoints (with token) - -#### Identities/Users -``` -GET /api/identities/users -Authorization: Bearer -Result: ✅ PASS (returns user list) -``` - -#### Samba Shares -``` -GET /api/shares/samba -Response: Array of 5 shares (Share, Share1 duplicates) -Result: ✅ PASS -``` - -#### Files/Space -``` -GET /api/files/space -Response: {"detail":"[Errno 2] No such file or directory: 'zfs'"} -Result: ⚠️ EXPECTED (Container has no ZFS) -``` - ---- - -## Frontend Menu - Conditional Rendering - -**Status**: ✅ **WORKING PERFECTLY** - -On container (no ZFS): -``` -Visible: - ✓ Dashboard - ✓ Files - ✓ Identities - -Hidden: - ✗ Snapshots (correctly hidden) - ✗ Datasets (correctly hidden) -``` - -**Why**: Header.tsx calls `api.getSystemStatus()` on mount, checks `zfs_available` boolean, conditionally renders menu links. - ---- - -## Pages Load Status - -| Page | Loads | Functionality | -|------|-------|--------------| -| Dashboard | ✅ | Shows "Loading..." (no pools on container) | -| Login | ✅ | Form visible, can login | -| Snapshots | ✅ | Hidden from menu (ZFS unavailable) | -| Datasets | ✅ | Hidden from menu (ZFS unavailable) | -| Files | ✅ | File manager UI loads | -| Identities | ✅ | User/group management UI loads | - ---- - -## Bundle Size & Performance - -``` -Next.js Build Output: - Dashboard (/) 2.81 kB - Datasets 2.9 kB - Files 8 kB - Identities 4.38 kB - Snapshots 3.39 kB - Login 3.13 kB - -Total First Load JS: 87.4 kB (shared by all pages) -Static content: pre-rendered -``` - -**Assessment**: Lightweight, suitable for 4GB RAM Pi. - ---- - -## ZFS-Conditional Menu Feature Testing - -### The Feature -- Frontend detects ZFS availability via `/api/status` -- Snapshots + Datasets menu links only render if `zfs_available: true` -- Files + Identities always visible (not ZFS-dependent) - -### Test Method -1. Deploy to container without ZFS -2. Call `api.getSystemStatus()` on mount -3. Check response: `zfs_available: false` -4. Verify menu items hidden in DOM - -### Result -✅ **FEATURE COMPLETE AND WORKING** - -Menu correctly shows: -- Snapshots: HIDDEN ✓ -- Datasets: HIDDEN ✓ -- Files: VISIBLE ✓ -- Identities: VISIBLE ✓ - ---- - -## Known Issues - -### None found during testing - ---- - -## Next Steps - -1. ✅ Frontend deployed and tested on test-LXC -2. ⏳ Ready for production Pi deployment (10.66.120.3) -3. ⏳ Full testing on Pi with actual ZFS pools - ---- - -## Production Deployment (Pi 10.66.120.3) - -When ready: -```bash -# Build and export frontend -npm run build -npm run export - -# Deploy to Pi -scp -r out/* root@10.66.120.3:/opt/zmb-webui/backend/static/ - -# Verify -curl http://10.66.120.3:9090/ | head -20 -``` - -On Pi (with ZFS): -- Menu will show: Dashboard, Snapshots, Datasets, Files, Identities (all 5) -- Pool cards will display actual ZFS pools -- Snapshot/dataset operations will be functional - ---- - -## Testing Checklist - -- [x] Backend running -- [x] Frontend deployed -- [x] Frontend HTML served -- [x] Login works (JWT auth) -- [x] API endpoints respond -- [x] ZFS-conditional menu works correctly -- [x] No console errors -- [x] Bundle size acceptable -- [x] All pages load - -**Overall Result: ✅ READY FOR PRODUCTION** diff --git a/backend/DEVLOG.md b/backend/DEVLOG.md deleted file mode 100644 index 5f41b87..0000000 --- a/backend/DEVLOG.md +++ /dev/null @@ -1,189 +0,0 @@ -# backend – Dev Log - -## 2026-04-14 22:06 – 22:11 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:11 – 22:15 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:15 – 22:17 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:18 – 22:20 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:21 – 22:22 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 22:22 – 22:23 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 10:53 – 20:00 (57h 07m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:01 – 20:01 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:02 – 20:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:44 – 10:45 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 17:16 – 17:21 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 17:22 – 17:23 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:27 – 22:28 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:29 – 22:30 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:34 – 22:35 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-06-03 09:22 – 09:24 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- -## 2026-06-03 09:24 – 09:28 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/next.config.ts | 13 +++++++++++++ - ---- diff --git a/backend/main.py b/backend/main.py index 520b8a8..34a2ddb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -15,11 +15,12 @@ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates # Add backend to path for imports sys.path.insert(0, str(Path(__file__).parent)) -from routers import auth, pools, datasets, snapshots, navigator, identities, shares, system +from routers import auth, pools, datasets, snapshots, navigator, identities, shares, system, pages from services.zfs_runner import zfs_runner # Configure logging @@ -114,7 +115,11 @@ async def websocket_endpoint(websocket: WebSocket): ws_clients.discard(websocket) -# Include routers (must be before static files mounting) +# Static assets (HTMX, PicoCSS — vendored) +_static_dir = os.path.join(os.path.dirname(__file__), "static") +app.mount("/static", StaticFiles(directory=_static_dir), name="static") + +# Include API routers first app.include_router(auth.router) app.include_router(pools.router) app.include_router(datasets.router) @@ -124,6 +129,9 @@ app.include_router(identities.router) app.include_router(shares.router) app.include_router(system.router) +# HTML pages router last (catches /, /login, /zfs, /fragments/*) +app.include_router(pages.router) + # Health check endpoint (no auth required) @@ -156,18 +164,6 @@ async def status_check(): } -# Root endpoint - API info only -@app.get("/") -async def root(request: Request): - """API info""" - return { - "name": "ZMB Webui API", - "version": "1.0.0", - "docs": "/docs", - "frontend": str(request.base_url) - } - - # Error handler @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): diff --git a/backend/requirements.txt b/backend/requirements.txt index 3219b01..37742ee 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,5 @@ fastapi>=0.110.0 +jinja2>=3.1.0 uvicorn[standard]>=0.27.0 pydantic>=2.6.0 pydantic-settings>=2.2.0 diff --git a/backend/routers/pages.py b/backend/routers/pages.py new file mode 100644 index 0000000..edf8c6d --- /dev/null +++ b/backend/routers/pages.py @@ -0,0 +1,293 @@ +""" +HTML Page routes (Jinja2 + HTMX) +Replaces Next.js frontend entirely. +""" + +import os +from fastapi import APIRouter, Request, Response, Form, Cookie +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from services import auth as auth_service +from services import system_info, zfs_runner +from services.shares import share_manager +from services.identities import list_users, list_groups +from services.file_manager import FileManager + +router = APIRouter() + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) + +_file_manager = FileManager() + + +def _require_user(token: str | None) -> str | None: + if not token: + return None + return auth_service.verify_token(token) + + +def _redirect_login(): + return RedirectResponse("/login", status_code=303) + + +def _ctx(**kwargs): + return kwargs or {} + + +# ── Auth ────────────────────────────────────────────────────────────────────── + +@router.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + return templates.TemplateResponse(request, "login.html") + + +@router.post("/login") +async def login_submit( + request: Request, + username: str = Form(...), + password: str = Form(...), +): + if not auth_service.authenticate_user(username, password): + return templates.TemplateResponse( + request, "login.html", + {"error": "Ungültiger Benutzername oder Passwort"}, + status_code=401, + ) + token = auth_service.create_access_token(username) + resp = RedirectResponse("/", status_code=303) + resp.set_cookie("token", token, httponly=True, samesite="lax", max_age=86400) + return resp + + +@router.post("/logout") +async def logout(): + resp = RedirectResponse("/login", status_code=303) + resp.delete_cookie("token") + return resp + + +# ── Pages ───────────────────────────────────────────────────────────────────── + +@router.get("/", response_class=HTMLResponse) +async def dashboard(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + hostname = system_info.get_hostname().get("hostname", "") + return templates.TemplateResponse(request, "dashboard.html", { + "active": "dashboard", "hostname": hostname, + }) + + +@router.get("/zfs", response_class=HTMLResponse) +async def zfs_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + pools_raw = zfs_runner.list_pools() + pools = [] + for p in pools_raw: + datasets = zfs_runner.list_datasets(p["name"]) + vdevs = zfs_runner.get_pool_status(p["name"]).get("vdevs", []) + pools.append({**p, "datasets": datasets, "vdevs": vdevs}) + return templates.TemplateResponse(request, "zfs.html", { + "active": "zfs", "pools": pools, + }) + + +@router.get("/snapshots", response_class=HTMLResponse) +async def snapshots_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + snapshots = zfs_runner.list_snapshots() + return templates.TemplateResponse(request, "snapshots.html", { + "active": "snapshots", "snapshots": snapshots, + }) + + +@router.get("/shares", response_class=HTMLResponse) +async def shares_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + return templates.TemplateResponse(request, "shares.html", {"active": "shares"}) + + +@router.get("/navigator", response_class=HTMLResponse) +async def navigator_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + return templates.TemplateResponse(request, "navigator.html", {"active": "navigator"}) + + +@router.get("/identities", response_class=HTMLResponse) +async def identities_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + return templates.TemplateResponse(request, "identities.html", {"active": "identities"}) + + +@router.get("/logs", response_class=HTMLResponse) +async def logs_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + return templates.TemplateResponse(request, "logs.html", {"active": "logs"}) + + +@router.get("/services", response_class=HTMLResponse) +async def services_page(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return _redirect_login() + return templates.TemplateResponse(request, "services.html", {"active": "services"}) + + +# ── HTMX Fragments ──────────────────────────────────────────────────────────── + +@router.get("/fragments/quick-stats", response_class=HTMLResponse) +async def frag_quick_stats(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/quick_stats.html", { + "cpu": system_info.get_cpu_info(), + "mem": system_info.get_memory(), + "uptime": system_info.get_uptime(), + "pools": zfs_runner.list_pools(), + }) + + +@router.get("/fragments/pools", response_class=HTMLResponse) +async def frag_pools(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/pools.html", { + "pools": zfs_runner.list_pools(), + }) + + +@router.get("/fragments/disk-usage", response_class=HTMLResponse) +async def frag_disk_usage(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + data = system_info.get_disk_usage() + return templates.TemplateResponse(request, "fragments/disk_usage.html", { + "filesystems": data.get("filesystems", []), + }) + + +@router.get("/fragments/net-traffic", response_class=HTMLResponse) +async def frag_net_traffic(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + data = system_info.get_network_traffic() + return templates.TemplateResponse(request, "fragments/net_traffic.html", { + "interfaces": data.get("interfaces", []), + }) + + +@router.get("/fragments/samba-shares", response_class=HTMLResponse) +async def frag_samba_shares(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/samba_shares.html", { + "shares": share_manager.list_samba_shares(), + }) + + +@router.get("/fragments/nfs-shares", response_class=HTMLResponse) +async def frag_nfs_shares(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/nfs_shares.html", { + "exports": share_manager.list_nfs_shares(), + }) + + +@router.get("/fragments/samba-config", response_class=HTMLResponse) +async def frag_samba_config(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + data = share_manager.get_samba_global_config() + return templates.TemplateResponse(request, "fragments/samba_config.html", { + "params": data.get("parameters", []), + }) + + +@router.post("/fragments/samba-config/toggle-macos", response_class=HTMLResponse) +async def frag_toggle_macos( + request: Request, + enable: str = "true", + token: str | None = Cookie(default=None), +): + if not _require_user(token): + return HTMLResponse("", status_code=401) + current_data = share_manager.get_samba_global_config() + params = {p["key"]: p["value"] for p in current_data.get("parameters", [])} + fruit = {"fruit:encoding": "native", "fruit:metadata": "stream", + "fruit:zero_file_id": "yes", "fruit:nfs_aces": "no"} + if enable == "true": + params.update(fruit) + else: + for k in fruit: + params.pop(k, None) + share_manager.set_samba_global_config(params) + updated = share_manager.get_samba_global_config() + return templates.TemplateResponse(request, "fragments/samba_config.html", { + "params": updated.get("parameters", []), + }) + + +@router.get("/fragments/users", response_class=HTMLResponse) +async def frag_users(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/users.html", { + "users": list_users(), + }) + + +@router.get("/fragments/groups", response_class=HTMLResponse) +async def frag_groups(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + return templates.TemplateResponse(request, "fragments/groups.html", { + "groups": list_groups(), + }) + + +@router.get("/fragments/logs", response_class=HTMLResponse) +async def frag_logs(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + data = system_info.get_journal_logs(50) + return templates.TemplateResponse(request, "fragments/logs.html", { + "logs": data.get("logs", []), + }) + + +@router.get("/fragments/services", response_class=HTMLResponse) +async def frag_services(request: Request, token: str | None = Cookie(default=None)): + if not _require_user(token): + return HTMLResponse("", status_code=401) + data = system_info.get_services() + return templates.TemplateResponse(request, "fragments/services.html", { + "services": data.get("services", []), + }) + + +@router.get("/fragments/navigator", response_class=HTMLResponse) +async def frag_navigator( + request: Request, + path: str = "/tank/share", + token: str | None = Cookie(default=None), +): + if not _require_user(token): + return HTMLResponse("", status_code=401) + try: + items = _file_manager.list_directory(path) + parent = str(os.path.dirname(path)) if path != "/" else None + return templates.TemplateResponse(request, "fragments/navigator.html", { + "items": items, "current_path": path, "parent_path": parent, "error": None, + }) + except Exception as e: + return templates.TemplateResponse(request, "fragments/navigator.html", { + "items": [], "current_path": path, "parent_path": None, "error": str(e), + }) diff --git a/backend/static/htmx.min.js b/backend/static/htmx.min.js new file mode 100644 index 0000000..59937d7 --- /dev/null +++ b/backend/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/backend/static/pico.min.css b/backend/static/pico.min.css new file mode 100644 index 0000000..e10ec26 --- /dev/null +++ b/backend/static/pico.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";/*! + * Pico CSS ✨ v2.1.1 (https://picocss.com) + * Copyright 2019-2025 - Licensed under MIT + */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(2, 154, 232, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#0172ad;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 114, 173, 0.5);--pico-primary-hover:#015887;--pico-primary-hover-background:#02659a;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(2, 154, 232, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:host(:not([data-theme])) details summary[role=button].contrast:not(.outline)::after,:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:host(:not([data-theme])) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before,:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown>a::after,details.dropdown>button::after,details.dropdown>summary::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown>summary:not([role]):active,details.dropdown>summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown>summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown>summary:not([role]):focus-visible{outline:0}details.dropdown>summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown>summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown>summary::after{transform:rotate(0) translateX(0)}nav details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown>summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown>summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown>summary+ul[dir=rtl]{right:0;left:auto}details.dropdown>summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown>summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown>summary+ul li a:active,details.dropdown>summary+ul li a:focus,details.dropdown>summary+ul li a:focus-visible,details.dropdown>summary+ul li a:hover,details.dropdown>summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown>summary+ul li label{width:100%}details.dropdown>summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open]>summary{margin-bottom:0}details.dropdown[open]>summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open]>summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header .close,dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article .close,dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html new file mode 100644 index 0000000..892c647 --- /dev/null +++ b/backend/templates/base.html @@ -0,0 +1,72 @@ + + + + + + {% block title %}ZMB Webui{% endblock %} + + + + {% block head %}{% endblock %} + + + + +
+ {% if flash_error %}
{{ flash_error }}
{% endif %} + {% if flash_ok %}
{{ flash_ok }}
{% endif %} + {% block content %}{% endblock %} +
+ + diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html new file mode 100644 index 0000000..67125e7 --- /dev/null +++ b/backend/templates/dashboard.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}Dashboard – ZMB Webui{% endblock %} +{% block content %} +
+
+

Dashboard

+ {{ hostname }} +
+ ◠ aktualisiert... +
+ + +
+

Lädt...

+
+ +
+ + +

Storage Pools

+
+

Lade Pools...

+
+ +
+ + +

Disk Usage

+
+

Lade...

+
+ +
+ + +

Network Traffic

+
+

Lade...

+
+ +{% endblock %} diff --git a/backend/templates/fragments/disk_usage.html b/backend/templates/fragments/disk_usage.html new file mode 100644 index 0000000..783cac8 --- /dev/null +++ b/backend/templates/fragments/disk_usage.html @@ -0,0 +1,29 @@ +{% macro fmt_bytes(b) %} + {%- if b == 0 %}0 B + {%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB + {%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB + {%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB + {%- else %}{{ "%.1f"|format(b/1099511627776) }} TB + {%- endif %} +{%- endmacro %} + +{% if not filesystems %} +

Keine Dateisysteme gefunden.

+{% else %} +
+{% for fs in filesystems %} +
+ {{ fs.mountpoint }} +
+
+
+
+
Total
{{ fmt_bytes(fs.total) }}
+
Belegt
{{ fmt_bytes(fs.used) }}
+
Frei
{{ fmt_bytes(fs.available) }}
+
+ {{ fs.filesystem }} +
+{% endfor %} +
+{% endif %} diff --git a/backend/templates/fragments/groups.html b/backend/templates/fragments/groups.html new file mode 100644 index 0000000..a23ee9e --- /dev/null +++ b/backend/templates/fragments/groups.html @@ -0,0 +1,16 @@ +{% if not groups %} +

Keine Gruppen gefunden.

+{% else %} + + + + {% for group in groups %} + + + + + + {% endfor %} + +
GruppeGIDMitglieder
{{ group.name }}{{ group.gid }}{{ group.members | join(', ') if group.members else '–' }}
+{% endif %} diff --git a/backend/templates/fragments/logs.html b/backend/templates/fragments/logs.html new file mode 100644 index 0000000..b961828 --- /dev/null +++ b/backend/templates/fragments/logs.html @@ -0,0 +1,9 @@ +
+{% if logs %} +{% for line in logs %} +
{{ line | e }}
+{% endfor %} +{% else %} +Keine Logs verfügbar. +{% endif %} +
diff --git a/backend/templates/fragments/navigator.html b/backend/templates/fragments/navigator.html new file mode 100644 index 0000000..8e95670 --- /dev/null +++ b/backend/templates/fragments/navigator.html @@ -0,0 +1,40 @@ +{% if error %} +

⚠ {{ error }}

+{% else %} +{% if parent_path %} +
+ + {{ current_path }} +
+{% endif %} + + + + {% for item in items %} + + + + + + + {% endfor %} + +
NameTypGrößeGeändert
+ {% if item.type == 'directory' %} + + {% else %} + 📄 {{ item.name }} + {% endif %} + {{ item.type }}{{ item.size }}{{ item.modified }}
+{% if not items %} +

Verzeichnis ist leer.

+{% endif %} +{% endif %} diff --git a/backend/templates/fragments/net_traffic.html b/backend/templates/fragments/net_traffic.html new file mode 100644 index 0000000..41d832b --- /dev/null +++ b/backend/templates/fragments/net_traffic.html @@ -0,0 +1,23 @@ +{% macro fmt_bytes(b) %} + {%- if b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB + {%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB + {%- else %}{{ "%.2f"|format(b/1073741824) }} GB + {%- endif %} +{%- endmacro %} + +
+{% for iface in interfaces %} + {% if iface.name not in ['lo'] %} +
+ {{ iface.name }} + + + + {% if iface.rx_drops > 0 or iface.tx_drops > 0 %} + + {% endif %} +
RX{{ fmt_bytes(iface.rx_bytes) }}{{ "{:,}".format(iface.rx_packets) }} pkt
TX{{ fmt_bytes(iface.tx_bytes) }}{{ "{:,}".format(iface.tx_packets) }} pkt
⚠ {{ iface.rx_drops + iface.tx_drops }} drops
+
+ {% endif %} +{% endfor %} +
diff --git a/backend/templates/fragments/nfs_shares.html b/backend/templates/fragments/nfs_shares.html new file mode 100644 index 0000000..fe03b60 --- /dev/null +++ b/backend/templates/fragments/nfs_shares.html @@ -0,0 +1,22 @@ +{% if not exports %} +

Keine NFS Exports konfiguriert.

+{% else %} + + + + {% for exp in exports %} + + + + + + {% endfor %} + +
PfadClientsAktionen
{{ exp.path }}{{ exp.clients }} + +
+{% endif %} diff --git a/backend/templates/fragments/pools.html b/backend/templates/fragments/pools.html new file mode 100644 index 0000000..aacf1f4 --- /dev/null +++ b/backend/templates/fragments/pools.html @@ -0,0 +1,44 @@ +{% macro fmt_bytes(b) %} + {%- if b == 0 %}0 B + {%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB + {%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB + {%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB + {%- else %}{{ "%.1f"|format(b/1099511627776) }} TB + {%- endif %} +{%- endmacro %} + +{% if not pools %} +

Keine ZFS Pools verfügbar.

+{% else %} +
+{% for pool in pools %} + {% set pct = (pool.allocated / pool.size * 100)|int if pool.size else 0 %} +
+
+ {{ pool.name }} + {{ pool.health }} +
+
+
+
+ + {{ fmt_bytes(pool.allocated) }} / {{ fmt_bytes(pool.size) }} ({{ pct }}%) + + {% if pool.status and pool.status != 'ONLINE' %} +

{{ pool.status }}

+ {% endif %} +
+ + +
+
+{% endfor %} +
+{% endif %} diff --git a/backend/templates/fragments/quick_stats.html b/backend/templates/fragments/quick_stats.html new file mode 100644 index 0000000..a8036bb --- /dev/null +++ b/backend/templates/fragments/quick_stats.html @@ -0,0 +1,40 @@ +{% macro fmt_bytes(b) %} + {%- if b == 0 %}0 B + {%- elif b < 1024 %}{{ b }} B + {%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB + {%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB + {%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB + {%- else %}{{ "%.1f"|format(b/1099511627776) }} TB + {%- endif %} +{%- endmacro %} + +
+

CPU Load

+

{{ cpu.load_average[0] if cpu.load_average else '–' }}

+ {{ cpu.count }} Kerne +
+ +
+

RAM

+

{{ fmt_bytes(mem.used) }}

+ von {{ fmt_bytes(mem.total) }} + {% set pct = (mem.used / mem.total * 100)|int if mem.total else 0 %} +
+
+
+
+ +
+

Uptime

+

{{ uptime.uptime_string if uptime else '–' }}

+
+ +
+

Pools

+

{{ pools|length }}

+ + {% for p in pools %} + {{ p.name }} + {% endfor %} + +
diff --git a/backend/templates/fragments/samba_config.html b/backend/templates/fragments/samba_config.html new file mode 100644 index 0000000..2f7ec44 --- /dev/null +++ b/backend/templates/fragments/samba_config.html @@ -0,0 +1,40 @@ +{% set fruit_keys = ['fruit:encoding','fruit:metadata','fruit:zero_file_id','fruit:nfs_aces'] %} +{% set has_macos = params | selectattr('key', 'in', fruit_keys) | list | length == 4 %} + +

Samba Global Config

+ + +
+
+
+ MacOS Shares (Fruit) +

Optimiert alle Shares für macOS (fruit, catia, streams_xattr)

+
+ +
+ {% if has_macos %} +
+ {% for p in params if p.key in fruit_keys %} + {{ p.key }} = {{ p.value }} + {% endfor %} +
+ {% endif %} +
+ + + + + + {% for p in params %} + + + + + {% endfor %} + +
ParameterWert
{{ p.key }}{{ p.value }}
diff --git a/backend/templates/fragments/samba_shares.html b/backend/templates/fragments/samba_shares.html new file mode 100644 index 0000000..3bec185 --- /dev/null +++ b/backend/templates/fragments/samba_shares.html @@ -0,0 +1,23 @@ +{% if not shares %} +

Keine Samba Shares konfiguriert.

+{% else %} + + + + {% for share in shares %} + + + + + + + {% endfor %} + +
NamePfadKommentarAktionen
{{ share.name }}{{ share.path }}{{ share.comment }} + +
+{% endif %} diff --git a/backend/templates/fragments/services.html b/backend/templates/fragments/services.html new file mode 100644 index 0000000..5c0218a --- /dev/null +++ b/backend/templates/fragments/services.html @@ -0,0 +1,16 @@ +{% if not services %} +

Keine Dienste gefunden.

+{% else %} + + + + {% for svc in services %} + + + + + + {% endfor %} + +
DienstStatusBeschreibung
{{ svc.name }}{{ svc.state or svc.active }}{{ svc.description }}
+{% endif %} diff --git a/backend/templates/fragments/users.html b/backend/templates/fragments/users.html new file mode 100644 index 0000000..b77ca27 --- /dev/null +++ b/backend/templates/fragments/users.html @@ -0,0 +1,30 @@ +{% if not users %} +

Keine Benutzer gefunden.

+{% else %} + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
BenutzerUIDShellStatusAktionen
{{ user.username }}{{ user.uid }}{{ user.shell }}{{ 'Gesperrt' if user.locked else 'Aktiv' }} +
+ + +
+
+{% endif %} diff --git a/backend/templates/identities.html b/backend/templates/identities.html new file mode 100644 index 0000000..f32277f --- /dev/null +++ b/backend/templates/identities.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% block title %}Identities – ZMB Webui{% endblock %} +{% block content %} +

Benutzer & Gruppen

+ +
+ + +
+ + +
+
+

Systembenutzer

+ +
+ + + +
+

Lade...

+
+
+ + + + + +{% endblock %} diff --git a/backend/templates/login.html b/backend/templates/login.html new file mode 100644 index 0000000..3a7c0eb --- /dev/null +++ b/backend/templates/login.html @@ -0,0 +1,28 @@ + + + + + + Login – ZMB Webui + + + + + + + diff --git a/backend/templates/logs.html b/backend/templates/logs.html new file mode 100644 index 0000000..2c2a9ad --- /dev/null +++ b/backend/templates/logs.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Logs – ZMB Webui{% endblock %} +{% block content %} +
+

System Logs

+ +
+ +
+

Lade...

+
+{% endblock %} diff --git a/backend/templates/navigator.html b/backend/templates/navigator.html new file mode 100644 index 0000000..5370910 --- /dev/null +++ b/backend/templates/navigator.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block title %}Navigator – ZMB Webui{% endblock %} +{% block content %} +

Datei Navigator

+ +
+ + +
+ + +{% endblock %} diff --git a/backend/templates/services.html b/backend/templates/services.html new file mode 100644 index 0000000..e3da28c --- /dev/null +++ b/backend/templates/services.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Services – ZMB Webui{% endblock %} +{% block content %} +
+

Systemdienste

+ +
+ +
+

Lade...

+
+{% endblock %} diff --git a/backend/templates/shares.html b/backend/templates/shares.html new file mode 100644 index 0000000..f93ee2f --- /dev/null +++ b/backend/templates/shares.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} +{% block title %}Shares – ZMB Webui{% endblock %} +{% block content %} +

File Sharing

+ + +
+ + + +
+ + +
+
+

Samba Shares

+ +
+ + + +
+

Lade...

+
+
+ + + + + + + + +{% endblock %} diff --git a/backend/templates/snapshots.html b/backend/templates/snapshots.html new file mode 100644 index 0000000..6befbc9 --- /dev/null +++ b/backend/templates/snapshots.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block title %}Snapshots – ZMB Webui{% endblock %} +{% block content %} +
+

Snapshots

+ {{ snapshots|length }} Snapshots +
+ +{% if not snapshots %} +

Keine Snapshots vorhanden.

+{% else %} +
+ + + + + + + + + + + {% for snap in snapshots %} + + + + + + + {% endfor %} + +
NameErstelltBelegtAktionen
{{ snap.name }}{{ snap.creation }}{{ snap.used }} +
+ + +
+
+
+{% endif %} +{% endblock %} diff --git a/backend/templates/zfs.html b/backend/templates/zfs.html new file mode 100644 index 0000000..f94a61e --- /dev/null +++ b/backend/templates/zfs.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% block title %}ZFS – ZMB Webui{% endblock %} +{% block content %} +

ZFS Pools & Datasets

+ +{% if not pools %} +

Keine ZFS Pools verfügbar.

+{% else %} + +{% for pool in pools %} +
+ {{ pool.name }} + {{ pool.health }} + + +
+ + + +
+ + + {% if pool.vdevs %} +

VDev Struktur

+ + + + {% for vdev in pool.vdevs %} + + + + + + {% if vdev.disks %} + {% for disk in vdev.disks %} + + + + + + {% endfor %} + {% endif %} + {% endfor %} + +
NameTypStatus
{{ vdev.name }}{{ vdev.type }}{{ vdev.state }}
   ↳ {{ disk.name }}disk{{ disk.state }}
+ {% endif %} + + + {% if pool.datasets %} +

Datasets

+ + + + {% for ds in pool.datasets %} + + + + + + + + {% endfor %} + +
NameTypBelegtVerfügbarKompression
{{ ds.name }}{{ ds.type }}{{ ds.used }}{{ ds.available }}{{ ds.compression }}
+ {% endif %} +
+
+{% endfor %} +{% endif %} +{% endblock %} diff --git a/frontend/DEVLOG.md b/frontend/DEVLOG.md deleted file mode 100644 index b94c3ee..0000000 --- a/frontend/DEVLOG.md +++ /dev/null @@ -1,1729 +0,0 @@ -# frontend – Dev Log - -## 2026-04-14 23:14 – 23:44 (30m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-14 23:46 – 00:18 (32m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 00:19 – 00:20 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 00:20 – 00:21 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 00:24 – 00:24 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 00:24 – 09:14 (8h 49m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:14 – 09:15 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:17 – 09:18 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:20 – 09:25 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:34 – 09:37 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:39 – 09:39 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:40 – 09:41 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:43 – 09:44 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 09:44 – 09:44 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:02 – 11:09 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:10 – 11:10 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:12 – 11:12 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:13 – 11:31 (18m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:35 – 11:42 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:54 – 11:57 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 11:58 – 11:59 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:00 – 12:00 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:02 – 12:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:03 – 12:09 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:13 – 12:19 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:43 – 12:50 (7m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 12:51 – 13:01 (10m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 13:02 – 13:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 13:37 – 13:38 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 15:02 – 15:13 (11m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 15:35 – 15:36 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 15:37 – 15:50 (13m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 15:51 – 15:51 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 15:53 – 15:55 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:11 – 16:15 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:16 – 16:16 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:17 – 16:19 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:19 – 16:20 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 16:22 – 16:23 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:50 – 22:56 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:57 – 22:58 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 22:59 – 23:00 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:01 – 23:02 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:03 – 23:03 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:04 – 23:07 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:09 – 23:11 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:14 – 23:15 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:16 – 23:17 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:17 – 23:18 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:20 – 23:22 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:23 – 23:23 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:23 – 23:23 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:23 – 23:25 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-15 23:27 – 23:28 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 00:03 – 10:50 (10h 47m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-16 10:51 – 10:51 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:22 – 20:25 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:25 – 20:26 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:27 – 20:28 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:29 – 20:29 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:31 – 20:33 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:35 – 20:38 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:38 – 20:39 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 20:39 – 20:55 (15m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:06 – 21:09 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:30 – 21:30 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:36 – 21:36 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:40 – 21:40 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:42 – 21:44 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 21:46 – 21:57 (11m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:01 – 22:01 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:02 – 22:07 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:08 – 22:10 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:12 – 22:13 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:14 – 22:19 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:20 – 22:26 (6m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:27 – 22:28 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:30 – 22:31 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:43 – 22:44 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:45 – 22:45 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:47 – 22:47 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-18 22:48 – 09:13 (10h 25m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:18 – 09:18 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:19 – 09:20 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:38 – 09:38 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:42 – 09:43 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:45 – 09:46 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:47 – 09:48 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:50 – 09:50 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:51 – 09:52 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:53 – 09:54 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:55 – 09:56 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 09:56 – 09:57 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:03 – 10:04 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:06 – 10:06 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:07 – 10:08 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:11 – 10:12 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:14 – 10:18 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:20 – 10:21 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:21 – 10:21 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:24 – 10:25 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:27 – 10:27 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:29 – 10:29 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:30 – 10:30 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:32 – 10:37 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:38 – 10:39 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:41 – 10:42 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:46 – 10:46 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:48 – 10:48 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:48 – 10:50 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:52 – 10:55 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:56 – 10:58 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 10:59 – 10:59 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:00 – 11:01 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:02 – 11:05 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:06 – 11:06 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:06 – 11:06 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:07 – 11:07 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:09 – 11:11 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:13 – 11:13 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:14 – 11:27 (12m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:29 – 11:30 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 11:32 – 11:34 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:04 – 12:05 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:06 – 12:06 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:07 – 12:11 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:13 – 12:16 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:16 – 12:18 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:21 – 12:22 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 12:28 – 12:32 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 16:19 – 16:19 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 16:22 – 16:26 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 16:36 – 16:36 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 16:39 – 16:41 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 16:55 – 17:12 (17m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 17:13 – 17:14 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:35 – 22:40 (5m) -**Beschreibung:** Claude Code Session -**Projekt:** backend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:44 – 22:45 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:47 – 22:47 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:48 – 22:49 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:53 – 22:55 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 22:55 – 22:56 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:10 – 23:11 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:12 – 23:15 (2m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:17 – 23:18 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:20 – 23:20 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:21 – 23:23 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-19 23:52 – 23:56 (4m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 00:00 – 00:01 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 00:12 – 00:13 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 00:14 – 00:15 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 00:19 – 00:20 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 00:21 – 00:21 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-20 10:44 – 10:45 (0m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:44 – 23:48 (3m) -**Beschreibung:** Claude Code Session -**Projekt:** cockpit_new - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-21 23:51 – 00:01 (9m) -**Beschreibung:** Claude Code Session -**Projekt:** frontend - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -Keine Änderungen ermittelbar. - ---- -## 2026-04-22 01:48 – 01:50 (1m) -**Beschreibung:** Claude Code Session -**Projekt:** zmb-webui - -### Commits -Keine Commits in dieser Session. - -### Geänderte Dateien -- frontend/app/file-sharing/page.tsx | 48 ++++++++++++++++++++++++-------------- - ---- diff --git a/memory/multiarch_requirements.md b/memory/multiarch_requirements.md deleted file mode 100644 index a56903d..0000000 --- a/memory/multiarch_requirements.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -name: Multi-Architektur Support -description: Backend und Frontend müssen auf ARM64, x86, und AMD64 laufen -type: feedback ---- - -Das System muss auf **mehreren CPU-Architekturen** funktionieren: - -**Why:** Der Pi ist ARM64, aber Production-Server sind oft x86/AMD64. Development kann auf verschiedenen Maschinen stattfinden. - -**How to apply:** - -## Backend (Python/FastAPI) - -✅ **Bereits kompatibel** – Python ist plattformübergreifend: -- Keine CPU-spezifischen Dependencies -- subprocess.run() funktioniert überall -- Nur: pwd/grp/pathlib/socket = Standard Library (überall verfügbar) - -⚠️ **Zu achten:** -- `psutil` optional (nur in system_info.py, mit Fallback) -- Systemd-spezifisch: `systemctl`, `useradd`, `groupdel` – nur auf Linux -- ZFS Tools müssen installiert sein (zpool, zfs Binaries) - -✓ **Test vor Deployment:** -```bash -# ARM64 Pi: -python3 main.py -# x86/AMD64 Server: -python3 main.py -``` - -## Frontend (Next.js/React) - -⚠️ **Wichtig:** Next.js baut zu **JavaScript/HTML** – läuft überall: -- `npm run build` auf jedem OS (Windows/Mac/Linux) -- Output ist plattformunabhängig (static HTML/JS) -- Runtime (Node.js) auch auf allen Architekturen - -⚠️ **Aber:** Build kann unterschiedlich lange dauern: -- Pi: 20-30min (ARM64 ist langsam) -- x86/AMD64: 2-5min - -**Strategie:** Build auf stärkerem Server, Deploy überall. - -## Deployment-Matrix - -``` -┌──────────────┬────────────┬────────────┬──────────────┐ -│ Architektur │ Python │ Next.js │ ZFS Tools │ -├──────────────┼────────────┼────────────┼──────────────┤ -│ ARM64 (Pi) │ ✓ läuft │ ✓ läuft │ ✓ installiert│ -│ x86 (32bit) │ ✓ läuft │ ✓ läuft │ ✓ paket │ -│ AMD64 (64bit)│ ✓ läuft │ ✓ läuft │ ✓ paket │ -│ x86_64 │ ✓ läuft │ ✓ läuft │ ✓ paket │ -└──────────────┴────────────┴────────────┴──────────────┘ -``` - -## Wichtige Gotchas - -### 1. Binaries in subprocess -- ❌ FALSCH: `subprocess.run(["zpool", ...])` – setzt zpool im PATH voraus -- ✓ RICHTIG: Ist OK, wird z.B. `which zpool` zur Laufzeit geprüft -- Aber: Dokumentation muss erwähnen "ZFS Tools müssen installiert sein" - -### 2. systemd Service -- Pi (Debian/Raspberry Pi OS): ✓ Hat systemd -- x86 (Ubuntu/Debian): ✓ Hat systemd -- AMD64 (CentOS/RHEL): ✓ Hat systemd -- Alpine Linux: ✗ Nutzt OpenRC (andere config nötig) - -### 3. File Paths -- Pi: `/opt/zmb-webui/backend` -- x86/AMD64: Gleich OK - -### 4. Package Manager -- Pi (Debian): `apt` -- Ubuntu: `apt` -- RHEL/CentOS: `dnf` / `yum` - -## Deployment Instructions - -### Für ARM64 Pi: -```bash -sudo bash backend/install.sh -# Autodetekt: Debian-based → apt -``` - -### Für x86/AMD64 (Debian/Ubuntu): -```bash -sudo bash backend/install.sh -# Autodetekt: Debian-based → apt -``` - -### Für x86/AMD64 (RHEL/CentOS): -```bash -# Manuell oder create backend/install-rhel.sh -``` - -## Zu tun vor Production - -1. ✓ **Python Code:** Schon arch-agnostisch -2. ⚠️ **install.sh:** Muss `os-release` checken statt zu assumieren -3. ⚠️ **Systemd Service:** Optional: create RHEL variant -4. ⚠️ **Frontend Build:** Dokumentieren dass auf stärkerem System gebaut wird -5. ✓ **Dependencies:** Alle plattformübergreifend verfügbar - -## Architektur-Check-Script - -```bash -# vor Deployment checken: -#!/bin/bash -echo "Architecture: $(uname -m)" -echo "OS: $(lsb_release -ds)" -echo "Python: $(python3 --version)" -echo "ZFS: $(zpool list 2>/dev/null && echo 'installed' || echo 'NOT installed')" -echo "systemd: $(systemctl 2>/dev/null && echo 'yes' || echo 'no')" -```