feat(acr-wedge+kiosk): SSE bridge for ACR122U / PC-SC readers

macOS keystroke injection from a CLI-launched Python hits multiple
TCC permission walls (Accessibility AND Automation, both attaching
to identities macOS often can't resolve cleanly). After bouncing
through Quartz, AppleScript, and pyautogui fallbacks, none of them
worked reliably in our test environment.

Switch to a proper IPC channel instead of pretending to be a
keyboard.

Daemon (wedge.py):
  - Adds a ThreadingHTTPServer on 127.0.0.1:8765 exposing /events
  - SSE stream pushes each detected UID as one event
  - 30s keep-alive comments to keep idle connections open
  - CORS: Access-Control-Allow-Origin: * (kiosk page may be on any
    client-domain HTTPS origin; SSE source is always localhost)
  - Keystroke injection kept as best-effort fallback for non-SSE
    clients

Kiosk JS (fusion_clock_nfc_kiosk.js):
  - Adds startWedgeSseListener() that opens EventSource to
    http://localhost:8765/events on setup
  - On message: same handleTap()/_onEnrollTap() flow as Web NFC + HID
  - EventSource auto-reconnects; first error is logged then silenced
  - http://localhost is a "potentially trustworthy origin" so this
    works from https:// pages without mixed-content blocking

Result: ACR122U + wedge.py daemon now drives the kiosk with zero
macOS permission prompts and no focused-window dependency. Same
input plumbing as Web NFC and HID — penalty/photo/activity log
fire identically.

Bump fusion_clock to 19.0.3.3.0.
This commit is contained in:
gsinghpal
2026-05-15 20:10:40 -04:00
parent d75198be9f
commit 00981a502a
3 changed files with 161 additions and 9 deletions

View File

@@ -450,6 +450,59 @@
}
}
// ──────────────────────────────────────────────────────────────
// Local wedge daemon SSE listener.
//
// If a `wedge.py` daemon is running on this machine (used for
// ACR122U / PC/SC readers that can't emit keystrokes themselves),
// it exposes a Server-Sent Events stream at
// http://localhost:8765/events that pushes each detected UID.
//
// Chrome treats http://localhost as a secure origin, so an HTTPS
// kiosk page can connect to it without mixed-content blocking.
// No keystroke injection, no Accessibility permission needed,
// no focused-window dependency.
//
// Routes the UID through the same handleTap()/_onEnrollTap() flow
// as Web NFC and USB HID — so photo, penalty, activity log all
// fire identically.
// ──────────────────────────────────────────────────────────────
const WEDGE_SSE_URL = "http://localhost:8765/events";
let _wedgeEs = null;
function startWedgeSseListener() {
try {
_wedgeEs = new EventSource(WEDGE_SSE_URL);
_wedgeEs.addEventListener("message", (ev) => {
const uid = (ev.data || "").trim().toUpperCase();
if (!uid) return;
debugLog("wedge SSE: " + uid + " state=" + currentState);
if (currentState === STATE.ENROLL) {
window.__nfcKiosk && window.__nfcKiosk._onEnrollTap &&
window.__nfcKiosk._onEnrollTap(uid);
} else if (currentState === STATE.IDLE) {
handleTap(uid);
} else {
debugLog(" → IGNORED: state=" + currentState);
}
});
_wedgeEs.addEventListener("open", () => {
debugLog("wedge SSE: connected to " + WEDGE_SSE_URL);
});
_wedgeEs.addEventListener("error", () => {
// EventSource auto-reconnects; this fires on every
// dropped connection. Log first occurrence only.
if (!_wedgeEs._loggedError) {
debugLog("wedge SSE: connection error (daemon may not be running) — will auto-retry");
_wedgeEs._loggedError = true;
}
});
debugLog("startWedgeSseListener: subscribed to " + WEDGE_SSE_URL);
} catch (e) {
debugLog("startWedgeSseListener: failed to start — " + e.message);
}
}
function startUsbHidListener() {
document.addEventListener("keydown", (e) => {
// Don't capture keystrokes inside form inputs — preserves
@@ -627,6 +680,8 @@
}
// USB HID listener: no permission needed, works on any platform.
startUsbHidListener();
// Local wedge daemon SSE listener (for ACR122U / PC/SC readers).
startWedgeSseListener();
// Camera: best-effort unless photoRequired forces it.
try {
await startCamera();