From b6888ca5da7ee8bd1cfcdef77e49cd13a92ad3cf Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 22 Aug 2025 07:16:14 +0200 Subject: [PATCH] =?UTF-8?q?Implementiere=20umfassendes=20Logging-System=20?= =?UTF-8?q?f=C3=BCr=20WOL-Ereignisse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- LOGGING_README.md | 110 ++++++++++++++++++++++++++++ cmd/server/main.go | 12 +++- internal/database/database.go | 112 ++++++++++++++++++++++++++++- internal/handlers/pc_handler.go | 86 ++++++++++++++++++++++ internal/models/log.go | 31 ++++++++ web/static/logs.js | 80 +++++++++++++++++++++ web/static/script.js | 67 ++++++++++++++++- web/static/style.css | 124 ++++++++++++++++++++++++++++++++ web/templates/index.html | 18 +++++ web/templates/logs.html | 89 +++++++++++++++++++++++ 10 files changed, 723 insertions(+), 6 deletions(-) create mode 100644 LOGGING_README.md create mode 100644 internal/models/log.go create mode 100644 web/static/logs.js create mode 100644 web/templates/logs.html 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 = ` + ${formattedTime} + ${escapeHtml(log.pc_name)} + ${escapeHtml(log.mac)} + ${escapeHtml(triggerText)} + `; + + tableBody.appendChild(row); + }); +} + +// Fehler anzeigen +function showError(message) { + const loading = document.getElementById('loading'); + loading.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.name)} ${this.escapeHtml(pc.mac)} ${this.escapeHtml(pc.ip || 'N/A')} @@ -174,6 +174,9 @@ class PCManager { `).join(''); + + // Tooltips für alle PC-Zeilen laden + this.loadTooltipsForAllPCs(); } async addPC() { @@ -368,6 +371,68 @@ class PCManager { 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 '
Keine WOL-Ereignisse
'; + } + + let content = '
Letzte WOL-Ereignisse:
'; + + 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})
`; + }); + + content += '
'; + return content; + } } // PC Manager initialisieren, wenn die Seite geladen ist diff --git a/web/static/style.css b/web/static/style.css index 95a885a..b25324a 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -172,3 +172,127 @@ body { ::-webkit-scrollbar-thumb:hover { 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; +} diff --git a/web/templates/index.html b/web/templates/index.html index 780aceb..c620c73 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -21,6 +21,24 @@ medisoftware Logo

{{.title}}

+ + + diff --git a/web/templates/logs.html b/web/templates/logs.html new file mode 100644 index 0000000..aea2b06 --- /dev/null +++ b/web/templates/logs.html @@ -0,0 +1,89 @@ + + + + + + Medi-WOL - Logs + + + + + + + + + + +
+
+
+
+ medisoftware Logo +

Medi-WOL

+
+ + + +
+
+ +
+
+
+
+
+ Wake-on-LAN Logs +
+
+
+
+
+ + + + + + + + + + + + +
ZeitstempelPC-NameMAC-AdresseAuslöser
+
+ +
+
+

Lade Logs...

+
+ + +
+
+
+
+
+
+ + + + +