|
|
@@ -4,11 +4,14 @@ declare(strict_types=1);
|
|
|
|
|
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
|
|
|
|
|
+header('Content-Type: text/html; charset=UTF-8');
|
|
|
+
|
|
|
$auth = app_admin_auth();
|
|
|
$auth->start();
|
|
|
$configRepository = app_config_repository();
|
|
|
$message = null;
|
|
|
$messageType = 'success';
|
|
|
+$rawConfigText = null;
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
$action = $_POST['action'] ?? '';
|
|
|
@@ -23,49 +26,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
app_redirect(app_url('/admin/'));
|
|
|
}
|
|
|
|
|
|
- $message = 'Login fehlgeschlagen. Bitte Zugangsdaten pruefen.';
|
|
|
+ $message = 'Login fehlgeschlagen. Bitte Zugangsdaten prüfen.';
|
|
|
$messageType = 'error';
|
|
|
}
|
|
|
|
|
|
if ($action === 'save_config' && $auth->isAuthenticated()) {
|
|
|
- $config = $configRepository->getConfig();
|
|
|
- $newPassword = trim((string) ($_POST['admin_password'] ?? ''));
|
|
|
- $config['app'] = [
|
|
|
- 'name' => trim((string) ($_POST['app_name'] ?? 'Getraenkeautomat 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'] ?? []);
|
|
|
+ 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'] ?? '');
|
|
|
|
|
|
- $configRepository->saveConfig($config);
|
|
|
- $message = 'Konfiguration gespeichert.';
|
|
|
- $messageType = 'success';
|
|
|
+ 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, $message, $messageType);
|
|
|
+renderAdmin($config, $rawConfigText, $message, $messageType);
|
|
|
|
|
|
function renderLogin(?string $message, string $messageType): void
|
|
|
{
|
|
|
@@ -98,7 +126,7 @@ function renderLogin(?string $message, string $messageType): void
|
|
|
<input type="password" name="password" required>
|
|
|
</label>
|
|
|
<button class="button button--primary" type="submit">Einloggen</button>
|
|
|
- <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Zurueck zum Dashboard</a>
|
|
|
+ <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Zurück zum Dashboard</a>
|
|
|
</form>
|
|
|
</section>
|
|
|
</main>
|
|
|
@@ -107,7 +135,7 @@ function renderLogin(?string $message, string $messageType): void
|
|
|
<?php
|
|
|
}
|
|
|
|
|
|
-function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
+function renderAdmin(array $config, string $rawConfigText, ?string $message, string $messageType): void
|
|
|
{
|
|
|
?>
|
|
|
<!DOCTYPE html>
|
|
|
@@ -125,7 +153,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<div>
|
|
|
<p class="eyebrow">Adminpanel</p>
|
|
|
<h1>Konfiguration der Automaten</h1>
|
|
|
- <p>Hier werden API-Token, Zugangsdaten, Faecher und Alarmwege direkt in der JSON-Config gepflegt.</p>
|
|
|
+ <p>Hier werden API-Token, Zugangsdaten, Fächer und Alarmwege direkt in der JSON-Config gepflegt.</p>
|
|
|
</div>
|
|
|
<div class="inline-actions">
|
|
|
<a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Dashboard</a>
|
|
|
@@ -137,6 +165,25 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<div class="message message--<?= htmlspecialchars($messageType, ENT_QUOTES) ?>"><?= htmlspecialchars($message, ENT_QUOTES) ?></div>
|
|
|
<?php endif; ?>
|
|
|
|
|
|
+ <section class="form-section">
|
|
|
+ <div class="panel__header">
|
|
|
+ <div>
|
|
|
+ <h2>JSON-Config direkt bearbeiten</h2>
|
|
|
+ <p>Die komplette Konfiguration kann hier als Klartext angezeigt und direkt als JSON gespeichert werden.</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <form method="post" class="admin-form">
|
|
|
+ <input type="hidden" name="action" value="save_raw_config">
|
|
|
+ <label>
|
|
|
+ Konfiguration als JSON
|
|
|
+ <textarea class="config-textarea" name="raw_config" spellcheck="false"><?= htmlspecialchars($rawConfigText, ENT_QUOTES) ?></textarea>
|
|
|
+ </label>
|
|
|
+ <div class="section-actions">
|
|
|
+ <button class="button button--primary" type="submit">JSON speichern</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </section>
|
|
|
+
|
|
|
<form method="post" class="admin-form" id="admin-form">
|
|
|
<input type="hidden" name="action" value="save_config">
|
|
|
|
|
|
@@ -156,7 +203,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<input type="number" min="5" name="dashboard_refresh_seconds" value="<?= (int) ($config['app']['dashboard_refresh_seconds'] ?? 15) ?>" required>
|
|
|
</label>
|
|
|
<label>
|
|
|
- Absender fuer Email
|
|
|
+ Absender für Email
|
|
|
<input type="email" name="default_from_email" value="<?= htmlspecialchars((string) ($config['app']['default_from_email'] ?? ''), ENT_QUOTES) ?>">
|
|
|
</label>
|
|
|
<label>
|
|
|
@@ -164,7 +211,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<input type="text" name="base_path" value="<?= htmlspecialchars((string) ($config['app']['base_path'] ?? ''), ENT_QUOTES) ?>" placeholder="/auswertung">
|
|
|
</label>
|
|
|
</div>
|
|
|
- <p class="field-help">Leer lassen fuer den Domain-Root. Fuer Unterordner z. B. <code>/auswertung</code> eintragen.</p>
|
|
|
+ <p class="field-help">Leer lassen für den Domain-Root. Für Unterordner z. B. <code>/auswertung</code> eintragen.</p>
|
|
|
</section>
|
|
|
|
|
|
<section class="admin-grid">
|
|
|
@@ -186,7 +233,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
</label>
|
|
|
<label>
|
|
|
Neues Passwort
|
|
|
- <input type="password" name="admin_password" placeholder="Leer lassen, um es nicht zu aendern">
|
|
|
+ <input type="password" name="admin_password" placeholder="Leer lassen, um es nicht zu ändern">
|
|
|
</label>
|
|
|
</div>
|
|
|
</section>
|
|
|
@@ -196,9 +243,10 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<div class="panel__header">
|
|
|
<div>
|
|
|
<h2>Webhooks</h2>
|
|
|
- <p>Mehrere Ziele sind moeglich. Header koennen als JSON hinterlegt werden.</p>
|
|
|
+ <p>Mehrere Ziele sind möglich. Header können als JSON hinterlegt werden.</p>
|
|
|
+ <p>Webhooks sind Alert-Ziele, wenn eine Alarmstufe bei einem Fach erreicht wurde</p>
|
|
|
</div>
|
|
|
- <button class="button button--secondary" type="button" data-add="webhook">Webhook hinzufuegen</button>
|
|
|
+ <button class="button button--secondary" type="button" data-add="webhook">Webhook hinzufügen</button>
|
|
|
</div>
|
|
|
<div class="repeat-stack" id="webhook-stack">
|
|
|
<?php foreach (($config['alerts']['webhooks'] ?? []) as $index => $webhook): ?>
|
|
|
@@ -210,10 +258,10 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<section class="form-section">
|
|
|
<div class="panel__header">
|
|
|
<div>
|
|
|
- <h2>Email-Empfaenger</h2>
|
|
|
- <p>Emails werden ueber die Serverkonfiguration von <code>mail()</code> versendet.</p>
|
|
|
+ <h2>Email-Empfänger</h2>
|
|
|
+ <p>Emails werden über die Serverkonfiguration von <code>mail()</code> versendet.</p>
|
|
|
</div>
|
|
|
- <button class="button button--secondary" type="button" data-add="email">Email-Empfaenger hinzufuegen</button>
|
|
|
+ <button class="button button--secondary" type="button" data-add="email">Email-Empfänger hinzufügen</button>
|
|
|
</div>
|
|
|
<div class="repeat-stack" id="email-stack">
|
|
|
<?php foreach (($config['alerts']['emails'] ?? []) as $index => $email): ?>
|
|
|
@@ -225,10 +273,10 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
|
|
|
<section class="form-section">
|
|
|
<div class="panel__header">
|
|
|
<div>
|
|
|
- <h2>Automaten und Faecher</h2>
|
|
|
- <p>Jeder Automat enthaelt beliebig viele Faecher, die jeweils genau einem Sensor zugeordnet sind.</p>
|
|
|
+ <h2>Automaten und Fächer</h2>
|
|
|
+ <p>Jeder Automat enthält beliebig viele Fächer, die jeweils genau einem Sensor zugeordnet sind.</p>
|
|
|
</div>
|
|
|
- <button class="button button--secondary" type="button" data-add="machine">Automat hinzufuegen</button>
|
|
|
+ <button class="button button--secondary" type="button" data-add="machine">Automat hinzufügen</button>
|
|
|
</div>
|
|
|
<div class="repeat-stack" id="machine-stack">
|
|
|
<?php foreach (($config['machines'] ?? []) as $machineIndex => $machine): ?>
|
|
|
@@ -354,7 +402,7 @@ function renderEmailRow(int|string $index, array $email): string
|
|
|
?>
|
|
|
<div class="repeat-card" data-email-row>
|
|
|
<div class="panel__header">
|
|
|
- <h4>Email-Empfaenger</h4>
|
|
|
+ <h4>Email-Empfänger</h4>
|
|
|
<button class="button button--danger" type="button" data-remove="email">Entfernen</button>
|
|
|
</div>
|
|
|
<div class="field-grid">
|
|
|
@@ -391,10 +439,10 @@ function renderMachineRow(int|string $machineIndex, array $machine): string
|
|
|
<div class="panel__header">
|
|
|
<div>
|
|
|
<h4>Automat</h4>
|
|
|
- <p>Faecher koennen direkt darunter verwaltet werden.</p>
|
|
|
+ <p>Fächer können direkt darunter verwaltet werden.</p>
|
|
|
</div>
|
|
|
<div class="inline-actions">
|
|
|
- <button class="button button--secondary" type="button" data-add="slot" data-machine-index="<?= htmlspecialchars((string) $machineIndex, ENT_QUOTES) ?>">Fach hinzufuegen</button>
|
|
|
+ <button class="button button--secondary" type="button" data-add="slot" data-machine-index="<?= htmlspecialchars((string) $machineIndex, ENT_QUOTES) ?>">Fach hinzufügen</button>
|
|
|
<button class="button button--danger" type="button" data-remove="machine">Automat entfernen</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -567,3 +615,13 @@ 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;
|
|
|
+}
|