Frontend-API-URL ueber MONITOR_API_BASE konfigurieren statt aus dem Port raten

Die Basis-URL der API wurde bisher aus dem Port der Seite abgeleitet
(office.html: `if (location.port === "8090")`). Laeuft das Frontend hinter
anderen Ports - in Produktion 4443 (web) / 4444 (api) - griff die Bedingung
nicht, der relative Default /api/v1 lief gegen den Webserver ins Leere, und
die Szene fiel still auf Demo-Daten zurueck: fremde Agenten-Namen und
erfundene Ticketzahlen bei gleichzeitig leeren Detail-Modals.

Die URL kommt jetzt aus MONITOR_API_BASE (.env, Default passend zum
Port-Mapping in docker-compose.yml). Der Webserver liefert den Wert zur
Laufzeit als /config.js aus; das nginx-Image setzt ihn beim Start per
envsubst in die Config ein (default.conf -> default.conf.template). Damit
bleibt html/ ein rein statisches, read-only gemountetes Verzeichnis - kein
Build-Schritt, kein eigenes Entrypoint-Skript. Ein leerer Wert bedeutet
"gleicher Origin" und faellt auf /api/v1 zurueck.

Zusaetzlich macht ein roter Banner "API nicht erreichbar!" den Demo-Fallback
sichtbar, statt ihn wie bisher stillschweigend als echte Daten auszugeben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-07-28 22:54:36 +02:00
parent 9f1fa520d7
commit ba52522415
10 changed files with 113 additions and 31 deletions

View file

@ -3,6 +3,14 @@
# Administrator-Key zur Registrierung neuer Agenten (POST /api/v1/agents, Header X-Admin-Key)
MONITOR_ADMIN_API_KEY=change-me
# Basis-URL, unter der das Frontend (Browser!) die REST-API erreicht. Der Default passt zum
# Port-Mapping des api-Service in docker-compose.yml (8091). Wer die Ports dort aendert oder
# das System hinter einem Reverse Proxy betreibt, muss diesen Wert entsprechend nachziehen -
# z.B. https://monitor.example.org:4444/api/v1.
# Leer lassen, wenn Frontend und API unter demselben Origin ausgeliefert werden (dann wird
# die relative URL /api/v1 verwendet).
MONITOR_API_BASE=http://localhost:8091/api/v1
# Codeberg-Repo, aus dem Backlog/Done per Sync-Skript befuellt werden
CODEBERG_OWNER=fds2610
CODEBERG_REPO=monitor

5
API.md
View file

@ -33,6 +33,11 @@ Implementierung: Vanilla-PHP (kein Framework), Persistenz in SQLite
- Alle Antworten tragen `Access-Control-Allow-Origin: *` (CORS), da Frontend und API im
Docker-Compose-Setup auf unterschiedlichen Ports laufen (siehe
[docker-compose.yml](docker-compose.yml): Frontend `:8090`, API `:8091`).
- Welche Basis-URL das Frontend anspricht, wird nicht geraten, sondern konfiguriert: Der
Webserver liefert sie zur Laufzeit als `/config.js`
(`window.MONITOR_API_BASE`) aus, gespeist aus der Umgebungsvariable `MONITOR_API_BASE`
(siehe `.env.example` und `docker/nginx/default.conf.template`). Leerer Wert = gleicher
Origin, dann wird der relative Pfad `/api/v1` verwendet.
## Authentifizierung

View file

@ -19,7 +19,8 @@ die dazugehörigen Klärungen in [Detailfragen.md](Detailfragen.md) und
Server-Sent Events, Polling-Fallback), Klick auf Board/Monitore öffnet Detail-Modals,
Agenten laufen animiert zwischen Schreibtisch/Board/Monitor-Wand/Kaffee-Ecke (Phase 3-5,
siehe Visualization.md Abschnitt 9). Ist die API nicht erreichbar, zeigt die Szene
ersatzweise Demo-Daten.
ersatzweise Demo-Daten — kenntlich gemacht durch einen roten Banner „API nicht erreichbar!".
Die Basis-URL der API kommt aus `MONITOR_API_BASE` (siehe `.env.example`).
- Weiterhin: klassische Ping-Überwachung (`check_fds.sh`, `hosts.conf`, `mail_fds.py`) als
eine Datenquelle für den System-Monitor; die alte tabellarische Statusseite ist mit
Phase 7 entfallen (F16), `status.txt` speist weiterhin den System-Monitor
@ -53,6 +54,14 @@ docker compose run --rm api composer test # PHPUnit (API)
docker compose run --rm frontend # Node-Tests (Frontend-Layoutlogik)
```
Der Browser erreicht die API nicht über das Compose-Netzwerk, sondern über die in
`MONITOR_API_BASE` (`.env`) hinterlegte URL — der Default `http://localhost:8091/api/v1` passt
zum Port-Mapping oben. **Wer die Ports in `docker-compose.yml` ändert oder das System hinter
einem Reverse Proxy betreibt, muss `MONITOR_API_BASE` entsprechend nachziehen**, sonst zeigt
die Szene den Banner „API nicht erreichbar!" und fällt auf Demo-Daten zurück. Der Webserver
liefert den Wert zur Laufzeit als `/config.js` aus (`docker/nginx/default.conf.template`), ein
Build-Schritt ist dafür nicht nötig.
Details zur API: [api/README.md](api/README.md). Details zu den Endpunkten: [API.md](API.md).

View file

@ -8,6 +8,9 @@ services:
# Optional: echte status.txt aus der Produktion zum Testen einbinden, z.B.
# - ./status.txt:/status.txt:ro
working_dir: /app
# Achtung: Eine Aenderung des Host-Ports muss in .env (MONITOR_API_BASE) nachgezogen
# werden - das Frontend laeuft im Browser und erreicht die API nicht ueber das
# Compose-Netzwerk, sondern ueber genau diesen Port.
ports:
- "8091:8080"
environment:
@ -31,8 +34,14 @@ services:
# Ueberlagert den Platzhalter html/API.md - der muss existieren, weil in
# einem read-only gemounteten Verzeichnis kein Mountpoint angelegt werden kann.
- ./API.md:/usr/share/nginx/html/API.md:ro
# Setzt office.html als Einstiegsseite (sonst 403 auf der Basis-URL).
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
# Setzt office.html als Einstiegsseite (sonst 403 auf der Basis-URL) und liefert
# /config.js aus. Als Template gemountet, damit das nginx-Image beim Start
# MONITOR_API_BASE per envsubst einsetzt (Ausgabe: /etc/nginx/conf.d/default.conf).
- ./docker/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
environment:
# Wird in docker/nginx/default.conf.template eingesetzt und als /config.js an den
# Browser ausgeliefert. Leer = gleicher Origin (relatives /api/v1).
MONITOR_API_BASE: ${MONITOR_API_BASE:-}
ports:
- "8090:80"

View file

@ -1,15 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
# Einstiegsseite ist die Buero-Visualisierung - ohne diese Direktive liefert
# nginx beim Aufruf der Basis-URL ein 403 (Directory-Listing ist deaktiviert,
# und eine index.html gibt es nicht).
index office.html;
location / {
try_files $uri $uri/ =404;
}
}

View file

@ -0,0 +1,29 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
# Einstiegsseite ist die Buero-Visualisierung - ohne diese Direktive liefert
# nginx beim Aufruf der Basis-URL ein 403 (Directory-Listing ist deaktiviert,
# und eine index.html gibt es nicht).
index office.html;
# Laufzeit-Konfiguration fuers Frontend: Das html/-Verzeichnis ist read-only gemountet,
# deshalb wird die API-Basis-URL nicht als Datei abgelegt, sondern hier aus der Umgebung
# ausgeliefert (MONITOR_API_BASE, siehe .env). Das nginx-Image ersetzt ${MONITOR_API_BASE}
# beim Containerstart per envsubst - nur Variablen, die tatsaechlich in der Umgebung
# definiert sind, nginx-eigene wie $uri bleiben unangetastet. Ist MONITOR_API_BASE gar
# nicht definiert, bleibt der Platzhalter stehen und nginx verweigert den Start
# ("unknown \"monitor_api_base\" variable") - besser ein lauter Fehler als eine still
# falsche URL. docker-compose.yml setzt die Variable daher immer (ggf. leer).
location = /config.js {
default_type application/javascript;
add_header Cache-Control "no-store";
return 200 'window.MONITOR_API_BASE = "${MONITOR_API_BASE}";';
}
location / {
try_files $uri $uri/ =404;
}
}

View file

@ -66,6 +66,24 @@ header.office-header p {
font-size: 0.9rem;
}
/* Warnbanner, wenn die API nicht erreichbar ist und die Szene auf Demo-Daten zurueckfaellt
(siehe setApiBanner() in js/office-render.js). */
.api-banner {
flex: 0 0 auto;
padding: 10px 24px;
background: #dc2626;
color: #ffffff;
font-weight: 700;
text-align: center;
letter-spacing: 0.02em;
}
/* Muss nach der Regel oben stehen: ohne diese Zeile wuerde das hidden-Attribut vom
flex-Wert ueberstimmt und der Banner waere dauerhaft sichtbar. */
.api-banner[hidden] {
display: none;
}
#office-scene {
flex: 1 1 auto;
/* min-height: 0 hebt die Flex-Default-Mindesthoehe (min-content) auf - sonst kann

View file

@ -1,13 +1,23 @@
// Duenne fetch()-Wrapper fuer die Phase-1-REST-API (siehe API.md). Reine Daten-Ebene, kein
// DOM-Zugriff - office-render.js ruft diese Funktionen auf und reicht die Ergebnisse an die
// Layoutlogik (office-scene.js) weiter. Basis-URL ist per window.MONITOR_API_BASE
// ueberschreibbar (z.B. wenn Frontend und API auf unterschiedlichen Ports laufen, siehe
// docker-compose.yml: web auf :8090, api auf :8091).
// ueberschreibbar - gesetzt wird sie zur Laufzeit von /config.js, das der Webserver aus der
// Umgebungsvariable MONITOR_API_BASE erzeugt (siehe .env.example und
// docker/nginx/default.conf.template). Noetig, weil Frontend und API auf unterschiedlichen
// Origins laufen koennen (Dev: web :8090, api :8091); die API liefert dafuer CORS-Header,
// siehe api/src/Http/Response.php.
const DEFAULT_API_BASE = "/api/v1";
function apiBase() {
return (typeof window !== "undefined" && window.MONITOR_API_BASE) || DEFAULT_API_BASE;
const configured = typeof window !== "undefined" ? window.MONITOR_API_BASE : null;
// Leer = gleicher Origin. Der startsWith-Check faengt einen nicht ersetzten Platzhalter
// ab (z.B. wenn config.js von einem anderen Webserver ohne Substitution ausgeliefert
// wird) - der waere sonst eine truthy Muell-URL.
if (!configured || configured.startsWith("${")) {
return DEFAULT_API_BASE;
}
return configured;
}
async function getJson(path) {

View file

@ -1013,6 +1013,17 @@ function agentPlacements(agents, deskLayout) {
return placements;
}
// Der Demo-Fallback unten sieht auf den ersten Blick wie eine echte Szene aus (fremde
// Agenten-Namen, erfundene Ticketzahlen, aber leere Detail-Modals). Damit das auf einem
// Produktiv-Monitor nicht fuer echte Daten gehalten wird, blendet der Fallback zusaetzlich
// einen roten Banner ein - und wieder aus, sobald die API antwortet.
function setApiBanner(visible) {
const banner = document.getElementById("api-status-banner");
if (banner) {
banner.hidden = !visible;
}
}
async function boot() {
const container = document.getElementById("office-scene");
if (!container) {
@ -1039,9 +1050,11 @@ async function boot() {
events: data.events,
kpis: data.kpis,
});
setApiBanner(false);
} catch (err) {
console.warn("[office] Live-API nicht erreichbar, zeige Demo-Daten.", err);
renderOfficeScene(container);
setApiBanner(true);
}
}

View file

@ -18,18 +18,14 @@
</p>
</header>
<div id="api-status-banner" class="api-banner" role="alert" hidden>API nicht erreichbar!</div>
<main id="office-scene" aria-label="Buero-Szene"></main>
<script>
// Docker-Compose-Dev-Setup (siehe docker-compose.yml): Frontend auf Port 8090, API auf
// Port 8091 - unterschiedliche Origins, daher die explizite Basis-URL (die API liefert
// dafuer CORS-Header, siehe api/src/Http/Response.php). Bei einem Produktions-Setup mit
// gemeinsamem Origin/Reverse-Proxy kann dieser Block entfernt werden, Default ist dann
// das gleiche Origin unter /api/v1 (siehe html/js/office-api.js).
if (location.port === "8090") {
window.MONITOR_API_BASE = `${location.protocol}//${location.hostname}:8091/api/v1`;
}
</script>
<!-- Laufzeit-Konfiguration (API-Basis-URL): wird vom Webserver aus der Umgebungsvariable
MONITOR_API_BASE erzeugt, siehe docker/nginx/default.conf.template und .env.example.
Muss vor dem Modul stehen - ein klassisches Script laeuft synchron vor jedem Modul. -->
<script src="/config.js"></script>
<script type="module" src="js/office-render.js"></script>
</body>
</html>