Agent-Praesenz mit Zehn-Minuten-Timeout versehen

Ein registrierter Agent gilt im Bueromonitor nur noch als anwesend,
solange last_seen_at hoechstens zehn Minuten zurueckliegt (isAgentPresent,
office-scene.js). Meldet er sich laenger nicht, bleibt sein Schreibtisch
frei und er erscheint auch nicht mehr im Ueberlauf-Ruheplatz vor der Tuer,
statt weiterhin faelschlich als anwesend dargestellt zu werden.

Schliesst #17
This commit is contained in:
fds2610-bot 2026-07-29 17:02:49 +02:00
parent db92f5126e
commit e96b2e1843
3 changed files with 65 additions and 2 deletions

View file

@ -27,6 +27,7 @@ import {
mapAgentsToDesks,
normalizeAgentForDisplay,
resolveAgentVisualState,
isAgentPresent,
pickIdleActivity,
AnimationQueue,
detectDoorVisits,
@ -1003,6 +1004,7 @@ function agentPlacements(agents, deskLayout) {
let overflowIndex = 0;
for (const raw of agents) {
if (seatedIds.has(raw.id)) continue;
if (!isAgentPresent(raw)) continue;
const agent = normalizeAgentForDisplay(raw);
const homeFeet = {
x: 70 + (overflowIndex % 3) * 26,

View file

@ -355,20 +355,49 @@ export function resolveAgentVisualState(state) {
return AGENT_STATE_VISUALS[state] ?? AGENT_STATE_VISUALS.idle;
}
// Praesenz-Timeout (Issue #17): ein registrierter Agent gilt nur noch als anwesend, solange
// last_seen_at nicht laenger als zehn Minuten zurueckliegt. Meldet er sich laenger nicht,
// bleibt sein Schreibtisch/Platz leer, obwohl er weiterhin registriert ist.
export const AGENT_PRESENCE_TIMEOUT_MS = 10 * 60 * 1000;
/**
* @param {{last_seen_at?: string}} agent
* @param {number} [now] - Referenzzeitpunkt in ms seit Epoch (injizierbar fuer Tests).
* @returns {boolean} true, wenn der Agent als anwesend gilt. Fehlt last_seen_at (z.B.
* Demo-Daten ohne API-Felder) oder ist es nicht parsbar, gilt der Agent defensiv als
* anwesend, statt faelschlich einen leeren Platz zu zeigen.
*/
export function isAgentPresent(agent, now = Date.now()) {
if (!agent || !agent.last_seen_at) {
return true;
}
const lastSeenAt = new Date(agent.last_seen_at).getTime();
if (Number.isNaN(lastSeenAt)) {
return true;
}
return now - lastSeenAt <= AGENT_PRESENCE_TIMEOUT_MS;
}
/**
* Ordnet eine rohe Agentenliste (GET /api/v1/agents) den Schreibtischen zu - anders als
* buildDeskLayout()/Demo-Daten steht der Sitzplatz hier nicht in der Array-Reihenfolge,
* sondern im Feld desk_index (API.md "Agenten"). Agenten ohne Schreibtisch (role "user"
* oder alle Schreibtische belegt, desk_index === null) erscheinen hier nicht - sie kommen
* gemaess F2 nur anlassbezogen durch eine Tuer und haben keinen festen Platz.
* gemaess F2 nur anlassbezogen durch eine Tuer und haben keinen festen Platz. Agenten, die
* laenger als AGENT_PRESENCE_TIMEOUT_MS nicht mehr gesehen wurden (last_seen_at, Issue #17),
* erscheinen ebenfalls nicht - ihr Schreibtisch bleibt frei.
*
* @param {Array<object>} agents
* @param {number} [deskCount]
* @param {number} [now] - Referenzzeitpunkt fuer den Praesenz-Check (injizierbar fuer Tests).
* @returns {Array<object|null>}
*/
export function mapAgentsToDesks(agents = [], deskCount = DEFAULT_DESK_COUNT) {
export function mapAgentsToDesks(agents = [], deskCount = DEFAULT_DESK_COUNT, now = Date.now()) {
const desks = new Array(deskCount).fill(null);
for (const agent of agents) {
if (!isAgentPresent(agent, now)) {
continue;
}
const deskIndex = agent.desk_index;
if (typeof deskIndex === "number" && deskIndex >= 0 && deskIndex < deskCount) {
desks[deskIndex] = agent;

View file

@ -24,6 +24,8 @@ import {
resolveAgentVisualState,
mapAgentsToDesks,
normalizeAgentForDisplay,
isAgentPresent,
AGENT_PRESENCE_TIMEOUT_MS,
pickIdleActivity,
AnimationQueue,
DOOR_VISIT_TYPES,
@ -281,6 +283,36 @@ test("mapAgentsToDesks ignoriert Agenten ohne Schreibtisch (desk_index null, Rol
assert.ok(desks.every((d) => d === null));
});
test("isAgentPresent gilt als anwesend, solange last_seen_at hoechstens zehn Minuten zurueckliegt (Issue #17)", () => {
const now = Date.parse("2026-07-29T12:00:00Z");
assert.equal(
isAgentPresent({ last_seen_at: "2026-07-29T11:50:00Z" }, now),
true,
"genau zehn Minuten alt gilt noch als anwesend"
);
assert.equal(
isAgentPresent({ last_seen_at: "2026-07-29T11:49:59Z" }, now),
false,
"aelter als zehn Minuten gilt nicht mehr als anwesend"
);
});
test("isAgentPresent faellt bei fehlendem/kaputtem last_seen_at defensiv auf anwesend zurueck", () => {
assert.equal(isAgentPresent({ id: 1 }), true);
assert.equal(isAgentPresent({ id: 1, last_seen_at: "nicht-parsbar" }), true);
});
test("mapAgentsToDesks laesst den Schreibtisch eines Agenten frei, der laenger als zehn Minuten nicht gesehen wurde", () => {
const now = Date.parse("2026-07-29T12:00:00Z");
const staleSeenAt = new Date(now - AGENT_PRESENCE_TIMEOUT_MS - 1000).toISOString();
const desks = mapAgentsToDesks(
[{ id: 1, desk_index: 0, last_seen_at: staleSeenAt }],
2,
now
);
assert.equal(desks[0], null);
});
test("normalizeAgentForDisplay leitet name/pose/location aus display_name/state ab", () => {
const normalized = normalizeAgentForDisplay({ id: 1, display_name: "Alice", state: "working" });
assert.equal(normalized.name, "Alice");