feat(fusion_clock): Web NFC integration on kiosk page

Adds NDEFReader scan loop, onNfcReading tap dispatcher, handleTap
state machine, postJson helper, capturePhoto stub (Task 17), and
setup wizard activation with error display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-14 01:22:55 -04:00
parent c9be68a575
commit 66e9749853

View File

@@ -112,20 +112,99 @@
// ──────────────────────────────────────────────────────────────
// Setup wizard
// ──────────────────────────────────────────────────────────────
// ──────────────────────────────────────────────────────────────
// Web NFC reader
// ──────────────────────────────────────────────────────────────
let ndefReader = null;
let nfcReady = false;
async function startNfcReader() {
if (!("NDEFReader" in window)) {
throw new Error("Web NFC not supported on this browser/device. Use Chrome on Android.");
}
ndefReader = new NDEFReader();
await ndefReader.scan();
ndefReader.addEventListener("reading", onNfcReading);
ndefReader.addEventListener("readingerror", () => {
console.warn("[nfc-kiosk] reading error; reader still active");
});
nfcReady = true;
}
function onNfcReading(event) {
// event.serialNumber is the card UID — works for raw MIFARE access cards
const uid = (event.serialNumber || "").toUpperCase();
if (!uid) return;
if (currentState === STATE.ENROLL) {
// Enroll Mode handles taps differently (wired up in Task 18)
window.__nfcKiosk._onEnrollTap && window.__nfcKiosk._onEnrollTap(uid);
return;
}
if (currentState !== STATE.IDLE) return; // ignore taps mid-result
handleTap(uid);
}
async function handleTap(uid) {
setState(STATE.PROCESSING);
let photoB64 = "";
try {
photoB64 = await capturePhoto();
} catch (e) {
console.warn("[nfc-kiosk] camera capture failed", e);
// Server enforces photo_required if needed
}
try {
const result = await postJson("/fusion_clock/kiosk/nfc/tap", { card_uid: uid, photo_b64: photoB64 });
if (result.error === "debounce") {
// silent — back to IDLE
setState(STATE.IDLE);
return;
}
setState(STATE.RESULT, result);
} catch (e) {
setState(STATE.RESULT, { error: "network", message: "No connection. Please try again." });
}
}
async function postJson(url, params) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method: "call", params }),
});
const json = await res.json();
return json.result || {};
}
// ──────────────────────────────────────────────────────────────
// Camera capture (real implementation in Task 17, stub for now)
// ──────────────────────────────────────────────────────────────
async function capturePhoto() {
return ""; // overridden in Task 17
}
// ──────────────────────────────────────────────────────────────
// Setup wizard activation
// ──────────────────────────────────────────────────────────────
const setupBtn = document.getElementById("nfc_setup_start");
if (setupBtn) {
setupBtn.addEventListener("click", async () => {
// Web NFC + camera activation lives in Tasks 16 + 17
setState(STATE.IDLE);
try {
await startNfcReader();
setState(STATE.IDLE);
} catch (e) {
stateContainer.innerHTML = `
<div class="nfc-kiosk__setup">
<h2 style="color:#d9374e">Setup failed</h2>
<p>${escapeHtml(e.message)}</p>
</div>
`;
}
});
}
// Expose a tiny API for later tasks
window.__nfcKiosk = {
setState,
STATE,
photoRequired,
debugEnabled,
locationConfigured,
setState, STATE, photoRequired, debugEnabled, locationConfigured,
handleTap, // exposed for mock-tap debug (Task 19)
};
})();