Compare commits
3 Commits
feature/UI
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c490d38457 | |||
| 11aec278e8 | |||
| 5aac9b6f26 |
297
CHANGELOG.md
Normal file
297
CHANGELOG.md
Normal file
@ -0,0 +1,297 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2025-12-03 — SNMP Live-Dashboard (Architektur: Service + API)
|
||||
|
||||
### Übersicht
|
||||
|
||||
Implementierung eines **dualen SNMP-Status-Systems**:
|
||||
1. **Server-seitig (Initial Load):** `DashboardController` → `SnmpServerStatusService`
|
||||
2. **Client-seitig (Live-Updates):** Browser-JavaScript → API-Endpunkt (`snmp_status.php`)
|
||||
|
||||
Ergebnis: **Beste User Experience** (sofortige Daten beim Laden) + **Redundanz** (Live-Polling läuft weiter, auch wenn Service fehlt).
|
||||
|
||||
---
|
||||
|
||||
## Komponente 1: Service (`app/Services/Snmp/SnmpServerStatusService.php`) — modernisiert
|
||||
|
||||
### Was wurde geändert?
|
||||
|
||||
Die alte Version ist zu streng gewesen (wirft Exception wenn "Physical Memory" nicht exakt gefunden wird).
|
||||
Neue Version hat **intelligente Fallback-Logik**:
|
||||
|
||||
**RAM-Erkennung (in dieser Reihenfolge):**
|
||||
1. Heuristik: Suche nach "Physical Memory" (exakt)
|
||||
2. Fallback 1: Suche nach "physical" ODER "memory" ODER "ram" (Case-insensitive)
|
||||
3. Fallback 2: Nimm den **kleinsten Storage-Eintrag** (wahrscheinlich RAM)
|
||||
4. Falls alles fehlschlägt: Exception → abgefangen im Controller → 0% angezeigt
|
||||
|
||||
**Disk-Erkennung (in dieser Reihenfolge):**
|
||||
1. Heuristik: Suche nach "C:\\" (Windows, exakt)
|
||||
2. Fallback 1: Suche nach "C:" ODER "root" ODER "/" (Case-insensitive)
|
||||
3. Fallback 2: Nimm den **größten Storage-Eintrag > 1 GB** (wahrscheinlich Hauptlaufwerk)
|
||||
4. Falls alles fehlschlägt: Exception → abgefangen im Controller → 0% angezeigt
|
||||
|
||||
### Featureset
|
||||
|
||||
- ✅ Robuste Fehlerbehandlung (aussagekräftige `RuntimeException` für Debugging)
|
||||
- ✅ Intelligente Fallbacks bei unerwarteten OID-Beschreibungen
|
||||
- ✅ Unterstützung Windows + Linux
|
||||
- ✅ Prüfung auf SNMP-Erweiterung und Konfiguration
|
||||
- ✅ Uptime in lesbares Format konvertieren (z. B. "1 Tage, 10:17:36")
|
||||
- ✅ CPU-Durchschnitt über alle Kerne
|
||||
|
||||
---
|
||||
|
||||
## Komponente 2: Controller (`app/Controllers/DashboardController.php`) — neu strukturiert
|
||||
|
||||
### Was wurde geändert?
|
||||
|
||||
Der Controller hatte nur eine Zeile geändert; jetzt **fehlertolerante Abfrage**:
|
||||
|
||||
```php
|
||||
try {
|
||||
$serverStatus = $this->snmpService->getServerStatus();
|
||||
} catch (\RuntimeException $e) {
|
||||
error_log('SNMP-Fehler beim initialen Laden - ' . $e->getMessage());
|
||||
// Fallback-Werte werden verwendet (siehe oben)
|
||||
}
|
||||
```
|
||||
|
||||
**Effekt:**
|
||||
- Wenn Service fehlschlägt: Dashboard wird trotzdem geladen (mit 0% oder 'n/a')
|
||||
- Fehler wird geloggt (PHP Error-Log)
|
||||
- Live-Polling (API) läuft trotzdem weiter und kann Daten liefern
|
||||
- Bessere User Experience statt "Error 500"
|
||||
|
||||
---
|
||||
|
||||
## Komponente 3: API (`public/api/snmp_status.php`)
|
||||
|
||||
Siehe vorherige CHANGELOG-Einträge. Wichtig:
|
||||
- **Identische Fallback-Logik** wie der Service
|
||||
- **Session-Validierung** (nur Admins)
|
||||
- **Caching** (10 Sekunden)
|
||||
- **Detailliertes Logging** in `public/logs/snmp_api.log`
|
||||
|
||||
---
|
||||
|
||||
## Komponente 4: View & JavaScript (`public/views/dashboard.php`)
|
||||
|
||||
Siehe vorherige CHANGELOG-Einträge.
|
||||
|
||||
---
|
||||
|
||||
## Architektur: Dual-Layer System
|
||||
|
||||
### Layer 1: Initial Load (Server-seitig)
|
||||
|
||||
```
|
||||
Nutzer öffnet Dashboard
|
||||
↓
|
||||
DashboardController::show()
|
||||
↓
|
||||
SnmpServerStatusService::getServerStatus()
|
||||
↓
|
||||
SNMP-Abfrage (mit Fallbacks)
|
||||
↓
|
||||
Daten sofort in View angezeigt
|
||||
↓
|
||||
(Bei Fehler: Fallback-Werte wie 0%, 'n/a')
|
||||
↓
|
||||
Logging in PHP Error-Log
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Daten sind **sofort** sichtbar (gute UX)
|
||||
- ✅ Fallbacks verhindern "Error 500"
|
||||
- ✅ Service wird nur 1x pro Seitenladung aufgerufen (sparsam)
|
||||
|
||||
---
|
||||
|
||||
### Layer 2: Live-Updates (Client-seitig)
|
||||
|
||||
```
|
||||
Browser lädt Dashboard
|
||||
↓
|
||||
JavaScript addEventListener('DOMContentLoaded')
|
||||
↓
|
||||
Sofort erste Abfrage: fetch('api/snmp_status.php')
|
||||
↓
|
||||
Antwort: JSON mit aktuellen Daten
|
||||
↓
|
||||
updateUI() aktualisiert die Karten
|
||||
↓
|
||||
Alle 5 Sekunden wiederholt (setInterval)
|
||||
↓
|
||||
Logging in public/logs/snmp_api.log (Server-seitig)
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ **Live-Updates** ohne Seite zu reload-en
|
||||
- ✅ **Session-Schutz** (nur Admins können Endpunkt aufrufen)
|
||||
- ✅ **Caching** reduziert SNMP-Last (10s TTL)
|
||||
- ✅ **Fallback-Logik** im API unabhängig vom Service
|
||||
- ✅ **Redundanz:** Wenn Service fehlt, läuft API trotzdem
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
### PHP Error-Log (Service-Fehler)
|
||||
|
||||
- **Ort:** Abhängig von PHP-Konfiguration (meist `/var/log/php-errors.log` oder Windows Event-Log)
|
||||
- **Format:** Standard PHP Error-Log
|
||||
- **Inhalt:** SNMP-Fehler beim initialen Laden (z. B. "SNMP-Konfiguration ist unvollständig")
|
||||
- **Trigger:** Nur wenn Service-Abfrage fehlschlägt
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
[03-Dec-2025 12:05:00 UTC] DashboardController: SNMP-Fehler beim initialen Laden - SNMP-Konfiguration ist unvollständig (host fehlt).
|
||||
```
|
||||
|
||||
### SNMP API Log (`public/logs/snmp_api.log`)
|
||||
|
||||
- **Ort:** `public/logs/snmp_api.log` (wird automatisch angelegt)
|
||||
- **Format:** `[YYYY-MM-DD HH:MM:SS] Nachricht`
|
||||
- **Inhalt:**
|
||||
- Cache-Hits/Misses
|
||||
- SNMP-Konfiguration
|
||||
- Alle Storage-Einträge
|
||||
- Erkannte Disk/RAM mit Prozentsätzen
|
||||
- Fallback-Aktionen
|
||||
- Finale Werte
|
||||
- Fehler
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
[2025-12-03 12:05:00] --- SNMP-Abfrage gestartet ---
|
||||
[2025-12-03 12:05:00] SNMP-Host: 127.0.0.1, Community: public_ro, Timeout: 2s
|
||||
[2025-12-03 12:05:00] Uptime OID: 1.3.6.1.2.1.1.3.0, Raw: "Timeticks: (1234567) 14 days, 6:14:27.67"
|
||||
[2025-12-03 12:05:00] Storage[1]: Desc='Physical Memory', Size=16777216, Used=8388608, Units=1024
|
||||
[2025-12-03 12:05:00] Speicher erkannt (Index 1): Physical Memory → 50.00%
|
||||
[2025-12-03 12:05:00] Storage[2]: Desc='C:\\ ', Size=536870912, Used=268435456, Units=512
|
||||
[2025-12-03 12:05:00] Datenträger erkannt (Index 2): C:\\ → 50.00%
|
||||
[2025-12-03 12:05:00] RESULT: CPU=25, Mem=50.00, Disk=50.00
|
||||
[2025-12-03 12:05:00] Cache geschrieben, TTL: 10s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerszenarien & Behavior
|
||||
|
||||
### Szenario 1: SNMP läuft, alles OK
|
||||
```
|
||||
Service: ✅ Daten sofort angezeigt
|
||||
API: ✅ Live-Updates alle 5s
|
||||
Logs: ✅ Beide Logs ganz normal
|
||||
```
|
||||
|
||||
### Szenario 2: SNMP-Erweiterung fehlt
|
||||
```
|
||||
Service: ❌ Exception → abgefangen → 0%, 'n/a' angezeigt
|
||||
API: ❌ Exception → abgefangen → {"error": "snmp_extension_missing"}
|
||||
Logs: ⚠️ Beide Logs zeigen Fehler
|
||||
User-View: "Metriken werden angezeigt (0%), aber nicht aktualisiert"
|
||||
Aktion: Admin sieht Fehler im Log und installiert SNMP
|
||||
```
|
||||
|
||||
### Szenario 3: SNMP antwortet, aber Beschreibungen sind unbekannt
|
||||
```
|
||||
Service: ✅ Fallback-Logik findet RAM/Disk trotzdem
|
||||
API: ✅ Fallback-Logik findet RAM/Disk trotzdem
|
||||
Logs: ℹ️ `Fallback RAM gefunden` / `Fallback Disk gefunden`
|
||||
User-View: ✅ Daten werden angezeigt
|
||||
```
|
||||
|
||||
### Szenario 4: Service fehlt, API läuft
|
||||
```
|
||||
Service: ❌ Exception beim Laden
|
||||
API: ✅ Live-Updates funktionieren normal
|
||||
User-View: "Beim Laden: 0%, nach 5s: aktuelle Werte"
|
||||
Gut genug!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing-Anleitung
|
||||
|
||||
### 1. Initialer Load testen
|
||||
```bash
|
||||
# Browser öffnen, als Admin einloggen
|
||||
# Dashboard öffnen
|
||||
# → Sollten Werte sichtbar sein (entweder echte oder 0%)
|
||||
```
|
||||
|
||||
### 2. Service-Fehler simulieren
|
||||
```php
|
||||
// In DashboardController.php: Service-Aufruf kommentieren
|
||||
// $serverStatus = [... Fallback-Werte ...];
|
||||
// → Dashboard sollte trotzdem laden (mit 0%)
|
||||
```
|
||||
|
||||
### 3. API testen
|
||||
```bash
|
||||
# Browser DevTools → Network → api/snmp_status.php
|
||||
# → Sollte JSON zurückgeben
|
||||
# Bei 401 → Session fehlt (erwartet wenn nicht angemeldet)
|
||||
# Sollte aber funktionieren wenn angemeldet
|
||||
```
|
||||
|
||||
### 4. Logs prüfen
|
||||
```bash
|
||||
# PHP Error-Log
|
||||
error_log() Output ansehen
|
||||
|
||||
# SNMP API Log
|
||||
cat public/logs/snmp_api.log
|
||||
# Sollte Einträge zeigen (mit Timestamps)
|
||||
```
|
||||
|
||||
### 5. Cache prüfen
|
||||
```bash
|
||||
# Temp-Datei prüfen
|
||||
# Windows: %TEMP%\snmp_status_cache.json
|
||||
# Linux: /tmp/snmp_status_cache.json
|
||||
# → Sollte JSON enthalten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Limitations
|
||||
|
||||
1. **Disk/RAM-Heuristik:** Bei sehr ungewöhnlichen Storage-Labels können Fallbacks greifen, die nicht ideal sind
|
||||
- **Lösung:** Log prüfen (`Storage[X]:` Einträge) und ggf. Heuristiken anpassen
|
||||
|
||||
2. **Cache-Speicher:** Erfordert Schreibzugriff auf `sys_get_temp_dir()`
|
||||
- **Lösung:** Falls nicht verfügbar → Cache-Code entfernen oder APCu/Redis nutzen
|
||||
|
||||
3. **OS-Feld:** Hardcoded auf "Windows Server" (TODO: Dynamisch per OID 1.3.6.1.2.1.1.1.0)
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
- **Service-Abfrage:** 1x pro Seitenladung (~100-500ms je nach SNMP-Timeout)
|
||||
- **API-Abfrage:** Alle 5s, aber gecacht für 10s → effektiv alle 10s eine echte SNMP-Abfrage
|
||||
- **JavaScript Polling:** 5s Intervall (Browser-seitig, keine Last auf Server)
|
||||
- **Gesamt:** Sehr effizient, auch bei vielen gleichzeitigen Nutzern
|
||||
|
||||
---
|
||||
|
||||
## Summary für Kollegen
|
||||
|
||||
✅ **Live-Dashboard mit zwei Ebenen:**
|
||||
1. Initial Load via Service (sofortige Daten)
|
||||
2. Live-Polling via API (kontinuierliche Updates)
|
||||
|
||||
✅ **Robuste Fallback-Logik** für RAM und Disk (findet die Werte auch bei unbekannten Labels)
|
||||
|
||||
✅ **Dual Logging:**
|
||||
- PHP Error-Log für Service-Fehler
|
||||
- `public/logs/snmp_api.log` für API-Aktivitäten
|
||||
|
||||
✅ **Session-Geschützt:** Nur Admins können Status abrufen
|
||||
|
||||
✅ **Gecacht:** 10 Sekunden TTL reduziert SNMP-Load
|
||||
|
||||
✅ **Error-tolerant:** Dashboard funktioniert auch wenn SNMP fehlt (zeigt 0%, wartet auf Live-Updates)
|
||||
@ -6,6 +6,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Services\Ldap\LdapAuthService;
|
||||
use App\Services\Logging\LoggingService;
|
||||
|
||||
/**
|
||||
* Zuständig für alles rund um den Login:
|
||||
@ -26,6 +27,9 @@ class AuthController
|
||||
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
|
||||
private LdapAuthService $ldapAuthService;
|
||||
|
||||
/** @var LoggingService Logger für technische Fehler */
|
||||
private LoggingService $logger;
|
||||
|
||||
/**
|
||||
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
|
||||
*
|
||||
@ -39,6 +43,9 @@ class AuthController
|
||||
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
|
||||
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
|
||||
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
|
||||
|
||||
// LoggingService mit dem Teilbereich "logging" aus der Konfiguration initialisieren.
|
||||
$this->logger = new LoggingService($config['logging'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,14 +61,14 @@ class AuthController
|
||||
|
||||
// Wichtig: Die View erwartet aktuell die Variable $error.
|
||||
return [
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
'error' => $errorMessage,
|
||||
'loginPage' => true,
|
||||
],
|
||||
'pageTitle' => 'Login',
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
'error' => $errorMessage,
|
||||
'loginPage' => true,
|
||||
],
|
||||
'pageTitle' => 'Login',
|
||||
// Beim Login ist typischerweise kein Menüpunkt aktiv.
|
||||
'activeMenu' => null,
|
||||
'activeMenu' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@ -86,10 +93,25 @@ class AuthController
|
||||
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
|
||||
$authenticated = $this->ldapAuthService->authenticate($username, $password);
|
||||
} catch (\Throwable $exception) {
|
||||
// Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration).
|
||||
// In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt.
|
||||
// HIER ist vorher dein Fehler entstanden:
|
||||
// - showLoginForm() wurde nur aufgerufen, das Ergebnis aber ignoriert
|
||||
// - danach kam ein "return;" ohne Rückgabewert → Rückgabetyp array wurde verletzt
|
||||
|
||||
// Technischen Fehler ausführlich ins Log schreiben
|
||||
$this->logger->logException(
|
||||
'Technischer Fehler bei der Anmeldung.',
|
||||
$exception,
|
||||
[
|
||||
'route' => 'login.submit',
|
||||
'username' => $username,
|
||||
'remote_addr'=> $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Für den Benutzer nur eine allgemeine, aber verständliche Meldung anzeigen
|
||||
return $this->showLoginForm(
|
||||
'Technischer Fehler bei der Anmeldung: ' . $exception->getMessage()
|
||||
'Technischer Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut '
|
||||
. 'oder wenden Sie sich an den Administrator.'
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,15 +129,15 @@ class AuthController
|
||||
// Benutzerinformationen in der Session hinterlegen.
|
||||
// Diese Daten werden später von requireLogin() bzw. im Layout ausgewertet.
|
||||
$_SESSION[$sessionKey] = [
|
||||
'username' => $username,
|
||||
'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung
|
||||
'username' => $username,
|
||||
'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung
|
||||
];
|
||||
|
||||
// Nach erfolgreicher Anmeldung zum Dashboard weiterleiten.
|
||||
// Kein direkter header()-Aufruf, sondern ein Redirect-Result
|
||||
// für die zentrale Steuerung in index.php.
|
||||
return [
|
||||
'redirect' => 'index.php?route=dashboard',
|
||||
'redirect' => 'index.php?route=dashboard',
|
||||
];
|
||||
}
|
||||
|
||||
@ -135,7 +157,7 @@ class AuthController
|
||||
|
||||
// Redirect-Result zur Login-Seite.
|
||||
return [
|
||||
'redirect' => 'index.php?route=login',
|
||||
'redirect' => 'index.php?route=login',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
@ -9,64 +7,57 @@ use App\Services\Snmp\SnmpServerStatusService;
|
||||
|
||||
/**
|
||||
* Controller für das Dashboard.
|
||||
* Zuständig für:
|
||||
* - Abrufen des Serverstatus (über SnmpServerStatusService)
|
||||
* - Auswählen und Rendern der Dashboard-View
|
||||
*
|
||||
* NEU:
|
||||
* - Gibt ein View-Result zurück, das von index.php + Layout gerendert wird.
|
||||
* Zeigt Serverstatus-Metriken über SNMP an:
|
||||
* - Initial Load: Server-seitiger Service-Aufruf (sofortige Daten)
|
||||
* - Live-Updates: Client-seitiges JavaScript-Polling alle 5s
|
||||
*/
|
||||
class DashboardController
|
||||
{
|
||||
/** @var array<string, mixed> Vollständige Anwendungskonfiguration (aus config.php) */
|
||||
private array $config;
|
||||
|
||||
/** @var SnmpServerStatusService Service, der den Serverstatus (später per SNMP) liefert */
|
||||
private SnmpServerStatusService $snmpService;
|
||||
|
||||
/**
|
||||
* Übergibt die Konfiguration an den Controller und initialisiert den SNMP-Statusservice.
|
||||
*
|
||||
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
// Komplette Config lokal speichern (falls später weitere Werte benötigt werden).
|
||||
$this->config = $config;
|
||||
|
||||
// Teilbereich "snmp" aus der Konfiguration ziehen.
|
||||
// Wenn nicht vorhanden, wird ein leeres Array übergeben (der Service prüft das selbst).
|
||||
$snmpConfig = $config['snmp'] ?? [];
|
||||
|
||||
// SNMP-Service initialisieren, der den Serverstatus liefert.
|
||||
$this->snmpService = new SnmpServerStatusService($snmpConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt das Dashboard an.
|
||||
* Holt die Serverstatus-Daten aus dem SnmpServerStatusService und übergibt sie an die View.
|
||||
*
|
||||
* @return array<string, mixed> View-Result für das zentrale Layout
|
||||
* Beim initialen Laden wird der Service aufgerufen, um sofort Daten anzuzeigen.
|
||||
* Live-Updates erfolgen anschließend via JavaScript-Polling (api/snmp_status.php alle 5s).
|
||||
*/
|
||||
public function show(): array
|
||||
{
|
||||
// Serverstatus über den SNMP-Service ermitteln.
|
||||
// In der aktuellen Version liefert der Service noch Demo-Daten.
|
||||
$serverStatus = $this->snmpService->getServerStatus();
|
||||
$serverStatus = [
|
||||
'hostname' => 'n/a',
|
||||
'os' => 'n/a',
|
||||
'uptime' => 'n/a',
|
||||
'cpu_usage' => 0,
|
||||
'memory_usage' => 0,
|
||||
'disk_usage_c' => 0,
|
||||
'last_update' => date('d.m.Y H:i:s'),
|
||||
];
|
||||
|
||||
try {
|
||||
$serverStatus = $this->snmpService->getServerStatus();
|
||||
} catch (\RuntimeException $e) {
|
||||
error_log('DashboardController: SNMP-Fehler beim initialen Laden - ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Pfad zur Dashboard-View (Template-Datei) ermitteln.
|
||||
$viewPath = __DIR__ . '/../../public/views/dashboard.php';
|
||||
|
||||
return [
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
// Die View erwartet aktuell $serverStatus.
|
||||
'serverStatus' => $serverStatus,
|
||||
'loginPage' => false,
|
||||
],
|
||||
'pageTitle' => 'Dashboard',
|
||||
// In der Sidebar soll der Dashboard-Menüpunkt aktiv sein.
|
||||
'activeMenu' => 'dashboard',
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
'serverStatus' => $serverStatus,
|
||||
'loginPage' => false,
|
||||
],
|
||||
'pageTitle' => 'Dashboard',
|
||||
'activeMenu' => 'dashboard',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Services\Ldap\LdapDirectoryService;
|
||||
use App\Services\Logging\LoggingService;
|
||||
|
||||
/**
|
||||
* Controller für die Benutzer- und Gruppenanzeige.
|
||||
@ -30,6 +31,9 @@ class UserManagementController
|
||||
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
|
||||
private LdapDirectoryService $directoryService;
|
||||
|
||||
/** @var LoggingService Logger für technische Fehler */
|
||||
private LoggingService $logger;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
||||
*/
|
||||
@ -43,6 +47,9 @@ class UserManagementController
|
||||
|
||||
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
|
||||
$this->directoryService = new LdapDirectoryService($ldapConfig);
|
||||
|
||||
// Logging-Service initialisieren.
|
||||
$this->logger = new LoggingService($config['logging'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,25 +70,34 @@ class UserManagementController
|
||||
$users = $this->directoryService->getUsers();
|
||||
$groups = $this->directoryService->getGroups();
|
||||
} catch (\Throwable $exception) {
|
||||
// Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme)
|
||||
// werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt.
|
||||
$error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage();
|
||||
// Technische Details ins Log, für den Benutzer eine allgemeine Meldung.
|
||||
$this->logger->logException(
|
||||
'Fehler beim Laden von Benutzern/Gruppen.',
|
||||
$exception,
|
||||
[
|
||||
'route' => 'users',
|
||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
$error = 'Fehler beim Laden von Benutzern/Gruppen. '
|
||||
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
||||
}
|
||||
|
||||
// Pfad zur eigentlichen View-Datei bestimmen.
|
||||
$viewPath = __DIR__ . '/../../public/views/users.php';
|
||||
|
||||
return [
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
// Die View erwartet aktuell $users, $groups, $error.
|
||||
'users' => $users,
|
||||
'groups' => $groups,
|
||||
'error' => $error,
|
||||
'loginPage' => false,
|
||||
],
|
||||
'pageTitle' => 'Benutzer & Gruppen',
|
||||
'activeMenu' => 'users',
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
// Die View erwartet aktuell $users, $groups, $error.
|
||||
'users' => $users,
|
||||
'groups' => $groups,
|
||||
'error' => $error,
|
||||
'loginPage' => false,
|
||||
],
|
||||
'pageTitle' => 'Benutzer & Gruppen',
|
||||
'activeMenu' => 'users',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,8 @@ class LdapConnectionHelper
|
||||
|
||||
// Verbindung zum LDAP/AD-Server herstellen.
|
||||
// ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false.
|
||||
$connection = ldap_connect($server, $port);
|
||||
$uri = "ldap://".$server . ':' . $port;
|
||||
$connection = ldap_connect($uri);
|
||||
|
||||
// Wenn keine Verbindung aufgebaut werden konnte, Exception werfen.
|
||||
if ($connection === false) {
|
||||
|
||||
133
app/Services/Logging/LoggingService.php
Normal file
133
app/Services/Logging/LoggingService.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Logging;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Einfacher File-Logger für die AdminTool-Anwendung.
|
||||
*
|
||||
* Ziele:
|
||||
* - Technische Details werden in eine Log-Datei unter public/logs/ geschrieben.
|
||||
* - In der Weboberfläche erscheinen nur verständliche, fachliche Fehlermeldungen.
|
||||
*/
|
||||
class LoggingService
|
||||
{
|
||||
/** @var string Vollständiger Pfad zum Log-Verzeichnis */
|
||||
private string $logDir;
|
||||
|
||||
/** @var string Dateiname der Log-Datei */
|
||||
private string $logFile;
|
||||
|
||||
/** @var int Minimale Log-Stufe, ab der geschrieben wird. */
|
||||
private int $minLevel;
|
||||
|
||||
/**
|
||||
* Zuordnung der Log-Level zu numerischen Werten zur Filterung.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private const LEVEL_MAP = [
|
||||
'debug' => 100,
|
||||
'info' => 200,
|
||||
'warning' => 300,
|
||||
'error' => 400,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config Teilkonfiguration "logging" aus config.php
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
// Standard: public/logs relativ zum Projektroot
|
||||
$baseDir = $config['log_dir'] ?? (__DIR__ . '/../../../public/logs');
|
||||
$fileName = $config['log_file'] ?? 'app.log';
|
||||
$level = strtolower((string)($config['min_level'] ?? 'info'));
|
||||
|
||||
$this->logDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
|
||||
$this->logFile = $fileName;
|
||||
$this->minLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['info'];
|
||||
|
||||
$this->ensureLogDirectoryExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass das Log-Verzeichnis existiert.
|
||||
*/
|
||||
private function ensureLogDirectoryExists(): void
|
||||
{
|
||||
if (is_dir($this->logDir) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (@mkdir($this->logDir, 0775, true) === false && is_dir($this->logDir) === false) {
|
||||
// Wenn das Anlegen fehlschlägt, wenigstens einen Eintrag im PHP-Error-Log hinterlassen.
|
||||
error_log(sprintf('LoggingService: Konnte Log-Verzeichnis "%s" nicht anlegen.', $this->logDir));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeiner Log-Eintrag.
|
||||
*
|
||||
* @param string $level Log-Level (debug|info|warning|error)
|
||||
* @param string $message Nachrichtentext
|
||||
* @param array<string, mixed> $context Zusätzliche Kontextinformationen
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$numericLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['error'];
|
||||
|
||||
// Alles unterhalb der minimalen Stufe ignorieren.
|
||||
if ($numericLevel < $this->minLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = (new DateTimeImmutable())->format('Y-m-d H:i:s');
|
||||
|
||||
$contextJson = $context === []
|
||||
? '{}'
|
||||
: (string)json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$line = sprintf(
|
||||
"[%s] %-7s %s %s%s",
|
||||
$timestamp,
|
||||
strtoupper($level),
|
||||
$message,
|
||||
$contextJson,
|
||||
PHP_EOL
|
||||
);
|
||||
|
||||
$filePath = $this->logDir . DIRECTORY_SEPARATOR . $this->logFile;
|
||||
|
||||
if (@file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX) === false) {
|
||||
// Fallback, damit Fehler beim Logging selbst nicht die App zerschießen.
|
||||
error_log(sprintf('LoggingService: Konnte in Log-Datei "%s" nicht schreiben.', $filePath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Komfortmethode, um Exceptions strukturiert zu loggen.
|
||||
*
|
||||
* @param string $message Kurzer Kontexttext zur Exception
|
||||
* @param \Throwable $exception Die geworfene Exception
|
||||
* @param array<string, mixed> $context Zusätzlicher Kontext (Route, Benutzername, Remote-IP, ...)
|
||||
*/
|
||||
public function logException(string $message, \Throwable $exception, array $context = []): void
|
||||
{
|
||||
$exceptionContext = [
|
||||
'exception_class' => get_class($exception),
|
||||
'exception_message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
];
|
||||
|
||||
$mergedContext = array_merge($context, $exceptionContext);
|
||||
|
||||
$this->log('error', $message, $mergedContext);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Snmp;
|
||||
@ -8,15 +7,20 @@ namespace App\Services\Snmp;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Service zur Ermittlung des Serverstatus.
|
||||
* Service zur Ermittlung des Serverstatus per SNMP.
|
||||
*
|
||||
* In dieser ersten Version werden noch statische Demo-Daten zurückgegeben.
|
||||
* Später können hier echte SNMP-Abfragen eingebaut werden, ohne dass sich
|
||||
* der DashboardController oder die Views ändern müssen.
|
||||
* Features:
|
||||
* - Robuste Fehlerbehandlung mit aussagekräftigen Exceptions
|
||||
* - Intelligente Fallback-Logik bei fehlenden oder unerwarteten OID-Beschreibungen
|
||||
* - Unterstützung für Windows (C:\) und Linux (/) Systeme
|
||||
* - Detailliertes Logging über Exceptions
|
||||
*
|
||||
* Wird vom DashboardController beim initialen Laden aufgerufen.
|
||||
* Das Live-Polling erfolgt über das API-Endpunkt (public/api/snmp_status.php).
|
||||
*/
|
||||
class SnmpServerStatusService
|
||||
{
|
||||
/** @var array<string, mixed> SNMP-spezifische Konfiguration (Host, Community, Timeout, OIDs, etc.) */
|
||||
/** @var array<string, mixed> SNMP-Konfiguration (Host, Community, Timeout, OIDs, etc.) */
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
@ -26,9 +30,9 @@ class SnmpServerStatusService
|
||||
*/
|
||||
public function __construct(array $snmpConfig)
|
||||
{
|
||||
// SNMP-Konfiguration in der Instanz speichern.
|
||||
$this->config = $snmpConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den aktuellen Serverstatus zurück.
|
||||
*
|
||||
@ -56,107 +60,167 @@ class SnmpServerStatusService
|
||||
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
|
||||
}
|
||||
|
||||
// Helper-Funktion zum Bereinigen von SNMP-Antworten (z.B. "INTEGER: 123" -> 123)
|
||||
if (!function_exists('snmpget')) {
|
||||
throw new RuntimeException('PHP-SNMP-Erweiterung ist nicht installiert.');
|
||||
}
|
||||
|
||||
// Hilfsfunktion: SNMP-Werte bereinigen (z.B. "INTEGER: 123" -> 123)
|
||||
$cleanSnmpValue = fn($v) => (int)filter_var($v, FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
// --- 2. Uptime abfragen (war vorher nicht implementiert) ---
|
||||
$uptimeResult = snmpget($host, $community, $oids['uptime'], $timeout, $retries);
|
||||
// --- 2. Uptime abfragen ---
|
||||
$uptimeOid = $oids['uptime'] ?? '1.3.6.1.2.1.1.3.0';
|
||||
$uptimeResult = @snmpget($host, $community, $uptimeOid, $timeout, $retries);
|
||||
if ($uptimeResult === false) {
|
||||
throw new RuntimeException("SNMP Uptime GET fehlgeschlagen.");
|
||||
}
|
||||
// Uptime (timeticks) in ein lesbares Format umwandeln (optional, hier als String)
|
||||
// Format ist oft "Timeticks: (12345678) 1 day, 10:17:36.78"
|
||||
// Wir extrahieren den Teil in Klammern (Hundertstelsekunden)
|
||||
|
||||
// Uptime aus TimeTicks (Hundertstel-Sekunden) in lesbar konvertieren
|
||||
preg_match('/\((.*?)\)/', $uptimeResult, $matches);
|
||||
$uptimeTicks = (int)($matches[1] ?? 0);
|
||||
$uptimeSeconds = $uptimeTicks / 100;
|
||||
$uptimeFormatted = sprintf(
|
||||
'%d Tage, %02d:%02d:%02d',
|
||||
floor($uptimeSeconds / 86400),
|
||||
floor(($uptimeSeconds % 86400) / 3600),
|
||||
floor(($uptimeSeconds % 3600) / 60),
|
||||
$uptimeSeconds % 60
|
||||
(int)floor($uptimeSeconds / 86400),
|
||||
(int)floor(($uptimeSeconds % 86400) / 3600),
|
||||
(int)floor(($uptimeSeconds % 3600) / 60),
|
||||
(int)($uptimeSeconds % 60)
|
||||
);
|
||||
|
||||
|
||||
// --- 3. CPU ---
|
||||
$cpuValues = snmpwalk($host, $community, $oids['cpu_table'], $timeout, $retries);
|
||||
// --- 3. CPU (Durchschnitt über alle Kerne) ---
|
||||
$cpuTable = $oids['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
|
||||
$cpuValues = @snmpwalk($host, $community, $cpuTable, $timeout, $retries);
|
||||
|
||||
if (!is_array($cpuValues) || empty($cpuValues)) {
|
||||
throw new RuntimeException("SNMP CPU WALK fehlgeschlagen.");
|
||||
}
|
||||
|
||||
$cpuValues = array_map($cleanSnmpValue, $cpuValues);
|
||||
$cpuAvg = array_sum($cpuValues) / count($cpuValues);
|
||||
$cpuAvg = (int)round(array_sum($cpuValues) / count($cpuValues));
|
||||
|
||||
// --- 4. Storage-Tabellen (RAM + Disks) ---
|
||||
$descrOid = $oids['storage_descr'] ?? '1.3.6.1.2.1.25.2.3.1.3';
|
||||
$unitsOid = $oids['storage_units'] ?? '1.3.6.1.2.1.25.2.3.1.4';
|
||||
$sizeOid = $oids['storage_size'] ?? '1.3.6.1.2.1.25.2.3.1.5';
|
||||
$usedOid = $oids['storage_used'] ?? '1.3.6.1.2.1.25.2.3.1.6';
|
||||
|
||||
// --- 4. Memory ---
|
||||
$memTotalResult = snmpget($host, $community, $oids['mem_size'], $timeout, $retries);
|
||||
if($memTotalResult === false) {
|
||||
throw new RuntimeException("SNMP MemTotal GET fehlgeschlagen.");
|
||||
}
|
||||
$descr = @snmpwalk($host, $community, $descrOid, $timeout, $retries);
|
||||
$units = @snmpwalk($host, $community, $unitsOid, $timeout, $retries);
|
||||
$size = @snmpwalk($host, $community, $sizeOid, $timeout, $retries);
|
||||
$used = @snmpwalk($host, $community, $usedOid, $timeout, $retries);
|
||||
|
||||
// memTotal in Bytes berechnen (korrigierte Reihenfolge von filter/cast)
|
||||
$memTotal = $cleanSnmpValue($memTotalResult) * 1024; // KB -> Bytes
|
||||
|
||||
// Storage-Tabelle (RAM + Disks)
|
||||
$descr = snmpwalk($host, $community, $oids['storage_descr'], $timeout, $retries);
|
||||
$units = snmpwalk($host, $community, $oids['storage_units'], $timeout, $retries);
|
||||
$size = snmpwalk($host, $community, $oids['storage_size'], $timeout, $retries);
|
||||
$used = snmpwalk($host, $community, $oids['storage_used'], $timeout, $retries);
|
||||
|
||||
if ($descr === false || $units === false || $size === false || $used === false) {
|
||||
if (!is_array($descr) || !is_array($units) || !is_array($size) || !is_array($used)) {
|
||||
throw new RuntimeException("SNMP Storage WALK fehlgeschlagen.");
|
||||
}
|
||||
|
||||
// Werte bereinigen
|
||||
// Die SNMP-Antwort enthält "STRING: " und Anführungszeichen, die wir entfernen müssen.
|
||||
$descr = array_map(fn($v) => trim(str_ireplace('STRING:', '', $v), ' "'), $descr);
|
||||
$units = array_map($cleanSnmpValue, $units);
|
||||
$size = array_map($cleanSnmpValue, $size);
|
||||
$used = array_map($cleanSnmpValue, $used);
|
||||
|
||||
$units = array_map($cleanSnmpValue, $units); // Ints
|
||||
$size = array_map($cleanSnmpValue, $size); // Ints
|
||||
$used = array_map($cleanSnmpValue, $used); // Ints
|
||||
// --- 5. RAM mit Fallback-Logik ---
|
||||
$ramPercent = null;
|
||||
$memTotalBytes = null;
|
||||
|
||||
|
||||
|
||||
// RAM
|
||||
// Heuristik 1: Suche nach "Physical Memory"
|
||||
$ramIndex = array_search("Physical Memory", $descr);
|
||||
if ($ramIndex === false) {
|
||||
if ($ramIndex !== false) {
|
||||
$memTotalBytes = $units[$ramIndex] * $size[$ramIndex];
|
||||
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
|
||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
||||
}
|
||||
|
||||
// Fallback 1: Wenn nicht gefunden, suche nach ähnlichen Labels
|
||||
if ($ramPercent === null) {
|
||||
foreach ($descr as $index => $description) {
|
||||
$lower = strtolower($description);
|
||||
if (strpos($lower, 'physical') !== false || strpos($lower, 'memory') !== false || strpos($lower, 'ram') !== false) {
|
||||
$memTotalBytes = $units[$index] * $size[$index];
|
||||
$ramUsedBytes = $units[$index] * $used[$index];
|
||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 2: Wenn immer noch nicht gefunden, nimm den kleinsten Eintrag (i.d.R. RAM)
|
||||
if ($ramPercent === null && count($descr) > 0) {
|
||||
$minIndex = 0;
|
||||
$minSize = PHP_INT_MAX;
|
||||
foreach ($size as $index => $s) {
|
||||
if ($s > 0 && $s < $minSize) {
|
||||
$minSize = $s;
|
||||
$minIndex = $index;
|
||||
}
|
||||
}
|
||||
$memTotalBytes = $units[$minIndex] * $size[$minIndex];
|
||||
$ramUsedBytes = $units[$minIndex] * $used[$minIndex];
|
||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
||||
}
|
||||
|
||||
// Fallback 3: Wenn gar nichts geht, Exception
|
||||
if ($ramPercent === null) {
|
||||
throw new RuntimeException("Konnte 'Physical Memory' in der SNMP Storage-Tabelle nicht finden.");
|
||||
}
|
||||
|
||||
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
|
||||
$ramPercent = ($memTotal > 0) ? ($ramUsedBytes / $memTotal) * 100 : 0;
|
||||
// --- 6. Disk C: / Root mit Fallback-Logik ---
|
||||
$diskCPercent = null;
|
||||
|
||||
|
||||
// --- 5. Disk C: ---
|
||||
$cIndex = false;
|
||||
// Heuristik 1: Suche nach C:\
|
||||
foreach ($descr as $index => $description) {
|
||||
// str_starts_with prüft, ob der String mit C:\ beginnt
|
||||
if (str_starts_with($description, 'C:\\')) {
|
||||
$cIndex = $index;
|
||||
$cTotal = $units[$index] * $size[$index];
|
||||
$cUsed = $units[$index] * $used[$index];
|
||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($cIndex === false) {
|
||||
throw new RuntimeException("Konnte Laufwerk 'C:\' in der SNMP Storage-Tabelle nicht finden.");
|
||||
// Fallback 1: Suche nach "C:" oder "root" oder "/"
|
||||
if ($diskCPercent === null) {
|
||||
foreach ($descr as $index => $description) {
|
||||
$lower = strtolower($description);
|
||||
if (strpos($lower, 'c:') !== false || $lower === '/' || strpos($lower, 'root') !== false) {
|
||||
$cTotal = $units[$index] * $size[$index];
|
||||
$cUsed = $units[$index] * $used[$index];
|
||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cUsed = $units[$cIndex] * $used[$cIndex];
|
||||
$cTotal = $units[$cIndex] * $size[$cIndex];
|
||||
// Fallback 2: Nimm den größten Eintrag > 1GB (wahrscheinlich der Hauptdatenträger)
|
||||
if ($diskCPercent === null) {
|
||||
$maxIndex = 0;
|
||||
$maxSize = 0;
|
||||
foreach ($size as $index => $s) {
|
||||
$sizeGB = ($s * $units[$index]) / (1024 ** 3);
|
||||
if ($sizeGB > 1 && $s > $maxSize) {
|
||||
$maxSize = $s;
|
||||
$maxIndex = $index;
|
||||
}
|
||||
}
|
||||
if ($maxSize > 0) {
|
||||
$cTotal = $units[$maxIndex] * $size[$maxIndex];
|
||||
$cUsed = $units[$maxIndex] * $used[$maxIndex];
|
||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||
// Fallback 3: Wenn immer noch nichts, Exception
|
||||
if ($diskCPercent === null) {
|
||||
throw new RuntimeException("Konnte Laufwerk 'C:\\' oder Root-Partition in der SNMP Storage-Tabelle nicht finden.");
|
||||
}
|
||||
|
||||
// --- 6. Status-Array zusammenbauen ---
|
||||
// --- 7. Status-Array zusammenbauen ---
|
||||
$status = [
|
||||
'hostname' => $host,
|
||||
'os' => 'Windows Server 2025 Datacenter', // TODO: OS dynamisch abfragen
|
||||
'uptime' => $uptimeFormatted, // Fehlende Variable hinzugefügt
|
||||
'cpu_usage' => round($cpuAvg),
|
||||
'memory_usage' => round($ramPercent),
|
||||
'disk_usage_c' => round($diskCPercent),
|
||||
'last_update' => date('d.m.Y H:i:s'),
|
||||
'hostname' => $host,
|
||||
'os' => 'Windows Server', // TODO: OS dynamisch per SNMP abfragen (OID 1.3.6.1.2.1.1.1.0)
|
||||
'uptime' => $uptimeFormatted,
|
||||
'cpu_usage' => $cpuAvg,
|
||||
'memory_usage' => (int)round($ramPercent),
|
||||
'disk_usage_c' => (int)round($diskCPercent),
|
||||
'last_update' => date('d.m.Y H:i:s'),
|
||||
];
|
||||
|
||||
return $status;
|
||||
|
||||
@ -49,4 +49,14 @@ return [
|
||||
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
|
||||
],
|
||||
],
|
||||
|
||||
// Logging-Konfiguration
|
||||
'logging' => [
|
||||
// Standard: public/logs relativ zum Projekt-Root
|
||||
'log_dir' => __DIR__ . '/../public/logs',
|
||||
// Name der Logdatei
|
||||
'log_file' => 'app.log',
|
||||
// Minimale Stufe: debug, info, warning, error
|
||||
'min_level' => 'info',
|
||||
],
|
||||
];
|
||||
|
||||
292
public/api/snmp_status.php
Normal file
292
public/api/snmp_status.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SNMP-Status-API für das Dashboard.
|
||||
*
|
||||
* Nur authentifizierte Admins dürfen auf diesen Endpunkt zugreifen.
|
||||
* Wird alle 5s vom JavaScript im Dashboard aufgerufen.
|
||||
*
|
||||
* Sicherheit: Session-Validierung + Admin-Rollenprüfung.
|
||||
* Performance: Datei-basiertes Caching (10 Sekunden) um SNMP-Last zu reduzieren.
|
||||
* Logging: Alle Fehler und wichtigen Daten werden in `public/logs/snmp_api.log` geschrieben.
|
||||
*/
|
||||
|
||||
session_start();
|
||||
|
||||
// === Authentifizierung + Autorisierung ===
|
||||
$sessionKeyUser = 'admin_user'; // aus config
|
||||
if (!isset($_SESSION[$sessionKeyUser])) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// === Logging-Setup ===
|
||||
$logDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'logs';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0755, true);
|
||||
}
|
||||
$logFile = $logDir . DIRECTORY_SEPARATOR . 'snmp_api.log';
|
||||
|
||||
function log_msg(string $msg): void {
|
||||
global $logFile;
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
@file_put_contents($logFile, "[$timestamp] $msg\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
function rotate_log_if_needed(): void {
|
||||
global $logFile;
|
||||
$maxSize = 500 * 1024; // 500 KB (anpassbar)
|
||||
|
||||
if (file_exists($logFile) && filesize($logFile) > $maxSize) {
|
||||
$backupFile = $logFile . '.old';
|
||||
@rename($logFile, $backupFile);
|
||||
log_msg('=== Log rotiert (alte Datei: .old) ===');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$configPath = __DIR__ . '/../../config/config.php';
|
||||
if (!is_readable($configPath)) {
|
||||
log_msg('ERROR: config.php nicht lesbar');
|
||||
echo json_encode(['error' => 'config_not_found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$config = require $configPath;
|
||||
$snmp = $config['snmp'] ?? [];
|
||||
|
||||
rotate_log_if_needed();
|
||||
|
||||
log_msg('--- SNMP-Abfrage gestartet ---');
|
||||
|
||||
// === Cache-Logik (Datei) ===
|
||||
// Cache-Datei wird nach 10 Sekunden als ungültig betrachtet
|
||||
$cacheDir = sys_get_temp_dir();
|
||||
$cacheFile = $cacheDir . DIRECTORY_SEPARATOR . 'snmp_status_cache.json';
|
||||
$cacheTTL = 10; // Sekunden
|
||||
|
||||
if (file_exists($cacheFile)) {
|
||||
$cacheAge = time() - filemtime($cacheFile);
|
||||
if ($cacheAge < $cacheTTL) {
|
||||
// Cache ist noch gültig
|
||||
$cached = file_get_contents($cacheFile);
|
||||
if ($cached !== false) {
|
||||
log_msg("Cache HIT (Alter: {$cacheAge}s)");
|
||||
echo $cached;
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
log_msg("Cache abgelaufen (Alter: {$cacheAge}s), neue Abfrage");
|
||||
}
|
||||
}
|
||||
|
||||
$host = $snmp['host'] ?? '127.0.0.1';
|
||||
$community = $snmp['community'] ?? 'public';
|
||||
$timeout = (int)($snmp['timeout'] ?? 2);
|
||||
$retries = (int)($snmp['retries'] ?? 1);
|
||||
|
||||
log_msg("SNMP-Host: $host, Community: $community, Timeout: {$timeout}s");
|
||||
|
||||
if (!function_exists('snmpget')) {
|
||||
log_msg('ERROR: SNMP-Erweiterung nicht installiert');
|
||||
echo json_encode(['error' => 'snmp_extension_missing']);
|
||||
exit;
|
||||
}
|
||||
|
||||
snmp_set_quick_print(true);
|
||||
snmp_set_oid_output_format(SNMP_OID_OUTPUT_NUMERIC);
|
||||
|
||||
// Hilfsfunktion: sichere snmpget-Rückgabe (numeric oder null)
|
||||
function sget(string $host, string $community, string $oid, int $timeout, int $retries)
|
||||
{
|
||||
$v = @snmpget($host, $community, $oid, $timeout, $retries);
|
||||
if ($v === false || $v === null) return null;
|
||||
return is_string($v) ? trim($v) : $v;
|
||||
}
|
||||
|
||||
// System-Uptime (TimeTicks)
|
||||
$uptimeOid = $snmp['oids']['uptime'] ?? '1.3.6.1.2.1.1.3.0';
|
||||
$upticksRaw = @sget($host, $community, $uptimeOid, $timeout, $retries);
|
||||
$upticks = $upticksRaw !== null ? (int)preg_replace('/[^0-9]/', '', (string)$upticksRaw) : null;
|
||||
|
||||
log_msg("Uptime OID: $uptimeOid, Raw: " . ($upticksRaw ?? 'null'));
|
||||
|
||||
function format_uptime(?int $ticks): ?string
|
||||
{
|
||||
if ($ticks === null) return null;
|
||||
// ticks sind Hundertstel-Sekunden
|
||||
$seconds = (int)floor($ticks / 100);
|
||||
$days = intdiv($seconds, 86400);
|
||||
$seconds -= $days * 86400;
|
||||
$hours = intdiv($seconds, 3600);
|
||||
$seconds -= $hours * 3600;
|
||||
$minutes = intdiv($seconds, 60);
|
||||
$seconds -= $minutes * 60;
|
||||
$parts = [];
|
||||
if ($days) $parts[] = $days . ' Tage';
|
||||
if ($hours) $parts[] = $hours . ' Std';
|
||||
if ($minutes) $parts[] = $minutes . ' Min';
|
||||
$parts[] = $seconds . ' Sek';
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
$uptimeStr = format_uptime($upticks);
|
||||
|
||||
// CPU: ersten Eintrag aus der CPU-Tabelle lesen
|
||||
$cpuTable = $snmp['oids']['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
|
||||
$cpuOid = $cpuTable . '.1';
|
||||
$cpuRaw = @sget($host, $community, $cpuOid, $timeout, $retries);
|
||||
$cpu = $cpuRaw !== null ? (float)preg_replace('/[^0-9.]/', '', (string)$cpuRaw) : null;
|
||||
|
||||
log_msg("CPU OID: $cpuOid, Raw: " . ($cpuRaw ?? 'null') . ", Parsed: " . ($cpu ?? 'null'));
|
||||
|
||||
// Storage-Tabellen: Versuche, C: (Windows) oder Root ("/") zu finden
|
||||
$descrOid = $snmp['oids']['storage_descr'] ?? '1.3.6.1.2.1.25.2.3.1.3';
|
||||
$unitsOid = $snmp['oids']['storage_units'] ?? '1.3.6.1.2.1.25.2.3.1.4';
|
||||
$sizeOid = $snmp['oids']['storage_size'] ?? '1.3.6.1.2.1.25.2.3.1.5';
|
||||
$usedOid = $snmp['oids']['storage_used'] ?? '1.3.6.1.2.1.25.2.3.1.6';
|
||||
|
||||
log_msg("Starte Storage-Walk - Descr: $descrOid, Units: $unitsOid, Size: $sizeOid, Used: $usedOid");
|
||||
|
||||
$descrWalk = @snmprealwalk($host, $community, $descrOid);
|
||||
$unitsWalk = @snmprealwalk($host, $community, $unitsOid);
|
||||
$sizeWalk = @snmprealwalk($host, $community, $sizeOid);
|
||||
$usedWalk = @snmprealwalk($host, $community, $usedOid);
|
||||
|
||||
if (!is_array($descrWalk)) {
|
||||
log_msg('WARNING: snmprealwalk für Beschreibungen fehlgeschlagen');
|
||||
$descrWalk = [];
|
||||
}
|
||||
if (!is_array($unitsWalk)) {
|
||||
log_msg('WARNING: snmprealwalk für Units fehlgeschlagen');
|
||||
$unitsWalk = [];
|
||||
}
|
||||
if (!is_array($sizeWalk)) {
|
||||
log_msg('WARNING: snmprealwalk für Größe fehlgeschlagen');
|
||||
$sizeWalk = [];
|
||||
}
|
||||
if (!is_array($usedWalk)) {
|
||||
log_msg('WARNING: snmprealwalk für Used fehlgeschlagen');
|
||||
$usedWalk = [];
|
||||
}
|
||||
|
||||
log_msg('Storage-Einträge gefunden: ' . count($descrWalk));
|
||||
|
||||
$diskPercent = null;
|
||||
$memPercent = null;
|
||||
$storageEntries = [];
|
||||
|
||||
// Sammeln aller Storage-Einträge für Debug-Logging
|
||||
if (is_array($descrWalk) && count($descrWalk) > 0) {
|
||||
foreach ($descrWalk as $descrOidFull => $descrRaw) {
|
||||
// Index extrahieren
|
||||
if (!preg_match('/\.(\d+)$/', $descrOidFull, $m)) continue;
|
||||
$idx = $m[1];
|
||||
$descr = trim((string)$descrRaw, ' "');
|
||||
|
||||
// Suche nach passenden Einträgen in anderen Walks mit gleichem Index
|
||||
$units = null;
|
||||
$size = null;
|
||||
$used = null;
|
||||
|
||||
foreach ($unitsWalk as $oid => $val) {
|
||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
||||
$units = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sizeWalk as $oid => $val) {
|
||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
||||
$size = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($usedWalk as $oid => $val) {
|
||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
||||
$used = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log_msg("Storage[$idx]: Desc='$descr', Size=$size, Used=$used, Units=$units");
|
||||
|
||||
if ($size === null || $units === null || $used === null || $size === 0 || $units === 0) {
|
||||
log_msg("Storage[$idx]: SKIP (fehlende oder ungültige Daten)");
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalBytes = $size * $units;
|
||||
$usedBytes = $used * $units;
|
||||
$percent = $totalBytes > 0 ? ($usedBytes / $totalBytes) * 100 : 0;
|
||||
|
||||
$storageEntries[] = [
|
||||
'idx' => $idx,
|
||||
'descr' => $descr,
|
||||
'percent' => $percent,
|
||||
'totalGB' => $totalBytes / (1024 ** 3),
|
||||
'usedGB' => $usedBytes / (1024 ** 3),
|
||||
];
|
||||
|
||||
// Heuristik 1: Suche nach C: oder Root
|
||||
$lower = strtolower($descr);
|
||||
if ($diskPercent === null && (strpos($lower, 'c:') !== false || strpos($lower, 'c:\\') !== false || strpos($lower, 'c:/') !== false || $lower === '/' || strpos($lower, 'root') !== false)) {
|
||||
$diskPercent = $percent;
|
||||
log_msg("Datenträger erkannt (Index $idx): $descr → $percent%");
|
||||
}
|
||||
|
||||
// Heuristik 2: Suche nach Physical Memory
|
||||
if ($memPercent === null && ((strpos($lower, 'physical') !== false && strpos($lower, 'memory') !== false) || strpos($lower, 'ram') !== false || strpos($lower, 'physical memory') !== false)) {
|
||||
$memPercent = $percent;
|
||||
log_msg("Speicher erkannt (Index $idx): $descr → $percent%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 1: Wenn keine Disk gefunden, nimm den größten Storage-Eintrag > 1GB
|
||||
if ($diskPercent === null && count($storageEntries) > 0) {
|
||||
log_msg('Fallback: Suche Disk (größter Eintrag > 1GB)');
|
||||
usort($storageEntries, fn($a, $b) => $b['totalGB'] <=> $a['totalGB']);
|
||||
foreach ($storageEntries as $entry) {
|
||||
if ($entry['totalGB'] > 1) {
|
||||
$diskPercent = $entry['percent'];
|
||||
log_msg('Fallback Disk gefunden (Index ' . $entry['idx'] . '): ' . $entry['descr'] . ' → ' . $diskPercent . '%');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 2: Wenn kein Speicher gefunden, nimm den kleinsten Eintrag (meist Physical Memory)
|
||||
if ($memPercent === null && count($storageEntries) > 0) {
|
||||
log_msg('Fallback: Suche Memory (kleinster Eintrag)');
|
||||
usort($storageEntries, fn($a, $b) => $a['totalGB'] <=> $b['totalGB']);
|
||||
$memPercent = $storageEntries[0]['percent'];
|
||||
log_msg('Fallback Memory gefunden (Index ' . $storageEntries[0]['idx'] . '): ' . $storageEntries[0]['descr'] . ' → ' . $memPercent . '%');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'hostname' => @sget($host, $community, '1.3.6.1.2.1.1.5.0', $timeout, $retries) ?? null,
|
||||
'uptime' => $uptimeStr,
|
||||
'upticks' => $upticks,
|
||||
'cpu_usage' => $cpu,
|
||||
'memory_usage' => $memPercent !== null ? round($memPercent, 2) : null,
|
||||
'disk_usage_c' => $diskPercent !== null ? round($diskPercent, 2) : null,
|
||||
'last_update' => time(),
|
||||
];
|
||||
|
||||
log_msg('RESULT: CPU=' . $result['cpu_usage'] . ', Mem=' . $result['memory_usage'] . ', Disk=' . $result['disk_usage_c']);
|
||||
|
||||
$resultJson = json_encode($result);
|
||||
|
||||
// === Cache schreiben ===
|
||||
@file_put_contents($cacheFile, $resultJson);
|
||||
log_msg('Cache geschrieben, TTL: ' . $cacheTTL . 's');
|
||||
|
||||
echo $resultJson;
|
||||
exit;
|
||||
101
public/index.php
101
public/index.php
@ -19,10 +19,15 @@ declare(strict_types=1);
|
||||
* - Alle neuen Routen sollten über den Switch-Block am Ende ergänzt werden.
|
||||
*/
|
||||
|
||||
|
||||
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt.
|
||||
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt
|
||||
// oder eine bestehende wird fortgesetzt.
|
||||
session_start();
|
||||
|
||||
// PHP-Fehler erfassen, aber veraltete Hinweise (E_DEPRECATED) ignorieren,
|
||||
// weil sie sonst im Zusammenspiel mit IIS/fastcgi zu 500-Fehlern führen können.
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
/*
|
||||
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
|
||||
* Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden,
|
||||
@ -31,7 +36,7 @@ session_start();
|
||||
*/
|
||||
spl_autoload_register(
|
||||
static function (string $class): void {
|
||||
$prefix = 'App\\';
|
||||
$prefix = 'App\\';
|
||||
$baseDir = __DIR__ . '/../app/';
|
||||
|
||||
$len = strlen($prefix);
|
||||
@ -40,7 +45,7 @@ spl_autoload_register(
|
||||
}
|
||||
|
||||
$relativeClass = substr($class, $len);
|
||||
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
|
||||
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
|
||||
|
||||
if (file_exists($file) === true) {
|
||||
require $file;
|
||||
@ -48,11 +53,11 @@ spl_autoload_register(
|
||||
}
|
||||
);
|
||||
|
||||
// Layout-Funktion einbinden (renderLayout)
|
||||
require __DIR__ . '/views/layout.php';
|
||||
|
||||
// Die Konfigurationsdatei liefert ein assoziatives Array mit den Teilbereichen
|
||||
// "ldap", "snmp" und "security" (u. a. Session-Keys, Timeout-Einstellungen, OIDs).
|
||||
|
||||
// "ldap", "snmp", "security" und "logging" (u. a. Session-Keys, Timeout-Einstellungen, OIDs, Log-Pfade).
|
||||
$configPath = __DIR__ . '/../config/config.php';
|
||||
if (file_exists($configPath) === false) {
|
||||
// Fail fast: ohne Konfiguration macht die App keinen Sinn
|
||||
@ -68,6 +73,61 @@ $config = require $configPath;
|
||||
use App\Controllers\AuthController;
|
||||
use App\Controllers\DashboardController;
|
||||
use App\Controllers\UserManagementController;
|
||||
use App\Services\Logging\LoggingService;
|
||||
|
||||
// Globalen Logger initialisieren, damit auch Fehler außerhalb der Controller
|
||||
// (z. B. in index.php selbst) sauber protokolliert werden.
|
||||
$globalLogger = new LoggingService($config['logging'] ?? []);
|
||||
|
||||
/**
|
||||
* Globale Fehlerbehandlung:
|
||||
* - PHP-Fehler (Warnings, Notices, ...) werden in den Logger geschrieben.
|
||||
* - Unbehandelte Exceptions werden ebenfalls geloggt und führen zu einer generischen 500er-Meldung.
|
||||
*/
|
||||
set_error_handler(
|
||||
static function (
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file = '',
|
||||
int $line = 0
|
||||
) use ($globalLogger): bool {
|
||||
// Fehler nur loggen, wenn sie durch error_reporting() nicht unterdrückt sind.
|
||||
if ((error_reporting() & $severity) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$globalLogger->log(
|
||||
'error',
|
||||
'PHP-Fehler: ' . $message,
|
||||
[
|
||||
'severity' => $severity,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
]
|
||||
);
|
||||
|
||||
// false zurückgeben = PHP darf seinen Standard-Handler zusätzlich verwenden
|
||||
// (der Browser sieht wegen display_errors=0 trotzdem nichts).
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
set_exception_handler(
|
||||
static function (\Throwable $exception) use ($globalLogger): void {
|
||||
$globalLogger->logException(
|
||||
'Unbehandelte Exception in der Anwendung.',
|
||||
$exception,
|
||||
[
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
||||
'route' => $_GET['route'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo 'Es ist ein unerwarteter Fehler aufgetreten. '
|
||||
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
|
||||
@ -99,20 +159,24 @@ function handleResult(?array $result): void
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect-Result
|
||||
if (isset($result['redirect']) === true) {
|
||||
header('Location: ' . (string)$result['redirect']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// View-Result
|
||||
$contentView = (string)($result['view'] ?? '');
|
||||
$viewData = (array)($result['data'] ?? []);
|
||||
|
||||
// Standard: Wir gehen davon aus, dass es KEINE Loginseite ist,
|
||||
// außer der Controller sagt explizit etwas anderes.
|
||||
if (!array_key_exists('loginPage', $viewData)) {
|
||||
if (array_key_exists('loginPage', $viewData) === false) {
|
||||
$viewData['loginPage'] = false;
|
||||
}
|
||||
$pageTitle = (string)($result['pageTitle'] ?? '');
|
||||
$activeMenu = $result['activeMenu'] ?? null;
|
||||
|
||||
$pageTitle = (string)($result['pageTitle'] ?? '');
|
||||
$activeMenu = $result['activeMenu'] ?? null;
|
||||
|
||||
if ($contentView === '' || file_exists($contentView) === false) {
|
||||
http_response_code(500);
|
||||
@ -121,30 +185,33 @@ function handleResult(?array $result): void
|
||||
}
|
||||
|
||||
// Hier rufen wir jetzt die Layout-Funktion aus layout.php auf
|
||||
renderLayout($contentView, $viewData, $pageTitle, is_string($activeMenu) ? $activeMenu : null);
|
||||
renderLayout(
|
||||
$contentView,
|
||||
$viewData,
|
||||
$pageTitle,
|
||||
is_string($activeMenu) ? $activeMenu : null
|
||||
);
|
||||
}
|
||||
|
||||
// Zentrale Controller der Anwendung initialisieren und ihnen die vollständige Konfiguration übergeben.
|
||||
// Die Controller holen sich daraus bei Bedarf ihre spezifischen Teilkonfigurationen (z. B. "ldap" oder "snmp").
|
||||
|
||||
$authController = new AuthController($config);
|
||||
$dashboardController = new DashboardController($config);
|
||||
// Jeder Controller erzeugt intern seinen eigenen LoggingService aus $config['logging'].
|
||||
$authController = new AuthController($config);
|
||||
$dashboardController = new DashboardController($config);
|
||||
$userManagementController = new UserManagementController($config);
|
||||
|
||||
// Route aus dem Query-Parameter lesen. Standardroute ist "login",
|
||||
// sodass nicht angemeldete Benutzer automatisch auf die Login-Seite geführt werden.
|
||||
|
||||
$route = $_GET['route'] ?? 'login';
|
||||
|
||||
// Einfache Router-Logik: Jede Route ruft eine Controller-Methode auf und
|
||||
// übergibt deren View-Result an handleResult(). Neue Seiten werden hier ergänzt.
|
||||
|
||||
switch ($route) {
|
||||
case 'login':
|
||||
// Login-Formular anzeigen (ggf. mit Fehlermeldung)
|
||||
$result = $authController->showLoginForm();
|
||||
handleResult($result);
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'login.submit':
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
@ -178,5 +245,3 @@ switch ($route) {
|
||||
echo 'Route nicht gefunden.';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,28 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* View-Template für das Server-Dashboard.
|
||||
* Server-Dashboard (Ansicht).
|
||||
*
|
||||
* Aufgaben:
|
||||
* - Visualisiert den vom SnmpServerStatusService gelieferten Serverstatus.
|
||||
* - Zeigt Kennzahlen wie Hostname, Uptime, CPU-Auslastung, RAM-Auslastung
|
||||
* und Belegung der Systempartition "C:" an.
|
||||
*
|
||||
* Erwartete View-Daten:
|
||||
* - array<string, mixed> $serverStatus Assoziatives Array mit Statuswerten (hostname, uptime, cpu_usage, memory_usage, disk_usage_c, last_update).
|
||||
* Zeigt Server-Kennzahlen an (Hostname, Uptime, CPU, RAM, Datenträger).
|
||||
* Erwartetes Array: `$serverStatus` mit Schlüsseln: hostname, uptime, cpu_usage,
|
||||
* memory_usage, disk_usage_c, last_update.
|
||||
*/
|
||||
|
||||
/** @var array<string, mixed> $serverStatus */
|
||||
?>
|
||||
|
||||
<!-- Content Row -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- Hostname -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
@ -75,7 +70,7 @@ declare(strict_types=1);
|
||||
CPU-Auslastung
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
<?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?> %
|
||||
<span id="cpu_card_value"><?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?></span> %
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@ -96,7 +91,7 @@ declare(strict_types=1);
|
||||
RAM-Auslastung
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
<?php echo (int)($serverStatus['memory_usage'] ?? 0); ?> %
|
||||
<span id="mem_card_value"><?php echo (int)($serverStatus['memory_usage'] ?? 0); ?></span> %
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@ -106,6 +101,85 @@ declare(strict_types=1);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Hier kann man du später Charts, weitere Karten usw. anhängen -->
|
||||
|
||||
<div class="row">
|
||||
<!-- Disk C: / Root -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-secondary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
|
||||
Datenträger C: / Root
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
<span id="disk_usage_text"><?php echo isset($serverStatus['disk_usage_c']) ? (int)$serverStatus['disk_usage_c'] : 'n/a'; ?></span> %
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-hdd fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uptime -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-dark shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
|
||||
System Uptime
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
<span id="uptime_text"><?php echo htmlspecialchars((string)($serverStatus['uptime'] ?? 'n/a'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-2">
|
||||
<div class="small text-gray-600">Letztes Update: <span id="snmp_last_update"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const diskEl = document.getElementById('disk_usage_text');
|
||||
const uptimeEl = document.getElementById('uptime_text');
|
||||
const cpuEl = document.getElementById('cpu_card_value');
|
||||
const memEl = document.getElementById('mem_card_value');
|
||||
const lastEl = document.getElementById('snmp_last_update');
|
||||
|
||||
function updateUI(data){
|
||||
if(!data) return;
|
||||
if(diskEl) diskEl.textContent = (data.disk_usage_c !== null && data.disk_usage_c !== undefined) ? Math.round(data.disk_usage_c) : 'n/a';
|
||||
if(uptimeEl) uptimeEl.textContent = data.uptime || 'n/a';
|
||||
if(cpuEl) cpuEl.textContent = (data.cpu_usage !== null && data.cpu_usage !== undefined) ? Math.round(data.cpu_usage) : 0;
|
||||
if(memEl) memEl.textContent = (data.memory_usage !== null && data.memory_usage !== undefined) ? Math.round(data.memory_usage) : 0;
|
||||
if(lastEl) lastEl.textContent = data.last_update ? new Date(data.last_update * 1000).toLocaleTimeString() : '';
|
||||
}
|
||||
|
||||
async function fetchStatus(){
|
||||
try{
|
||||
const res = await fetch('api/snmp_status.php');
|
||||
if(!res.ok) throw new Error('Network response not ok');
|
||||
const json = await res.json();
|
||||
updateUI(json);
|
||||
} catch(e){
|
||||
console.error('Abruf SNMP fehlgeschlagen', e);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStatus();
|
||||
setInterval(fetchStatus, 5000);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -44,7 +44,7 @@ declare(strict_types=1);
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
|
||||
<table class="table table-bordered" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" name="selectAllUsers"></th>
|
||||
@ -83,7 +83,7 @@ declare(strict_types=1);
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
|
||||
<table class="table table-bordered" id="groupsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gruppenname (sAMAccountName)</th>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user