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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 / {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue