Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
All notable changes to FaceID. The Home Assistant app shows this file in the
update dialog; standalone users can watch GitHub releases.

## 5.1.1 — 2026-08-11

- **Direct route navigation:** every camera bubble in the visit clip player is now a
real touch-friendly button. Selecting it immediately stops the current clip and
loads that exact recognition event.
- **Clear playlist state:** the active clip, fully played clips and missing Frigate
recordings have distinct accessible states. After playback ends, the bubbles remain
available for instant replay or investigation in any order.

## 5.1.0 — 2026-08-11

- **Temporary Guest Access:** create a guest from one quality-checked photo, limit
Expand Down
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "5.1.0"
VERSION = "5.1.1"
9 changes: 9 additions & 0 deletions faceid-addon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
All notable changes to FaceID. The Home Assistant app shows this file in the
update dialog; standalone users can watch GitHub releases.

## 5.1.1 — 2026-08-11

- **Direct route navigation:** every camera bubble in the visit clip player is now a
real touch-friendly button. Selecting it immediately stops the current clip and
loads that exact recognition event.
- **Clear playlist state:** the active clip, fully played clips and missing Frigate
recordings have distinct accessible states. After playback ends, the bubbles remain
available for instant replay or investigation in any order.

## 5.1.0 — 2026-08-11

- **Temporary Guest Access:** create a guest from one quality-checked photo, limit
Expand Down
2 changes: 1 addition & 1 deletion faceid-addon/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "5.1.0"
VERSION = "5.1.1"
4 changes: 2 additions & 2 deletions faceid-addon/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: FaceID
version: "5.1.0"
version: "5.1.1"
slug: faceid
description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors
url: https://github.com/r11a/faceid
Expand All @@ -12,7 +12,7 @@ boot: auto
init: false
ingress: true
ingress_port: 8600
ingress_entry: ui-5.1.0
ingress_entry: ui-5.1.1
panel_icon: mdi:face-recognition
panel_title: FaceID
ports:
Expand Down
23 changes: 16 additions & 7 deletions faceid-addon/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
.sync-card input{position:absolute;top:14px;right:14px;z-index:2;width:20px;height:20px}
.sync-card.done{opacity:.55}.progress-note{padding:12px;border-right:3px solid var(--acc);background:var(--panel);color:var(--dim)}
.product-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(285px,1fr));gap:14px}.product-card{border:1px solid var(--line);border-radius:20px;background:linear-gradient(150deg,#24272a,#181a1c);padding:17px;box-shadow:0 12px 34px #0004}.product-card.ready{border-color:#50785a}.product-card.review{border-color:#78613e}.product-card.new{border-style:dashed}.product-card .card-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.state-chip{display:inline-flex;border-radius:99px;padding:4px 9px;font-size:11px;font-weight:800;background:var(--panel2);color:var(--dim)}.state-chip.ready{color:#aee6b8}.state-chip.review{color:#f2cb8c}.wizard{border:1px solid color-mix(in oklab,var(--acc) 55%,var(--line));border-radius:22px;background:linear-gradient(135deg,color-mix(in oklab,var(--panel) 88%,var(--acc) 12%),var(--panel));padding:22px;margin-bottom:22px}.wizard h2{font:800 24px var(--sans);color:var(--fg);text-transform:none;letter-spacing:0}.wizard-flow{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0}.wizard-flow div{display:flex;gap:9px;align-items:center;color:var(--dim)}.wizard-flow b{width:28px;height:28px;border-radius:9px;background:var(--acc);color:#152018;display:grid;place-items:center}.intercom-preview{position:relative;border-radius:18px;overflow:hidden;background:#08090a;min-height:280px;display:grid;place-items:center}.intercom-preview img{width:100%;max-height:62vh;object-fit:contain}.intercom-guide{position:absolute;border:3px solid #74cbe8;border-radius:42% 42% 48% 48%;box-shadow:0 0 0 9999px #0004;pointer-events:none}.result-banner{padding:13px 16px;border-radius:14px;background:var(--panel2);border-right:4px solid var(--accent2);margin:12px 0}.result-banner.excellent{border-color:var(--good)}.result-banner.improve,.result-banner.acceptable{border-color:var(--warn)}.result-banner.no_face{border-color:var(--bad)}.danger{color:#ffd1d5!important;border-color:#824a50!important}.user-dialog-grid{display:grid;grid-template-columns:180px 1fr;gap:18px}.user-dialog-grid .avatar{width:180px;height:180px}.upload-report{display:grid;gap:7px;margin-top:12px}.upload-report div{padding:9px;border-radius:10px;background:var(--panel2)}
.guest-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}.guest-card{display:grid;grid-template-columns:82px 1fr;gap:13px;padding:15px;border:1px solid var(--line);border-radius:18px;background:var(--panel)}.guest-card img{width:82px;height:82px;object-fit:cover;border-radius:14px}.guest-card .card-actions{grid-column:1/-1;display:flex;gap:7px;flex-wrap:wrap}.site-stage{position:relative;min-height:520px;border:1px solid var(--line);border-radius:22px;overflow:hidden;background-color:#151719;background-image:linear-gradient(#ffffff08 1px,transparent 1px),linear-gradient(90deg,#ffffff08 1px,transparent 1px);background-size:32px 32px}.site-stage svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.site-node{position:absolute;transform:translate(-50%,-50%);min-width:118px;padding:10px 12px;border:1px solid #5d6862;border-radius:14px;background:#222628ee;color:var(--fg);box-shadow:0 10px 28px #0007;cursor:grab;touch-action:none}.site-node.restricted{border-color:var(--bad)}.site-node.intercom,.site-node.entry,.site-node.entry_exit{border-color:var(--good)}.site-node small{display:block;color:var(--dim);margin-top:3px}.traffic-bar{height:10px;border-radius:99px;background:linear-gradient(90deg,#92d3a0,#e5b96f,#d97878);min-width:3px}.playlist-progress{display:flex;gap:6px;flex-wrap:wrap;padding:10px 0}.playlist-progress span{padding:5px 9px;border-radius:99px;background:var(--panel2);color:var(--dim)}.playlist-progress span.on{background:var(--acc);color:#101410}.playlist-progress span.done{color:var(--good)}
.guest-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}.guest-card{display:grid;grid-template-columns:82px 1fr;gap:13px;padding:15px;border:1px solid var(--line);border-radius:18px;background:var(--panel)}.guest-card img{width:82px;height:82px;object-fit:cover;border-radius:14px}.guest-card .card-actions{grid-column:1/-1;display:flex;gap:7px;flex-wrap:wrap}.site-stage{position:relative;min-height:520px;border:1px solid var(--line);border-radius:22px;overflow:hidden;background-color:#151719;background-image:linear-gradient(#ffffff08 1px,transparent 1px),linear-gradient(90deg,#ffffff08 1px,transparent 1px);background-size:32px 32px}.site-stage svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.site-node{position:absolute;transform:translate(-50%,-50%);min-width:118px;padding:10px 12px;border:1px solid #5d6862;border-radius:14px;background:#222628ee;color:var(--fg);box-shadow:0 10px 28px #0007;cursor:grab;touch-action:none}.site-node.restricted{border-color:var(--bad)}.site-node.intercom,.site-node.entry,.site-node.entry_exit{border-color:var(--good)}.site-node small{display:block;color:var(--dim);margin-top:3px}.traffic-bar{height:10px;border-radius:99px;background:linear-gradient(90deg,#92d3a0,#e5b96f,#d97878);min-width:3px}.playlist-progress{display:flex;gap:6px;flex-wrap:wrap;padding:10px 0}.playlist-progress button{min-height:44px;padding:8px 13px;border:1px solid var(--line);border-radius:99px;background:var(--panel2);color:var(--dim);cursor:pointer;font:750 12px var(--sans);touch-action:manipulation}.playlist-progress button:hover,.playlist-progress button:focus-visible{border-color:var(--acc);color:var(--fg);outline:none}.playlist-progress button.on{background:var(--acc);border-color:var(--acc);color:#101410;box-shadow:0 0 0 3px color-mix(in oklab,var(--acc) 22%,transparent)}.playlist-progress button.done{color:var(--good);border-color:#4d7055}.playlist-progress button.missing{color:var(--warn);text-decoration:line-through}
.liveness-visual{position:relative;margin:14px 0;border-radius:16px;overflow:hidden;background:#090a0b}.liveness-visual img{display:block;width:100%;height:auto}.face-outline{display:none;position:absolute;border:3px solid var(--warn);border-radius:12px;box-shadow:0 0 0 2px #0009,0 0 22px color-mix(in oklab,var(--warn) 65%,transparent);pointer-events:none}.face-outline.live{border-color:var(--good);box-shadow:0 0 0 2px #0009,0 0 22px color-mix(in oklab,var(--good) 65%,transparent)}.face-outline.spoof{border-color:var(--bad);box-shadow:0 0 0 2px #0009,0 0 22px color-mix(in oklab,var(--bad) 65%,transparent)}.face-outline span{position:absolute;right:-3px;bottom:calc(100% + 5px);white-space:nowrap;padding:5px 9px;border-radius:8px;background:#101214e8;border:1px solid currentColor;color:#fff;font:800 12px var(--mono)}.live-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:10px 0}.live-metric{background:var(--panel2);border:1px solid var(--line);border-radius:11px;padding:9px}.live-metric b,.live-metric span{display:block}.live-metric b{font-size:15px}.live-metric span{color:var(--dim);font-size:10px;margin-top:2px}.guidance-list{display:grid;gap:6px;margin-top:9px}.guidance-list div{padding:8px 10px;border-radius:9px;background:#ffffff08}.guidance-list div::before{content:'←';color:var(--acc);margin-left:7px;font-weight:900}
@media(max-width:850px){.hero{grid-template-columns:1fr}.summary-grid{grid-template-columns:repeat(2,1fr)}nav{top:64px}}
@media(max-width:850px){.filters{grid-template-columns:1fr 1fr}.event-media{grid-template-columns:1fr}}
Expand Down Expand Up @@ -267,7 +267,7 @@ <h1><span>Face</span>ID <small style="font-weight:500;color:var(--dim)">מרכז
<dialog id="videoOverlay" class="video-overlay" onclick="if(event.target===this)closeEventVideo()"><div class="video-panel" onclick="event.stopPropagation()"><button class="ghost video-close" onclick="closeEventVideo()">✕ סגור</button><video id="eventVideo" controls playsinline preload="metadata"></video><div id="videoMessage" class="stat" style="display:none;padding:18px;text-align:center"></div></div></dialog>

<script>
const UI_VERSION='5.1.0';
const UI_VERSION='5.1.1';
const $=s=>document.querySelector(s), main=$('#main');
let tab='dashboard', persons={}, selected=new Set(), suggestThr=0.40,currentSession={role:'admin',tabs:['*'],enforced:false};
let workspace=localStorage.getItem('faceid-workspace')||'operate';
Expand Down Expand Up @@ -1094,17 +1094,26 @@ <h2>מעברים נפוצים</h2><div class="row">${data.analytics.transitions.

async function showRoutePlaylist(timeline){
if(!timeline?.length){toast('אין אירועים עם קליפים במסלול הזה','error');return}
routePlaylist={items:timeline,index:0,missing:0};const overlay=$('#videoOverlay'),panel=overlay.querySelector('.video-panel');
routePlaylist={items:timeline,index:0,missing:new Set(),played:new Set()};const overlay=$('#videoOverlay'),panel=overlay.querySelector('.video-panel');
let status=$('#routePlaylistStatus');if(!status){status=document.createElement('div');status.id='routePlaylistStatus';panel.insertBefore(status,$('#eventVideo'))}
if(!overlay.open)overlay.showModal();await playRouteStep();
}
async function playRouteStep(){
if(!routePlaylist)return;const {items,index}=routePlaylist,video=$('#eventVideo'),message=$('#videoMessage'),status=$('#routePlaylistStatus');
if(index>=items.length){video.style.display='none';message.style.display='block';message.innerHTML=`המסלול הסתיים. נוגנו ${items.length-routePlaylist.missing} קליפים${routePlaylist.missing?` ודולגו ${routePlaylist.missing} קליפים שלא נשמרו ב־Frigate`:''}.`;return}
status.innerHTML=`<b>ניגון מסלול · ${index+1}/${items.length}</b><div class="playlist-progress">${items.map((x,i)=>`<span class="${i===index?'on':i<index?'done':''}">${i<index?'✓ ':''}${esc(x.camera)}</span>`).join('')}</div>`;
if(index>=items.length){video.style.display='none';message.style.display='block';message.innerHTML=`המסלול הסתיים. נוגנו ${routePlaylist.played.size} קליפים${routePlaylist.missing.size?` ודולגו ${routePlaylist.missing.size} קליפים שלא נשמרו ב־Frigate`:''}. לחצו על מצלמה כדי לנגן שוב.`;routePlaylist.index=items.length;renderRouteButtons();return}
renderRouteButtons();
const item=items[index];message.style.display='block';message.textContent=`מכין קליפ ממצלמת ${item.camera}…`;video.style.display='none';
try{await j(`api/activity/${encodeURIComponent(item.event_id)}/clip/status?prepare=true`);video.onerror=()=>{if(routePlaylist){routePlaylist.missing++;routePlaylist.index++;playRouteStep()}};video.onloadedmetadata=()=>{message.style.display='none';video.style.display='block'};video.onended=()=>{if(routePlaylist){routePlaylist.index++;playRouteStep()}};video.src=`api/activity/${encodeURIComponent(item.event_id)}/clip`;video.load();video.play().catch(()=>{})}
catch(e){if(routePlaylist){routePlaylist.missing++;routePlaylist.index++;playRouteStep()}}
try{await j(`api/activity/${encodeURIComponent(item.event_id)}/clip/status?prepare=true`);video.onerror=()=>{if(routePlaylist){routePlaylist.missing.add(index);routePlaylist.index=index+1;playRouteStep()}};video.onloadedmetadata=()=>{if(routePlaylist)routePlaylist.missing.delete(index);message.style.display='none';video.style.display='block';renderRouteButtons()};video.onended=()=>{if(routePlaylist){routePlaylist.played.add(index);routePlaylist.index=index+1;playRouteStep()}};video.src=`api/activity/${encodeURIComponent(item.event_id)}/clip`;video.load();video.play().catch(()=>{})}
catch(e){if(routePlaylist){routePlaylist.missing.add(index);routePlaylist.index=index+1;playRouteStep()}}
}
function renderRouteButtons(){
if(!routePlaylist)return;const status=$('#routePlaylistStatus'),{items,index,played,missing}=routePlaylist;
status.innerHTML=`<b>${index<items.length?`ניגון מסלול · ${index+1}/${items.length}`:'המסלול הסתיים'}</b><div class="playlist-progress">${items.map((x,i)=>`<button type="button" class="${i===index?'on':played.has(i)?'done':missing.has(i)?'missing':''}" onclick="goToRouteClip(${i})" aria-label="עבור לקליפ ${i+1} ממצלמת ${esc(x.camera)}" title="לחצו כדי לעבור לקליפ הזה">${played.has(i)?'✓ ':missing.has(i)?'! ':''}${esc(x.camera)}</button>`).join('')}</div>`;
}
function goToRouteClip(index){
if(!routePlaylist||index<0||index>=routePlaylist.items.length)return;const video=$('#eventVideo');
video.pause();video.onerror=null;video.onloadedmetadata=null;video.onended=null;video.removeAttribute('src');video.load();
routePlaylist.index=index;playRouteStep();
}

async function renderVisits(){
Expand Down
Loading
Loading