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
This commit is contained in:
110
LOGGING_README.md
Normal file
110
LOGGING_README.md
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
31
internal/models/log.go
Normal 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"`
|
||||||
|
}
|
||||||
80
web/static/logs.js
Normal file
80
web/static/logs.js
Normal 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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -21,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
89
web/templates/logs.html
Normal 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>
|
||||||
Reference in New Issue
Block a user