From 504ca23442c9b4956cf1278bb9aec54628ea0b11 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 21 Aug 2025 14:37:07 +0200 Subject: [PATCH] Add IP field, ping service, status endpoint and UI; update README --- README.md | 29 +++++++++++- cmd/server/main.go | 3 +- internal/database/database.go | 29 ++++++++---- internal/handlers/pc_handler.go | 45 +++++++++++++++--- internal/models/pc.go | 14 +++++- internal/ping/ping.go | 81 +++++++++++++++++++++++++++++++++ web/static/script.js | 54 +++++++++++++++++++--- web/templates/index.html | 21 ++++++++- 8 files changed, 248 insertions(+), 28 deletions(-) create mode 100644 internal/ping/ping.go diff --git a/README.md b/README.md index c136977..c76b156 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Ein moderner Wake-on-LAN Manager, entwickelt mit Go und einer schönen Web-Oberf - **PC-Verwaltung**: Hinzufügen, Anzeigen, Bearbeiten und Löschen von PC-Einträgen - **Wake-on-LAN**: Ein-Klick-Aufwecken von Computern über MAC-Adressen +- **IP-Adressverwaltung**: Pro Gerät wird eine IP-Adresse gespeichert +- **Online-Status (Ping)**: Geräte können per Ping geprüft und im UI als Online/Offline angezeigt werden - **Moderne Web-Oberfläche**: Responsive Design mit Bootstrap und FontAwesome - **SQLite-Datenbank**: Einfache lokale Datenspeicherung - **Cross-Platform**: Läuft auf Windows und Linux @@ -91,17 +93,23 @@ Falls weder Kommandozeilenparameter noch Umgebungsvariable gesetzt sind, wird Po ### PC hinzufügen 1. Geben Sie den Namen des PCs ein 2. Geben Sie die MAC-Adresse im Format `XX:XX:XX:XX:XX:XX` ein -3. Klicken Sie auf "PC hinzufügen" +3. Geben Sie die IP-Adresse des PCs ein (z. B. `192.168.0.10`) +4. Klicken Sie auf "PC hinzufügen" ### PC bearbeiten 1. Klicken Sie auf den "Bearbeiten"-Button neben dem gewünschten PC -2. Ändern Sie Name und/oder MAC-Adresse +2. Ändern Sie Name, MAC-Adresse und/oder IP-Adresse 3. Klicken Sie auf "Speichern" ### PC aufwecken - Klicken Sie auf den "Aufwecken"-Button neben dem gewünschten PC - Das System sendet automatisch ein Wake-on-LAN Paket +### Online-Status prüfen (Ping) +- Klicken Sie auf den Button "Status aktualisieren" in der Tabelle +- Die Online/Offline-Badges werden pro Gerät aktualisiert +- Optional kann ein automatisches Intervall ergänzt werden (siehe Entwicklung) + ### PC löschen - Klicken Sie auf den "Löschen"-Button neben dem gewünschten PC - Bestätigen Sie die Löschung @@ -261,11 +269,22 @@ medi-wol/ - `PUT /api/pcs/:id` - PC aktualisieren - `DELETE /api/pcs/:id` - PC löschen - `POST /api/pcs/:id/wake` - PC aufwecken +- `GET /api/pcs/status` - Online-Status aller PCs abrufen (Ping) ## Datenbank Die Anwendung verwendet SQLite als lokale Datenbank. Die Datenbankdatei `medi-wol.db` wird automatisch im Projektverzeichnis erstellt. +### Tabellenstruktur `pcs` +| Spalte | Typ | Hinweis | +|-------------|----------|------------------------| +| id | INTEGER | Primärschlüssel | +| name | TEXT | Pflichtfeld | +| mac | TEXT | Pflichtfeld, eindeutig | +| ip | TEXT | Pflichtfeld | +| created_at | DATETIME | Automatisch | +| updated_at | DATETIME | Automatisch | + ## Wake-on-LAN Das System sendet Magic Packets an die gespeicherten MAC-Adressen. Stellen Sie sicher, dass: @@ -285,6 +304,12 @@ go run cmd/server/main.go go run cmd/server/main.go -port 9090 ``` +#### Hinweise zum Ping +- Der Ping wird aktuell über den Button "Status aktualisieren" ausgelöst (`GET /api/pcs/status`). +- Unter Windows wird der Systembefehl `ping -n 1 -w 1000 ` verwendet, unter Linux/macOS `ping -c 1 -W 1 `. +- Falls ICMP blockiert ist, wird als Fallback eine kurze TCP-Portprobe versucht (80, sonst 22). +- Optional kann ein automatisches Intervall im Frontend ergänzt werden (z. B. alle 30s). + ### Build für Produktion ```bash # Windows diff --git a/cmd/server/main.go b/cmd/server/main.go index a550b0f..b00698f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -65,12 +65,13 @@ func main() { r.PUT("/api/pcs/:id", pcHandler.UpdatePC) r.DELETE("/api/pcs/:id", pcHandler.DeletePC) r.POST("/api/pcs/:id/wake", pcHandler.WakePC) + r.GET("/api/pcs/status", pcHandler.GetPCStatus) // Server starten serverAddr := fmt.Sprintf(":%d", port) log.Printf("Server startet auf Port %d...", port) log.Printf("Web-Oberfläche verfügbar unter: http://localhost%s", serverAddr) - + if err := r.Run(serverAddr); err != nil { log.Fatal("Fehler beim Starten des Servers:", err) } diff --git a/internal/database/database.go b/internal/database/database.go index 4c2d72b..c875dbc 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -27,6 +27,7 @@ func InitDB() (*DB, error) { id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, mac TEXT NOT NULL UNIQUE, + ip TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );` @@ -36,13 +37,19 @@ func InitDB() (*DB, error) { return nil, err } + // Füge IP-Spalte hinzu, falls sie nicht existiert + _, err = db.Exec("ALTER TABLE pcs ADD COLUMN ip TEXT DEFAULT ''") + if err != nil { + // Spalte existiert bereits, ignorieren + } + log.Println("Datenbank erfolgreich initialisiert") return &DB{db}, nil } // GetAllPCs holt alle PCs aus der Datenbank func (db *DB) GetAllPCs() ([]models.PC, error) { - rows, err := db.Query("SELECT id, name, mac, created_at, updated_at FROM pcs ORDER BY name") + rows, err := db.Query("SELECT id, name, mac, ip, created_at, updated_at FROM pcs ORDER BY name") if err != nil { return nil, err } @@ -51,7 +58,7 @@ func (db *DB) GetAllPCs() ([]models.PC, error) { var pcs []models.PC for rows.Next() { var pc models.PC - err := rows.Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.CreatedAt, &pc.UpdatedAt) + err := rows.Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.CreatedAt, &pc.UpdatedAt) if err != nil { return nil, err } @@ -62,11 +69,11 @@ func (db *DB) GetAllPCs() ([]models.PC, error) { } // CreatePC erstellt einen neuen PC-Eintrag -func (db *DB) CreatePC(name, mac string) (*models.PC, error) { +func (db *DB) CreatePC(name, mac, ip string) (*models.PC, error) { now := time.Now() result, err := db.Exec( - "INSERT INTO pcs (name, mac, created_at, updated_at) VALUES (?, ?, ?, ?)", - name, mac, now, now, + "INSERT INTO pcs (name, mac, ip, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", + name, mac, ip, now, now, ) if err != nil { return nil, err @@ -81,17 +88,18 @@ func (db *DB) CreatePC(name, mac string) (*models.PC, error) { ID: int(id), Name: name, MAC: mac, + IP: ip, CreatedAt: now, UpdatedAt: now, }, nil } // UpdatePC aktualisiert einen bestehenden PC-Eintrag -func (db *DB) UpdatePC(id int, name, mac string) (*models.PC, error) { +func (db *DB) UpdatePC(id int, name, mac, ip string) (*models.PC, error) { now := time.Now() _, err := db.Exec( - "UPDATE pcs SET name = ?, mac = ?, updated_at = ? WHERE id = ?", - name, mac, now, id, + "UPDATE pcs SET name = ?, mac = ?, ip = ?, updated_at = ? WHERE id = ?", + name, mac, ip, now, id, ) if err != nil { return nil, err @@ -101,6 +109,7 @@ func (db *DB) UpdatePC(id int, name, mac string) (*models.PC, error) { ID: id, Name: name, MAC: mac, + IP: ip, UpdatedAt: now, }, nil } @@ -115,9 +124,9 @@ func (db *DB) DeletePC(id int) error { func (db *DB) GetPCByID(id int) (*models.PC, error) { var pc models.PC err := db.QueryRow( - "SELECT id, name, mac, created_at, updated_at FROM pcs WHERE id = ?", + "SELECT id, name, mac, ip, created_at, updated_at FROM pcs WHERE id = ?", id, - ).Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.CreatedAt, &pc.UpdatedAt) + ).Scan(&pc.ID, &pc.Name, &pc.MAC, &pc.IP, &pc.CreatedAt, &pc.UpdatedAt) if err != nil { return nil, err diff --git a/internal/handlers/pc_handler.go b/internal/handlers/pc_handler.go index 837d491..6ddb8d8 100644 --- a/internal/handlers/pc_handler.go +++ b/internal/handlers/pc_handler.go @@ -3,6 +3,7 @@ package handlers import ( "medi-wol/internal/database" "medi-wol/internal/models" + "medi-wol/internal/ping" "medi-wol/internal/wol" "net/http" "strconv" @@ -12,15 +13,17 @@ import ( // PCHandler verwaltet die HTTP-Anfragen für PC-Operationen type PCHandler struct { - db *database.DB - wolService *wol.Service + db *database.DB + wolService *wol.Service + pingService *ping.PingService } // NewPCHandler erstellt einen neuen PC-Handler func NewPCHandler(db *database.DB, wolService *wol.Service) *PCHandler { return &PCHandler{ - db: db, - wolService: wolService, + db: db, + wolService: wolService, + pingService: ping.NewPingService(), } } @@ -69,7 +72,7 @@ func (h *PCHandler) CreatePC(c *gin.Context) { } // PC erstellen - pc, err := h.db.CreatePC(req.Name, req.MAC) + pc, err := h.db.CreatePC(req.Name, req.MAC, req.IP) if err != nil { c.JSON(http.StatusInternalServerError, models.PCResponse{ Success: false, @@ -116,7 +119,7 @@ func (h *PCHandler) UpdatePC(c *gin.Context) { } // PC aktualisieren - pc, err := h.db.UpdatePC(id, req.Name, req.MAC) + pc, err := h.db.UpdatePC(id, req.Name, req.MAC, req.IP) if err != nil { c.JSON(http.StatusInternalServerError, models.PCResponse{ Success: false, @@ -196,3 +199,33 @@ func (h *PCHandler) WakePC(c *gin.Context) { Message: "Wake-on-LAN Paket erfolgreich gesendet an " + pc.Name, }) } + +// GetPCStatus gibt den Online-Status aller PCs zurück +func (h *PCHandler) GetPCStatus(c *gin.Context) { + pcs, err := h.db.GetAllPCs() + if err != nil { + c.JSON(http.StatusInternalServerError, models.PCResponse{ + Success: false, + Message: "Fehler beim Laden der PCs: " + err.Error(), + }) + return + } + + // Online-Status für alle PCs überprüfen + var statusList []models.PCStatus + for _, pc := range pcs { + online := h.pingService.IsOnline(pc.IP) + statusList = append(statusList, models.PCStatus{ + ID: pc.ID, + Name: pc.Name, + MAC: pc.MAC, + IP: pc.IP, + Online: online, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "status": statusList, + }) +} diff --git a/internal/models/pc.go b/internal/models/pc.go index 539ca68..28d6c5b 100644 --- a/internal/models/pc.go +++ b/internal/models/pc.go @@ -9,7 +9,8 @@ type PC struct { ID int `json:"id" db:"id"` Name string `json:"name" db:"name"` MAC string `json:"mac" db:"mac"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + IP string `json:"ip" db:"ip"` + CreatedAt time.Time `json:"created_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } @@ -17,12 +18,14 @@ type PC struct { type CreatePCRequest struct { Name string `json:"name" binding:"required"` MAC string `json:"mac" binding:"required"` + IP string `json:"ip" binding:"required"` } // UpdatePCRequest repräsentiert die Anfrage zum Aktualisieren eines PCs type UpdatePCRequest struct { Name string `json:"name" binding:"required"` MAC string `json:"mac" binding:"required"` + IP string `json:"ip" binding:"required"` } // PCResponse repräsentiert die Antwort für PC-Operationen @@ -32,3 +35,12 @@ type PCResponse struct { PC *PC `json:"pc,omitempty"` PCs []PC `json:"pcs,omitempty"` } + +// PCStatus repräsentiert den Online-Status eines PCs +type PCStatus struct { + ID int `json:"id"` + Name string `json:"name"` + MAC string `json:"mac"` + IP string `json:"ip"` + Online bool `json:"online"` +} diff --git a/internal/ping/ping.go b/internal/ping/ping.go new file mode 100644 index 0000000..05aa1dc --- /dev/null +++ b/internal/ping/ping.go @@ -0,0 +1,81 @@ +package ping + +import ( + "fmt" + "net" + "os/exec" + "runtime" + "time" +) + +// PingService bietet Funktionen zum Überprüfen des Online-Status von PCs +type PingService struct{} + +// NewPingService erstellt eine neue Instanz des Ping-Services +func NewPingService() *PingService { + return &PingService{} +} + +// IsOnline überprüft, ob ein PC online ist +func (ps *PingService) IsOnline(ip string) bool { + if ip == "" { + return false + } + + // Verwende system-spezifischen Ping-Befehl + switch runtime.GOOS { + case "windows": + return ps.pingWindows(ip) + case "linux", "darwin": + return ps.pingUnix(ip) + default: + return ps.pingGeneric(ip) + } +} + +// pingWindows führt einen Ping unter Windows aus +func (ps *PingService) pingWindows(ip string) bool { + cmd := exec.Command("ping", "-n", "1", "-w", "1000", ip) + err := cmd.Run() + return err == nil +} + +// pingUnix führt einen Ping unter Unix-Systemen aus +func (ps *PingService) pingUnix(ip string) bool { + cmd := exec.Command("ping", "-c", "1", "-W", "1", ip) + err := cmd.Run() + return err == nil +} + +// pingGeneric ist eine plattformunabhängige Alternative +func (ps *PingService) pingGeneric(ip string) bool { + // Versuche TCP-Verbindung auf Port 80 (HTTP) + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:80", ip), 2*time.Second) + if err != nil { + // Versuche TCP-Verbindung auf Port 22 (SSH) + conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:22", ip), 2*time.Second) + if err != nil { + return false + } + } + defer conn.Close() + return true +} + +// CheckAllPCs überprüft den Online-Status aller PCs +func (ps *PingService) CheckAllPCs(pcs []interface{}) map[int]bool { + results := make(map[int]bool) + + for _, pc := range pcs { + // Type assertion für PC-Interface + if pcMap, ok := pc.(map[string]interface{}); ok { + if id, ok := pcMap["id"].(float64); ok { + if ip, ok := pcMap["ip"].(string); ok { + results[int(id)] = ps.IsOnline(ip) + } + } + } + } + + return results +} diff --git a/web/static/script.js b/web/static/script.js index 4bfe9a4..bc41684 100644 --- a/web/static/script.js +++ b/web/static/script.js @@ -39,6 +39,11 @@ class PCManager { document.getElementById('clearSearchBtn').addEventListener('click', () => { this.clearSearch(); }); + + // Status aktualisieren Button + document.getElementById('refreshStatusBtn').addEventListener('click', () => { + this.refreshStatus(); + }); } async loadPCs() { @@ -129,6 +134,13 @@ class PCManager { ${this.escapeHtml(pc.name)} ${this.escapeHtml(pc.mac)} + ${this.escapeHtml(pc.ip || 'N/A')} + + + + ${pc.online ? 'Online' : 'Offline'} + + ${new Date(pc.created_at).toLocaleDateString('de-DE')}
@@ -136,7 +148,7 @@ class PCManager { title="PC aufwecken"> Aufwecken - @@ -153,8 +165,9 @@ class PCManager { async addPC() { const name = document.getElementById('pcName').value.trim(); const mac = document.getElementById('macAddress').value.trim(); + const ip = document.getElementById('ipAddress').value.trim(); - if (!name || !mac) { + if (!name || !mac || !ip) { this.showNotification('Warnung', 'Bitte füllen Sie alle Felder aus', 'warning'); return; } @@ -165,7 +178,7 @@ class PCManager { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ name, mac }) + body: JSON.stringify({ name, mac, ip }) }); const data = await response.json(); @@ -182,11 +195,12 @@ class PCManager { } } - editPC(id, name, mac) { + editPC(id, name, mac, ip) { // Modal mit PC-Daten füllen document.getElementById('editPCId').value = id; document.getElementById('editPCName').value = name; document.getElementById('editMACAddress').value = mac; + document.getElementById('editIPAddress').value = ip; // Modal öffnen const editModal = new bootstrap.Modal(document.getElementById('editPCModal')); @@ -197,8 +211,9 @@ class PCManager { const id = document.getElementById('editPCId').value; const name = document.getElementById('editPCName').value.trim(); const mac = document.getElementById('editMACAddress').value.trim(); + const ip = document.getElementById('editIPAddress').value.trim(); - if (!name || !mac) { + if (!name || !mac || !ip) { this.showNotification('Warnung', 'Bitte füllen Sie alle Felder aus', 'warning'); return; } @@ -209,7 +224,7 @@ class PCManager { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ name, mac }) + body: JSON.stringify({ name, mac, ip }) }); const data = await response.json(); @@ -288,6 +303,33 @@ class PCManager { bsToast.show(); } + async refreshStatus() { + try { + const response = await fetch('/api/pcs/status'); + const data = await response.json(); + + if (data.success) { + // Status für jeden PC aktualisieren + data.status.forEach(pcStatus => { + const statusElement = document.getElementById(`status-${pcStatus.id}`); + if (statusElement) { + statusElement.className = `badge ${pcStatus.online ? 'bg-success' : 'bg-danger'}`; + statusElement.innerHTML = ` + + ${pcStatus.online ? 'Online' : 'Offline'} + `; + } + }); + + this.showNotification('Info', 'Online-Status aktualisiert', 'info'); + } else { + this.showNotification('Fehler', data.message, 'danger'); + } + } catch (error) { + this.showNotification('Fehler', 'Fehler beim Aktualisieren des Online-Status', 'danger'); + } + } + escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; diff --git a/web/templates/index.html b/web/templates/index.html index 4fc2ef8..b762bff 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -31,19 +31,26 @@
-
+
-
+
+
+
+ + +
+
+ 0 PCs gefunden
@@ -91,6 +101,8 @@ Name MAC-Adresse + IP-Adresse + Status Erstellt am Aktionen @@ -136,6 +148,11 @@
+
+ + +