Add IP field, ping service, status endpoint and UI; update README

This commit is contained in:
2025-08-21 14:37:07 +02:00
parent 4a56cbd310
commit 504ca23442
8 changed files with 248 additions and 28 deletions

View File

@ -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 <IP>` verwendet, unter Linux/macOS `ping -c 1 -W 1 <IP>`.
- 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

View File

@ -65,6 +65,7 @@ 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)

View File

@ -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

View File

@ -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"
@ -14,6 +15,7 @@ import (
type PCHandler struct {
db *database.DB
wolService *wol.Service
pingService *ping.PingService
}
// NewPCHandler erstellt einen neuen PC-Handler
@ -21,6 +23,7 @@ func NewPCHandler(db *database.DB, wolService *wol.Service) *PCHandler {
return &PCHandler{
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,
})
}

View File

@ -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"`
}

81
internal/ping/ping.go Normal file
View File

@ -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
}

View File

@ -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 {
<tr>
<td><strong>${this.escapeHtml(pc.name)}</strong></td>
<td><code>${this.escapeHtml(pc.mac)}</code></td>
<td><code>${this.escapeHtml(pc.ip || 'N/A')}</code></td>
<td>
<span class="badge ${pc.online ? 'bg-success' : 'bg-danger'}" id="status-${pc.id}">
<i class="fas fa-${pc.online ? 'wifi' : 'times'}"></i>
${pc.online ? 'Online' : 'Offline'}
</span>
</td>
<td>${new Date(pc.created_at).toLocaleDateString('de-DE')}</td>
<td>
<div class="btn-group" role="group">
@ -136,7 +148,7 @@ class PCManager {
title="PC aufwecken">
<i class="fas fa-power-off"></i> Aufwecken
</button>
<button class="btn btn-warning btn-sm" onclick="pcManager.editPC(${pc.id}, '${this.escapeHtml(pc.name)}', '${this.escapeHtml(pc.mac)}')"
<button class="btn btn-warning btn-sm" onclick="pcManager.editPC(${pc.id}, '${this.escapeHtml(pc.name)}', '${this.escapeHtml(pc.mac)}', '${this.escapeHtml(pc.ip || '')}')"
title="PC bearbeiten">
<i class="fas fa-edit"></i> Bearbeiten
</button>
@ -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 = `
<i class="fas fa-${pcStatus.online ? 'wifi' : 'times'}"></i>
${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;

View File

@ -31,19 +31,26 @@
<div class="card-body">
<form id="addPCForm">
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="pcName" class="form-label">PC-Name</label>
<input type="text" class="form-control" id="pcName" required>
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
<div class="mb-3">
<label for="macAddress" class="form-label">MAC-Adresse</label>
<input type="text" class="form-control" id="macAddress"
placeholder="XX:XX:XX:XX:XX:XX" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="ipAddress" class="form-label">IP-Adresse</label>
<input type="text" class="form-control" id="ipAddress"
placeholder="192.168.1.100" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> PC hinzufügen
@ -81,6 +88,9 @@
</div>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-outline-info me-2" id="refreshStatusBtn" title="Online-Status aktualisieren">
<i class="fas fa-sync-alt"></i> Status aktualisieren
</button>
<span class="badge bg-primary" id="resultCount">0 PCs gefunden</span>
</div>
</div>
@ -91,6 +101,8 @@
<tr>
<th>Name</th>
<th>MAC-Adresse</th>
<th>IP-Adresse</th>
<th>Status</th>
<th>Erstellt am</th>
<th>Aktionen</th>
</tr>
@ -136,6 +148,11 @@
<input type="text" class="form-control" id="editMACAddress"
placeholder="XX:XX:XX:XX:XX:XX" required>
</div>
<div class="mb-3">
<label for="editIPAddress" class="form-label">IP-Adresse</label>
<input type="text" class="form-control" id="editIPAddress"
placeholder="192.168.1.100" required>
</div>
</form>
</div>
<div class="modal-footer">