2026-09-30 23:12:51 +01:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Fingerprint Agent · YOUreka Tools</ title >
2026-10-01 02:03:07 +01:00
< link rel = "stylesheet" href = "https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" >
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/@carbon/styles@1.69.0/css/styles.min.css" >
2026-09-30 23:12:51 +01:00
< link rel = "stylesheet" href = "/assets/style.css" >
2026-10-01 02:03:07 +01:00
< link rel = "icon" href = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' rx='4' fill='%230f62fe'/><path d='M12 5a6 6 0 0 0-6 6c0 2.6.8 4.1 1.5 5.2' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M15.5 19a7.5 7.5 0 0 0 3-9.7A6 6 0 0 0 7.1 7.9' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M9.5 18.5a5.5 5.5 0 0 1-.8-7.6 3.8 3.8 0 0 1 7.2 1.5 4.3 4.3 0 0 1-3.4 4.6' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/></svg>" >
< script src = "https://cdn.socket.io/4.4.0/socket.io.min.js" integrity = "sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj" crossorigin = "anonymous" ></ script >
< script type = "module" src = "https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/button.min.js" ></ script >
< script type = "module" src = "https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/notification.min.js" ></ script >
< script type = "module" src = "https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/progress-bar.min.js" ></ script >
< script type = "module" src = "https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tag.min.js" ></ script >
2026-09-30 23:12:51 +01:00
</ head >
< body class = "app" >
< div id = "app" >
< nav class = "nav" >
< div class = "container" >
< a class = "brand" href = "/" >
< span class = "brand-mark" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" aria-hidden = "true" >< path d = "M12 2a8 8 0 0 0-8 8c0 3.5 1 5.5 2 7" />< path d = "M16 21a10 10 0 0 0 4-13 8 8 0 0 0-15.7-1" />< path d = "M8 21a8 8 0 0 1-1-11 5 5 0 0 1 9.7 1.4A6 6 0 0 1 12 17" />< path d = "M12 21a4 4 0 0 0 4-4 4 4 0 0 0-1.2-2.9" /></ svg >
</ span >
YOUreka < small > Tools</ small >
</ a >
< div class = "nav-right" v-cloak v-if = "session" >
2026-10-01 02:03:07 +01:00
< cds-tag v-bind:type = "agent.connected ? 'green' : 'red'"
v-bind:title = "agent.version ? 'fpagent ' + agent.version : ''" > {{ agent.connected ? 'Agent connected' : 'Agent offline' }}</ cds-tag >
2026-09-30 23:12:51 +01:00
< span class = "user-chip" >
< span class = "avatar" > {{ initials }}</ span >
{{ session.firstName || session.phone }}
</ span >
< a class = "linklike" href = "/auth/logout" > Sign out</ a >
</ div >
</ div >
</ nav >
< main class = "container" v-cloak v-if = "session" >
< div class = "page-head" >
< div >
< h1 > Fingerprint Agent</ h1 >
< p class = "sub" >
Enrollment wizard for staff fingerprints on this laptop
< span v-if = "agent.version" class = "faint" > · fpagent {{ agent.version }}</ span >
</ p >
</ div >
</ div >
2026-10-01 02:03:07 +01:00
< cds-inline-notification v-if = "notif.show" v-bind:kind = "notif.kind" v-bind:title = "notif.title"
v-bind:subtitle = "notif.subtitle" hide-close-button style = "margin-bottom: 1rem;" >
</ cds-inline-notification >
2026-09-30 23:12:51 +01:00
2026-10-01 02:03:07 +01:00
<!-- 1 · locate profile -->
2026-09-30 23:12:51 +01:00
< section class = "card panel" >
< div class = "panel-head" >
< span class = "step-chip" > 1</ span >
< h2 > Locate profile</ h2 >
< span class = "hint" v-if = "domainsLastCheckedLabel" > Permissions checked {{ domainsLastCheckedLabel }}</ span >
</ div >
< div class = "row-gap" style = "align-items:flex-end;" >
< div class = "flex-fill" style = "min-width:220px;" >
2026-10-01 02:03:07 +01:00
< label class = "cds--label" for = "domain" > Domain</ label >
< select id = "domain" class = "cds--select-input" v-model = "domain" >
2026-09-30 23:12:51 +01:00
< option v-for = "d in session.domains" v-bind:key = "d.code" v-bind:value = "d.code" > {{ d.name }} ({{ d.code }})</ option >
</ select >
</ div >
< div class = "flex-fill" style = "min-width:170px;" >
2026-10-01 02:03:07 +01:00
< label class = "cds--label" for = "reference" > Staff reference</ label >
< input id = "reference" class = "cds--text-input" v-model . trim = "reference" v-on:keyup . enter = "lookup" placeholder = "e.g. UC/0231" >
2026-09-30 23:12:51 +01:00
</ div >
< div style = "min-width:130px; max-width:170px;" >
2026-10-01 02:03:07 +01:00
< label class = "cds--label" for = "otpLookup" > OTP</ label >
< input id = "otpLookup" class = "cds--text-input" v-model . trim = "otpLookup" v-on:keyup . enter = "lookup" autocomplete = "one-time-code" >
2026-09-30 23:12:51 +01:00
</ div >
2026-10-01 02:03:07 +01:00
< cds-button v-bind:disabled = "!canLookup" v-on:click = "lookup" >
2026-09-30 23:12:51 +01:00
< span v-if = "!busy.lookup" > Get details</ span >
< span v-else > Checking… </ span >
2026-10-01 02:03:07 +01:00
</ cds-button >
< cds-button kind = "ghost" v-bind:disabled = "!refreshEnabled" v-on:click = "refreshDomains"
v-bind:title = "refreshEnabled ? 'Re-check which domains you may capture for' : 'Permissions were checked recently — refresh unlocks ' + refreshEtaLabel" >
2026-09-30 23:12:51 +01:00
< span v-if = "!refreshing" > Refresh domains</ span >
< span v-else > Refreshing… </ span >
2026-10-01 02:03:07 +01:00
</ cds-button >
2026-09-30 23:12:51 +01:00
</ div >
2026-10-01 02:03:07 +01:00
< cds-inline-notification v-if = "!session.domains.length" kind = "warning" title = "No permitted domains" hide-close-button class = "mt-1"
subtitle = "You are not permitted to capture fingerprints for any domain. Contact your domain administrator if you believe this is a mistake." >
</ cds-inline-notification >
2026-09-30 23:12:51 +01:00
</ section >
2026-10-01 02:03:07 +01:00
<!-- 2 · capture -->
2026-09-30 23:12:51 +01:00
< section class = "card panel" v-if = "staff" >
< div class = "panel-head" >
< span class = "step-chip" > 2</ span >
< h2 > Capture fingerprints</ h2 >
< span class = "hint" > Two fingers are required · place the finger on the scanner each scan</ span >
</ div >
< dl class = "details" >
< div >< dt > Name</ dt >< dd > {{ staff.name }}</ dd ></ div >
< div >< dt > Reference</ dt >< dd > {{ staff.reference }}</ dd ></ div >
< div >< dt > Domain reference</ dt >< dd > {{ staff.domainReference }}</ dd ></ div >
< div >< dt > Rank</ dt >< dd > {{ staff.domainRankDescription }}</ dd ></ div >
< div >< dt > Prior enrollments</ dt >< dd > {{ staff.fingerPrintCaptureCount }}</ dd ></ div >
</ dl >
< div class = "finger-grid mt-2" >
< div class = "finger" v-for = "(label, finger) in fingerLabels" v-bind:key = "finger"
v-bind:class = "{ capturing: capturing === finger, done: fingers[finger].status === 'done' }" >
< div class = "finger-head" >
< h3 > {{ label }}</ h3 >
2026-10-01 02:03:07 +01:00
< cds-tag v-bind:type = "fingerTag(finger).type" > {{ fingerTag(finger).text }}</ cds-tag >
2026-09-30 23:12:51 +01:00
</ div >
2026-10-01 02:03:07 +01:00
< cds-progress-bar v-if = "fingers[finger].status === 'scanning'" v-bind:value = "Math.min(fingers[finger].stage, maxStage)"
v-bind:max = "maxStage" v-bind:label = "'Scan ' + Math.min(fingers[finger].stage, maxStage) + ' of ' + maxStage" >
</ cds-progress-bar >
< p class = "meta" v-if = "fingers[finger].status === 'scanning'" > Place the {{ label.toLowerCase() }} on the scanner.</ p >
2026-09-30 23:12:51 +01:00
< p class = "meta" v-if = "fingers[finger].status === 'done'" > Enrollment ID < strong > {{ fingers[finger].id }}</ strong ></ p >
< p class = "meta" v-if = "fingers[finger].status === 'failed'" > The last attempt failed. You can try again.</ p >
< div class = "row-gap mt-1" >
2026-10-01 02:03:07 +01:00
< cds-button v-if = "fingers[finger].status !== 'done'" v-bind:disabled = "capturing !== null" v-on:click = "capture(finger)" >
2026-09-30 23:12:51 +01:00
{{ fingers[finger].status === 'failed' ? 'Retry capture' : 'Capture fingerprint' }}
2026-10-01 02:03:07 +01:00
</ cds-button >
< cds-button kind = "secondary" v-if = "fingers[finger].status === 'done'" v-bind:disabled = "capturing !== null" v-on:click = "capture(finger)" >
2026-09-30 23:12:51 +01:00
Recapture
2026-10-01 02:03:07 +01:00
</ cds-button >
< cds-button kind = "danger" v-if = "capturing === finger" v-on:click = "cancelCapture" > Cancel</ cds-button >
2026-09-30 23:12:51 +01:00
</ div >
</ div >
</ div >
</ section >
2026-10-01 02:03:07 +01:00
<!-- 3 · save -->
2026-09-30 23:12:51 +01:00
< section class = "card panel" v-if = "bothCaptured" >
< div class = "panel-head" >
< span class = "step-chip" > 3</ span >
< h2 > Save enrollment</ h2 >
< span class = "hint" > Both fingerprints will be saved for {{ staff ? staff.name : '' }} ({{ reference }})</ span >
</ div >
< div class = "row-gap" style = "align-items:flex-end;" >
2026-10-01 02:03:07 +01:00
< div style = "min-width:150px; max-width:200px;" >
< label class = "cds--label" for = "otpSave" > OTP</ label >
< input id = "otpSave" class = "cds--text-input" v-model . trim = "otpSave" autocomplete = "one-time-code" >
2026-09-30 23:12:51 +01:00
</ div >
2026-10-01 02:03:07 +01:00
< cds-button v-bind:disabled = "!canSave" v-on:click = "save" >
< span v-if = "!busy.save" > Save enrollment</ span >
2026-09-30 23:12:51 +01:00
< span v-else > Saving… </ span >
2026-10-01 02:03:07 +01:00
</ cds-button >
2026-09-30 23:12:51 +01:00
</ div >
</ section >
</ main >
< main class = "container" v-cloak v-if = "loading && !session" >
< div class = "narrow center-v" >
< div class = "card panel" style = "text-align:center;" > Loading your session… </ div >
</ div >
</ main >
< footer class = "foot" v-cloak v-if = "session" >
< div class = "container" >
< span > Capture agent < strong > {{ agent.captureAgentId || 'connecting…' }}</ strong ></ span >
< span > Signed in as {{ session.phone }}</ span >
</ div >
</ footer >
< script src = "https://cdn.jsdelivr.net/npm/vue@2" ></ script >
< script src = "https://unpkg.com/axios/dist/axios.min.js" ></ script >
< script src = "/assets/fp/app.js" ></ script >
</ div >
</ body >
</ html >