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:
parent
db92f5126e
commit
e96b2e1843
3 changed files with 65 additions and 2 deletions
|
|
@ -27,6 +27,7 @@ import {
|
||||||
mapAgentsToDesks,
|
mapAgentsToDesks,
|
||||||
normalizeAgentForDisplay,
|
normalizeAgentForDisplay,
|
||||||
resolveAgentVisualState,
|
resolveAgentVisualState,
|
||||||
|
isAgentPresent,
|
||||||
pickIdleActivity,
|
pickIdleActivity,
|
||||||
AnimationQueue,
|
AnimationQueue,
|
||||||
detectDoorVisits,
|
detectDoorVisits,
|
||||||
|
|
@ -1003,6 +1004,7 @@ function agentPlacements(agents, deskLayout) {
|
||||||
let overflowIndex = 0;
|
let overflowIndex = 0;
|
||||||
for (const raw of agents) {
|
for (const raw of agents) {
|
||||||
if (seatedIds.has(raw.id)) continue;
|
if (seatedIds.has(raw.id)) continue;
|
||||||
|
if (!isAgentPresent(raw)) continue;
|
||||||
const agent = normalizeAgentForDisplay(raw);
|
const agent = normalizeAgentForDisplay(raw);
|
||||||
const homeFeet = {
|
const homeFeet = {
|
||||||
x: 70 + (overflowIndex % 3) * 26,
|
x: 70 + (overflowIndex % 3) * 26,
|
||||||
|
|
|
||||||
|
|
@ -355,20 +355,49 @@ export function resolveAgentVisualState(state) {
|
||||||
return AGENT_STATE_VISUALS[state] ?? AGENT_STATE_VISUALS.idle;
|
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
|
* 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,
|
* 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"
|
* 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
|
* 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 {Array<object>} agents
|
||||||
* @param {number} [deskCount]
|
* @param {number} [deskCount]
|
||||||
|
* @param {number} [now] - Referenzzeitpunkt fuer den Praesenz-Check (injizierbar fuer Tests).
|
||||||
* @returns {Array<object|null>}
|
* @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);
|
const desks = new Array(deskCount).fill(null);
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
|
if (!isAgentPresent(agent, now)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const deskIndex = agent.desk_index;
|
const deskIndex = agent.desk_index;
|
||||||
if (typeof deskIndex === "number" && deskIndex >= 0 && deskIndex < deskCount) {
|
if (typeof deskIndex === "number" && deskIndex >= 0 && deskIndex < deskCount) {
|
||||||
desks[deskIndex] = agent;
|
desks[deskIndex] = agent;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import {
|
||||||
resolveAgentVisualState,
|
resolveAgentVisualState,
|
||||||
mapAgentsToDesks,
|
mapAgentsToDesks,
|
||||||
normalizeAgentForDisplay,
|
normalizeAgentForDisplay,
|
||||||
|
isAgentPresent,
|
||||||
|
AGENT_PRESENCE_TIMEOUT_MS,
|
||||||
pickIdleActivity,
|
pickIdleActivity,
|
||||||
AnimationQueue,
|
AnimationQueue,
|
||||||
DOOR_VISIT_TYPES,
|
DOOR_VISIT_TYPES,
|
||||||
|
|
@ -281,6 +283,36 @@ test("mapAgentsToDesks ignoriert Agenten ohne Schreibtisch (desk_index null, Rol
|
||||||
assert.ok(desks.every((d) => d === null));
|
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", () => {
|
test("normalizeAgentForDisplay leitet name/pose/location aus display_name/state ab", () => {
|
||||||
const normalized = normalizeAgentForDisplay({ id: 1, display_name: "Alice", state: "working" });
|
const normalized = normalizeAgentForDisplay({ id: 1, display_name: "Alice", state: "working" });
|
||||||
assert.equal(normalized.name, "Alice");
|
assert.equal(normalized.name, "Alice");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue