// identity's registration page logic. Vanilla JS, no framework — see // register.html's top comment for why this is the fallback path, not the primary // (voice) one. "use strict"; const params = new URLSearchParams(location.search); const API = (params.get("api") || "").replace(/\/$/, ""); const TOKEN = params.get("token") || ""; const DEVICE_ID = params.get("device") || "unknown"; if (!API || !TOKEN) { document.body.innerHTML = '
identity not configured — missing ' + "?api=&token= in the URL.
"; throw new Error("identity: missing ?api=/&token= query params"); } function api(path, options) { options = options || {}; options.headers = Object.assign({ Authorization: `Bearer ${TOKEN}` }, options.headers || {}); return fetch(`${API}${path}`, options).then((res) => res.json().then((body) => { if (!res.ok && res.status !== 409) throw new Error(body.error || `${res.status} ${res.statusText}`); return body; }) ); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } // --- Camera (best-effort — registration still works with a text name + voice-found // candidate even if the camera fails; the photo is an audit artifact, never load- // bearing for the actual identity decision, see ../README.md) ----------------- const video = document.getElementById("camera-preview"); const cameraError = document.getElementById("camera-error"); let stream = null; navigator.mediaDevices ?.getUserMedia({ video: { facingMode: "user" }, audio: false }) .then((s) => { stream = s; video.srcObject = s; }) .catch((err) => { cameraError.textContent = `Camera unavailable: ${err.message} (registration still works without it)`; cameraError.hidden = false; }); function capturePhoto() { if (!stream) return Promise.resolve(null); const canvas = document.getElementById("captured-frame"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext("2d").drawImage(video, 0, 0); return new Promise((resolve) => { canvas.toBlob( (blob) => { if (!blob) return resolve(null); api("/register/photo", { method: "POST", body: blob, headers: { "Content-Type": "image/jpeg" } }) .then((r) => resolve(r.photo_id || null)) .catch(() => resolve(null)); }, "image/jpeg", 0.85 ); }); } // --- Registration ------------------------------------------------------------ const form = document.getElementById("register-form"); const status = document.getElementById("register-status"); const picker = document.getElementById("candidate-picker"); const candidateList = document.getElementById("candidate-list"); function attemptRegister(name, entityId, noDevice) { status.textContent = "Registering…"; picker.hidden = true; return capturePhoto().then((photoId) => api("/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, device_id: DEVICE_ID, photo_id: photoId, entity_id: entityId, no_device: !!noDevice, }), }) ).then((result) => { if (result.ok) { status.textContent = result.message; form.reset(); loadPeople(); return; } if (result.reason === "ambiguous") { status.textContent = result.message; candidateList.innerHTML = result.candidates .map( (c) => `` ) .join(""); picker.hidden = false; candidateList.querySelectorAll("[data-entity]").forEach((btn) => { btn.addEventListener("click", () => attemptRegister(name, btn.dataset.entity, false)); }); return; } status.textContent = result.message || "Could not register."; }).catch((err) => { status.textContent = `Error: ${err.message}`; }); } form.addEventListener("submit", (event) => { event.preventDefault(); const name = document.getElementById("f-name").value.trim(); const noDevice = document.getElementById("f-no-device").checked; if (name) attemptRegister(name, null, noDevice); }); // --- Guest (no name, no device — see identity/README.md) ----------------------- const guestBtn = document.getElementById("guest-btn"); const guestStatus = document.getElementById("guest-status"); guestBtn.addEventListener("click", () => { guestStatus.textContent = "Adding guest…"; capturePhoto() .then((photoId) => api("/register/guest", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_id: DEVICE_ID, photo_id: photoId }), }) ) .then((result) => { guestStatus.textContent = result.message; loadPeople(); }) .catch((err) => { guestStatus.textContent = `Error: ${err.message}`; }); }); // --- Profile pictures ---------------------------------------------------------- // Fetched via blob, not a plainNobody registered yet.
'; return; } el.innerHTML = people .map( (p) => `Could not load: ${escapeHtml(err.message)}
`; }); } loadPeople();