Pārlūkot izejas kodu

adding hosting on subfolder, configurable in config

Josef Straßl 1 mēnesi atpakaļ
vecāks
revīzija
f00679273e

+ 2 - 1
data/config.json

@@ -3,7 +3,8 @@
         "name": "Getraenkeautomat Monitor",
         "timezone": "Europe/Berlin",
         "dashboard_refresh_seconds": 15,
-        "default_from_email": "monitor@example.local"
+        "default_from_email": "monitor@example.local",
+        "base_path": ""
     },
     "api": {
         "bearer_token": "demo-esp32-token"

+ 19 - 1
docs/CONFIG.md

@@ -26,7 +26,8 @@ Beispiel:
   "name": "Getraenkeautomat Monitor",
   "timezone": "Europe/Berlin",
   "dashboard_refresh_seconds": 15,
-  "default_from_email": "monitor@example.local"
+  "default_from_email": "monitor@example.local",
+  "base_path": ""
 }
 ```
 
@@ -40,6 +41,23 @@ Felder:
   - Polling-Intervall fuer das Dashboard
 - `default_from_email`
   - Absenderadresse fuer `mail()`
+- `base_path`
+  - optionaler URL-Pfad fuer Deployments unterhalb der Domain
+  - leer lassen fuer Betrieb direkt unter `/`
+  - Beispiel fuer `domain.de/auswertung/`: `"/auswertung"`
+  - fuehrende und abschliessende Slashes werden beim Speichern normalisiert
+
+Beispiel fuer einen Betrieb unter `https://domain.de/auswertung/`:
+
+```json
+{
+  "name": "Getraenkeautomat Monitor",
+  "timezone": "Europe/Berlin",
+  "dashboard_refresh_seconds": 15,
+  "default_from_email": "monitor@example.local",
+  "base_path": "/auswertung"
+}
+```
 
 ## Bereich `api`
 

+ 12 - 6
public/admin/index.php

@@ -20,7 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
         );
 
         if ($success) {
-            app_redirect('/admin/');
+            app_redirect(app_url('/admin/'));
         }
 
         $message = 'Login fehlgeschlagen. Bitte Zugangsdaten pruefen.';
@@ -35,6 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             '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')),
@@ -75,7 +76,7 @@ function renderLogin(?string $message, string $messageType): void
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>Adminpanel Login</title>
-        <link rel="stylesheet" href="/styles.css">
+        <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
     </head>
     <body>
         <main class="auth-page">
@@ -97,7 +98,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="/">Zurueck zum Dashboard</a>
+                    <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Zurueck zum Dashboard</a>
                 </form>
             </section>
         </main>
@@ -115,7 +116,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>Adminpanel</title>
-        <link rel="stylesheet" href="/styles.css">
+        <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
     </head>
     <body>
         <main class="admin-page">
@@ -127,8 +128,8 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
                         <p>Hier werden API-Token, Zugangsdaten, Faecher und Alarmwege direkt in der JSON-Config gepflegt.</p>
                     </div>
                     <div class="inline-actions">
-                        <a class="button button--ghost" href="/">Dashboard</a>
-                        <a class="button button--secondary" href="/admin/logout.php">Logout</a>
+                        <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Dashboard</a>
+                        <a class="button button--secondary" href="<?= htmlspecialchars(app_url('/admin/logout.php'), ENT_QUOTES) ?>">Logout</a>
                     </div>
                 </div>
 
@@ -158,7 +159,12 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
                                 Absender fuer Email
                                 <input type="email" name="default_from_email" value="<?= htmlspecialchars((string) ($config['app']['default_from_email'] ?? ''), ENT_QUOTES) ?>">
                             </label>
+                            <label>
+                                Basis-Pfad
+                                <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>
                     </section>
 
                     <section class="admin-grid">

+ 1 - 1
public/admin/logout.php

@@ -5,4 +5,4 @@ declare(strict_types=1);
 require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
 
 app_admin_auth()->logout();
-app_redirect('/admin/');
+app_redirect(app_url('/admin/'));

+ 4 - 1
public/app.js

@@ -3,11 +3,14 @@ const dataNode = document.getElementById('initial-status');
 if (dataNode) {
   let currentStatus = JSON.parse(dataNode.textContent || '{}');
   let activeMachine = 'all';
+  const basePath = dataNode.dataset.basePath || '';
   const gridNode = document.getElementById('machine-grid');
   const filterNode = document.getElementById('machine-filter');
   const alertNode = document.getElementById('alert-list');
   const generatedAtNode = document.getElementById('generated-at');
 
+  const appUrl = (path) => `${basePath}${path}`;
+
   const escapeHtml = (value) =>
     String(value)
       .replaceAll('&', '&amp;')
@@ -176,7 +179,7 @@ if (dataNode) {
 
   const refresh = async () => {
     try {
-      const response = await fetch('/api/v1/status.php', { cache: 'no-store' });
+      const response = await fetch(appUrl('/api/v1/status.php'), { cache: 'no-store' });
       if (!response.ok) {
         return;
       }

+ 9 - 5
public/index.php

@@ -13,7 +13,7 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title><?= htmlspecialchars($appName, ENT_QUOTES) ?></title>
-    <link rel="stylesheet" href="/styles.css">
+    <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
 </head>
 <body>
     <div class="page-shell">
@@ -40,8 +40,8 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
                 </div>
             </div>
             <div class="hero__actions">
-                <a class="button button--primary" href="/admin/">Adminpanel</a>
-                <a class="button button--ghost" href="/api/v1/status.php" target="_blank" rel="noreferrer">Status JSON</a>
+                <a class="button button--primary" href="<?= htmlspecialchars(app_url('/admin/'), ENT_QUOTES) ?>">Adminpanel</a>
+                <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/api/v1/status.php'), ENT_QUOTES) ?>" target="_blank" rel="noreferrer">Status JSON</a>
             </div>
         </header>
 
@@ -77,7 +77,11 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
         </main>
     </div>
 
-    <script id="initial-status" type="application/json"><?= json_encode($status, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>
-    <script src="/app.js" defer></script>
+    <script
+        id="initial-status"
+        type="application/json"
+        data-base-path="<?= htmlspecialchars(app_base_path(), ENT_QUOTES) ?>"
+    ><?= json_encode($status, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>
+    <script src="<?= htmlspecialchars(app_url('/app.js'), ENT_QUOTES) ?>" defer></script>
 </body>
 </html>

+ 1 - 1
public/openapi.yaml

@@ -13,7 +13,7 @@ info:
     The readings endpoint uses Bearer token authentication. The status endpoint is
     intentionally public so the dashboard can poll it without a login.
 servers:
-  - url: /
+  - url: ./
     description: Same-origin deployment
 tags:
   - name: Readings

+ 1 - 0
src/ConfigRepository.php

@@ -16,6 +16,7 @@ final class ConfigRepository
                 'timezone' => 'Europe/Berlin',
                 'dashboard_refresh_seconds' => 15,
                 'default_from_email' => 'monitor@example.local',
+                'base_path' => '',
             ],
             'api' => [
                 'bearer_token' => 'change-me-token',

+ 36 - 0
src/bootstrap.php

@@ -94,6 +94,42 @@ function app_redirect(string $location): never
     exit;
 }
 
+function app_normalize_base_path(?string $basePath): string
+{
+    $normalized = trim((string) $basePath);
+    if ($normalized === '' || $normalized === '/') {
+        return '';
+    }
+
+    $normalized = '/' . trim($normalized, '/');
+
+    return $normalized === '/' ? '' : $normalized;
+}
+
+function app_base_path(): string
+{
+    static $basePath = null;
+
+    if ($basePath === null) {
+        $config = app_config_repository()->getConfig();
+        $basePath = app_normalize_base_path((string) ($config['app']['base_path'] ?? ''));
+    }
+
+    return $basePath;
+}
+
+function app_url(string $path = '/'): string
+{
+    $basePath = app_base_path();
+    $normalizedPath = '/' . ltrim($path, '/');
+
+    if ($normalizedPath === '/') {
+        return $basePath === '' ? '/' : $basePath . '/';
+    }
+
+    return $basePath . $normalizedPath;
+}
+
 function app_read_json_body(): array
 {
     $raw = file_get_contents('php://input');