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:
parent
f785440872
commit
ec380d2ff8
8 changed files with 103 additions and 27 deletions
|
|
@ -20,4 +20,7 @@ CODEBERG_TOKEN=
|
||||||
|
|
||||||
# Abstand zwischen zwei Codeberg-Sync-Laeufen in Sekunden (Service "sync" in
|
# Abstand zwischen zwei Codeberg-Sync-Laeufen in Sekunden (Service "sync" in
|
||||||
# docker-compose.yml). Der erste Lauf passiert direkt beim Start des Services.
|
# 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
|
MONITOR_SYNC_INTERVAL=600
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,10 @@ services:
|
||||||
# Wird in docker/nginx/default.conf.template eingesetzt und als /config.js an den
|
# Wird in docker/nginx/default.conf.template eingesetzt und als /config.js an den
|
||||||
# Browser ausgeliefert. Leer = gleicher Origin (relatives /api/v1).
|
# Browser ausgeliefert. Leer = gleicher Origin (relatives /api/v1).
|
||||||
MONITOR_API_BASE: ${MONITOR_API_BASE:-}
|
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:
|
ports:
|
||||||
- "8090:80"
|
- "8090:80"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,20 @@ server {
|
||||||
index office.html;
|
index office.html;
|
||||||
|
|
||||||
# Laufzeit-Konfiguration fuers Frontend: Das html/-Verzeichnis ist read-only gemountet,
|
# 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
|
# deshalb werden API-Basis-URL und Sync-Intervall nicht als Datei abgelegt, sondern hier
|
||||||
# ausgeliefert (MONITOR_API_BASE, siehe .env). Das nginx-Image ersetzt ${MONITOR_API_BASE}
|
# aus der Umgebung ausgeliefert (MONITOR_API_BASE, MONITOR_SYNC_INTERVAL, siehe .env).
|
||||||
# beim Containerstart per envsubst - nur Variablen, die tatsaechlich in der Umgebung
|
# Das nginx-Image ersetzt ${MONITOR_API_BASE}/${MONITOR_SYNC_INTERVAL} beim Containerstart
|
||||||
# definiert sind, nginx-eigene wie $uri bleiben unangetastet. Ist MONITOR_API_BASE gar
|
# per envsubst - nur Variablen, die tatsaechlich in der Umgebung definiert sind,
|
||||||
# nicht definiert, bleibt der Platzhalter stehen und nginx verweigert den Start
|
# 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
|
# ("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 {
|
location = /config.js {
|
||||||
default_type application/javascript;
|
default_type application/javascript;
|
||||||
add_header Cache-Control "no-store";
|
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 / {
|
location / {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ header.office-header {
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,10 +61,14 @@ header.office-header h1 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header.office-header p {
|
/* Rein kosmetischer Countdown bis zum naechsten Codeberg-Sync (Issue #12), rechtsbuendig
|
||||||
margin: 0;
|
anstelle des frueheren erklaerenden Fliesstexts. Tabular-nums verhindert Wackeln der
|
||||||
|
Breite, wenn sich die Ziffernzahl aendert (z.B. 9s -> 10s). */
|
||||||
|
.reload-countdown {
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 0.9rem;
|
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
|
/* 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) {
|
@media (max-height: 640px) {
|
||||||
header.office-header {
|
header.office-header {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import {
|
||||||
resolveDoorVisitVisual,
|
resolveDoorVisitVisual,
|
||||||
advanceDoorVisitStage,
|
advanceDoorVisitStage,
|
||||||
DOOR_VISIT_STAGES,
|
DOOR_VISIT_STAGES,
|
||||||
|
resolveReloadIntervalSeconds,
|
||||||
|
nextCountdownSeconds,
|
||||||
} from "./office-scene.js";
|
} from "./office-scene.js";
|
||||||
import {
|
import {
|
||||||
DEMO_TICKET_COUNTS,
|
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() {
|
async function boot() {
|
||||||
const container = document.getElementById("office-scene");
|
const container = document.getElementById("office-scene");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
|
@ -1064,4 +1085,5 @@ async function boot() {
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
boot();
|
boot();
|
||||||
|
startReloadCountdown();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -538,3 +538,30 @@ export function advanceDoorVisitStage(stage) {
|
||||||
}
|
}
|
||||||
return DOOR_VISIT_STAGES[index + 1];
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,19 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="office-header">
|
<header class="office-header">
|
||||||
<h1>Buero</h1>
|
<h1>Agent-Status</h1>
|
||||||
<p>
|
<span id="reload-countdown" class="reload-countdown" aria-hidden="true"></span>
|
||||||
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>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="api-status-banner" class="api-banner" role="alert" hidden>API nicht erreichbar!</div>
|
<div id="api-status-banner" class="api-banner" role="alert" hidden>API nicht erreichbar!</div>
|
||||||
|
|
||||||
<main id="office-scene" aria-label="Buero-Szene"></main>
|
<main id="office-scene" aria-label="Buero-Szene"></main>
|
||||||
|
|
||||||
<!-- Laufzeit-Konfiguration (API-Basis-URL): wird vom Webserver aus der Umgebungsvariable
|
<!-- Laufzeit-Konfiguration (API-Basis-URL, Sync-Intervall fuer den kosmetischen
|
||||||
MONITOR_API_BASE erzeugt, siehe docker/nginx/default.conf.template und .env.example.
|
Reload-Countdown): wird vom Webserver aus den Umgebungsvariablen MONITOR_API_BASE
|
||||||
Muss vor dem Modul stehen - ein klassisches Script laeuft synchron vor jedem Modul. -->
|
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 src="/config.js"></script>
|
||||||
<script type="module" src="js/office-render.js"></script>
|
<script type="module" src="js/office-render.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ import {
|
||||||
detectDoorVisits,
|
detectDoorVisits,
|
||||||
resolveDoorVisitVisual,
|
resolveDoorVisitVisual,
|
||||||
advanceDoorVisitStage,
|
advanceDoorVisitStage,
|
||||||
|
DEFAULT_RELOAD_INTERVAL_SECONDS,
|
||||||
|
resolveReloadIntervalSeconds,
|
||||||
|
nextCountdownSeconds,
|
||||||
} from "../../html/js/office-scene.js";
|
} from "../../html/js/office-scene.js";
|
||||||
|
|
||||||
test("buildBoardColumnLayout liefert alle Spalten in API.md-Reihenfolge", () => {
|
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", () => {
|
test("advanceDoorVisitStage liefert bei unbekannter Stage null", () => {
|
||||||
assert.equal(advanceDoorVisitStage("unbekannt"), 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);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue