19 Commits

Author SHA1 Message Date
6a84c767f2 Fix: Go-Version auf 1.18 kompatibel gemacht und Build-Probleme behoben
- go.mod: Version von 1.23.0 auf 1.18 korrigiert
- golang.org/x/sys von v0.35.0 auf v0.15.0 downgraded für Go 1.18 Kompatibilität
- README aktualisiert: Go 1.18 als Mindestanforderung dokumentiert
- Build-Anweisungen verbessert mit Multi-Platform Build-Empfehlungen
- Wichtiger Hinweis zu Go-Version-Kompatibilität hinzugefügt
2025-08-22 09:27:07 +02:00
2bfe8f6196 Go-Version auf 1.23.0 zurückgesetzt und go.sum aktualisiert 2025-08-22 09:12:31 +02:00
bea61b0c95 Merge Branch 'Urlaubsmodus' (Urlaubsmodus, Settings-API, Dummy-PC, DB-Migration) 2025-08-22 08:54:18 +02:00
ce2eea5f43 DB-Migration: fehlende autostart_* Spalten hinzufügen und Defaults setzen 2025-08-22 08:53:18 +02:00
bdeabaad2e README: Urlaubsmodus, Settings-API und automatischen Dummy-PC dokumentiert 2025-08-22 08:52:24 +02:00
5c13a34bbf InitDB: Dummy-PC 'Test' automatisch anlegen, falls pcs leer ist 2025-08-22 08:46:19 +02:00
d85181e797 Startup: Dummy-PC 'Test' automatisch anlegen bei leerer Datenbank 2025-08-22 08:42:38 +02:00
b860571f40 Logo durch MediSoftware_Logo_rb_cut.png ersetzt 2025-08-22 08:38:58 +02:00
1dfa0c9b32 Urlaubsmodus implementiert - Globale Checkbox zum Deaktivieren des Schedulers 2025-08-22 08:35:13 +02:00
0488e4ec47 Update README 2025-08-22 08:22:02 +02:00
5b03441b4d dist/ Verzeichnis aus Repository entfernt - Build-Artefakte gehören nicht ins Git 2025-08-22 08:18:19 +02:00
49c3ece8f5 Update .gitignore 2025-08-22 08:17:14 +02:00
12655429ac README: Umfassende Dokumentation des Scheduler-Systems und Logging-Features 2025-08-22 08:15:39 +02:00
9f49a1b865 Setup: Icon für mediWOL WEB UI Verknüpfung auf SetupIconFile gesetzt 2025-08-22 08:12:05 +02:00
774e3c953f Setup: Automatisches Öffnen der Web-Oberfläche nach Service-Start 2025-08-22 07:59:08 +02:00
5bfc380a37 Autostart-Funktionalität implementiert: Crontab-Syntax, Scheduler und UI-Integration 2025-08-22 07:49:45 +02:00
b6888ca5da Implementiere umfassendes Logging-System für WOL-Ereignisse
- Neue Log-Tabelle in der Datenbank
- Automatisches Logging bei WOL-Button-Klicks
- Dedizierte Logs-Seite mit Bootstrap-Design
- Tooltips mit letzten 5 WOL-Ereignissen pro PC
- API-Endpunkte für Log-Verwaltung
- Einheitliches Design zwischen Haupt- und Logs-Seite
- Vollständige Dokumentation des Logging-Systems
2025-08-22 07:16:14 +02:00
2f4920cc27 Add application icon for Inno Setup and web favicon - Create medi-wol.ico for Windows installer with blue circle and white W - Add favicon.ico, favicon.png, favicon-16x16.png for web app - Update Inno Setup script to use SetupIconFile and UninstallDisplayIcon - Add favicon links to HTML template head section - Icon features: MediSoftware blue theme, Wake-on-LAN W symbol, transparent background 2025-08-21 18:48:50 +02:00
64222fb7a8 Update README with NSSM requirements and Windows Installer documentation - Add NSSM.EXE requirement for installer directory - Document Windows Installer features and build process - Update project structure to include installer directory - Add firewall configuration details 2025-08-21 18:24:21 +02:00
26 changed files with 1674 additions and 149 deletions

2
.gitignore vendored
View File

@ -46,3 +46,5 @@ Thumbs.db
# Temporary files # Temporary files
tmp/ tmp/
temp/ temp/
dist/

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

220
README.md
View File

@ -8,6 +8,9 @@ Ein moderner Wake-on-LAN Manager, entwickelt mit Go und einer schönen Web-Oberf
- **Wake-on-LAN**: Ein-Klick-Aufwecken von Computern über MAC-Adressen - **Wake-on-LAN**: Ein-Klick-Aufwecken von Computern über MAC-Adressen
- **IP-Adressverwaltung**: Pro Gerät wird eine IP-Adresse gespeichert - **IP-Adressverwaltung**: Pro Gerät wird eine IP-Adresse gespeichert
- **Online-Status (Ping)**: Geräte können per Ping geprüft und im UI als Online/Offline angezeigt werden - **Online-Status (Ping)**: Geräte können per Ping geprüft und im UI als Online/Offline angezeigt werden
- **Automatischer Start**: Scheduler für geplante Wake-on-LAN Ereignisse mit Crontab-Syntax
- **Urlaubsmodus**: Globaler Schalter, der den geplanten Autostart für alle Geräte deaktiviert
- **Logging-System**: Vollständige Protokollierung aller WOL-Ereignisse (Button-Klick und Scheduler)
- **Moderne Web-Oberfläche**: Responsive Design mit Bootstrap und FontAwesome - **Moderne Web-Oberfläche**: Responsive Design mit Bootstrap und FontAwesome
- **SQLite-Datenbank**: Einfache lokale Datenspeicherung - **SQLite-Datenbank**: Einfache lokale Datenspeicherung
- **Cross-Platform**: Läuft auf Windows und Linux - **Cross-Platform**: Läuft auf Windows und Linux
@ -15,7 +18,7 @@ Ein moderner Wake-on-LAN Manager, entwickelt mit Go und einer schönen Web-Oberf
## Voraussetzungen ## Voraussetzungen
- Go 1.21 oder höher - Go 1.18 oder höher
- Git - Git
## Installation ## Installation
@ -114,8 +117,62 @@ 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
### Autostart konfigurieren
1. **Autostart aktivieren**: Aktivieren Sie die Checkbox "Autostart aktiv"
2. **Crontab-Syntax eingeben**: Verwenden Sie das Format `Minute Stunde Tag Monat Wochentag`
- **Standard**: `30 7 * * Mon-Fri` (Mo-Fr um 7:30 Uhr)
- **Beispiele**:
- `0 8 * * *` = Täglich um 8:00 Uhr
- `0 9 * * Mon,Wed,Fri` = Mo, Mi, Fr um 9:00 Uhr
- `30 7 * * 1-5` = Mo-Fr um 7:30 Uhr
3. **Hilfe**: Link zu [crontab.guru](https://crontab.guru/) für Crontab-Syntax
4. **Speichern**: Änderungen werden automatisch vom Scheduler übernommen
### Urlaubsmodus
- Aktivieren/Deaktivieren über die Checkbox "Urlaubsmodus" auf der Startseite
- Tooltip: "Der Urlaubsmodus deaktiviert den geplanten Autostart für alle Geräte"
- Wirkung: Solange aktiv, führt der Scheduler keine geplanten WOL-Tasks aus
### Logs einsehen
- **Tooltips**: Die letzten 5 Log-Einträge werden als Tooltip über jeder PC-Zeile angezeigt
- **Logs-Seite**: Vollständige Log-Übersicht unter `/logs`
- **Log-Details**: Jeder Eintrag enthält Timestamp, PC-Name, MAC-Adresse und Auslöser (Button/Scheduler)
## 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
> **Wichtig**: Das Projekt verwendet Go 1.18 und kompatible Abhängigkeiten. Für neuere Go-Versionen müssen die Abhängigkeiten entsprechend angepasst werden.
### Windows Build ### Windows Build
#### Lokaler Build (Windows) #### Lokaler Build (Windows)
@ -251,18 +308,30 @@ medi-wol/
│ ├── database/ # Datenbanklogik │ ├── database/ # Datenbanklogik
│ ├── handlers/ # HTTP-Handler │ ├── handlers/ # HTTP-Handler
│ ├── models/ # Datenmodelle │ ├── models/ # Datenmodelle
│ ├── scheduler/ # Autostart-Scheduler für WOL-Ereignisse
│ └── wol/ # Wake-on-LAN Service │ └── wol/ # Wake-on-LAN Service
├── 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
├── AUTOSTART_README.md # Dokumentation des Autostart-Systems
├── LOGGING_README.md # Dokumentation des Logging-Systems
└── 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
### PC-Management
- `GET /` - Hauptseite - `GET /` - Hauptseite
- `GET /api/pcs` - Alle PCs abrufen - `GET /api/pcs` - Alle PCs abrufen
- `POST /api/pcs` - Neuen PC erstellen - `POST /api/pcs` - Neuen PC erstellen
@ -271,19 +340,53 @@ medi-wol/
- `POST /api/pcs/:id/wake` - PC aufwecken - `POST /api/pcs/:id/wake` - PC aufwecken
- `GET /api/pcs/status` - Online-Status aller PCs abrufen (Ping) - `GET /api/pcs/status` - Online-Status aller PCs abrufen (Ping)
### Settings
- `GET /api/settings/vacation-mode` - Status des Urlaubsmodus abrufen
- `POST /api/settings/vacation-mode` - Urlaubsmodus setzen (`{ "vacation_mode": true|false }`)
### Logging
- `GET /logs` - Logs-Seite anzeigen
- `GET /api/logs` - Alle Log-Einträge abrufen
- `GET /api/logs/pc/:id` - Logs für einen spezifischen PC abrufen
- `GET /api/logs/pc/:id/recent` - Die letzten 5 Logs für einen PC abrufen
## Datenbank ## Datenbank
Die Anwendung verwendet SQLite als lokale Datenbank. Die Datenbankdatei `medi-wol.db` wird automatisch im Projektverzeichnis erstellt. Die Anwendung verwendet SQLite als lokale Datenbank. Die Datenbankdatei `medi-wol.db` wird automatisch im Projektverzeichnis erstellt.
Hinweis: Beim ersten Start bzw. wenn die Tabelle `pcs` leer ist, wird automatisch ein Dummy-PC angelegt:
- Name: `Test`, IP: `192.168.0.1`, MAC: `00:11:22:33:AA:FF`, Cron: `30 7 * * Mon-Fri`, Autostart: deaktiviert
### Tabellenstruktur `pcs` ### Tabellenstruktur `pcs`
| Spalte | Typ | Hinweis | | Spalte | Typ | Hinweis |
|-------------|----------|------------------------| |------------------|----------|--------------------------------------------|
| id | INTEGER | Primärschlüssel | | id | INTEGER | Primärschlüssel |
| name | TEXT | Pflichtfeld | | name | TEXT | Pflichtfeld |
| mac | TEXT | Pflichtfeld, eindeutig | | mac | TEXT | Pflichtfeld, eindeutig |
| ip | TEXT | Pflichtfeld | | ip | TEXT | Pflichtfeld |
| created_at | DATETIME | Automatisch | | autostart_cron | TEXT | Crontab-Syntax für Autostart (Standard: `30 7 * * Mon-Fri`) |
| updated_at | DATETIME | Automatisch | | autostart_enabled| BOOLEAN | Autostart aktiviert (Standard: false) |
| created_at | DATETIME | Automatisch |
| updated_at | DATETIME | Automatisch |
### Tabellenstruktur `settings`
| Spalte | Typ | Hinweis |
|-----------|----------|---------------------------------|
| id | INTEGER | Primärschlüssel |
| key | TEXT | Eindeutiger Schlüssel |
| value | TEXT | Wert (z. B. "true"/"false") |
| created_at| DATETIME | Automatisch |
| updated_at| DATETIME | Automatisch |
### Tabellenstruktur `wol_logs`
| Spalte | Typ | Hinweis |
|-------------|----------|--------------------------------------------|
| id | INTEGER | Primärschlüssel |
| timestamp | DATETIME | Zeitstempel des WOL-Ereignisses |
| pc_id | INTEGER | Referenz auf PC (Foreign Key) |
| pc_name | TEXT | Name des PCs zum Zeitpunkt des Ereignisses |
| mac | TEXT | MAC-Adresse des PCs |
| trigger | TEXT | Auslöser: "button" oder "cron" |
## Wake-on-LAN ## Wake-on-LAN
@ -317,6 +420,11 @@ go build -ldflags="-s -w" -o medi-wol.exe cmd/server/main.go
# Linux # Linux
go build -ldflags="-s -w" -o medi-wol cmd/server/main.go go build -ldflags="-s -w" -o medi-wol cmd/server/main.go
# Multi-Platform Build (empfohlen)
./build.sh # Linux/macOS
# oder
.\build.bat # Windows
``` ```
### Tests ausführen ### Tests ausführen
@ -336,17 +444,61 @@ golint ./...
go vet ./... go vet ./...
``` ```
## Scheduler-System
### Übersicht
Der integrierte Scheduler ermöglicht es, Wake-on-LAN Ereignisse automatisch zu geplanten Zeiten auszuführen. Dies ist ideal für:
- **Bürozeiten**: PCs starten automatisch vor Arbeitsbeginn
- **Wartungsfenster**: Regelmäßige Systemstarts für Updates
- **Energiesparen**: PCs werden nur bei Bedarf gestartet
### Funktionsweise
1. **Crontab-Parser**: Unterstützt Standard Crontab-Syntax
2. **Minuten-Intervall**: Prüft jede Minute auf fällige Aufgaben
3. **Automatische Ausführung**: Sendet WOL-Pakete ohne Benutzerinteraktion
4. **Logging**: Alle Scheduler-Ereignisse werden protokolliert
### Unterstützte Crontab-Formate
- **Einfache Werte**: `30 7 * * Mon-Fri` (Mo-Fr um 7:30 Uhr)
- **Bereiche**: `0 8-18 * * *` (8:00-18:00 Uhr stündlich)
- **Wochentage**: `0 9 * * Mon,Wed,Fri` (Mo, Mi, Fr um 9:00 Uhr)
- **Wochentag-Bereiche**: `0 8 * * Mon-Fri` (Mo-Fr um 8:00 Uhr)
### Konfiguration
- **Standard**: `30 7 * * Mon-Fri` (Mo-Fr um 7:30 Uhr)
- **Anpassung**: Über die Web-Oberfläche bei jedem PC individuell
- **Aktivierung**: Checkbox "Autostart aktiv" pro PC
- **Sofortige Übernahme**: Änderungen werden sofort vom Scheduler berücksichtigt
## 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
@ -391,49 +543,3 @@ docker run -d -p 9090:9090 -e PORT=9090 medi-wol:latest
Dieses Projekt ist für den internen Gebrauch bestimmt. Dieses Projekt ist für den internen Gebrauch bestimmt.
## Support
Bei Fragen oder Problemen wenden Sie sich an das Entwicklungsteam.
Hier sind drei einfache Wege nimm Variante A (empfohlen) für die neueste Version.
- A) Offizielles Tarball (empfohlen)
1) Alte Installation entfernen (falls vorhanden):
```bash
sudo rm -rf /usr/local/go
```
2) Tarball laden und installieren (ersetze 1.22.6 und Arch bei Bedarf: amd64/arm64):
```bash
wget https://go.dev/dl/go1.22.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.6.linux-amd64.tar.gz
```
3) PATH setzen (dauerhaft):
```bash
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
source ~/.profile
```
4) Prüfen:
```bash
go version
```
- B) Snap (schnell, meist aktuell)
```bash
sudo snap install go --classic
go version
```
- C) Ubuntu-Paket (einfach, aber oft älter)
```bash
sudo apt update
sudo apt install -y golang-go
go version
```
Hinweise:
- Standard-GOPATH ist ~/go. Optional:
```bash
echo 'export GOPATH=$HOME/go' >> ~/.profile
echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.profile
source ~/.profile
```

0
build.sh Normal file → Executable file
View File

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"
) )
@ -45,21 +48,45 @@ func main() {
} }
defer db.Close() defer db.Close()
// Dummy-PC anlegen, falls keine Geräte vorhanden sind
pcs, err := db.GetAllPCs()
if err != nil {
log.Printf("Warnung: Konnte PCs nicht laden: %v", err)
} else if len(pcs) == 0 {
_, createErr := db.CreatePC(
"Test",
"00:11:22:33:AA:FF",
"192.168.0.1",
"30 7 * * Mon-Fri",
false,
)
if createErr != nil {
log.Printf("Warnung: Konnte Dummy-PC nicht anlegen: %v", createErr)
} else {
log.Println("Dummy-PC 'Test' automatisch angelegt (leere Datenbank)")
}
}
// 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 +94,34 @@ 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)
// Settings-API
r.GET("/api/settings/vacation-mode", pcHandler.GetVacationMode)
r.POST("/api/settings/vacation-mode", pcHandler.SetVacationMode)
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.

4
go.mod
View File

@ -1,10 +1,10 @@
module medi-wol module medi-wol
go 1.23.0 go 1.18
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
golang.org/x/sys v0.35.0 golang.org/x/sys v0.15.0
modernc.org/sqlite v1.28.0 modernc.org/sqlite v1.28.0
) )

13
go.sum
View File

@ -16,7 +16,6 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -28,10 +27,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -46,7 +43,6 @@ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNa
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -83,11 +79,10 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
@ -110,9 +105,7 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@ -126,9 +119,7 @@ modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -21,20 +21,20 @@ 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
PrivilegesRequired=admin PrivilegesRequired=admin
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64os
ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64os
[Languages] [Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl" Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
Name: "startservice"; Description: "{cm:StartServiceAfterInstall}"; GroupDescription: "{cm:ServiceOptions}" Name: "startservice"; Description: "{cm:StartServiceAfterInstall}"; GroupDescription: "{cm:ServiceOptions}"
[Files] [Files]
@ -46,10 +46,8 @@ 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}\mediWOL WEB UI"; Filename: "cmd.exe"; Parameters: "/c start http://localhost:{code:GetPort}"; IconFilename: "medi-wol.ico"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Run] [Run]
; Installiere Medi-WOL als Windows-Dienst mit NSSM ; Installiere Medi-WOL als Windows-Dienst mit NSSM
@ -74,6 +72,8 @@ Filename: "{app}\nssm.exe"; Parameters: "set ""{#MyAppServiceName}"" AppStopMeth
Filename: "{app}\nssm.exe"; Parameters: "set ""{#MyAppServiceName}"" AppStopMethodThreads 1500"; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "set ""{#MyAppServiceName}"" AppStopMethodThreads 1500"; Flags: runhidden
; Service nach Installation starten mit Verzögerung ; Service nach Installation starten mit Verzögerung
Filename: "{app}\nssm.exe"; Parameters: "start ""{#MyAppServiceName}"""; StatusMsg: "Starte Medi-WOL Dienst..."; Flags: runhidden waituntilterminated; Check: ShouldStartService Filename: "{app}\nssm.exe"; Parameters: "start ""{#MyAppServiceName}"""; StatusMsg: "Starte Medi-WOL Dienst..."; Flags: runhidden waituntilterminated; Check: ShouldStartService
; Warte auf Service-Start und öffne dann die Web-Oberfläche
Filename: "cmd.exe"; Parameters: "/c timeout /t 3 /nobreak >nul && start http://localhost:{code:GetPort}"; StatusMsg: "Öffne Medi-WOL Web-Oberfläche..."; Flags: runhidden; Check: ShouldStartService
; Windows Firewall-Ausnahmen für den Service hinzufügen ; Windows Firewall-Ausnahmen für den Service hinzufügen
Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#MyAppServiceName} - Inbound"" dir=in action=allow program=""{app}\{#MyAppExeName}"" enable=yes"; StatusMsg: "Erstelle Windows Firewall-Ausnahme (Eingehend)..."; Flags: runhidden Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#MyAppServiceName} - Inbound"" dir=in action=allow program=""{app}\{#MyAppExeName}"" enable=yes"; StatusMsg: "Erstelle Windows Firewall-Ausnahme (Eingehend)..."; Flags: runhidden
Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#MyAppServiceName} - Outbound"" dir=out action=allow program=""{app}\{#MyAppExeName}"" enable=yes"; StatusMsg: "Erstelle Windows Firewall-Ausnahme (Ausgehend)..."; Flags: runhidden Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#MyAppServiceName} - Outbound"" dir=out action=allow program=""{app}\{#MyAppExeName}"" enable=yes"; StatusMsg: "Erstelle Windows Firewall-Ausnahme (Ausgehend)..."; Flags: runhidden
@ -81,13 +81,13 @@ Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#MyApp
[UninstallRun] [UninstallRun]
; Stoppe den Windows-Dienst vor der Deinstallation ; Stoppe den Windows-Dienst vor der Deinstallation
Filename: "{app}\nssm.exe"; Parameters: "stop ""{#MyAppServiceName}"""; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "stop ""{#MyAppServiceName}"""; Flags: runhidden; RunOnceId: "StopService"
; Entferne den Windows-Dienst ; Entferne den Windows-Dienst
Filename: "{app}\nssm.exe"; Parameters: "remove ""{#MyAppServiceName}"" confirm"; StatusMsg: "Entferne Medi-WOL Dienst..."; Flags: runhidden Filename: "{app}\nssm.exe"; Parameters: "remove ""{#MyAppServiceName}"" confirm"; StatusMsg: "Entferne Medi-WOL Dienst..."; Flags: runhidden; RunOnceId: "RemoveService"
; Entferne Windows Firewall-Regeln ; Entferne Windows Firewall-Regeln
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Inbound"""; Flags: runhidden Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Inbound"""; Flags: runhidden; RunOnceId: "RemoveFirewallInbound"
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Outbound"""; Flags: runhidden Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Outbound"""; Flags: runhidden; RunOnceId: "RemoveFirewallOutbound"
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Port {code:GetPort}"""; Flags: runhidden Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#MyAppServiceName} - Port {code:GetPort}"""; Flags: runhidden; RunOnceId: "RemoveFirewallPort"
[Code] [Code]
var var
@ -148,9 +148,6 @@ end;
[CustomMessages] [CustomMessages]
german.CreateDesktopIcon=Desktop-Verknüpfung erstellen
german.CreateQuickLaunchIcon=Quick Launch-Verknüpfung erstellen
german.AdditionalIcons=Zusätzliche Verknüpfungen:
german.ServiceOptions=Service-Optionen: german.ServiceOptions=Service-Optionen:
german.StartServiceAfterInstall=Service nach der Installation starten german.StartServiceAfterInstall=Service nach der Installation starten
german.UninstallProgram=Medi-WOL entfernen german.UninstallProgram=Medi-WOL entfernen

BIN
installer/medi-wol.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -21,22 +21,94 @@ 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 { if err != nil {
return nil, err return nil, err
} }
// Migration: fehlende Spalten hinzufügen (für bestehende DBs)
// autostart_cron
_, err = db.Exec("ALTER TABLE pcs ADD COLUMN autostart_cron TEXT DEFAULT '30 7 * * Mon-Fri'")
if err != nil {
// Spalte existiert bereits, ignorieren
}
// autostart_enabled
_, err = db.Exec("ALTER TABLE pcs ADD COLUMN autostart_enabled BOOLEAN DEFAULT 0")
if err != nil {
// Spalte existiert bereits, ignorieren
}
// Standardwerte für evtl. NULL-Einträge setzen
_, _ = db.Exec("UPDATE pcs SET autostart_cron = '30 7 * * Mon-Fri' WHERE autostart_cron IS NULL")
_, _ = db.Exec("UPDATE pcs SET autostart_enabled = 0 WHERE autostart_enabled IS NULL")
// 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 {
return nil, err
}
// Settings-Tabelle erstellen
createSettingsTableSQL := `
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createSettingsTableSQL)
if err != nil {
return nil, err
}
// Standard-Einstellungen setzen
_, err = db.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('vacation_mode', 'false')")
if err != nil {
return nil, err
}
// Falls keine PCs vorhanden sind, Dummy-PC anlegen
var pcCount int
err = db.QueryRow("SELECT COUNT(*) FROM pcs").Scan(&pcCount)
if err != nil {
log.Printf("Warnung: Konnte Anzahl der PCs nicht ermitteln: %v", err)
} else if pcCount == 0 {
_, insErr := db.Exec(
"INSERT INTO pcs (name, mac, ip, autostart_cron, autostart_enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
"Test", "00:11:22:33:AA:FF", "192.168.0.1", "30 7 * * Mon-Fri", 0,
)
if insErr != nil {
log.Printf("Warnung: Konnte Dummy-PC nicht anlegen: %v", insErr)
} else {
log.Println("Dummy-PC 'Test' in InitDB automatisch angelegt (leere Datenbank)")
}
}
// Füge IP-Spalte hinzu, falls sie nicht existiert // Füge IP-Spalte hinzu, falls sie nicht existiert
_, err = db.Exec("ALTER TABLE pcs ADD COLUMN ip TEXT DEFAULT ''") _, err = db.Exec("ALTER TABLE pcs ADD COLUMN ip TEXT DEFAULT ''")
if err != nil { if err != nil {
@ -49,7 +121,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 +130,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 +141,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 +157,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 +200,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 +210,142 @@ 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
}
// GetSetting holt eine Einstellung aus der Datenbank
func (db *DB) GetSetting(key string) (string, error) {
var value string
err := db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
if err != nil {
return "", err
}
return value, nil
}
// SetSetting setzt eine Einstellung in der Datenbank
func (db *DB) SetSetting(key, value string) error {
now := time.Now()
_, err := db.Exec(
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)",
key, value, now,
)
return err
}
// IsVacationModeEnabled prüft, ob der Urlaubsmodus aktiviert ist
func (db *DB) IsVacationModeEnabled() (bool, error) {
value, err := db.GetSetting("vacation_mode")
if err != nil {
return false, err
}
return value == "true", 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,126 @@ 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,
})
}
// GetVacationMode gibt den aktuellen Urlaubsmodus-Status zurück
func (h *PCHandler) GetVacationMode(c *gin.Context) {
enabled, err := h.db.IsVacationModeEnabled()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Fehler beim Laden des Urlaubsmodus: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"vacation_mode": enabled,
})
}
// SetVacationMode setzt den Urlaubsmodus
func (h *PCHandler) SetVacationMode(c *gin.Context) {
var req struct {
VacationMode bool `json:"vacation_mode" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Ungültige Anfrage: " + err.Error(),
})
return
}
value := "false"
if req.VacationMode {
value = "true"
}
err := h.db.SetSetting("vacation_mode", value)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Fehler beim Speichern des Urlaubsmodus: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Urlaubsmodus erfolgreich aktualisiert",
"vacation_mode": req.VacationMode,
})
}

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,185 @@
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() {
// Prüfe zuerst, ob der Urlaubsmodus aktiviert ist
vacationMode, err := s.db.IsVacationModeEnabled()
if err != nil {
log.Printf("Fehler beim Prüfen des Urlaubsmodus: %v", err)
return
}
if vacationMode {
log.Println("Urlaubsmodus aktiviert - Autostart deaktiviert")
return
}
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

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

@ -12,6 +12,7 @@ class PCManager {
init() { init() {
this.loadPCs(); this.loadPCs();
this.loadVacationMode();
this.setupEventListeners(); this.setupEventListeners();
this.startAutoStatus(); this.startAutoStatus();
} }
@ -47,6 +48,11 @@ class PCManager {
document.getElementById('refreshStatusBtn').addEventListener('click', () => { document.getElementById('refreshStatusBtn').addEventListener('click', () => {
this.refreshStatus(); this.refreshStatus();
}); });
// Urlaubsmodus Checkbox
document.getElementById('vacationMode').addEventListener('change', (e) => {
this.setVacationMode(e.target.checked);
});
} }
async loadPCs() { async loadPCs() {
@ -145,7 +151,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 +163,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 +189,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 +212,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 +235,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 +254,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 +268,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 +404,122 @@ 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;
}
// Urlaubsmodus laden
async loadVacationMode() {
try {
const response = await fetch('/api/settings/vacation-mode');
const data = await response.json();
if (data.success) {
const vacationModeCheckbox = document.getElementById('vacationMode');
if (vacationModeCheckbox) {
vacationModeCheckbox.checked = data.vacation_mode;
}
}
} catch (error) {
console.error('Fehler beim Laden des Urlaubsmodus:', error);
}
}
// Urlaubsmodus setzen
async setVacationMode(enabled) {
try {
const response = await fetch('/api/settings/vacation-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
vacation_mode: enabled
})
});
const data = await response.json();
if (data.success) {
const status = enabled ? 'aktiviert' : 'deaktiviert';
this.showNotification('Erfolg', `Urlaubsmodus ${status}`, 'success');
} else {
this.showNotification('Fehler', data.message, 'danger');
// Checkbox zurücksetzen bei Fehler
const vacationModeCheckbox = document.getElementById('vacationMode');
if (vacationModeCheckbox) {
vacationModeCheckbox.checked = !enabled;
}
}
} catch (error) {
console.error('Fehler beim Setzen des Urlaubsmodus:', error);
this.showNotification('Fehler', 'Fehler beim Setzen des Urlaubsmodus', 'danger');
// Checkbox zurücksetzen bei Fehler
const vacationModeCheckbox = document.getElementById('vacationMode');
if (vacationModeCheckbox) {
vacationModeCheckbox.checked = !enabled;
}
}
}
} }
// 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,40 @@
<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>
<!-- Urlaubsmodus-Checkbox -->
<div class="row mb-3">
<div class="col-12 text-center">
<div class="form-check d-inline-block">
<input class="form-check-input" type="checkbox" id="vacationMode" style="transform: scale(1.2);">
<label class="form-check-label ms-2" for="vacationMode">
<strong>Urlaubsmodus</strong>
</label>
<i class="fas fa-info-circle ms-2 text-muted"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Der Urlaubsmodus deaktiviert den geplanten Autostart für alle Geräte"></i>
</div>
</div> </div>
</div> </div>
@ -52,6 +91,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 +217,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>