feat(fusion_clock): NFC kiosk — enrollment, manager page, sounds, lock, profile photos

Kiosk work across this session (19.0.3.6.0 -> 19.0.3.10.0):
- Program-from-unknown-tap: amber prompt -> Manager PIN -> pick/create employee
  -> binds the captured UID (no re-tap). Reassign moves a card between employees.
- Manager page (gear, when unlocked): search employees + tag status; assign/re-tag,
  clear tag, archive employee, + new employee. Server-gated by the enroll password.
- Screen lock: kiosk starts locked (tap-only); Unlock -> Manager PIN, Lock button;
  PIN remembered for the session so the gear never re-prompts.
- Sounds: pleasant + loud sine chimes (rising in / descending out) + a low "denied"
  tone for wrong/unknown taps. Gated by fusion_clock.enable_sounds.
- Guided profile-photo capture for employees with no picture (clock-in or enroll):
  live camera + oval face guide -> capture -> preview -> save to hr.employee.
- PIN no longer re-renders per digit; centered result card; 12h time; clock-out shows
  "Worked Xh Ym this shift"; modern clock idle icon; faster animations/result timers;
  session keep-alive so the kiosk login never expires.
- New endpoints: create_employee, clear_tag, delete_employee (archive), verify_pin,
  save_profile_photo; enroll gains force-reassign.
- Docs: fusion_clock is now developed in Claude Code (dropped Cursor references).

Spec/plan under fusion_clock/docs/superpowers/. Deployed live on entech
(odoo-entech / LXC 111 on pve-worker5), v19.0.3.10.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-30 17:21:33 -04:00
parent 2a16f80d8d
commit 55898dd1d4
10 changed files with 1002 additions and 84 deletions

View File

@@ -57,6 +57,7 @@ html:has(#nfc_kiosk_root) {
padding: 2rem;
box-sizing: border-box;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
background: var(--nfc-bg);
@@ -106,18 +107,19 @@ html:has(#nfc_kiosk_root) {
top: 1.25rem;
left: 50%;
transform: translateX(-50%);
max-height: 52px;
max-width: 220px;
max-height: 64px;
max-width: 260px;
object-fit: contain;
background: rgba(255, 255, 255, 0.20);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
padding: 0.5rem 0.85rem;
border-radius: 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.18);
padding: 0.6rem 1.1rem;
border-radius: 1rem;
border: 2px solid hsla(var(--nfc-h), 85%, 72%, 0.95);
box-shadow:
0 6px 24px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
0 8px 28px rgba(0, 0, 0, 0.4),
0 0 30px hsla(var(--nfc-h), 90%, 62%, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.7);
box-sizing: content-box;
animation: nfc-logo-in 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@@ -190,6 +192,29 @@ html:has(#nfc_kiosk_root) {
border-radius: 999px;
}
.nfc-kiosk__lock {
position: absolute;
bottom: 1.5rem;
right: 4.85rem; // sits just left of the ⚙
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
background: rgba(255,255,255,0.04);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: var(--nfc-text-muted);
border: 1px solid rgba(255,255,255,0.08);
cursor: pointer;
font-size: 1.05rem;
display: none; // JS shows it only when unlocked
align-items: center;
justify-content: center;
z-index: 3;
}
// Unlock button sits in the primary bottom-right corner (shown only while locked)
#nfc_unlock_btn { right: 1.5rem; }
.nfc-kiosk__settings {
position: absolute;
bottom: 1.5rem;
@@ -234,7 +259,7 @@ html:has(#nfc_kiosk_root) {
// State container — base fade-in for whatever child renders
// ─────────────────────────────────────────────────────────────────────
#nfc_state_container > * {
animation: nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
animation: nfc-state-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes nfc-state-in {
@@ -251,6 +276,7 @@ html:has(#nfc_kiosk_root) {
flex-direction: column;
align-items: center;
gap: 2rem;
margin-top: 7rem; // push icon + prompt down so waves clear the clock/time
}
.nfc-kiosk__icon-svg {
@@ -263,6 +289,7 @@ html:has(#nfc_kiosk_root) {
.nfc-chip {
animation: nfc-chip-pulse 2.5s ease-in-out infinite;
transform-origin: center;
transform-box: fill-box;
}
.nfc-wave {
@@ -340,8 +367,10 @@ html:has(#nfc_kiosk_root) {
max-width: 720px;
padding: 2.5rem 3rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
text-align: center;
gap: 1.25rem;
position: relative;
&--success {
@@ -350,7 +379,7 @@ html:has(#nfc_kiosk_root) {
0 20px 60px rgba(0,0,0,0.5),
0 0 80px rgba(24,169,87,0.35),
inset 0 1px 0 rgba(255,255,255,0.1);
animation: nfc-success-burst 700ms cubic-bezier(0.16, 1, 0.3, 1);
animation: nfc-success-burst 350ms cubic-bezier(0.16, 1, 0.3, 1);
}
&--error {
@@ -359,7 +388,7 @@ html:has(#nfc_kiosk_root) {
0 20px 60px rgba(0,0,0,0.5),
0 0 60px rgba(217,55,78,0.3),
inset 0 1px 0 rgba(255,255,255,0.1);
animation: nfc-shake 350ms ease-in-out, nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
animation: nfc-shake 350ms ease-in-out, nfc-state-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
}
@@ -406,7 +435,7 @@ html:has(#nfc_kiosk_root) {
flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.2);
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
animation: nfc-avatar-in 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation: nfc-avatar-in 300ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes nfc-avatar-in {
@@ -415,11 +444,11 @@ html:has(#nfc_kiosk_root) {
}
.nfc-kiosk__result-text {
flex: 1;
text-align: center;
.name { font-size: 2.25rem; font-weight: 600; letter-spacing: -0.02em; }
.action { font-size: 1.5rem; margin-top: 0.5rem; opacity: 0.95; font-weight: 400; }
.hours { font-size: 1.05rem; opacity: 0.75; margin-top: 0.5rem; }
.hours { font-size: 1.35rem; opacity: 0.9; margin-top: 0.6rem; font-weight: 500; }
}
// ─────────────────────────────────────────────────────────────────────
@@ -477,9 +506,11 @@ html:has(#nfc_kiosk_root) {
.nfc-kiosk__enroll-panel {
@extend %nfc-glass;
padding: 2.5rem;
padding: 2rem;
width: 80vw;
max-width: 720px;
max-height: 92vh;
overflow-y: auto;
h2 {
font-size: 1.5rem;
@@ -495,8 +526,8 @@ html:has(#nfc_kiosk_root) {
margin: 1rem 0;
button {
font-size: 2rem;
padding: 1.5rem 0;
font-size: 1.7rem;
padding: 1.1rem 0;
background: rgba(255,255,255,0.05);
color: var(--nfc-text);
border: 1px solid rgba(255,255,255,0.1);
@@ -511,12 +542,12 @@ html:has(#nfc_kiosk_root) {
}
.pin-display {
font-size: 2.5rem;
font-size: 2.2rem;
letter-spacing: 0.5rem;
text-align: center;
margin: 1rem 0 1.5rem;
margin: 0.75rem 0 1rem;
font-variant-numeric: tabular-nums;
min-height: 3rem;
min-height: 2.6rem;
color: hsl(var(--nfc-h), 80%, 70%);
}
@@ -578,6 +609,114 @@ html:has(#nfc_kiosk_root) {
}
}
// Amber accent for the "unknown card → program it" prompt + the inline
// status line in the new-employee form.
.nfc-kiosk__enroll-panel.nfc-kiosk__unknown {
border-color: rgba(224, 168, 62, 0.55);
box-shadow:
0 20px 60px rgba(0,0,0,0.5),
0 0 60px rgba(224, 168, 62, 0.28),
inset 0 1px 0 rgba(255,255,255,0.08);
.unknown-icon { font-size: 3.5rem; line-height: 1; margin-bottom: 0.5rem; color: #e0a83e; }
h2 { color: #e0a83e; }
}
.nfc-kiosk__enroll-panel .enroll-msg {
min-height: 1.4rem;
margin: 0.25rem 0 0.5rem;
color: var(--nfc-error);
font-size: 0.95rem;
text-align: center;
}
// Manager page rows (employee + tag status + per-row actions)
.nfc-kiosk__enroll-panel .manager-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 0.4rem;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-wrap: wrap;
.m-info { display: flex; align-items: baseline; gap: 0.5rem; flex: 1; min-width: 12rem; }
.m-name { font-size: 1.05rem; }
.m-dept { color: var(--nfc-text-muted); font-size: 0.8rem; }
.m-tag { font-size: 0.78rem; color: var(--nfc-text-muted); white-space: nowrap; }
.m-tag--on { color: hsl(var(--nfc-h), 70%, 66%); }
.m-actions { display: flex; gap: 0.4rem; flex-shrink: 0; flex-wrap: wrap; }
.m-btn {
font-size: 0.85rem;
padding: 0.45rem 0.85rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
color: var(--nfc-text);
border: 1px solid rgba(255,255,255,0.1);
cursor: pointer;
&.m-danger { color: #ff8b9a; border-color: rgba(217,55,78,0.45); }
&:active { transform: scale(0.96); }
}
}
// ─────────────────────────────────────────────────────────────────────
// Guided profile-photo capture — live camera with an oval face guide
// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__photo-panel {
@extend %nfc-glass;
padding: 1.5rem;
width: 80vw;
max-width: 540px;
max-height: 92vh;
overflow-y: auto;
text-align: center;
h2 { font-size: 1.4rem; margin: 0 0 1rem; font-weight: 400; }
}
.nfc-photo-stage {
position: relative;
width: 100%;
aspect-ratio: 3 / 4;
max-height: 56vh;
margin: 0 auto;
border-radius: 1rem;
overflow: hidden;
background: #000;
}
.nfc-photo-video,
.nfc-photo-preview {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.nfc-photo-video { transform: scaleX(-1); } // mirror for a natural selfie view
.nfc-photo-guide {
position: absolute;
top: 48%;
left: 50%;
width: 60%;
height: 66%;
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-hint {
position: absolute;
left: 0;
right: 0;
bottom: 0.75rem;
color: #fff;
font-size: 0.95rem;
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.9);
pointer-events: none;
}
// ─────────────────────────────────────────────────────────────────────
// Reduced-motion fallback — respect users who prefer no animation
// ─────────────────────────────────────────────────────────────────────