feat(fusion_clock): NFC photo capture — 10s auto-capture, vertical oval, cache-busted avatar

- Profile photo DID save (verified: image_1920 attachment persists); the
  "doesn't update" was a browser-cache miss. Add ?unique=<write_date> to the
  result-card avatar URL so a freshly-captured photo shows on clock in/out.
- Capture now starts a 10-second countdown (time to get into frame) then
  auto-snaps; the button toggles to Cancel while counting.
- Face guide is now a VERTICAL oval (aspect-ratio 3/4) over a portrait stage —
  it was rendering horizontal. Faces are taller than wide.

Deployed live to entech (LXC 111) as 19.0.3.11.3; frontend bundle verified to
compile clean and contain the new rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-30 18:14:19 -04:00
parent 09cea73e50
commit e26a7cd9e8
4 changed files with 73 additions and 11 deletions

View File

@@ -1039,6 +1039,7 @@
<h2>Take ${escapeHtml(employeeName)}'s photo</h2>
<div class="nfc-photo-stage" id="photo_stage">
<div class="nfc-photo-guide"></div>
<div class="nfc-photo-countdown" id="photo_countdown"></div>
<div class="nfc-photo-hint">Center the face in the oval</div>
</div>
<div class="actions" style="justify-content:space-between">
@@ -1055,11 +1056,46 @@
v.srcObject = cameraStream;
stage.insertBefore(v, stage.firstChild);
v.play().catch(() => {});
document.getElementById("photo_capture").addEventListener("click", () => {
const captureBtn = document.getElementById("photo_capture");
const skipBtn = document.getElementById("photo_skip");
const cdEl = document.getElementById("photo_countdown");
const hintEl = stage.querySelector(".nfc-photo-hint");
let cdTimer = null;
const doCapture = () => {
captured = _captureProfileSquare(v);
if (captured) renderPreview(); else finish();
};
// Click Capture → 10s countdown (time to get into frame) → auto-snap.
// While counting, the button becomes Cancel (cdTimer also acts as the flag).
captureBtn.addEventListener("click", () => {
if (cdTimer) {
clearInterval(cdTimer); cdTimer = null;
cdEl.classList.remove("is-active"); cdEl.textContent = "";
if (hintEl) hintEl.textContent = "Center the face in the oval";
captureBtn.textContent = "Capture";
return;
}
let n = 10;
captureBtn.textContent = "Cancel";
if (hintEl) hintEl.textContent = "Get ready…";
cdEl.textContent = n;
cdEl.classList.add("is-active");
cdTimer = setInterval(() => {
n -= 1;
if (n <= 0) {
clearInterval(cdTimer); cdTimer = null;
cdEl.classList.remove("is-active"); cdEl.textContent = "";
doCapture();
return;
}
cdEl.textContent = n;
}, 1000);
});
skipBtn.addEventListener("click", () => {
if (cdTimer) { clearInterval(cdTimer); cdTimer = null; }
finish();
});
document.getElementById("photo_skip").addEventListener("click", finish);
}
renderLive();
}

View File

@@ -683,9 +683,10 @@ html:has(#nfc_kiosk_root) {
}
.nfc-photo-stage {
position: relative;
width: 100%;
aspect-ratio: 3 / 4;
max-height: 56vh;
aspect-ratio: 3 / 4; // portrait — width follows the (height-driven) box
height: 56vh;
max-height: 440px;
max-width: 100%;
margin: 0 auto;
border-radius: 1rem;
overflow: hidden;
@@ -703,16 +704,36 @@ html:has(#nfc_kiosk_root) {
.nfc-photo-video { transform: scaleX(-1); } // mirror for a natural selfie view
.nfc-photo-guide {
position: absolute;
top: 48%;
top: 47%;
left: 50%;
width: 60%;
height: 66%;
width: 54%;
aspect-ratio: 3 / 4; // VERTICAL oval — a face is taller than it is wide
transform: translate(-50%, -50%);
border: 3px dashed rgba(255, 255, 255, 0.92);
border-radius: 50%;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); // dim everything outside the oval
pointer-events: none;
}
.nfc-photo-countdown {
position: absolute;
top: 47%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 6rem;
font-weight: 200;
line-height: 1;
color: #fff;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.85);
pointer-events: none;
opacity: 0;
&.is-active { opacity: 1; animation: nfc-cd-pop 1s ease-out infinite; }
}
@keyframes nfc-cd-pop {
0% { transform: translate(-50%, -50%) scale(1.25); opacity: 0.35; }
40% { opacity: 1; }
100% { transform: translate(-50%, -50%) scale(0.9); opacity: 0.7; }
}
.nfc-photo-hint {
position: absolute;
left: 0;