106 lines
3.6 KiB
JavaScript
106 lines
3.6 KiB
JavaScript
/*
|
|
* Wires floorplan3d.js to identity's GET /floorplan/presence.
|
|
*
|
|
* One request gives the plan AND who is in it, so there is no second call and no window
|
|
* where the rooms are drawn but the people are missing.
|
|
*/
|
|
"use strict";
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
const API = (params.get("api") || "").replace(/\/$/, "");
|
|
const TOKEN = params.get("token") || "";
|
|
const POLL_MS = Number(params.get("poll") || 15000);
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
if (!API || !TOKEN) {
|
|
$("error").hidden = false;
|
|
$("error").textContent = "Not configured — needs ?api=&token= in the URL.";
|
|
throw new Error("floorplan: missing ?api=/&token=");
|
|
}
|
|
|
|
const plan = new Floorplan3D($("plan"), {
|
|
// identity serves the picture; this builds the URL rather than the SDK knowing about
|
|
// any particular service.
|
|
photoUrl: (personId) => `${API}/people/${personId}/photo?token=${encodeURIComponent(TOKEN)}`,
|
|
});
|
|
|
|
let levels = [];
|
|
let activeLevelId = params.get("level") ? Number(params.get("level")) : null;
|
|
|
|
function load() {
|
|
fetch(`${API}/floorplan/presence`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
.then((res) => {
|
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
return res.json();
|
|
})
|
|
.then((data) => {
|
|
$("error").hidden = true;
|
|
levels = data.levels || [];
|
|
if (activeLevelId === null && levels.length) activeLevelId = levels[0].id;
|
|
renderLevelButtons();
|
|
|
|
const level = levels.find((l) => l.id === activeLevelId) || levels[0] || null;
|
|
$("level-name").textContent = level ? level.name : "No levels drawn yet";
|
|
plan.setLevel(level, data.unplaced || []);
|
|
|
|
// The honest count, in the same sentence: placed people and the ones the system
|
|
// cannot locate. A view that silently omits the second number implies it knows
|
|
// where everybody is.
|
|
const placed = (level ? level.rooms || [] : []).reduce(
|
|
(n, room) => n + (room.occupants || []).length, 0
|
|
);
|
|
const unplaced = (data.unplaced || []).length;
|
|
const rooms = level ? level.rooms || [] : [];
|
|
const exact = rooms.reduce(
|
|
(n, room) => n + (room.occupants || []).filter((p) => p.position).length, 0
|
|
);
|
|
const anonymous = rooms.reduce((n, room) => n + (room.targets || []).length, 0);
|
|
|
|
// Every number the view actually knows, said out loud. A summary that reported
|
|
// only "3 placed" would imply the picture is complete when two of the dots are
|
|
// unnamed blobs and one person is somewhere unknown.
|
|
const parts = [`${placed} placed`];
|
|
if (exact) parts.push(`${exact} exact`);
|
|
if (anonymous) parts.push(`${anonymous} unidentified`);
|
|
if (unplaced) parts.push(`${unplaced} home but not locatable`);
|
|
$("summary").textContent = parts.join(" · ");
|
|
})
|
|
.catch((err) => {
|
|
$("error").hidden = false;
|
|
$("error").textContent = `identity: ${err.message}`;
|
|
})
|
|
.finally(() => setTimeout(load, POLL_MS));
|
|
}
|
|
|
|
function renderLevelButtons() {
|
|
if (levels.length < 2) {
|
|
$("levels").innerHTML = "";
|
|
return;
|
|
}
|
|
$("levels").innerHTML = levels
|
|
.map(
|
|
(l) =>
|
|
`<button data-id="${l.id}" class="${l.id === activeLevelId ? "active" : ""}">${escapeHtml(
|
|
l.name
|
|
)}</button>`
|
|
)
|
|
.join("");
|
|
$("levels")
|
|
.querySelectorAll("button")
|
|
.forEach((btn) =>
|
|
btn.addEventListener("click", () => {
|
|
activeLevelId = Number(btn.dataset.id);
|
|
load();
|
|
})
|
|
);
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s ?? "").replace(/[&<>"']/g, (c) =>
|
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
|
|
);
|
|
}
|
|
|
|
load();
|