start(); $configRepository = app_config_repository(); $message = null; $messageType = 'success'; $rawConfigText = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'login') { $success = $auth->login( trim((string) ($_POST['username'] ?? '')), (string) ($_POST['password'] ?? '') ); if ($success) { app_redirect(app_url('/admin/')); } $message = 'Login fehlgeschlagen. Bitte Zugangsdaten prüfen.'; $messageType = 'error'; } if ($action === 'save_config' && $auth->isAuthenticated()) { try { $config = $configRepository->getConfig(); $newPassword = trim((string) ($_POST['admin_password'] ?? '')); $config['app'] = [ 'name' => trim((string) ($_POST['app_name'] ?? 'Getränkeautomat Monitor')), 'timezone' => trim((string) ($_POST['timezone'] ?? 'Europe/Berlin')), 'dashboard_refresh_seconds' => max(5, (int) ($_POST['dashboard_refresh_seconds'] ?? 15)), 'default_from_email' => trim((string) ($_POST['default_from_email'] ?? 'monitor@example.local')), 'base_path' => app_normalize_base_path((string) ($_POST['base_path'] ?? '')), ]; $config['api'] = [ 'bearer_token' => trim((string) ($_POST['bearer_token'] ?? 'change-me-token')), ]; $config['admin'] = [ 'username' => trim((string) ($_POST['admin_username'] ?? 'admin')), 'password_hash' => $newPassword !== '' ? password_hash($newPassword, PASSWORD_BCRYPT) : (string) ($config['admin']['password_hash'] ?? ''), ]; $config['alerts'] = [ 'webhooks' => normalizeWebhooks($_POST['webhooks'] ?? []), 'emails' => normalizeEmails($_POST['emails'] ?? []), ]; $config['machines'] = normalizeMachines($_POST['machines'] ?? []); $configRepository->saveConfig($config); $message = 'Konfiguration gespeichert.'; $messageType = 'success'; } catch (Throwable $exception) { $message = 'Konfiguration konnte nicht gespeichert werden: ' . $exception->getMessage(); $messageType = 'error'; } } if ($action === 'save_raw_config' && $auth->isAuthenticated()) { $rawConfigText = (string) ($_POST['raw_config'] ?? ''); try { $decoded = json_decode($rawConfigText, true, 512, JSON_THROW_ON_ERROR); if (!is_array($decoded) || array_is_list($decoded)) { throw new JsonException('Die JSON-Config muss ein JSON-Objekt sein.'); } $configRepository->saveConfig($decoded); $message = 'JSON-Config gespeichert.'; $messageType = 'success'; $rawConfigText = encodeConfigForEditor($decoded); } catch (Throwable $exception) { $message = 'JSON-Config ist ungültig: ' . $exception->getMessage(); $messageType = 'error'; } } } $config = $configRepository->getConfig(); $rawConfigText ??= encodeConfigForEditor($config); if (!$auth->isAuthenticated()) { renderLogin($message, $messageType); exit; } renderAdmin($config, $rawConfigText, $message, $messageType); function renderLogin(?string $message, string $messageType): void { ?> Adminpanel Login

Adminbereich

Konfiguration sichern

Logge dich mit den statischen Zugangsdaten aus der JSON-Config ein.

Zurück zum Dashboard
Adminpanel

Adminpanel

Konfiguration der Automaten

Hier werden API-Token, Zugangsdaten, Fächer und Alarmwege direkt in der JSON-Config gepflegt.

JSON-Config direkt bearbeiten

Die komplette Konfiguration kann hier als Klartext angezeigt und direkt als JSON gespeichert werden.

Allgemein

Leer lassen für den Domain-Root. Für Unterordner z. B. /auswertung eintragen.

API

ESP32-Clients senden dieses Token im Header Authorization: Bearer ....

Adminzugang

Webhooks

Mehrere Ziele sind möglich. Header können als JSON hinterlegt werden.

Webhooks sind Alert-Ziele, wenn eine Alarmstufe bei einem Fach erreicht wurde

$webhook): ?>

Email-Empfänger

Emails werden über die Serverkonfiguration von mail() versendet.

$email): ?>

Automaten und Fächer

Jeder Automat enthält beliebig viele Fächer, die jeweils genau einem Sensor zugeordnet sind.

$machine): ?>

Webhook

Email-Empfänger

Automat

Fächer können direkt darunter verwaltet werden.

$slot): ?>
Fach
$label, 'name' => trim((string) ($row['name'] ?? '')), 'type' => $type, 'url' => $url, 'enabled' => (string) ($row['enabled'] ?? '1') === '1', 'headers' => is_array($headers) ? $headers : [], ]; // Add Telegram-specific fields if ($type === 'telegram') { $item['bot_token'] = trim((string) ($row['bot_token'] ?? '')); $item['chat_id'] = trim((string) ($row['chat_id'] ?? '')); } $items[] = $item; } return array_values($items); } function normalizeEmails(array $rows): array { $items = []; foreach ($rows as $row) { $label = trim((string) ($row['label'] ?? '')); $address = trim((string) ($row['address'] ?? '')); if ($label === '' && $address === '') { continue; } $items[] = [ 'label' => $label, 'name' => trim((string) ($row['name'] ?? '')), 'address' => $address, 'enabled' => (string) ($row['enabled'] ?? '1') === '1', ]; } return array_values($items); } function normalizeMachines(array $rows): array { $items = []; foreach ($rows as $row) { $id = trim((string) ($row['id'] ?? '')); $name = trim((string) ($row['name'] ?? '')); if ($id === '' && $name === '') { continue; } $slots = []; foreach (($row['slots'] ?? []) as $slot) { $sensorId = trim((string) ($slot['sensor_id'] ?? '')); if ($sensorId === '') { continue; } $slots[] = [ 'sensor_id' => $sensorId, 'label' => trim((string) ($slot['label'] ?? $sensorId)), 'product_name' => trim((string) ($slot['product_name'] ?? '')), 'full_distance_mm' => (float) ($slot['full_distance_mm'] ?? 0), 'empty_distance_mm' => (float) ($slot['empty_distance_mm'] ?? 0), 'distance_per_unit' => max(0.01, (float) ($slot['distance_per_unit'] ?? 1)), 'alert_below_units' => max(0, (int) ($slot['alert_below_units'] ?? 0)), ]; } $items[] = [ 'id' => $id, 'name' => $name, 'location' => trim((string) ($row['location'] ?? '')), 'slots' => $slots, ]; } return array_values($items); } function parseIdList(string $value): array { $parts = array_map('trim', explode(',', $value)); return array_values(array_filter($parts, static fn (string $item): bool => $item !== '')); } function encodeConfigForEditor(array $config): string { $json = json_encode( $config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); return $json === false ? '{}' : $json; }