3 Commits

18 changed files with 792 additions and 11 deletions

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

@ -54,12 +54,12 @@ func main() {
// 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,6 +67,14 @@ 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)
// Log-Routen
r.GET("/api/logs", pcHandler.GetAllLogs)
r.GET("/api/logs/pc/:id", pcHandler.GetLogsByPCID)
r.GET("/api/logs/pc/:id/recent", pcHandler.GetRecentLogsByPCID)
// Statische Dateien bereitstellen (nach den spezifischen Routen)
r.Static("/static", "./web/static")
// Server starten // Server starten
serverAddr := fmt.Sprintf(":%d", port) serverAddr := fmt.Sprintf(":%d", port)
log.Printf("Medi-WOL startet auf Port %d...", port) log.Printf("Medi-WOL startet auf Port %d...", port)

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,8 +21,8 @@ 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,
@ -32,7 +32,24 @@ func InitDB() (*DB, error) {
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
} }
@ -134,3 +151,92 @@ func (db *DB) GetPCByID(id int) (*models.PC, error) {
return &pc, nil return &pc, 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()
@ -194,6 +202,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,
@ -229,3 +244,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"`
}

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>
@ -174,6 +174,9 @@ class PCManager {
</td> </td>
</tr> </tr>
`).join(''); `).join('');
// Tooltips für alle PC-Zeilen laden
this.loadTooltipsForAllPCs();
} }
async addPC() { async addPC() {
@ -368,6 +371,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,127 @@ 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;
}
/* 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>

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>