Compare commits

..

3 Commits

Author SHA1 Message Date
c490d38457 develop (#24)
Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/24
Co-authored-by: blaerf <blaerf@gmx.de>
Co-committed-by: blaerf <blaerf@gmx.de>
2025-12-05 09:20:21 +00:00
11aec278e8 public/views/login.php aktualisiert
Hinweistext geändert
2025-12-04 11:38:51 +00:00
5aac9b6f26 develop (#15)
Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/15
2025-12-04 11:37:42 +00:00
12 changed files with 1132 additions and 167 deletions

297
CHANGELOG.md Normal file
View 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)

View File

@ -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'] ?? []);
}
/**
@ -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.'
);
}

View File

@ -1,6 +1,4 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Controllers;
@ -9,63 +7,56 @@ 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 File

@ -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,9 +70,18 @@ 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.

View File

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

View 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);
}
}

View File

@ -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,106 +60,166 @@ 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;
}
}
// --- 6. Status-Array zusammenbauen ---
// 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.");
}
// --- 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),
'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'),
];

View File

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

View File

@ -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,
@ -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,18 +159,22 @@ 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;
@ -121,24 +185,27 @@ 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").
// 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)
@ -178,5 +245,3 @@ switch ($route) {
echo 'Route nicht gefunden.';
break;
}

View File

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

View File

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