diff --git a/LOGGING_README.md b/LOGGING_README.md new file mode 100644 index 0000000..99ea4c1 --- /dev/null +++ b/LOGGING_README.md @@ -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 diff --git a/cmd/server/main.go b/cmd/server/main.go index f8494e7..c507f8f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -54,12 +54,12 @@ func main() { // Gin Router konfigurieren r := gin.Default() - // Statische Dateien bereitstellen - r.Static("/static", "./web/static") + // HTML-Templates laden r.LoadHTMLGlob("web/templates/*") // Routen definieren r.GET("/", pcHandler.Index) + r.GET("/logs", pcHandler.Logs) r.GET("/api/pcs", pcHandler.GetAllPCs) r.POST("/api/pcs", pcHandler.CreatePC) r.PUT("/api/pcs/:id", pcHandler.UpdatePC) @@ -67,6 +67,14 @@ func main() { r.POST("/api/pcs/:id/wake", pcHandler.WakePC) 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 serverAddr := fmt.Sprintf(":%d", port) log.Printf("Medi-WOL startet auf Port %d...", port) diff --git a/internal/database/database.go b/internal/database/database.go index c875dbc..f322ea3 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -21,8 +21,8 @@ func InitDB() (*DB, error) { return nil, err } - // Tabelle erstellen - createTableSQL := ` + // PC-Tabelle erstellen + createPCTableSQL := ` CREATE TABLE IF NOT EXISTS pcs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, @@ -32,7 +32,24 @@ func InitDB() (*DB, error) { 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 { return nil, err } @@ -134,3 +151,92 @@ func (db *DB) GetPCByID(id int) (*models.PC, error) { 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 +} diff --git a/internal/handlers/pc_handler.go b/internal/handlers/pc_handler.go index 6ddb8d8..54276f8 100644 --- a/internal/handlers/pc_handler.go +++ b/internal/handlers/pc_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "log" "medi-wol/internal/database" "medi-wol/internal/models" "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 func (h *PCHandler) GetAllPCs(c *gin.Context) { pcs, err := h.db.GetAllPCs() @@ -194,6 +202,13 @@ func (h *PCHandler) WakePC(c *gin.Context) { 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{ Success: true, Message: "Wake-on-LAN Paket erfolgreich gesendet an " + pc.Name, @@ -229,3 +244,74 @@ func (h *PCHandler) GetPCStatus(c *gin.Context) { "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, + }) +} diff --git a/internal/models/log.go b/internal/models/log.go new file mode 100644 index 0000000..b23e382 --- /dev/null +++ b/internal/models/log.go @@ -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"` +} diff --git a/web/static/logs.js b/web/static/logs.js new file mode 100644 index 0000000..f32e816 --- /dev/null +++ b/web/static/logs.js @@ -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 = ` +
${escapeHtml(message)}
`; +} + +// HTML-Escaping für Sicherheit +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/web/static/script.js b/web/static/script.js index 24b45c7..3aa6306 100644 --- a/web/static/script.js +++ b/web/static/script.js @@ -145,7 +145,7 @@ class PCManager { pcList.style.display = 'block'; tableBody.innerHTML = this.filteredPCs.map(pc => ` -${this.escapeHtml(pc.mac)}${this.escapeHtml(pc.ip || 'N/A')}
+ | Zeitstempel | +PC-Name | +MAC-Adresse | +Auslöser | +
|---|
Lade Logs...
+