4 Commits

21 changed files with 1313 additions and 69 deletions

130
AUTOSTART_README.md Normal file
View 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.

110
LOGGING_README.md Normal file
View File

@ -0,0 +1,110 @@
# Medi-WOL Logging-System
## Übersicht
Das Medi-WOL Logging-System protokolliert alle Wake-on-LAN (WOL) Ereignisse in der Datenbank und bietet verschiedene Möglichkeiten, diese Logs anzuzeigen und zu verwalten.
## Funktionen
### 1. Automatisches Logging
- **WOL-Ereignisse**: Jedes Mal, wenn ein PC über die Web-Oberfläche aufgeweckt wird, wird automatisch ein Log-Eintrag erstellt
- **Auslöser-Tracking**: Unterscheidung zwischen manuellen Button-Klicks und automatischen Cron-Jobs
- **Vollständige Protokollierung**: Timestamp, PC-Name, MAC-Adresse und Auslöser werden gespeichert
### 2. Log-Anzeige
- **Dedizierte Logs-Seite**: Übersicht aller WOL-Ereignisse unter `/logs`
- **Tooltips**: Zeigt die letzten 5 WOL-Ereignisse für jeden PC als Tooltip über der Tabellenzeile
- **Sortierung**: Logs werden nach Zeitstempel sortiert (neueste zuerst)
### 3. API-Endpunkte
- `GET /api/logs` - Alle Log-Einträge abrufen
- `GET /api/logs/pc/:id` - Alle Logs für einen bestimmten PC
- `GET /api/logs/pc/:id/recent` - Die letzten 5 Logs für einen bestimmten PC
## Datenbankstruktur
### Tabelle: `wol_logs`
```sql
CREATE TABLE wol_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
pc_id INTEGER NOT NULL,
pc_name TEXT NOT NULL,
mac TEXT NOT NULL,
trigger TEXT NOT NULL,
FOREIGN KEY (pc_id) REFERENCES pcs (id) ON DELETE CASCADE
);
```
### Felder
- **id**: Eindeutige ID des Log-Eintrags
- **timestamp**: Zeitstempel des WOL-Ereignisses
- **pc_id**: Referenz auf den PC (Foreign Key)
- **pc_name**: Name des PCs (für bessere Lesbarkeit)
- **mac**: MAC-Adresse des PCs
- **trigger**: Auslöser des WOL-Ereignisses ("button" oder "cron")
## Verwendung
### 1. Logs-Seite aufrufen
Navigieren Sie zu `http://localhost:8080/logs` um alle WOL-Logs anzuzeigen.
### 2. Tooltips verwenden
- Bewegen Sie den Mauszeiger über eine PC-Zeile in der Haupttabelle
- Ein Tooltip zeigt die letzten 5 WOL-Ereignisse für diesen PC an
### 3. API verwenden
```javascript
// Alle Logs abrufen
const response = await fetch('/api/logs');
const logs = await response.json();
// Logs für einen bestimmten PC abrufen
const pcLogs = await fetch(`/api/logs/pc/${pcId}`);
const logs = await pcLogs.json();
// Letzte 5 Logs für einen PC abrufen
const recentLogs = await fetch(`/api/logs/pc/${pcId}/recent`);
const logs = await recentLogs.json();
```
## Erweiterungen
### Cron-Job Logging
Das System ist vorbereitet für automatische WOL-Ereignisse über Cron-Jobs. Diese würden mit dem Trigger "cron" protokolliert werden.
### Log-Bereinigung
Für Produktionsumgebungen sollte eine Log-Bereinigung implementiert werden, um alte Log-Einträge nach einem bestimmten Zeitraum zu löschen.
### Export-Funktionalität
Zukünftige Versionen könnten CSV- oder PDF-Export der Logs bieten.
## Technische Details
### Implementierung
- **Backend**: Go mit Gin-Framework
- **Datenbank**: SQLite mit Foreign Key Constraints
- **Frontend**: HTML, CSS, JavaScript mit Bootstrap
- **Tooltips**: Bootstrap Tooltip-System mit HTML-Inhalt
### Sicherheit
- HTML-Escaping für alle Benutzereingaben
- Validierung der API-Parameter
- Keine sensiblen Daten in den Logs
### Performance
- Indizierte Datenbankabfragen
- Lazy Loading der Tooltips
- Begrenzte Anzahl von Log-Einträgen pro Tooltip (5)
## Fehlerbehebung
### Häufige Probleme
1. **Tooltips werden nicht angezeigt**: Überprüfen Sie, ob Bootstrap korrekt geladen ist
2. **Logs werden nicht gespeichert**: Überprüfen Sie die Datenbankverbindung und -berechtigungen
3. **Fehler beim Laden der Logs**: Überprüfen Sie die API-Endpunkte und die Datenbankstruktur
### Debugging
- Überprüfen Sie die Browser-Konsole auf JavaScript-Fehler
- Überprüfen Sie die Server-Logs auf Backend-Fehler
- Testen Sie die API-Endpunkte direkt mit einem HTTP-Client

View File

@ -114,6 +114,37 @@ Falls weder Kommandozeilenparameter noch Umgebungsvariable gesetzt sind, wird Po
- Klicken Sie auf den "Löschen"-Button neben dem gewünschten PC - Klicken Sie auf den "Löschen"-Button neben dem gewünschten PC
- Bestätigen Sie die Löschung - Bestätigen Sie die Löschung
## Windows Installer
### Features
Der Windows Installer (`medi-wol-setup.exe`) bietet:
- **Automatische Service-Installation** mit NSSM
- **Port-Konfiguration** während der Installation (Standard: 9000)
- **Windows Firewall-Regeln** werden automatisch erstellt:
- Eingehende Verbindungen zur Anwendung
- Ausgehende Verbindungen (für Wake-on-LAN)
- Port-spezifische TCP-Regeln
- **Service-Start-Option** (Checkbox für automatischen Start)
- **Saubere Deinstallation** mit Entfernung aller Firewall-Regeln
### Voraussetzungen für den Build
```bash
# NSSM in das Installer-Verzeichnis kopieren
copy "I:\MARKUS\beszel\nssm.exe" installer\nssm.exe
# Oder von der offiziellen Quelle herunterladen
# https://nssm.cc/download
```
### Build des Installers
```bash
# Mit build.bat (automatisch)
.\build.bat
# Oder manuell mit Inno Setup Compiler
ISCC.exe installer\medi-wol-setup.iss
```
## Build-Anweisungen ## Build-Anweisungen
### Windows Build ### Windows Build
@ -255,12 +286,20 @@ medi-wol/
├── web/ # Web-Oberfläche ├── web/ # Web-Oberfläche
│ ├── static/ # CSS, JavaScript │ ├── static/ # CSS, JavaScript
│ └── templates/ # HTML-Templates │ └── templates/ # HTML-Templates
├── installer/ # Windows Installer
│ ├── medi-wol-setup.iss # Inno Setup Script
│ └── nssm.exe # NSSM für Service-Management
├── go.mod # Go-Module ├── go.mod # Go-Module
├── build.bat # Windows Build-Skript ├── build.bat # Windows Build-Skript (inkl. Installer)
├── build.sh # Linux Build-Skript ├── build.sh # Linux Build-Skript
└── README.md # Diese Datei └── README.md # Diese Datei
``` ```
### Installer-Verzeichnis
Das `installer/` Verzeichnis enthält alle Dateien für den Windows Installer:
- **`medi-wol-setup.iss`** - Inno Setup Konfigurationsdatei
- **`nssm.exe`** - NSSM (Non-Sucking Service Manager) für Windows Services
## API-Endpunkte ## API-Endpunkte
- `GET /` - Hauptseite - `GET /` - Hauptseite
@ -339,14 +378,32 @@ go vet ./...
## Deployment ## Deployment
### Windows Service ### Windows Service
#### Mit NSSM (Non-Sucking Service Manager)
```bash ```bash
# Mit NSSM (Non-Sucking Service Manager) # Service installieren
nssm install Medi-WOL "C:\path\to\medi-wol.exe" nssm install Medi-WOL "C:\path\to\medi-wol.exe"
nssm set Medi-WOL AppDirectory "C:\path\to\medi-wol" nssm set Medi-WOL AppDirectory "C:\path\to\medi-wol"
nssm set Medi-WOL AppParameters "-port 9090" # Optional: Port setzen nssm set Medi-WOL AppParameters "-port 9090" # Optional: Port setzen
nssm start Medi-WOL nssm start Medi-WOL
``` ```
#### Windows Installer (Empfohlen)
Der Windows Installer konfiguriert den Service automatisch mit NSSM:
- **Automatische Service-Installation** mit NSSM
- **Windows Firewall-Regeln** werden automatisch erstellt
- **Port-Konfiguration** während der Installation
- **Service-Start-Option** (Checkbox für automatischen Start)
**Wichtig:** Für den Windows Installer muss `nssm.exe` im `installer/` Verzeichnis verfügbar sein:
```bash
# NSSM in das Installer-Verzeichnis kopieren
copy "I:\MARKUS\beszel\nssm.exe" installer\nssm.exe
# Oder von der offiziellen Quelle herunterladen
# https://nssm.cc/download
```
### Linux Systemd Service ### Linux Systemd Service
```ini ```ini
# /etc/systemd/system/medi-wol.service # /etc/systemd/system/medi-wol.service

View File

@ -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,18 +51,23 @@ 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)
// Gin Router konfigurieren // Gin Router konfigurieren
r := gin.Default() r := gin.Default()
// Statische Dateien bereitstellen // HTML-Templates laden
r.Static("/static", "./web/static")
r.LoadHTMLGlob("web/templates/*") r.LoadHTMLGlob("web/templates/*")
// Routen definieren // Routen definieren
r.GET("/", pcHandler.Index) r.GET("/", pcHandler.Index)
r.GET("/logs", pcHandler.Logs)
r.GET("/api/pcs", pcHandler.GetAllPCs) r.GET("/api/pcs", pcHandler.GetAllPCs)
r.POST("/api/pcs", pcHandler.CreatePC) r.POST("/api/pcs", pcHandler.CreatePC)
r.PUT("/api/pcs/:id", pcHandler.UpdatePC) r.PUT("/api/pcs/:id", pcHandler.UpdatePC)
@ -67,12 +75,30 @@ func main() {
r.POST("/api/pcs/:id/wake", pcHandler.WakePC) r.POST("/api/pcs/:id/wake", pcHandler.WakePC)
r.GET("/api/pcs/status", pcHandler.GetPCStatus) r.GET("/api/pcs/status", pcHandler.GetPCStatus)
// Server starten // Log-Routen
serverAddr := fmt.Sprintf(":%d", port) r.GET("/api/logs", pcHandler.GetAllLogs)
log.Printf("Medi-WOL startet auf Port %d...", port) r.GET("/api/logs/pc/:id", pcHandler.GetLogsByPCID)
log.Printf("Web-Oberfläche verfügbar unter: http://localhost%s", serverAddr) r.GET("/api/logs/pc/:id/recent", pcHandler.GetRecentLogsByPCID)
if err := r.Run(serverAddr); err != nil { // Statische Dateien bereitstellen (nach den spezifischen Routen)
log.Fatal("Fehler beim Starten des Servers:", err) r.Static("/static", "./web/static")
}
// Graceful Shutdown konfigurieren
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 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...")
} }

Binary file not shown.

Binary file not shown.

View File

@ -21,6 +21,8 @@ AllowNoIcons=yes
LicenseFile=..\LICENSE LicenseFile=..\LICENSE
OutputDir=..\dist OutputDir=..\dist
OutputBaseFilename=medi-wol-setup OutputBaseFilename=medi-wol-setup
SetupIconFile=medi-wol.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
@ -46,10 +48,10 @@ Source: "..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Run] [Run]
; Installiere Medi-WOL als Windows-Dienst mit NSSM ; Installiere Medi-WOL als Windows-Dienst mit NSSM

BIN
installer/medi-wol.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -21,18 +21,37 @@ func InitDB() (*DB, error) {
return nil, err return nil, err
} }
// Tabelle erstellen // PC-Tabelle erstellen
createTableSQL := ` createPCTableSQL := `
CREATE TABLE IF NOT EXISTS pcs ( CREATE TABLE IF NOT EXISTS pcs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
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
);` );`
_, err = db.Exec(createTableSQL) _, err = db.Exec(createPCTableSQL)
if err != nil {
return nil, err
}
// Log-Tabelle erstellen
createLogTableSQL := `
CREATE TABLE IF NOT EXISTS wol_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
pc_id INTEGER NOT NULL,
pc_name TEXT NOT NULL,
mac TEXT NOT NULL,
trigger TEXT NOT NULL,
FOREIGN KEY (pc_id) REFERENCES pcs (id) ON DELETE CASCADE
);`
_, err = db.Exec(createLogTableSQL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -49,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
} }
@ -58,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
} }
@ -69,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
@ -85,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
} }
@ -124,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
@ -134,3 +157,113 @@ 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
func (db *DB) CreateLog(pcID int, pcName, mac, trigger string) (*models.LogEvent, error) {
now := time.Now()
result, err := db.Exec(
"INSERT INTO wol_logs (timestamp, pc_id, pc_name, mac, trigger) VALUES (?, ?, ?, ?, ?)",
now, pcID, pcName, mac, trigger,
)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &models.LogEvent{
ID: int(id),
Timestamp: now,
PCID: pcID,
PCName: pcName,
MAC: mac,
Trigger: trigger,
}, nil
}
// GetAllLogs holt alle Log-Einträge aus der Datenbank
func (db *DB) GetAllLogs() ([]models.LogEvent, error) {
rows, err := db.Query("SELECT id, timestamp, pc_id, pc_name, mac, trigger FROM wol_logs ORDER BY timestamp DESC")
if err != nil {
return nil, err
}
defer rows.Close()
var logs []models.LogEvent
for rows.Next() {
var log models.LogEvent
err := rows.Scan(&log.ID, &log.Timestamp, &log.PCID, &log.PCName, &log.MAC, &log.Trigger)
if err != nil {
return nil, err
}
logs = append(logs, log)
}
return logs, nil
}
// GetLogsByPCID holt alle Log-Einträge für einen bestimmten PC
func (db *DB) GetLogsByPCID(pcID int) ([]models.LogEvent, error) {
rows, err := db.Query("SELECT id, timestamp, pc_id, pc_name, mac, trigger FROM wol_logs WHERE pc_id = ? ORDER BY timestamp DESC", pcID)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []models.LogEvent
for rows.Next() {
var log models.LogEvent
err := rows.Scan(&log.ID, &log.Timestamp, &log.PCID, &log.PCName, &log.MAC, &log.Trigger)
if err != nil {
return nil, err
}
logs = append(logs, log)
}
return logs, nil
}
// GetRecentLogsByPCID holt die letzten 5 Log-Einträge für einen bestimmten PC
func (db *DB) GetRecentLogsByPCID(pcID int) ([]models.LogEvent, error) {
rows, err := db.Query("SELECT id, timestamp, pc_id, pc_name, mac, trigger FROM wol_logs WHERE pc_id = ? ORDER BY timestamp DESC LIMIT 5", pcID)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []models.LogEvent
for rows.Next() {
var log models.LogEvent
err := rows.Scan(&log.ID, &log.Timestamp, &log.PCID, &log.PCName, &log.MAC, &log.Trigger)
if err != nil {
return nil, err
}
logs = append(logs, log)
}
return logs, nil
}

View File

@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"log"
"medi-wol/internal/database" "medi-wol/internal/database"
"medi-wol/internal/models" "medi-wol/internal/models"
"medi-wol/internal/ping" "medi-wol/internal/ping"
@ -34,6 +35,13 @@ func (h *PCHandler) Index(c *gin.Context) {
}) })
} }
// Logs zeigt die Logs-Seite an
func (h *PCHandler) Logs(c *gin.Context) {
c.HTML(http.StatusOK, "logs.html", gin.H{
"title": "Medi-WOL - Logs",
})
}
// GetAllPCs gibt alle gespeicherten PCs zurück // GetAllPCs gibt alle gespeicherten PCs zurück
func (h *PCHandler) GetAllPCs(c *gin.Context) { func (h *PCHandler) GetAllPCs(c *gin.Context) {
pcs, err := h.db.GetAllPCs() pcs, err := h.db.GetAllPCs()
@ -71,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,
@ -118,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,
@ -194,6 +212,13 @@ func (h *PCHandler) WakePC(c *gin.Context) {
return return
} }
// Log-Eintrag erstellen
_, logErr := h.db.CreateLog(pc.ID, pc.Name, pc.MAC, "button")
if logErr != nil {
// Log-Fehler nicht an den Benutzer weitergeben, nur intern protokollieren
log.Printf("Fehler beim Erstellen des Log-Eintrags: %v", logErr)
}
c.JSON(http.StatusOK, models.PCResponse{ c.JSON(http.StatusOK, models.PCResponse{
Success: true, Success: true,
Message: "Wake-on-LAN Paket erfolgreich gesendet an " + pc.Name, Message: "Wake-on-LAN Paket erfolgreich gesendet an " + pc.Name,
@ -216,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,
}) })
} }
@ -229,3 +256,74 @@ func (h *PCHandler) GetPCStatus(c *gin.Context) {
"status": statusList, "status": statusList,
}) })
} }
// GetAllLogs gibt alle Log-Einträge zurück
func (h *PCHandler) GetAllLogs(c *gin.Context) {
logs, err := h.db.GetAllLogs()
if err != nil {
c.JSON(http.StatusInternalServerError, models.LogResponse{
Success: false,
Message: "Fehler beim Laden der Logs: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, models.LogResponse{
Success: true,
Logs: logs,
})
}
// GetLogsByPCID gibt alle Log-Einträge für einen bestimmten PC zurück
func (h *PCHandler) GetLogsByPCID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, models.LogResponse{
Success: false,
Message: "Ungültige PC-ID",
})
return
}
logs, err := h.db.GetLogsByPCID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, models.LogResponse{
Success: false,
Message: "Fehler beim Laden der Logs: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, models.LogResponse{
Success: true,
Logs: logs,
})
}
// GetRecentLogsByPCID gibt die letzten 5 Log-Einträge für einen bestimmten PC zurück
func (h *PCHandler) GetRecentLogsByPCID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, models.LogResponse{
Success: false,
Message: "Ungültige PC-ID",
})
return
}
logs, err := h.db.GetRecentLogsByPCID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, models.LogResponse{
Success: false,
Message: "Fehler beim Laden der Logs: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, models.LogResponse{
Success: true,
Logs: logs,
})
}

31
internal/models/log.go Normal file
View File

@ -0,0 +1,31 @@
package models
import (
"time"
)
// LogEvent repräsentiert ein Log-Ereignis in der Datenbank
type LogEvent struct {
ID int `json:"id" db:"id"`
Timestamp time.Time `json:"timestamp" db:"timestamp"`
PCID int `json:"pc_id" db:"pc_id"`
PCName string `json:"pc_name" db:"pc_name"`
MAC string `json:"mac" db:"mac"`
Trigger string `json:"trigger" db:"trigger"` // "button" oder "cron"
}
// CreateLogRequest repräsentiert die Anfrage zum Erstellen eines neuen Log-Eintrags
type CreateLogRequest struct {
PCID int `json:"pc_id" binding:"required"`
PCName string `json:"pc_name" binding:"required"`
MAC string `json:"mac" binding:"required"`
Trigger string `json:"trigger" binding:"required"`
}
// LogResponse repräsentiert die Antwort für Log-Operationen
type LogResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Log *LogEvent `json:"log,omitempty"`
Logs []LogEvent `json:"logs,omitempty"`
}

View File

@ -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"`
} }

View 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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
web/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

BIN
web/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

80
web/static/logs.js Normal file
View File

@ -0,0 +1,80 @@
// Logs-Seite JavaScript
document.addEventListener('DOMContentLoaded', function() {
loadLogs();
});
// Alle Logs laden
async function loadLogs() {
try {
const response = await fetch('/api/logs');
const data = await response.json();
if (data.success) {
displayLogs(data.logs);
} else {
showError('Fehler beim Laden der Logs: ' + data.message);
}
} catch (error) {
showError('Fehler beim Laden der Logs: ' + error.message);
}
}
// Logs in der Tabelle anzeigen
function displayLogs(logs) {
const tableBody = document.getElementById('logsTableBody');
const loading = document.getElementById('loading');
const noLogs = document.getElementById('noLogs');
// Loading ausblenden
loading.style.display = 'none';
if (!logs || logs.length === 0) {
noLogs.style.display = 'block';
return;
}
// Tabelle leeren
tableBody.innerHTML = '';
// Logs hinzufügen
logs.forEach(log => {
const row = document.createElement('tr');
// Zeitstempel formatieren
const timestamp = new Date(log.timestamp);
const formattedTime = timestamp.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// Auslöser übersetzen
const triggerText = log.trigger === 'button' ? 'Button-Klick' :
log.trigger === 'cron' ? 'Automatisch' : log.trigger;
row.innerHTML = `
<td>${formattedTime}</td>
<td>${escapeHtml(log.pc_name)}</td>
<td>${escapeHtml(log.mac)}</td>
<td>${escapeHtml(triggerText)}</td>
`;
tableBody.appendChild(row);
});
}
// Fehler anzeigen
function showError(message) {
const loading = document.getElementById('loading');
loading.innerHTML = `<p class="error">${escapeHtml(message)}</p>`;
}
// HTML-Escaping für Sicherheit
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@ -145,7 +145,7 @@ class PCManager {
pcList.style.display = 'block'; pcList.style.display = 'block';
tableBody.innerHTML = this.filteredPCs.map(pc => ` tableBody.innerHTML = this.filteredPCs.map(pc => `
<tr> <tr data-pc-id="${pc.id}">
<td><strong>${this.escapeHtml(pc.name)}</strong></td> <td><strong>${this.escapeHtml(pc.name)}</strong></td>
<td><code>${this.escapeHtml(pc.mac)}</code></td> <td><code>${this.escapeHtml(pc.mac)}</code></td>
<td><code>${this.escapeHtml(pc.ip || 'N/A')}</code></td> <td><code>${this.escapeHtml(pc.ip || 'N/A')}</code></td>
@ -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>
@ -174,15 +183,20 @@ class PCManager {
</td> </td>
</tr> </tr>
`).join(''); `).join('');
// Tooltips für alle PC-Zeilen laden
this.loadTooltipsForAllPCs();
} }
async addPC() { async addPC() {
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;
} }
@ -192,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();
@ -209,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'));
@ -226,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;
} }
@ -238,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();
@ -368,6 +398,68 @@ class PCManager {
input.value = value.toUpperCase(); input.value = value.toUpperCase();
} }
// Tooltip für alle PCs laden
async loadTooltipsForAllPCs() {
for (const pc of this.filteredPCs) {
await this.loadTooltipForPC(pc.id);
}
}
// Tooltip für einen bestimmten PC laden
async loadTooltipForPC(pcId) {
try {
const response = await fetch(`/api/logs/pc/${pcId}/recent`);
const data = await response.json();
if (data.success) {
const tooltipContent = this.createTooltipContent(data.logs);
const row = document.querySelector(`tr[data-pc-id="${pcId}"]`);
if (row) {
// Tooltip-Attribut setzen
row.setAttribute('data-bs-toggle', 'tooltip');
row.setAttribute('data-bs-html', 'true');
row.setAttribute('title', tooltipContent);
// Bootstrap Tooltip initialisieren
new bootstrap.Tooltip(row, {
placement: 'top',
trigger: 'hover',
html: true
});
}
}
} catch (error) {
console.error(`Fehler beim Laden des Tooltips für PC ${pcId}:`, error);
}
}
// Tooltip-Inhalt erstellen
createTooltipContent(logs) {
if (!logs || logs.length === 0) {
return '<div class="text-muted">Keine WOL-Ereignisse</div>';
}
let content = '<div class="text-start"><strong>Letzte WOL-Ereignisse:</strong><br>';
logs.forEach(log => {
const timestamp = new Date(log.timestamp).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
const triggerText = log.trigger === 'button' ? 'Button' :
log.trigger === 'cron' ? 'Auto' : log.trigger;
content += `${timestamp} (${triggerText})<br>`;
});
content += '</div>';
return content;
}
} }
// PC Manager initialisieren, wenn die Seite geladen ist // PC Manager initialisieren, wenn die Seite geladen ist

View File

@ -172,3 +172,150 @@ body {
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
} }
/* Navigation */
.navbar {
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.navbar-nav .nav-link {
color: var(--brand-primary) !important;
font-weight: 500;
padding: 10px 20px;
border-radius: 25px;
transition: all 0.3s ease;
}
.navbar-nav .nav-link:hover,
.navbar-nav .nav-link.active {
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-accent) 100%);
color: white !important;
transform: translateY(-1px);
}
/* Logs-Seite */
.logs-container {
position: relative;
min-height: 400px;
}
.logs-table-container {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.logs-table {
margin: 0;
width: 100%;
}
.logs-table th {
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-accent) 100%);
color: white;
border: none;
padding: 15px;
font-weight: 600;
text-align: left;
}
.logs-table td {
padding: 12px 15px;
border-bottom: 1px solid #e9ecef;
vertical-align: middle;
}
.logs-table tbody tr:hover {
background-color: #f8f9fa;
}
.loading {
text-align: center;
padding: 40px;
color: var(--brand-primary);
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--brand-primary);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-logs {
text-align: center;
padding: 40px;
color: #6c757d;
}
.error {
color: #dc3545;
font-weight: 500;
}
/* Verbesserte Tooltips */
.tooltip {
font-size: 0.875rem;
}
.tooltip-inner {
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-accent) 100%);
color: white;
border-radius: 8px;
padding: 8px 12px;
max-width: 300px;
text-align: left;
}
.tooltip-arrow::before {
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;
margin-bottom: 30px;
}
.content-header h2 {
color: var(--brand-primary);
margin-bottom: 10px;
}
.content-header p {
color: #6c757d;
font-size: 1.1rem;
}

View File

@ -4,6 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.title}}</title> <title>{{.title}}</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<!-- Stylesheets -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="/static/style.css" rel="stylesheet"> <link href="/static/style.css" rel="stylesheet">
@ -16,6 +21,24 @@
<img src="/static/logo.png" alt="medisoftware Logo" /> <img src="/static/logo.png" alt="medisoftware Logo" />
<h1>{{.title}}</h1> <h1>{{.title}}</h1>
</div> </div>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="/">
<i class="fas fa-desktop"></i> PCs
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logs">
<i class="fas fa-list-alt"></i> Logs
</a>
</li>
</ul>
</div>
</nav>
</div> </div>
</div> </div>
@ -52,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>
@ -153,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">

89
web/templates/logs.html Normal file
View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Medi-WOL - Logs</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<!-- Stylesheets -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-12">
<div class="app-header mb-4">
<img src="/static/logo.png" alt="medisoftware Logo" />
<h1>Medi-WOL</h1>
</div>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-desktop"></i> PCs
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/logs">
<i class="fas fa-list-alt"></i> Logs
</a>
</li>
</ul>
</div>
</nav>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-list-alt"></i> Wake-on-LAN Logs
</h5>
</div>
<div class="card-body">
<div class="logs-container">
<div class="logs-table-container">
<table class="logs-table">
<thead>
<tr>
<th>Zeitstempel</th>
<th>PC-Name</th>
<th>MAC-Adresse</th>
<th>Auslöser</th>
</tr>
</thead>
<tbody id="logsTableBody">
<!-- Log-Einträge werden hier dynamisch eingefügt -->
</tbody>
</table>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Lade Logs...</p>
</div>
<div class="no-logs" id="noLogs" style="display: none;">
<p>Keine Log-Einträge gefunden.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/logs.js"></script>
</body>
</html>