Buero-Ueberschrift durch Agent-Status ersetzen, Erklaertext durch Reload-Countdown ersetzt

Der fett gedruckte Titel "Buero" ueber der Visualisierung heisst jetzt
"Agent-Status", der laengere erklaerende Fliesstext darunter entfaellt.
Stattdessen zeigt der Header rechtsbuendig einen rein kosmetischen
Sekunden-Countdown, der von MONITOR_SYNC_INTERVAL herunterzaehlt und nach
Erreichen von 0 wieder von vorn beginnt (Issue #12). Der Wert wird dazu wie
MONITOR_API_BASE per config.js/envsubst ans Frontend durchgereicht.
This commit is contained in:
fds2610-bot 2026-07-29 00:03:26 +02:00
parent f785440872
commit ec380d2ff8
8 changed files with 103 additions and 27 deletions

View file

@ -20,4 +20,7 @@ CODEBERG_TOKEN=
# Abstand zwischen zwei Codeberg-Sync-Laeufen in Sekunden (Service "sync" in
# docker-compose.yml). Der erste Lauf passiert direkt beim Start des Services.
# Speist ausserdem den rein kosmetischen Reload-Countdown im Header der Buero-Visualisierung
# (Service "web", siehe docker/nginx/default.conf.template) - der Countdown zaehlt nur mit,
# er loest selbst keinen echten Reload/Sync aus.
MONITOR_SYNC_INTERVAL=600

View file

@ -77,6 +77,10 @@ services:
# 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:-}
# Speist nur den kosmetischen Reload-Countdown im Header (Issue #12) - gleicher
# Default wie beim sync-Service oben, damit die Anzeige zur tatsaechlichen
# Sync-Frequenz passt, wenn MONITOR_SYNC_INTERVAL nicht gesetzt ist.
MONITOR_SYNC_INTERVAL: ${MONITOR_SYNC_INTERVAL:-600}
ports:
- "8090:80"

View file

@ -10,17 +10,20 @@ server {
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
# deshalb werden API-Basis-URL und Sync-Intervall nicht als Datei abgelegt, sondern hier
# aus der Umgebung ausgeliefert (MONITOR_API_BASE, MONITOR_SYNC_INTERVAL, siehe .env).
# Das nginx-Image ersetzt ${MONITOR_API_BASE}/${MONITOR_SYNC_INTERVAL} beim Containerstart
# per envsubst - nur Variablen, die tatsaechlich in der Umgebung definiert sind,
# nginx-eigene wie $uri bleiben unangetastet. Ist eine der beiden Variablen gar nicht
# definiert, bleibt ihr 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).
# falsche URL. docker-compose.yml setzt beide Variablen daher immer (ggf. mit Default).
# MONITOR_SYNC_INTERVAL speist nur den rein kosmetischen Reload-Countdown im Header
# (Issue #12) - hat keinen Einfluss auf den tatsaechlichen Update-Takt der Szene.
location = /config.js {
default_type application/javascript;
add_header Cache-Control "no-store";
return 200 'window.MONITOR_API_BASE = "${MONITOR_API_BASE}";';
return 200 'window.MONITOR_API_BASE = "${MONITOR_API_BASE}"; window.MONITOR_SYNC_INTERVAL = ${MONITOR_SYNC_INTERVAL};';
}
location / {

View file

@ -52,6 +52,7 @@ header.office-header {
padding: 12px 24px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
@ -60,10 +61,14 @@ header.office-header h1 {
font-size: 1.4rem;
}
header.office-header p {
margin: 0;
/* Rein kosmetischer Countdown bis zum naechsten Codeberg-Sync (Issue #12), rechtsbuendig
anstelle des frueheren erklaerenden Fliesstexts. Tabular-nums verhindert Wackeln der
Breite, wenn sich die Ziffernzahl aendert (z.B. 9s -> 10s). */
.reload-countdown {
color: #475569;
font-size: 0.9rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Warnbanner, wenn die API nicht erreichbar ist und die Szene auf Demo-Daten zurueckfaellt
@ -126,14 +131,6 @@ header.office-header p {
}
}
/* Bei knappem Platz weicht zuerst der erklaerende Fliesstext - er kostet in flachen
oder schmalen Fenstern mehrere Zeilen Hoehe, die der Szene fehlen. */
@media (max-height: 640px), (max-width: 760px) {
header.office-header p {
display: none;
}
}
@media (max-height: 640px) {
header.office-header {
padding: 8px 16px;

View file

@ -33,6 +33,8 @@ import {
resolveDoorVisitVisual,
advanceDoorVisitStage,
DOOR_VISIT_STAGES,
resolveReloadIntervalSeconds,
nextCountdownSeconds,
} from "./office-scene.js";
import {
DEMO_TICKET_COUNTS,
@ -1024,6 +1026,25 @@ function setApiBanner(visible) {
}
}
// Rein kosmetischer Countdown im Header (Issue #12): zaehlt unabhaengig vom tatsaechlichen
// SSE-/Polling-Takt von MONITOR_SYNC_INTERVAL (aus config.js, siehe office.html) auf 0
// herunter und startet danach von vorn - loest selbst keinen Reload aus.
function startReloadCountdown() {
const countdownEl = document.getElementById("reload-countdown");
if (!countdownEl) {
return;
}
const intervalSeconds = resolveReloadIntervalSeconds(window.MONITOR_SYNC_INTERVAL);
let remaining = intervalSeconds;
countdownEl.textContent = `${remaining}s`;
window.setInterval(() => {
remaining = nextCountdownSeconds(remaining, intervalSeconds);
countdownEl.textContent = `${remaining}s`;
}, 1000);
}
async function boot() {
const container = document.getElementById("office-scene");
if (!container) {
@ -1064,4 +1085,5 @@ async function boot() {
document.addEventListener("DOMContentLoaded", () => {
boot();
startReloadCountdown();
});

View file

@ -538,3 +538,30 @@ export function advanceDoorVisitStage(stage) {
}
return DOOR_VISIT_STAGES[index + 1];
}
// ---------------------------------------------------------------------------------------
// Reload-Countdown im Header (Issue #12): rein kosmetischer Sekundenzaehler, der die
// Sync-Frequenz des Backends (MONITOR_SYNC_INTERVAL, siehe .env.example) im Header
// widerspiegelt - ohne selbst irgendetwas neu zu laden (siehe office-render.js).
// ---------------------------------------------------------------------------------------
export const DEFAULT_RELOAD_INTERVAL_SECONDS = 600;
/**
* @param {unknown} rawValue - z.B. window.MONITOR_SYNC_INTERVAL (aus config.js).
* @returns {number} gueltige Intervall-Laenge in Sekunden, faellt auf den Default zurueck,
* wenn der Rohwert fehlt oder keine positive Zahl ist.
*/
export function resolveReloadIntervalSeconds(rawValue) {
const parsed = Number(rawValue);
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : DEFAULT_RELOAD_INTERVAL_SECONDS;
}
/**
* @param {number} current - aktueller Zaehlerstand in Sekunden.
* @param {number} intervalSeconds - Intervall-Laenge, auf die nach Ablauf zurueckgesetzt wird.
* @returns {number} naechster Zaehlerstand (zaehlt herunter, springt bei <=1 zurueck auf den vollen Wert).
*/
export function nextCountdownSeconds(current, intervalSeconds) {
return current <= 1 ? intervalSeconds : current - 1;
}

View file

@ -9,22 +9,19 @@
</head>
<body>
<header class="office-header">
<h1>Buero</h1>
<p>
Kanban-Board, Monitor-Wand und Schreibtische zeigen live die Daten aus der
<a href="/API.md">REST-API</a> (Push-Updates per Server-Sent Events, Fallback:
Polling). Klick auf das Board oder einen Monitor oeffnet die Detailansicht. Ist
die API nicht erreichbar, zeigt die Szene ersatzweise Demo-Daten.
</p>
<h1>Agent-Status</h1>
<span id="reload-countdown" class="reload-countdown" aria-hidden="true"></span>
</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>
<!-- 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. -->
<!-- Laufzeit-Konfiguration (API-Basis-URL, Sync-Intervall fuer den kosmetischen
Reload-Countdown): wird vom Webserver aus den Umgebungsvariablen MONITOR_API_BASE
und MONITOR_SYNC_INTERVAL 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>

View file

@ -31,6 +31,9 @@ import {
detectDoorVisits,
resolveDoorVisitVisual,
advanceDoorVisitStage,
DEFAULT_RELOAD_INTERVAL_SECONDS,
resolveReloadIntervalSeconds,
nextCountdownSeconds,
} from "../../html/js/office-scene.js";
test("buildBoardColumnLayout liefert alle Spalten in API.md-Reihenfolge", () => {
@ -404,3 +407,23 @@ test("advanceDoorVisitStage durchlaeuft alle Stages und liefert am Ende null", (
test("advanceDoorVisitStage liefert bei unbekannter Stage null", () => {
assert.equal(advanceDoorVisitStage("unbekannt"), null);
});
test("resolveReloadIntervalSeconds uebernimmt gueltige positive Zahlen (auch als String)", () => {
assert.equal(resolveReloadIntervalSeconds(30), 30);
assert.equal(resolveReloadIntervalSeconds("45"), 45);
assert.equal(resolveReloadIntervalSeconds(12.4), 12);
});
test("resolveReloadIntervalSeconds faellt bei fehlendem/ungueltigem Wert auf den Default zurueck", () => {
assert.equal(resolveReloadIntervalSeconds(undefined), DEFAULT_RELOAD_INTERVAL_SECONDS);
assert.equal(resolveReloadIntervalSeconds(null), DEFAULT_RELOAD_INTERVAL_SECONDS);
assert.equal(resolveReloadIntervalSeconds(0), DEFAULT_RELOAD_INTERVAL_SECONDS);
assert.equal(resolveReloadIntervalSeconds(-5), DEFAULT_RELOAD_INTERVAL_SECONDS);
assert.equal(resolveReloadIntervalSeconds("nicht-numerisch"), DEFAULT_RELOAD_INTERVAL_SECONDS);
});
test("nextCountdownSeconds zaehlt herunter und springt bei 1 auf das volle Intervall zurueck", () => {
assert.equal(nextCountdownSeconds(30, 30), 29);
assert.equal(nextCountdownSeconds(2, 30), 1);
assert.equal(nextCountdownSeconds(1, 30), 30);
});