Autostart-Funktionalität implementiert: Crontab-Syntax, Scheduler und UI-Integration
This commit is contained in:
130
AUTOSTART_README.md
Normal file
130
AUTOSTART_README.md
Normal file
@ -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.
|
||||||
@ -6,9 +6,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"medi-wol/internal/database"
|
"medi-wol/internal/database"
|
||||||
"medi-wol/internal/handlers"
|
"medi-wol/internal/handlers"
|
||||||
|
"medi-wol/internal/scheduler"
|
||||||
"medi-wol/internal/wol"
|
"medi-wol/internal/wol"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -48,6 +51,11 @@ func main() {
|
|||||||
// Wake-on-LAN Service initialisieren
|
// Wake-on-LAN Service initialisieren
|
||||||
wolService := wol.NewService()
|
wolService := wol.NewService()
|
||||||
|
|
||||||
|
// Scheduler initialisieren und starten
|
||||||
|
scheduler := scheduler.NewScheduler(db, wolService)
|
||||||
|
scheduler.Start()
|
||||||
|
defer scheduler.Stop()
|
||||||
|
|
||||||
// Handler initialisieren
|
// Handler initialisieren
|
||||||
pcHandler := handlers.NewPCHandler(db, wolService)
|
pcHandler := handlers.NewPCHandler(db, wolService)
|
||||||
|
|
||||||
@ -75,12 +83,22 @@ func main() {
|
|||||||
// Statische Dateien bereitstellen (nach den spezifischen Routen)
|
// Statische Dateien bereitstellen (nach den spezifischen Routen)
|
||||||
r.Static("/static", "./web/static")
|
r.Static("/static", "./web/static")
|
||||||
|
|
||||||
// Server starten
|
// Graceful Shutdown konfigurieren
|
||||||
serverAddr := fmt.Sprintf(":%d", port)
|
quit := make(chan os.Signal, 1)
|
||||||
log.Printf("Medi-WOL startet auf Port %d...", port)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
log.Printf("Web-Oberfläche verfügbar unter: http://localhost%s", serverAddr)
|
|
||||||
|
|
||||||
if err := r.Run(serverAddr); err != nil {
|
// Server in Goroutine starten
|
||||||
log.Fatal("Fehler beim Starten des Servers:", err)
|
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...")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@ func InitDB() (*DB, error) {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
mac TEXT NOT NULL UNIQUE,
|
mac TEXT NOT NULL UNIQUE,
|
||||||
ip TEXT NOT NULL,
|
ip TEXT NOT NULL,
|
||||||
|
autostart_cron TEXT DEFAULT '30 7 * * Mon-Fri',
|
||||||
|
autostart_enabled BOOLEAN DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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
|
// GetAllPCs holt alle PCs aus der Datenbank
|
||||||
func (db *DB) GetAllPCs() ([]models.PC, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -75,7 +77,7 @@ func (db *DB) GetAllPCs() ([]models.PC, error) {
|
|||||||
var pcs []models.PC
|
var pcs []models.PC
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var pc models.PC
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -86,11 +88,11 @@ func (db *DB) GetAllPCs() ([]models.PC, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreatePC erstellt einen neuen PC-Eintrag
|
// 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()
|
now := time.Now()
|
||||||
result, err := db.Exec(
|
result, err := db.Exec(
|
||||||
"INSERT INTO pcs (name, mac, ip, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO pcs (name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
name, mac, ip, now, now,
|
name, mac, ip, autostartCron, autostartEnabled, now, now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -102,32 +104,36 @@ func (db *DB) CreatePC(name, mac, ip string) (*models.PC, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.PC{
|
return &models.PC{
|
||||||
ID: int(id),
|
ID: int(id),
|
||||||
Name: name,
|
Name: name,
|
||||||
MAC: mac,
|
MAC: mac,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
CreatedAt: now,
|
AutostartCron: autostartCron,
|
||||||
UpdatedAt: now,
|
AutostartEnabled: autostartEnabled,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePC aktualisiert einen bestehenden PC-Eintrag
|
// 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()
|
now := time.Now()
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"UPDATE pcs SET name = ?, mac = ?, ip = ?, updated_at = ? WHERE id = ?",
|
"UPDATE pcs SET name = ?, mac = ?, ip = ?, autostart_cron = ?, autostart_enabled = ?, updated_at = ? WHERE id = ?",
|
||||||
name, mac, ip, now, id,
|
name, mac, ip, autostartCron, autostartEnabled, now, id,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.PC{
|
return &models.PC{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: name,
|
Name: name,
|
||||||
MAC: mac,
|
MAC: mac,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
UpdatedAt: now,
|
AutostartCron: autostartCron,
|
||||||
|
AutostartEnabled: autostartEnabled,
|
||||||
|
UpdatedAt: now,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,9 +147,9 @@ func (db *DB) DeletePC(id int) error {
|
|||||||
func (db *DB) GetPCByID(id int) (*models.PC, error) {
|
func (db *DB) GetPCByID(id int) (*models.PC, error) {
|
||||||
var pc models.PC
|
var pc models.PC
|
||||||
err := db.QueryRow(
|
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,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -152,6 +158,27 @@ func (db *DB) GetPCByID(id int) (*models.PC, error) {
|
|||||||
return &pc, nil
|
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
|
// CreateLog erstellt einen neuen Log-Eintrag
|
||||||
func (db *DB) CreateLog(pcID int, pcName, mac, trigger string) (*models.LogEvent, error) {
|
func (db *DB) CreateLog(pcID int, pcName, mac, trigger string) (*models.LogEvent, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
@ -79,8 +79,13 @@ func (h *PCHandler) CreatePC(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standardwert für Autostart-Cron setzen, falls nicht angegeben
|
||||||
|
if req.AutostartCron == "" {
|
||||||
|
req.AutostartCron = "30 7 * * Mon-Fri"
|
||||||
|
}
|
||||||
|
|
||||||
// PC erstellen
|
// 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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, models.PCResponse{
|
c.JSON(http.StatusInternalServerError, models.PCResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
@ -126,8 +131,13 @@ func (h *PCHandler) UpdatePC(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standardwert für Autostart-Cron setzen, falls nicht angegeben
|
||||||
|
if req.AutostartCron == "" {
|
||||||
|
req.AutostartCron = "30 7 * * Mon-Fri"
|
||||||
|
}
|
||||||
|
|
||||||
// PC aktualisieren
|
// 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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, models.PCResponse{
|
c.JSON(http.StatusInternalServerError, models.PCResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
@ -231,11 +241,13 @@ func (h *PCHandler) GetPCStatus(c *gin.Context) {
|
|||||||
for _, pc := range pcs {
|
for _, pc := range pcs {
|
||||||
online := h.pingService.IsOnline(pc.IP)
|
online := h.pingService.IsOnline(pc.IP)
|
||||||
statusList = append(statusList, models.PCStatus{
|
statusList = append(statusList, models.PCStatus{
|
||||||
ID: pc.ID,
|
ID: pc.ID,
|
||||||
Name: pc.Name,
|
Name: pc.Name,
|
||||||
MAC: pc.MAC,
|
MAC: pc.MAC,
|
||||||
IP: pc.IP,
|
IP: pc.IP,
|
||||||
Online: online,
|
Online: online,
|
||||||
|
AutostartCron: pc.AutostartCron,
|
||||||
|
AutostartEnabled: pc.AutostartEnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,26 +6,32 @@ import (
|
|||||||
|
|
||||||
// PC repräsentiert einen Computer in der Datenbank
|
// PC repräsentiert einen Computer in der Datenbank
|
||||||
type PC struct {
|
type PC struct {
|
||||||
ID int `json:"id" db:"id"`
|
ID int `json:"id" db:"id"`
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
MAC string `json:"mac" db:"mac"`
|
MAC string `json:"mac" db:"mac"`
|
||||||
IP string `json:"ip" db:"ip"`
|
IP string `json:"ip" db:"ip"`
|
||||||
CreatedAt time.Time `json:"created_at" db:"updated_at"`
|
AutostartCron string `json:"autostart_cron" db:"autostart_cron"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
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
|
// CreatePCRequest repräsentiert die Anfrage zum Erstellen eines neuen PCs
|
||||||
type CreatePCRequest struct {
|
type CreatePCRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
MAC string `json:"mac" binding:"required"`
|
MAC string `json:"mac" binding:"required"`
|
||||||
IP string `json:"ip" 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
|
// UpdatePCRequest repräsentiert die Anfrage zum Aktualisieren eines PCs
|
||||||
type UpdatePCRequest struct {
|
type UpdatePCRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
MAC string `json:"mac" binding:"required"`
|
MAC string `json:"mac" binding:"required"`
|
||||||
IP string `json:"ip" 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
|
// PCResponse repräsentiert die Antwort für PC-Operationen
|
||||||
@ -38,9 +44,11 @@ type PCResponse struct {
|
|||||||
|
|
||||||
// PCStatus repräsentiert den Online-Status eines PCs
|
// PCStatus repräsentiert den Online-Status eines PCs
|
||||||
type PCStatus struct {
|
type PCStatus struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MAC string `json:"mac"`
|
MAC string `json:"mac"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
|
AutostartCron string `json:"autostart_cron"`
|
||||||
|
AutostartEnabled bool `json:"autostart_enabled"`
|
||||||
}
|
}
|
||||||
|
|||||||
174
internal/scheduler/scheduler.go
Normal file
174
internal/scheduler/scheduler.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -157,12 +157,21 @@ class PCManager {
|
|||||||
</td>
|
</td>
|
||||||
<td>${new Date(pc.created_at).toLocaleDateString('de-DE')}</td>
|
<td>${new Date(pc.created_at).toLocaleDateString('de-DE')}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div class="mb-1">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock"></i> Autostart:
|
||||||
|
${pc.autostart_enabled ?
|
||||||
|
`<span class="text-success">${this.escapeHtml(pc.autostart_cron || '30 7 * * Mon-Fri')}</span>` :
|
||||||
|
'<span class="text-muted">Deaktiviert</span>'
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button class="btn btn-success btn-sm" onclick="pcManager.wakePC(${pc.id})"
|
<button class="btn btn-success btn-sm" onclick="pcManager.wakePC(${pc.id})"
|
||||||
title="PC aufwecken">
|
title="PC aufwecken">
|
||||||
<i class="fas fa-power-off"></i> Aufwecken
|
<i class="fas fa-power-off"></i> Aufwecken
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning btn-sm" onclick="pcManager.editPC(${pc.id}, '${this.escapeHtml(pc.name)}', '${this.escapeHtml(pc.mac)}', '${this.escapeHtml(pc.ip || '')}')"
|
<button class="btn btn-warning btn-sm" onclick="pcManager.editPC(${pc.id}, '${this.escapeHtml(pc.name)}', '${this.escapeHtml(pc.mac)}', '${this.escapeHtml(pc.ip || '')}', '${this.escapeHtml(pc.autostart_cron || '')}', ${pc.autostart_enabled})"
|
||||||
title="PC bearbeiten">
|
title="PC bearbeiten">
|
||||||
<i class="fas fa-edit"></i> Bearbeiten
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
</button>
|
</button>
|
||||||
@ -183,9 +192,11 @@ class PCManager {
|
|||||||
const name = document.getElementById('pcName').value.trim();
|
const name = document.getElementById('pcName').value.trim();
|
||||||
const mac = document.getElementById('macAddress').value.trim();
|
const mac = document.getElementById('macAddress').value.trim();
|
||||||
const ip = document.getElementById('ipAddress').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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +206,13 @@ class PCManager {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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();
|
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
|
// Modal mit PC-Daten füllen
|
||||||
document.getElementById('editPCId').value = id;
|
document.getElementById('editPCId').value = id;
|
||||||
document.getElementById('editPCName').value = name;
|
document.getElementById('editPCName').value = name;
|
||||||
document.getElementById('editMACAddress').value = mac;
|
document.getElementById('editMACAddress').value = mac;
|
||||||
document.getElementById('editIPAddress').value = ip;
|
document.getElementById('editIPAddress').value = ip;
|
||||||
|
document.getElementById('editAutostartCron').value = autostartCron || '30 7 * * Mon-Fri';
|
||||||
|
document.getElementById('editAutostartEnabled').checked = autostartEnabled || false;
|
||||||
|
|
||||||
// Modal öffnen
|
// Modal öffnen
|
||||||
const editModal = new bootstrap.Modal(document.getElementById('editPCModal'));
|
const editModal = new bootstrap.Modal(document.getElementById('editPCModal'));
|
||||||
@ -229,9 +248,11 @@ class PCManager {
|
|||||||
const name = document.getElementById('editPCName').value.trim();
|
const name = document.getElementById('editPCName').value.trim();
|
||||||
const mac = document.getElementById('editMACAddress').value.trim();
|
const mac = document.getElementById('editMACAddress').value.trim();
|
||||||
const ip = document.getElementById('editIPAddress').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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +262,13 @@ class PCManager {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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();
|
const data = await response.json();
|
||||||
|
|||||||
@ -281,6 +281,29 @@ body {
|
|||||||
border-top-color: var(--brand-primary) !important;
|
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 */
|
||||||
.content-header {
|
.content-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -75,6 +75,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="autostartCron" class="form-label">Autostart (Crontab)</label>
|
||||||
|
<input type="text" class="form-control" id="autostartCron"
|
||||||
|
placeholder="30 7 * * Mon-Fri"
|
||||||
|
title="Format: Minute Stunde Tag Monat Wochentag">
|
||||||
|
<div class="form-text">
|
||||||
|
<a href="https://crontab.guru/" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Crontab-Guru für Hilfe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check mt-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autostartEnabled">
|
||||||
|
<label class="form-check-label" for="autostartEnabled">
|
||||||
|
Autostart aktiv
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-save"></i> PC hinzufügen
|
<i class="fas fa-save"></i> PC hinzufügen
|
||||||
</button>
|
</button>
|
||||||
@ -176,6 +201,25 @@
|
|||||||
<input type="text" class="form-control" id="editIPAddress"
|
<input type="text" class="form-control" id="editIPAddress"
|
||||||
placeholder="192.168.1.100" required>
|
placeholder="192.168.1.100" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editAutostartCron" class="form-label">Autostart (Crontab)</label>
|
||||||
|
<input type="text" class="form-control" id="editAutostartCron"
|
||||||
|
placeholder="30 7 * * Mon-Fri"
|
||||||
|
title="Format: Minute Stunde Tag Monat Wochentag">
|
||||||
|
<div class="form-text">
|
||||||
|
<a href="https://crontab.guru/" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Crontab-Guru für Hilfe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="editAutostartEnabled">
|
||||||
|
<label class="form-check-label" for="editAutostartEnabled">
|
||||||
|
Autostart aktiv
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
Reference in New Issue
Block a user