diff --git a/AUTOSTART_README.md b/AUTOSTART_README.md new file mode 100644 index 0000000..fb5eb51 --- /dev/null +++ b/AUTOSTART_README.md @@ -0,0 +1,130 @@ +# Medi-WOL Autostart-System + +## Übersicht + +Das Medi-WOL Autostart-System ermöglicht es, PCs automatisch zu bestimmten Zeitpunkten über Wake-on-LAN zu starten. Es verwendet die Standard-Crontab-Syntax für die Zeitplanung und integriert sich nahtlos in das bestehende Logging-System. + +## Funktionen + +### 1. **Autostart-Konfiguration pro PC** +- **Crontab-Syntax**: Unterstützt alle Standard-Crontab-Ausdrücke +- **Aktivierung/Deaktivierung**: Einzelne PCs können unabhängig voneinander konfiguriert werden +- **Standardwert**: "30 7 * * Mon-Fri" (Mo-Fr um 7:30 Uhr) + +### 2. **Crontab-Syntax-Unterstützung** +- **Minute**: 0-59 +- **Stunde**: 0-23 +- **Tag**: 1-31 +- **Monat**: 1-12 +- **Wochentag**: 0-6 (0=Sonntag) oder Mon, Tue, Wed, Thu, Fri, Sat, Sun + +### 3. **Automatische Ausführung** +- **Scheduler**: Läuft im Hintergrund und prüft jede Minute +- **Logging**: Alle automatischen WOL-Ereignisse werden mit Trigger "cron" protokolliert +- **Fehlerbehandlung**: Robuste Fehlerbehandlung bei WOL-Ausführung + +## Verwendung + +### 1. **PC mit Autostart hinzufügen** +1. Navigieren Sie zur Hauptseite +2. Füllen Sie alle Pflichtfelder aus (Name, MAC, IP) +3. Geben Sie den gewünschten Crontab-Ausdruck ein +4. Aktivieren Sie die Checkbox "Autostart aktiv" +5. Klicken Sie auf "PC hinzufügen" + +### 2. **Autostart bearbeiten** +1. Klicken Sie auf "Bearbeiten" bei dem gewünschten PC +2. Ändern Sie den Crontab-Ausdruck oder die Aktivierung +3. Klicken Sie auf "Speichern" + +### 3. **Crontab-Ausdrücke verstehen** +- **Hilfe**: Klicken Sie auf den Link zu [Crontab-Guru](https://crontab.guru/) +- **Beispiele**: + - `30 7 * * Mon-Fri` = Mo-Fr um 7:30 Uhr + - `0 8 * * 1-5` = Mo-Fr um 8:00 Uhr + - `0 9 * * 0,6` = Sa und So um 9:00 Uhr + - `*/15 * * * *` = Alle 15 Minuten + +## Technische Details + +### 1. **Datenbankstruktur** +```sql +ALTER TABLE pcs ADD COLUMN autostart_cron TEXT DEFAULT '30 7 * * Mon-Fri'; +ALTER TABLE pcs ADD COLUMN autostart_enabled BOOLEAN DEFAULT 0; +``` + +### 2. **Scheduler-Implementierung** +- **Go-Routine**: Läuft parallel zum Hauptserver +- **Ticker**: Prüft jede Minute auf auszuführende Aufgaben +- **Crontab-Parser**: Eigene Implementierung für häufige Anwendungsfälle +- **Graceful Shutdown**: Sauberes Beenden bei Server-Herunterfahrt + +### 3. **Crontab-Parser-Features** +- **Wildcards**: `*` für "alle" +- **Bereiche**: `1-5` für "1 bis 5" +- **Wochentage**: `Mon-Fri` für "Montag bis Freitag" +- **Einzelwerte**: `30` für "nur 30" + +## Beispiele + +### **Geschäftliche Anwendungen** +- **Büro-PCs**: `30 7 * * Mon-Fri` - Startet alle Büro-PCs Mo-Fr um 7:30 +- **Server**: `0 6 * * *` - Startet Server täglich um 6:00 +- **Backup-Systeme**: `0 2 * * 0` - Startet Backup-Systeme sonntags um 2:00 + +### **Private Anwendungen** +- **Home-Office**: `0 8 * * Mon-Fri` - Startet Arbeits-PC Mo-Fr um 8:00 +- **Gaming**: `0 18 * * Fri-Sun` - Startet Gaming-PC Fr-So um 18:00 +- **Medien-Server**: `0 20 * * *` - Startet Medien-Server täglich um 20:00 + +## Sicherheit und Monitoring + +### 1. **Logging** +- Alle automatischen WOL-Ereignisse werden protokolliert +- Trigger wird als "cron" gekennzeichnet +- Vollständige Protokollierung in der Logs-Seite + +### 2. **Fehlerbehandlung** +- WOL-Fehler werden geloggt, aber nicht an den Benutzer weitergegeben +- Scheduler läuft weiter, auch wenn einzelne WOL-Befehle fehlschlagen +- Robuste Behandlung ungültiger Crontab-Ausdrücke + +### 3. **Performance** +- Scheduler prüft nur alle PCs mit aktiviertem Autostart +- Effiziente Datenbankabfragen +- Minimale Server-Belastung + +## Erweiterungen + +### **Zukünftige Features** +- **Webhook-Integration**: Benachrichtigungen bei erfolgreichen/fehlgeschlagenen Autostarts +- **Erweiterte Crontab-Syntax**: Unterstützung für komplexere Ausdrücke +- **Zeitzonen**: Lokale Zeitzonen-Unterstützung +- **Bedingte Ausführung**: Nur starten wenn PC offline ist + +### **Monitoring und Alerting** +- **E-Mail-Benachrichtigungen**: Bei fehlgeschlagenen Autostarts +- **Dashboard**: Übersicht aller geplanten Autostarts +- **Statistiken**: Erfolgsrate und Ausführungszeiten + +## Fehlerbehebung + +### **Häufige Probleme** +1. **PC startet nicht**: Überprüfen Sie MAC-Adresse und Netzwerk-Konfiguration +2. **Falsche Zeit**: Überprüfen Sie den Crontab-Ausdruck mit Crontab-Guru +3. **Scheduler läuft nicht**: Überprüfen Sie die Server-Logs + +### **Debugging** +- **Server-Logs**: Zeigen alle Scheduler-Aktivitäten +- **Logs-Seite**: Zeigt alle WOL-Ereignisse (auch automatische) +- **Crontab-Validierung**: Verwenden Sie Crontab-Guru für Tests + +## Support + +### **Hilfreiche Links** +- [Crontab-Guru](https://crontab.guru/) - Online-Crontab-Editor und Validator +- [Cron-Wiki](https://en.wikipedia.org/wiki/Cron) - Detaillierte Crontab-Dokumentation +- [Crontab-Examples](https://crontab.guru/examples.html) - Häufige Crontab-Beispiele + +### **Kontakt** +Bei Fragen oder Problemen wenden Sie sich an das Medi-WOL-Entwicklungsteam. diff --git a/cmd/server/main.go b/cmd/server/main.go index c507f8f..f5cc109 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,9 +6,12 @@ import ( "log" "medi-wol/internal/database" "medi-wol/internal/handlers" + "medi-wol/internal/scheduler" "medi-wol/internal/wol" "os" + "os/signal" "strconv" + "syscall" "github.com/gin-gonic/gin" ) @@ -48,6 +51,11 @@ func main() { // Wake-on-LAN Service initialisieren wolService := wol.NewService() + // Scheduler initialisieren und starten + scheduler := scheduler.NewScheduler(db, wolService) + scheduler.Start() + defer scheduler.Stop() + // Handler initialisieren pcHandler := handlers.NewPCHandler(db, wolService) @@ -75,12 +83,22 @@ func main() { // Statische Dateien bereitstellen (nach den spezifischen Routen) r.Static("/static", "./web/static") - // Server starten - serverAddr := fmt.Sprintf(":%d", port) - log.Printf("Medi-WOL startet auf Port %d...", port) - log.Printf("Web-Oberfläche verfügbar unter: http://localhost%s", serverAddr) + // Graceful Shutdown konfigurieren + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - if err := r.Run(serverAddr); err != nil { - log.Fatal("Fehler beim Starten des Servers:", err) - } + // Server in Goroutine starten + go func() { + serverAddr := fmt.Sprintf(":%d", port) + log.Printf("Medi-WOL startet auf Port %d...", port) + log.Printf("Web-Oberfläche verfügbar unter: http://localhost%s", serverAddr) + + if err := r.Run(serverAddr); err != nil { + log.Fatal("Fehler beim Starten des Servers:", err) + } + }() + + // Auf Shutdown-Signal warten + <-quit + log.Println("Server wird heruntergefahren...") } diff --git a/internal/database/database.go b/internal/database/database.go index f322ea3..3db3f38 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -28,6 +28,8 @@ func InitDB() (*DB, error) { name TEXT NOT NULL, mac TEXT NOT NULL UNIQUE, ip TEXT NOT NULL, + autostart_cron TEXT DEFAULT '30 7 * * Mon-Fri', + autostart_enabled BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );` @@ -66,7 +68,7 @@ func InitDB() (*DB, error) { // GetAllPCs holt alle PCs aus der Datenbank func (db *DB) GetAllPCs() ([]models.PC, error) { - rows, err := db.Query("SELECT id, name, mac, ip, created_at, updated_at FROM pcs ORDER BY name") + rows, err := db.Query("SELECT id, name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at FROM pcs ORDER BY name") if err != nil { return nil, err } @@ -75,7 +77,7 @@ func (db *DB) GetAllPCs() ([]models.PC, error) { var pcs []models.PC for rows.Next() { var pc models.PC - err := rows.Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.CreatedAt, &pc.UpdatedAt) + err := rows.Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.AutostartCron, &pc.AutostartEnabled, &pc.CreatedAt, &pc.UpdatedAt) if err != nil { return nil, err } @@ -86,11 +88,11 @@ func (db *DB) GetAllPCs() ([]models.PC, error) { } // CreatePC erstellt einen neuen PC-Eintrag -func (db *DB) CreatePC(name, mac, ip string) (*models.PC, error) { +func (db *DB) CreatePC(name, mac, ip, autostartCron string, autostartEnabled bool) (*models.PC, error) { now := time.Now() result, err := db.Exec( - "INSERT INTO pcs (name, mac, ip, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", - name, mac, ip, now, now, + "INSERT INTO pcs (name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + name, mac, ip, autostartCron, autostartEnabled, now, now, ) if err != nil { return nil, err @@ -102,32 +104,36 @@ func (db *DB) CreatePC(name, mac, ip string) (*models.PC, error) { } return &models.PC{ - ID: int(id), - Name: name, - MAC: mac, - IP: ip, - CreatedAt: now, - UpdatedAt: now, + ID: int(id), + Name: name, + MAC: mac, + IP: ip, + AutostartCron: autostartCron, + AutostartEnabled: autostartEnabled, + CreatedAt: now, + UpdatedAt: now, }, nil } // UpdatePC aktualisiert einen bestehenden PC-Eintrag -func (db *DB) UpdatePC(id int, name, mac, ip string) (*models.PC, error) { +func (db *DB) UpdatePC(id int, name, mac, ip, autostartCron string, autostartEnabled bool) (*models.PC, error) { now := time.Now() _, err := db.Exec( - "UPDATE pcs SET name = ?, mac = ?, ip = ?, updated_at = ? WHERE id = ?", - name, mac, ip, now, id, + "UPDATE pcs SET name = ?, mac = ?, ip = ?, autostart_cron = ?, autostart_enabled = ?, updated_at = ? WHERE id = ?", + name, mac, ip, autostartCron, autostartEnabled, now, id, ) if err != nil { return nil, err } return &models.PC{ - ID: id, - Name: name, - MAC: mac, - IP: ip, - UpdatedAt: now, + ID: id, + Name: name, + MAC: mac, + IP: ip, + AutostartCron: autostartCron, + AutostartEnabled: autostartEnabled, + UpdatedAt: now, }, nil } @@ -141,9 +147,9 @@ func (db *DB) DeletePC(id int) error { func (db *DB) GetPCByID(id int) (*models.PC, error) { var pc models.PC err := db.QueryRow( - "SELECT id, name, mac, ip, created_at, updated_at FROM pcs WHERE id = ?", + "SELECT id, name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at FROM pcs WHERE id = ?", id, - ).Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.CreatedAt, &pc.UpdatedAt) + ).Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.AutostartCron, &pc.AutostartEnabled, &pc.CreatedAt, &pc.UpdatedAt) if err != nil { return nil, err @@ -152,6 +158,27 @@ func (db *DB) GetPCByID(id int) (*models.PC, error) { return &pc, nil } +// GetPCsWithAutostart holt alle PCs mit aktiviertem Autostart +func (db *DB) GetPCsWithAutostart() ([]models.PC, error) { + rows, err := db.Query("SELECT id, name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at FROM pcs WHERE autostart_enabled = 1") + if err != nil { + return nil, err + } + defer rows.Close() + + var pcs []models.PC + for rows.Next() { + var pc models.PC + err := rows.Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.AutostartCron, &pc.AutostartEnabled, &pc.CreatedAt, &pc.UpdatedAt) + if err != nil { + return nil, err + } + pcs = append(pcs, pc) + } + + return pcs, nil +} + // CreateLog erstellt einen neuen Log-Eintrag func (db *DB) CreateLog(pcID int, pcName, mac, trigger string) (*models.LogEvent, error) { now := time.Now() diff --git a/internal/handlers/pc_handler.go b/internal/handlers/pc_handler.go index 54276f8..167e41e 100644 --- a/internal/handlers/pc_handler.go +++ b/internal/handlers/pc_handler.go @@ -79,8 +79,13 @@ func (h *PCHandler) CreatePC(c *gin.Context) { return } + // Standardwert für Autostart-Cron setzen, falls nicht angegeben + if req.AutostartCron == "" { + req.AutostartCron = "30 7 * * Mon-Fri" + } + // PC erstellen - pc, err := h.db.CreatePC(req.Name, req.MAC, req.IP) + pc, err := h.db.CreatePC(req.Name, req.MAC, req.IP, req.AutostartCron, req.AutostartEnabled) if err != nil { c.JSON(http.StatusInternalServerError, models.PCResponse{ Success: false, @@ -126,8 +131,13 @@ func (h *PCHandler) UpdatePC(c *gin.Context) { return } + // Standardwert für Autostart-Cron setzen, falls nicht angegeben + if req.AutostartCron == "" { + req.AutostartCron = "30 7 * * Mon-Fri" + } + // PC aktualisieren - pc, err := h.db.UpdatePC(id, req.Name, req.MAC, req.IP) + pc, err := h.db.UpdatePC(id, req.Name, req.MAC, req.IP, req.AutostartCron, req.AutostartEnabled) if err != nil { c.JSON(http.StatusInternalServerError, models.PCResponse{ Success: false, @@ -231,11 +241,13 @@ func (h *PCHandler) GetPCStatus(c *gin.Context) { for _, pc := range pcs { online := h.pingService.IsOnline(pc.IP) statusList = append(statusList, models.PCStatus{ - ID: pc.ID, - Name: pc.Name, - MAC: pc.MAC, - IP: pc.IP, - Online: online, + ID: pc.ID, + Name: pc.Name, + MAC: pc.MAC, + IP: pc.IP, + Online: online, + AutostartCron: pc.AutostartCron, + AutostartEnabled: pc.AutostartEnabled, }) } diff --git a/internal/models/pc.go b/internal/models/pc.go index 28d6c5b..1f5ffb3 100644 --- a/internal/models/pc.go +++ b/internal/models/pc.go @@ -6,26 +6,32 @@ import ( // PC repräsentiert einen Computer in der Datenbank type PC struct { - ID int `json:"id" db:"id"` - Name string `json:"name" db:"name"` - MAC string `json:"mac" db:"mac"` - IP string `json:"ip" db:"ip"` - CreatedAt time.Time `json:"created_at" db:"updated_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + MAC string `json:"mac" db:"mac"` + IP string `json:"ip" db:"ip"` + AutostartCron string `json:"autostart_cron" db:"autostart_cron"` + AutostartEnabled bool `json:"autostart_enabled" db:"autostart_enabled"` + CreatedAt time.Time `json:"created_at" db:"updated_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // CreatePCRequest repräsentiert die Anfrage zum Erstellen eines neuen PCs type CreatePCRequest struct { - Name string `json:"name" binding:"required"` - MAC string `json:"mac" binding:"required"` - IP string `json:"ip" binding:"required"` + Name string `json:"name" binding:"required"` + MAC string `json:"mac" binding:"required"` + IP string `json:"ip" binding:"required"` + AutostartCron string `json:"autostart_cron"` + AutostartEnabled bool `json:"autostart_enabled"` } // UpdatePCRequest repräsentiert die Anfrage zum Aktualisieren eines PCs type UpdatePCRequest struct { - Name string `json:"name" binding:"required"` - MAC string `json:"mac" binding:"required"` - IP string `json:"ip" binding:"required"` + Name string `json:"name" binding:"required"` + MAC string `json:"mac" binding:"required"` + IP string `json:"ip" binding:"required"` + AutostartCron string `json:"autostart_cron"` + AutostartEnabled bool `json:"autostart_enabled"` } // PCResponse repräsentiert die Antwort für PC-Operationen @@ -38,9 +44,11 @@ type PCResponse struct { // PCStatus repräsentiert den Online-Status eines PCs type PCStatus struct { - ID int `json:"id"` - Name string `json:"name"` - MAC string `json:"mac"` - IP string `json:"ip"` - Online bool `json:"online"` + ID int `json:"id"` + Name string `json:"name"` + MAC string `json:"mac"` + IP string `json:"ip"` + Online bool `json:"online"` + AutostartCron string `json:"autostart_cron"` + AutostartEnabled bool `json:"autostart_enabled"` } diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go new file mode 100644 index 0000000..5d0abaf --- /dev/null +++ b/internal/scheduler/scheduler.go @@ -0,0 +1,174 @@ +package scheduler + +import ( + "log" + "medi-wol/internal/database" + "medi-wol/internal/models" + "medi-wol/internal/wol" + "strconv" + "strings" + "time" +) + +// Scheduler verwaltet geplante WOL-Ereignisse +type Scheduler struct { + db *database.DB + wolService *wol.Service + ticker *time.Ticker + stopChan chan bool +} + +// NewScheduler erstellt einen neuen Scheduler +func NewScheduler(db *database.DB, wolService *wol.Service) *Scheduler { + return &Scheduler{ + db: db, + wolService: wolService, + stopChan: make(chan bool), + } +} + +// Start startet den Scheduler +func (s *Scheduler) Start() { + s.ticker = time.NewTicker(1 * time.Minute) // Jede Minute prüfen + go s.run() + log.Println("Autostart-Scheduler gestartet") +} + +// Stop stoppt den Scheduler +func (s *Scheduler) Stop() { + if s.ticker != nil { + s.ticker.Stop() + } + close(s.stopChan) + log.Println("Autostart-Scheduler gestoppt") +} + +// run ist die Hauptschleife des Schedulers +func (s *Scheduler) run() { + for { + select { + case <-s.ticker.C: + s.checkAndExecuteScheduledTasks() + case <-s.stopChan: + return + } + } +} + +// checkAndExecuteScheduledTasks prüft alle geplanten Aufgaben +func (s *Scheduler) checkAndExecuteScheduledTasks() { + now := time.Now() + + // Alle PCs mit aktiviertem Autostart holen + pcs, err := s.db.GetPCsWithAutostart() + if err != nil { + log.Printf("Fehler beim Laden der PCs mit Autostart: %v", err) + return + } + + for _, pc := range pcs { + if s.shouldExecuteNow(pc.AutostartCron, now) { + s.executeWOL(pc) + } + } +} + +// shouldExecuteNow prüft, ob ein Crontab-Ausdruck jetzt ausgeführt werden soll +func (s *Scheduler) shouldExecuteNow(cronExpr string, now time.Time) bool { + parts := strings.Fields(cronExpr) + if len(parts) != 5 { + log.Printf("Ungültiger Crontab-Ausdruck: %s", cronExpr) + return false + } + + // Einfache Crontab-Parser-Implementierung + // Format: Minute Stunde Tag Monat Wochentag + minute := s.parseCronField(parts[0], 0, 59, now.Minute()) + hour := s.parseCronField(parts[1], 0, 23, now.Hour()) + day := s.parseCronField(parts[2], 1, 31, now.Day()) + month := s.parseCronField(parts[3], 1, 12, int(now.Month())) + weekday := s.parseCronField(parts[4], 0, 6, int(now.Weekday())) + + return minute && hour && day && month && weekday +} + +// parseCronField parst ein einzelnes Crontab-Feld +func (s *Scheduler) parseCronField(field string, min, max, current int) bool { + // Einfache Implementierung für häufige Fälle + if field == "*" { + return true + } + + // Einzelne Zahl + if num, err := strconv.Atoi(field); err == nil { + return num == current + } + + // Bereich (z.B. "1-5") + if strings.Contains(field, "-") { + parts := strings.Split(field, "-") + if len(parts) == 2 { + start, err1 := strconv.Atoi(parts[0]) + end, err2 := strconv.Atoi(parts[1]) + if err1 == nil && err2 == nil { + return current >= start && current <= end + } + } + } + + // Wochentag-Namen (z.B. "Mon-Fri") + if strings.Contains(field, "-") && (strings.Contains(field, "Mon") || strings.Contains(field, "Tue") || + strings.Contains(field, "Wed") || strings.Contains(field, "Thu") || + strings.Contains(field, "Fri") || strings.Contains(field, "Sat") || strings.Contains(field, "Sun")) { + return s.parseWeekdayRange(field, current) + } + + return false +} + +// parseWeekdayRange parst Wochentag-Bereiche +func (s *Scheduler) parseWeekdayRange(field string, current int) bool { + // Wochentag-Mapping (0=Sonntag, 1=Montag, ..., 6=Samstag) + weekdayMap := map[string]int{ + "Sun": 0, "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6, + } + + if strings.Contains(field, "-") { + parts := strings.Split(field, "-") + if len(parts) == 2 { + start, ok1 := weekdayMap[parts[0]] + end, ok2 := weekdayMap[parts[1]] + if ok1 && ok2 { + // Spezielle Behandlung für Wochentag-Bereiche + if start <= end { + return current >= start && current <= end + } else { + // Bereich über Mitternacht (z.B. Fri-Mon) + return current >= start || current <= end + } + } + } + } + + return false +} + +// executeWOL führt einen geplanten WOL-Befehl aus +func (s *Scheduler) executeWOL(pc models.PC) { + log.Printf("Führe geplanten WOL für PC %s (%s) aus", pc.Name, pc.MAC) + + // WOL-Paket senden + err := s.wolService.WakePC(pc.MAC) + if err != nil { + log.Printf("Fehler beim Senden des WOL-Pakets für PC %s: %v", pc.Name, err) + return + } + + // Log-Eintrag erstellen + _, logErr := s.db.CreateLog(pc.ID, pc.Name, pc.MAC, "cron") + if logErr != nil { + log.Printf("Fehler beim Erstellen des Log-Eintrags: %v", logErr) + } + + log.Printf("Geplanter WOL für PC %s erfolgreich ausgeführt", pc.Name) +} diff --git a/web/static/script.js b/web/static/script.js index 3aa6306..c0eb2c7 100644 --- a/web/static/script.js +++ b/web/static/script.js @@ -157,12 +157,21 @@ class PCManager { ${new Date(pc.created_at).toLocaleDateString('de-DE')} +
+ + Autostart: + ${pc.autostart_enabled ? + `${this.escapeHtml(pc.autostart_cron || '30 7 * * Mon-Fri')}` : + 'Deaktiviert' + } + +
- @@ -183,9 +192,11 @@ class PCManager { const name = document.getElementById('pcName').value.trim(); const mac = document.getElementById('macAddress').value.trim(); const ip = document.getElementById('ipAddress').value.trim(); + const autostartCron = document.getElementById('autostartCron').value.trim(); + const autostartEnabled = document.getElementById('autostartEnabled').checked; if (!name || !mac || !ip) { - this.showNotification('Warnung', 'Bitte füllen Sie alle Felder aus', 'warning'); + this.showNotification('Warnung', 'Bitte füllen Sie alle Pflichtfelder aus', 'warning'); return; } @@ -195,7 +206,13 @@ class PCManager { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ name, mac, ip }) + body: JSON.stringify({ + name, + mac, + ip, + autostart_cron: autostartCron || '30 7 * * Mon-Fri', + autostart_enabled: autostartEnabled + }) }); const data = await response.json(); @@ -212,12 +229,14 @@ class PCManager { } } - editPC(id, name, mac, ip) { + editPC(id, name, mac, ip, autostartCron, autostartEnabled) { // Modal mit PC-Daten füllen document.getElementById('editPCId').value = id; document.getElementById('editPCName').value = name; document.getElementById('editMACAddress').value = mac; document.getElementById('editIPAddress').value = ip; + document.getElementById('editAutostartCron').value = autostartCron || '30 7 * * Mon-Fri'; + document.getElementById('editAutostartEnabled').checked = autostartEnabled || false; // Modal öffnen const editModal = new bootstrap.Modal(document.getElementById('editPCModal')); @@ -229,9 +248,11 @@ class PCManager { const name = document.getElementById('editPCName').value.trim(); const mac = document.getElementById('editMACAddress').value.trim(); const ip = document.getElementById('editIPAddress').value.trim(); + const autostartCron = document.getElementById('editAutostartCron').value.trim(); + const autostartEnabled = document.getElementById('editAutostartEnabled').checked; if (!name || !mac || !ip) { - this.showNotification('Warnung', 'Bitte füllen Sie alle Felder aus', 'warning'); + this.showNotification('Warnung', 'Bitte füllen Sie alle Pflichtfelder aus', 'warning'); return; } @@ -241,7 +262,13 @@ class PCManager { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ name, mac, ip }) + body: JSON.stringify({ + name, + mac, + ip, + autostart_cron: autostartCron || '30 7 * * Mon-Fri', + autostart_enabled: autostartEnabled + }) }); const data = await response.json(); diff --git a/web/static/style.css b/web/static/style.css index b25324a..c0bfa5a 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -281,6 +281,29 @@ body { border-top-color: var(--brand-primary) !important; } +/* Autostart-Checkbox Styling - vereinfacht */ +.form-check { + display: flex; + align-items: center; + min-height: 38px; /* Gleiche Höhe wie form-control */ +} + +/* Spezifische Ausrichtung für die Autostart-Zeile */ +.row .col-md-6 .form-check { + height: 38px; + display: flex; + align-items: center; +} + +/* Mehr Abstand zwischen Checkbox und Label */ +.form-check-input[type="checkbox"] { + margin-right: 0.5em; /* Erhöhter Abstand zur Checkbox */ +} + +.form-check-label { + margin-left: 0.5em; /* Zusätzlicher Abstand zum Label */ +} + /* Content Header */ .content-header { text-align: center; diff --git a/web/templates/index.html b/web/templates/index.html index c620c73..f0aec79 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -75,6 +75,31 @@
+
+
+
+ + + +
+
+
+
+
+ + +
+
+
+
@@ -176,6 +201,25 @@ +
+ + +
+ + Crontab-Guru für Hilfe + +
+
+
+
+ + +
+