Feature: HTMX + Jinja2 Frontend ersetzt Next.js komplett
- Kein Node.js, kein npm, kein Build-Schritt mehr - HTMX 2.0.4 + PicoCSS 2 vendored in backend/static/ - Jinja2 Templates für alle 9 Seiten (Dashboard, ZFS, Snapshots, Shares, Identities, Logs, Services, Navigator, Login) - HTMX Fragments für Live-Updates (30s Polling Dashboard) - JWT als httpOnly Cookie statt localStorage - Disk Usage zeigt TB/PB korrekt (Jinja2 serverseitig formatiert) - Update-safe: nur Python-Deps, keine npm-Abhängigkeiten Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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 `<input type="text">`
|
||||
- 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<string[]>([])
|
||||
const [groupnames, setGroupnames] = useState<string[]>([])
|
||||
```
|
||||
|
||||
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
|
||||
<input list="owners-list" ... />
|
||||
<datalist id="owners-list">
|
||||
{usernames.map(name => <option key={name} value={name} />)}
|
||||
</datalist>
|
||||
```
|
||||
|
||||
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 `<datalist>` 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.**
|
||||
@@ -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@<pi-ip>:/tmp/zmb-webui-backend
|
||||
ssh root@<pi-ip>
|
||||
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@<server-ip>:/tmp/zmb-webui-backend
|
||||
ssh root@<server-ip>
|
||||
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 <repo> 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!** 🎯
|
||||
@@ -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 <repo> /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://<host-ip>: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. 🚀
|
||||
@@ -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 <newpassword>
|
||||
```
|
||||
|
||||
## 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="<token from login>"
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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!** 🚀
|
||||
@@ -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 → <node-name> → 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 <repo-url> /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@<container-ip>
|
||||
|
||||
# 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/<node>/pve-ssl.pem;
|
||||
ssl_certificate_key /etc/pve/nodes/<node>/pve-ssl.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<container-ip>: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://<proxmox-host-ip>: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 → <node> → 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!** 🚀
|
||||
@@ -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 <repo> /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@<container-ip>
|
||||
|
||||
# curl von Host
|
||||
curl http://<container-ip>: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@<container-ip>
|
||||
└── API: curl http://<container-ip>:8000/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Das war's!** Backend läuft auf Proxmox LXC mit vollständigem ZFS Management. 🚀
|
||||
@@ -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)
|
||||
-207
@@ -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 / <password>`
|
||||
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":"<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
|
||||
-200
@@ -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 <token>
|
||||
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**
|
||||
@@ -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 +++++++++++++
|
||||
|
||||
---
|
||||
+10
-14
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+4
File diff suppressed because one or more lines are too long
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}ZMB Webui{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<style>
|
||||
:root { --pico-font-size: 87.5%; }
|
||||
nav.top-nav { padding: 0.5rem 1rem; border-bottom: 1px solid var(--pico-muted-border-color); }
|
||||
nav.top-nav ul { margin: 0; }
|
||||
nav.top-nav a { text-decoration: none; padding: 0.4rem 0.7rem; border-radius: 4px; }
|
||||
nav.top-nav a.active, nav.top-nav a:hover { background: var(--pico-primary-background); color: var(--pico-primary-inverse); }
|
||||
nav.top-nav .brand { font-weight: bold; font-size: 1.1rem; color: var(--pico-primary); }
|
||||
main.container { padding-top: 1.5rem; }
|
||||
.htmx-indicator { opacity: 0; transition: opacity 200ms; }
|
||||
.htmx-request .htmx-indicator { opacity: 1; }
|
||||
.badge { display: inline-block; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.75rem; background: var(--pico-secondary-background); }
|
||||
.badge-green { background: #166534; color: #bbf7d0; }
|
||||
.badge-red { background: #7f1d1d; color: #fecaca; }
|
||||
.badge-yellow { background: #713f12; color: #fef08a; }
|
||||
.badge-blue { background: #1e3a5f; color: #bfdbfe; }
|
||||
.progress-bar-wrap { background: var(--pico-muted-border-color); border-radius: 4px; height: 8px; overflow: hidden; }
|
||||
.progress-bar { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
||||
.bar-blue { background: #3b82f6; }
|
||||
.bar-yellow { background: #eab308; }
|
||||
.bar-red { background: #ef4444; }
|
||||
.flash-error { background: #7f1d1d; color: #fecaca; padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
|
||||
.flash-ok { background: #166534; color: #bbf7d0; padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
|
||||
.grid-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
||||
.stat-card { padding: 1rem; border: 1px solid var(--pico-muted-border-color); border-radius: 8px; }
|
||||
.stat-card h3 { margin: 0 0 0.25rem 0; font-size: 0.8rem; color: var(--pico-muted-color); text-transform: uppercase; }
|
||||
.stat-card .value { font-size: 1.4rem; font-weight: bold; margin: 0; }
|
||||
table { font-size: 0.9rem; }
|
||||
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.actions button, .actions a[role=button] { padding: 0.25rem 0.6rem; font-size: 0.8rem; margin: 0; }
|
||||
details summary { cursor: pointer; }
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="top-nav">
|
||||
<ul>
|
||||
<li><span class="brand">📱 ZMB Webui</span></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/" class="{% if active == 'dashboard' %}active{% endif %}">Dashboard</a></li>
|
||||
<li><a href="/zfs" class="{% if active == 'zfs' %}active{% endif %}">ZFS</a></li>
|
||||
<li><a href="/snapshots" class="{% if active == 'snapshots' %}active{% endif %}">Snapshots</a></li>
|
||||
<li><a href="/shares" class="{% if active == 'shares' %}active{% endif %}">Shares</a></li>
|
||||
<li><a href="/navigator" class="{% if active == 'navigator' %}active{% endif %}">Navigator</a></li>
|
||||
<li><a href="/identities" class="{% if active == 'identities' %}active{% endif %}">Identities</a></li>
|
||||
<li><a href="/logs" class="{% if active == 'logs' %}active{% endif %}">Logs</a></li>
|
||||
<li><a href="/services" class="{% if active == 'services' %}active{% endif %}">Services</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<form method="post" action="/logout" style="margin:0">
|
||||
<button type="submit" class="outline secondary" style="padding:0.3rem 0.7rem;font-size:0.85rem">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
{% if flash_error %}<div class="flash-error">{{ flash_error }}</div>{% endif %}
|
||||
{% if flash_ok %}<div class="flash-ok">{{ flash_ok }}</div>{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem">
|
||||
<div>
|
||||
<h2 style="margin:0">Dashboard</h2>
|
||||
<small style="color:var(--pico-muted-color)">{{ hostname }}</small>
|
||||
</div>
|
||||
<span class="htmx-indicator" id="spinner">◠ aktualisiert...</span>
|
||||
</div>
|
||||
|
||||
<!-- Schnellstatistiken (alle 30s) -->
|
||||
<div class="grid-stats" id="quick-stats"
|
||||
hx-get="/fragments/quick-stats"
|
||||
hx-trigger="load, every 30s"
|
||||
hx-indicator="#spinner">
|
||||
<div class="stat-card"><h3>Lädt...</h3><p class="value">–</p></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Pools -->
|
||||
<h3>Storage Pools</h3>
|
||||
<div id="pools"
|
||||
hx-get="/fragments/pools"
|
||||
hx-trigger="load, every 30s"
|
||||
hx-indicator="#spinner">
|
||||
<p aria-busy="true">Lade Pools...</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Disk Usage -->
|
||||
<h3>Disk Usage</h3>
|
||||
<div id="disk-usage"
|
||||
hx-get="/fragments/disk-usage"
|
||||
hx-trigger="load"
|
||||
hx-indicator="#spinner">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Netzwerk -->
|
||||
<h3>Network Traffic</h3>
|
||||
<div id="net-traffic"
|
||||
hx-get="/fragments/net-traffic"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-indicator="#spinner">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
<p>Keine Dateisysteme gefunden.</p>
|
||||
{% else %}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem">
|
||||
{% for fs in filesystems %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<strong>{{ fs.mountpoint }}</strong>
|
||||
<div class="progress-bar-wrap" style="margin:0.4rem 0">
|
||||
<div class="progress-bar {% if fs.capacity > 85 %}bar-red{% elif fs.capacity > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ fs.capacity }}%"></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;font-size:0.8rem;gap:0.25rem">
|
||||
<div><div style="color:var(--pico-muted-color)">Total</div><strong>{{ fmt_bytes(fs.total) }}</strong></div>
|
||||
<div><div style="color:var(--pico-muted-color)">Belegt</div><strong>{{ fmt_bytes(fs.used) }}</strong></div>
|
||||
<div><div style="color:var(--pico-muted-color)">Frei</div><strong>{{ fmt_bytes(fs.available) }}</strong></div>
|
||||
</div>
|
||||
<small style="color:var(--pico-muted-color)">{{ fs.filesystem }}</small>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if not groups %}
|
||||
<p>Keine Gruppen gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Gruppe</th><th>GID</th><th>Mitglieder</th></tr></thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td><strong>{{ group.name }}</strong></td>
|
||||
<td>{{ group.gid }}</td>
|
||||
<td style="font-size:0.85rem">{{ group.members | join(', ') if group.members else '–' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div style="background:var(--pico-code-background,#1a1a2e);border-radius:6px;padding:1rem;overflow-x:auto;max-height:70vh;overflow-y:auto;font-family:monospace;font-size:0.8rem;line-height:1.5">
|
||||
{% if logs %}
|
||||
{% for line in logs %}
|
||||
<div style="{% if 'ERROR' in line or 'error' in line %}color:#fca5a5{% elif 'WARN' in line or 'warn' in line %}color:#fde68a{% elif 'INFO' in line %}color:#a5f3fc{% else %}color:var(--pico-muted-color){% endif %}">{{ line | e }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span style="color:var(--pico-muted-color)">Keine Logs verfügbar.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
{% if error %}
|
||||
<p style="color:#fca5a5">⚠ {{ error }}</p>
|
||||
{% else %}
|
||||
{% if parent_path %}
|
||||
<div style="margin-bottom:0.5rem">
|
||||
<button class="outline secondary"
|
||||
hx-get="/fragments/navigator?path={{ parent_path | urlencode }}"
|
||||
hx-target="#nav-content"
|
||||
hx-swap="innerHTML">↑ Übergeordnet</button>
|
||||
<span style="font-family:monospace;margin-left:0.5rem;color:var(--pico-muted-color)">{{ current_path }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th><th>Geändert</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if item.type == 'directory' %}
|
||||
<button class="outline secondary" style="padding:0.1rem 0.4rem;font-size:0.85rem"
|
||||
hx-get="/fragments/navigator?path={{ item.path | urlencode }}"
|
||||
hx-target="#nav-content"
|
||||
hx-swap="innerHTML">
|
||||
📁 {{ item.name }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span style="font-family:monospace;font-size:0.85rem">📄 {{ item.name }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge">{{ item.type }}</span></td>
|
||||
<td style="font-size:0.85rem">{{ item.size }}</td>
|
||||
<td style="font-size:0.8rem;color:var(--pico-muted-color)">{{ item.modified }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not items %}
|
||||
<p style="color:var(--pico-muted-color)">Verzeichnis ist leer.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem">
|
||||
{% for iface in interfaces %}
|
||||
{% if iface.name not in ['lo'] %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<strong>{{ iface.name }}</strong>
|
||||
<table style="margin:0.5rem 0;font-size:0.85rem">
|
||||
<tr><td style="color:var(--pico-muted-color)">RX</td><td><strong>{{ fmt_bytes(iface.rx_bytes) }}</strong></td><td style="color:var(--pico-muted-color)">{{ "{:,}".format(iface.rx_packets) }} pkt</td></tr>
|
||||
<tr><td style="color:var(--pico-muted-color)">TX</td><td><strong>{{ fmt_bytes(iface.tx_bytes) }}</strong></td><td style="color:var(--pico-muted-color)">{{ "{:,}".format(iface.tx_packets) }} pkt</td></tr>
|
||||
{% if iface.rx_drops > 0 or iface.tx_drops > 0 %}
|
||||
<tr><td colspan="3" style="color:#fca5a5;font-size:0.75rem">⚠ {{ iface.rx_drops + iface.tx_drops }} drops</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% if not exports %}
|
||||
<p>Keine NFS Exports konfiguriert.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Pfad</th><th>Clients</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for exp in exports %}
|
||||
<tr id="nfs-{{ loop.index }}">
|
||||
<td style="font-family:monospace">{{ exp.path }}</td>
|
||||
<td style="font-size:0.85rem">{{ exp.clients }}</td>
|
||||
<td>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/shares/nfs/{{ exp.path | urlencode }}"
|
||||
hx-confirm="NFS Export '{{ exp.path }}' löschen?"
|
||||
hx-target="#nfs-{{ loop.index }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
<p>Keine ZFS Pools verfügbar.</p>
|
||||
{% else %}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">
|
||||
{% for pool in pools %}
|
||||
{% set pct = (pool.allocated / pool.size * 100)|int if pool.size else 0 %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<strong>{{ pool.name }}</strong>
|
||||
<span class="badge {% if pool.health == 'ONLINE' %}badge-green{% elif pool.health == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ pool.health }}</span>
|
||||
</div>
|
||||
<div class="progress-bar-wrap" style="margin:0.5rem 0">
|
||||
<div class="progress-bar {% if pct > 85 %}bar-red{% elif pct > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ pct }}%"></div>
|
||||
</div>
|
||||
<small style="color:var(--pico-muted-color)">
|
||||
{{ fmt_bytes(pool.allocated) }} / {{ fmt_bytes(pool.size) }} ({{ pct }}%)
|
||||
</small>
|
||||
{% if pool.status and pool.status != 'ONLINE' %}
|
||||
<p style="margin:0.5rem 0 0;font-size:0.8rem;color:#fca5a5">{{ pool.status }}</p>
|
||||
{% endif %}
|
||||
<div class="actions" style="margin-top:0.75rem">
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/pools/{{ pool.name }}/scrub"
|
||||
hx-confirm="Scrub für {{ pool.name }} starten?"
|
||||
hx-target="closest article"
|
||||
hx-swap="none">Scrub</button>
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/pools/{{ pool.name }}/clear"
|
||||
hx-confirm="Fehler für {{ pool.name }} löschen?"
|
||||
hx-swap="none">Clear</button>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>CPU Load</h3>
|
||||
<p class="value">{{ cpu.load_average[0] if cpu.load_average else '–' }}</p>
|
||||
<small>{{ cpu.count }} Kerne</small>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>RAM</h3>
|
||||
<p class="value">{{ fmt_bytes(mem.used) }}</p>
|
||||
<small>von {{ fmt_bytes(mem.total) }}</small>
|
||||
{% set pct = (mem.used / mem.total * 100)|int if mem.total else 0 %}
|
||||
<div class="progress-bar-wrap" style="margin-top:0.4rem">
|
||||
<div class="progress-bar {% if pct > 85 %}bar-red{% elif pct > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ pct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Uptime</h3>
|
||||
<p class="value">{{ uptime.uptime_string if uptime else '–' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Pools</h3>
|
||||
<p class="value">{{ pools|length }}</p>
|
||||
<small>
|
||||
{% for p in pools %}
|
||||
<span class="badge {% if p.health == 'ONLINE' %}badge-green{% elif p.health == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ p.name }}</span>
|
||||
{% endfor %}
|
||||
</small>
|
||||
</div>
|
||||
@@ -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 %}
|
||||
|
||||
<h3>Samba Global Config</h3>
|
||||
|
||||
<!-- MacOS Toggle -->
|
||||
<article style="margin-bottom:1rem">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<strong>MacOS Shares (Fruit)</strong>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--pico-muted-color)">Optimiert alle Shares für macOS (fruit, catia, streams_xattr)</p>
|
||||
</div>
|
||||
<button class="{% if has_macos %}outline secondary{% else %}outline{% endif %}"
|
||||
hx-post="/fragments/samba-config/toggle-macos?enable={{ 'false' if has_macos else 'true' }}"
|
||||
hx-target="#samba-config"
|
||||
hx-swap="innerHTML">
|
||||
{% if has_macos %}MacOS deaktivieren{% else %}MacOS aktivieren{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% if has_macos %}
|
||||
<div style="margin-top:0.5rem">
|
||||
{% for p in params if p.key in fruit_keys %}
|
||||
<span class="badge badge-blue" style="margin:0.1rem">{{ p.key }} = {{ p.value }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<!-- Parameter Tabelle -->
|
||||
<table>
|
||||
<thead><tr><th>Parameter</th><th>Wert</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in params %}
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ p.key }}</td>
|
||||
<td style="font-size:0.85rem;word-break:break-all">{{ p.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,23 @@
|
||||
{% if not shares %}
|
||||
<p>Keine Samba Shares konfiguriert.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Pfad</th><th>Kommentar</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for share in shares %}
|
||||
<tr id="samba-{{ share.name }}">
|
||||
<td><strong>{{ share.name }}</strong></td>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ share.path }}</td>
|
||||
<td>{{ share.comment }}</td>
|
||||
<td>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/shares/samba/{{ share.name }}"
|
||||
hx-confirm="Share '{{ share.name }}' löschen?"
|
||||
hx-target="#samba-{{ share.name }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if not services %}
|
||||
<p>Keine Dienste gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Dienst</th><th>Status</th><th>Beschreibung</th></tr></thead>
|
||||
<tbody>
|
||||
{% for svc in services %}
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ svc.name }}</td>
|
||||
<td><span class="badge {% if svc.state == 'active' or svc.active == 'active' %}badge-green{% else %}badge-red{% endif %}">{{ svc.state or svc.active }}</span></td>
|
||||
<td style="font-size:0.85rem;color:var(--pico-muted-color)">{{ svc.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% if not users %}
|
||||
<p>Keine Benutzer gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Benutzer</th><th>UID</th><th>Shell</th><th>Status</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr id="user-{{ user.username }}">
|
||||
<td><strong>{{ user.username }}</strong></td>
|
||||
<td>{{ user.uid }}</td>
|
||||
<td style="font-family:monospace;font-size:0.8rem">{{ user.shell }}</td>
|
||||
<td><span class="badge {% if user.locked %}badge-red{% else %}badge-green{% endif %}">{{ 'Gesperrt' if user.locked else 'Aktiv' }}</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/identities/users/{{ user.username }}/samba-password"
|
||||
hx-prompt="Samba Passwort für {{ user.username }}:"
|
||||
hx-swap="none">Samba PW</button>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/identities/users/{{ user.username }}"
|
||||
hx-confirm="Benutzer '{{ user.username }}' löschen?"
|
||||
hx-target="#user-{{ user.username }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Identities – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Benutzer & Gruppen</h2>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1.5rem;border-bottom:1px solid var(--pico-muted-border-color);padding-bottom:0.5rem">
|
||||
<button onclick="showTab('users')" id="tab-users" class="outline">Benutzer</button>
|
||||
<button onclick="showTab('groups')" id="tab-groups" class="outline secondary">Gruppen</button>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div id="panel-users">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||
<h3 style="margin:0">Systembenutzer</h3>
|
||||
<button onclick="document.getElementById('new-user-form').style.display='block'">+ Neu</button>
|
||||
</div>
|
||||
|
||||
<div id="new-user-form" style="display:none;margin-bottom:1rem">
|
||||
<article>
|
||||
<h4>Neuer Benutzer</h4>
|
||||
<form hx-post="/api/identities/users" hx-target="#user-list" hx-swap="innerHTML"
|
||||
hx-on::after-request="this.reset();document.getElementById('new-user-form').style.display='none'">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div><label>Benutzername</label><input name="username" required></div>
|
||||
<div><label>Passwort</label><input name="password" type="password" required></div>
|
||||
</div>
|
||||
<div class="actions"><button type="submit">Erstellen</button><button type="button" class="outline secondary" onclick="document.getElementById('new-user-form').style.display='none'">Abbrechen</button></div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="user-list" hx-get="/fragments/users" hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div id="panel-groups" style="display:none">
|
||||
<div id="group-list" hx-get="/fragments/groups" hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(name) {
|
||||
['users','groups'].forEach(t => {
|
||||
document.getElementById('panel-' + t).style.display = t === name ? 'block' : 'none';
|
||||
document.getElementById('tab-' + t).className = t === name ? 'outline' : 'outline secondary';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login – ZMB Webui</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<style>
|
||||
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.login-box { width: 100%; max-width: 380px; padding: 2rem; }
|
||||
h1 { text-align: center; margin-bottom: 1.5rem; }
|
||||
.flash-error { background: #7f1d1d; color: #fecaca; padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h1>📱 ZMB Webui</h1>
|
||||
{% if error %}<div class="flash-error">{{ error }}</div>{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<label for="username">Benutzername</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="submit" style="width:100%">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logs – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
|
||||
<h2 style="margin:0">System Logs</h2>
|
||||
<button hx-get="/fragments/logs" hx-target="#log-content" hx-swap="innerHTML">↻ Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<div id="log-content"
|
||||
hx-get="/fragments/logs"
|
||||
hx-trigger="load, every 15s">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Navigator – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Datei Navigator</h2>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<input id="nav-path" type="text" value="/tank/share" style="font-family:monospace;margin:0">
|
||||
<button hx-get="/fragments/navigator"
|
||||
hx-include="#nav-path"
|
||||
hx-target="#nav-content"
|
||||
hx-swap="innerHTML">🔍 Öffnen</button>
|
||||
</div>
|
||||
|
||||
<div id="nav-content"
|
||||
hx-get="/fragments/navigator?path=/tank/share"
|
||||
hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Services – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
|
||||
<h2 style="margin:0">Systemdienste</h2>
|
||||
<button hx-get="/fragments/services" hx-target="#service-list" hx-swap="innerHTML">↻ Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<div id="service-list"
|
||||
hx-get="/fragments/services"
|
||||
hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Shares – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<h2>File Sharing</h2>
|
||||
|
||||
<!-- Tabs via CSS -->
|
||||
<div role="tablist" style="display:flex;gap:0.5rem;margin-bottom:1.5rem;border-bottom:1px solid var(--pico-muted-border-color);padding-bottom:0.5rem">
|
||||
<button role="tab" onclick="showTab('samba')" id="tab-samba" class="outline">Samba (SMB)</button>
|
||||
<button role="tab" onclick="showTab('nfs')" id="tab-nfs" class="outline secondary">NFS</button>
|
||||
<button role="tab" onclick="showTab('config')" id="tab-config" class="outline secondary">Samba Config</button>
|
||||
</div>
|
||||
|
||||
<!-- Samba Shares -->
|
||||
<div id="panel-samba">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||
<h3 style="margin:0">Samba Shares</h3>
|
||||
<button onclick="document.getElementById('new-samba-form').style.display='block'">+ Neu</button>
|
||||
</div>
|
||||
|
||||
<div id="new-samba-form" style="display:none;margin-bottom:1rem">
|
||||
<article>
|
||||
<h4>Neuer Samba Share</h4>
|
||||
<form hx-post="/api/shares/samba" hx-target="#samba-list" hx-swap="innerHTML"
|
||||
hx-on::after-request="this.reset();document.getElementById('new-samba-form').style.display='none'">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
|
||||
<div><label>Name</label><input name="name" required placeholder="myshare"></div>
|
||||
<div><label>Pfad</label><input name="path" required placeholder="/tank/share"></div>
|
||||
<div><label>Kommentar</label><input name="comment" placeholder="Beschreibung"></div>
|
||||
</div>
|
||||
<div class="actions"><button type="submit">Erstellen</button><button type="button" class="outline secondary" onclick="document.getElementById('new-samba-form').style.display='none'">Abbrechen</button></div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="samba-list"
|
||||
hx-get="/fragments/samba-shares"
|
||||
hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NFS -->
|
||||
<div id="panel-nfs" style="display:none">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||
<h3 style="margin:0">NFS Exports</h3>
|
||||
<button onclick="document.getElementById('new-nfs-form').style.display='block'">+ Neu</button>
|
||||
</div>
|
||||
|
||||
<div id="new-nfs-form" style="display:none;margin-bottom:1rem">
|
||||
<article>
|
||||
<h4>Neuer NFS Export</h4>
|
||||
<form hx-post="/api/shares/nfs" hx-target="#nfs-list" hx-swap="innerHTML"
|
||||
hx-on::after-request="this.reset();document.getElementById('new-nfs-form').style.display='none'">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
|
||||
<div><label>Pfad</label><input name="path" required placeholder="/tank/share"></div>
|
||||
<div><label>Clients</label><input name="clients" required placeholder="*(rw,sync,no_subtree_check)"></div>
|
||||
<div><label>Kommentar</label><input name="comment" placeholder="Beschreibung"></div>
|
||||
</div>
|
||||
<div class="actions"><button type="submit">Erstellen</button><button type="button" class="outline secondary" onclick="document.getElementById('new-nfs-form').style.display='none'">Abbrechen</button></div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="nfs-list"
|
||||
hx-get="/fragments/nfs-shares"
|
||||
hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Samba Config -->
|
||||
<div id="panel-config" style="display:none">
|
||||
<div id="samba-config"
|
||||
hx-get="/fragments/samba-config"
|
||||
hx-trigger="load">
|
||||
<p aria-busy="true">Lade...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(name) {
|
||||
['samba','nfs','config'].forEach(t => {
|
||||
document.getElementById('panel-' + t).style.display = t === name ? 'block' : 'none';
|
||||
const btn = document.getElementById('tab-' + t);
|
||||
btn.className = t === name ? 'outline' : 'outline secondary';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Snapshots – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
|
||||
<h2 style="margin:0">Snapshots</h2>
|
||||
<small style="color:var(--pico-muted-color)">{{ snapshots|length }} Snapshots</small>
|
||||
</div>
|
||||
|
||||
{% if not snapshots %}
|
||||
<p>Keine Snapshots vorhanden.</p>
|
||||
{% else %}
|
||||
<div style="overflow-x:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Belegt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snap in snapshots %}
|
||||
<tr id="snap-{{ loop.index }}">
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ snap.name }}</td>
|
||||
<td>{{ snap.creation }}</td>
|
||||
<td>{{ snap.used }}</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/snapshots/{{ snap.name | urlencode }}"
|
||||
hx-confirm="Snapshot {{ snap.name }} löschen?"
|
||||
hx-target="#snap-{{ loop.index }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
<button class="outline"
|
||||
hx-post="/api/snapshots/{{ snap.name | urlencode }}/clone"
|
||||
hx-swap="none">Klonen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}ZFS – ZMB Webui{% endblock %}
|
||||
{% block content %}
|
||||
<h2>ZFS Pools & Datasets</h2>
|
||||
|
||||
{% if not pools %}
|
||||
<p>Keine ZFS Pools verfügbar.</p>
|
||||
{% else %}
|
||||
|
||||
{% for pool in pools %}
|
||||
<details open>
|
||||
<summary><strong>{{ pool.name }}</strong>
|
||||
<span class="badge {% if pool.health == 'ONLINE' %}badge-green{% elif pool.health == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}" style="margin-left:0.5rem">{{ pool.health }}</span>
|
||||
</summary>
|
||||
|
||||
<div class="actions" style="margin:0.75rem 0">
|
||||
<button class="outline"
|
||||
hx-post="/api/pools/{{ pool.name }}/scrub"
|
||||
hx-confirm="Scrub für {{ pool.name }} starten?"
|
||||
hx-swap="none">🔍 Scrub</button>
|
||||
<button class="outline"
|
||||
hx-post="/api/pools/{{ pool.name }}/resilver"
|
||||
hx-confirm="Resilver für {{ pool.name }} starten?"
|
||||
hx-swap="none">🔄 Resilver</button>
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/pools/{{ pool.name }}/clear"
|
||||
hx-confirm="Fehler für {{ pool.name }} löschen?"
|
||||
hx-swap="none">✖ Clear Errors</button>
|
||||
</div>
|
||||
|
||||
<!-- VDev Tree -->
|
||||
{% if pool.vdevs %}
|
||||
<h4>VDev Struktur</h4>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for vdev in pool.vdevs %}
|
||||
<tr>
|
||||
<td>{{ vdev.name }}</td>
|
||||
<td>{{ vdev.type }}</td>
|
||||
<td><span class="badge {% if vdev.state == 'ONLINE' %}badge-green{% elif vdev.state == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ vdev.state }}</span></td>
|
||||
</tr>
|
||||
{% if vdev.disks %}
|
||||
{% for disk in vdev.disks %}
|
||||
<tr style="font-size:0.85rem">
|
||||
<td> ↳ {{ disk.name }}</td>
|
||||
<td><span style="color:var(--pico-muted-color)">disk</span></td>
|
||||
<td><span class="badge {% if disk.state == 'ONLINE' %}badge-green{% elif disk.state == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ disk.state }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- Datasets -->
|
||||
{% if pool.datasets %}
|
||||
<h4>Datasets</h4>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Belegt</th><th>Verfügbar</th><th>Kompression</th></tr></thead>
|
||||
<tbody>
|
||||
{% for ds in pool.datasets %}
|
||||
<tr>
|
||||
<td>{{ ds.name }}</td>
|
||||
<td><span class="badge">{{ ds.type }}</span></td>
|
||||
<td>{{ ds.used }}</td>
|
||||
<td>{{ ds.available }}</td>
|
||||
<td>{{ ds.compression }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</details>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
-1729
File diff suppressed because it is too large
Load Diff
@@ -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')"
|
||||
```
|
||||
Reference in New Issue
Block a user