diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c831b..8555a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to FaceID. The Home Assistant app shows this file in the update dialog; standalone users can watch GitHub releases. +## 0.25.1 — 2026-10-06 + +- **A failed assignment no longer leaves an empty person behind.** Assigning faces created + the person first and assigned afterwards, so when nothing could be assigned — stale ids, an + empty selection, or a second tab that got there first — the answer was HTTP 200 with + `{"assigned": 0}` and the person stayed. Through "Track as new" that meant an empty + `Unnamed-xxxxxx` in the gallery on every failed attempt, while the UI reported + `0 face(s) → Unnamed-ab12cd` and looked like success. It now answers 409 and removes a + person that was created for that very call; an existing person is never deleted, including + when its display name or a different casing was typed. +- **The action buttons show what they are doing.** ASSIGN, ASSIGN ALL and "Track as new" had + no hover, pressed or keyboard-focus style at all. Measured: 1.66:1 from rest to hover, + 1.50:1 from hover to pressed, with the label staying above 4.5:1 on both, and a focus ring + at 13:1 against the panel. Pressing also shifts by 1px, which `prefers-reduced-motion` + turns off. +- **Failures are visible.** A click whose request failed used to do nothing and say nothing: + the request helper is awaited in 25 handlers, most without their own error handling. A + single listener now reports every one of them, and still logs the original error to the + console so a programming mistake keeps its stack trace. + ## 0.25.0 — 2026-10-06 - **Folder mode is available in the Home Assistant app.** The directory watcher has existed diff --git a/app/webui.py b/app/webui.py index 700fa41..ae3f210 100644 --- a/app/webui.py +++ b/app/webui.py @@ -275,6 +275,11 @@ def unknowns(): def assign(body: AssignBody): persons_now = gallery.persons() slug = body.person if body.person in persons_now else gallery.create_person(body.person) + # Am aufgeloesten Slug entscheiden, nicht an der Eingabe: create_person ist + # absichtlich idempotent und gibt zu "Juli", "juli " oder einem zweiten Tab + # denselben vorhandenen Slug zurueck. Wer hier die Eingabe prueft, loescht + # bei null Treffern eine bestehende Person samt ihrer Gesichter. + created_now = slug not in persons_now name = gallery.persons()[slug]["name"] n = 0 for uid in body.ids: @@ -290,6 +295,18 @@ def assign(body: AssignBody): # Zuordnung ans Original-Event zurückspielen (Mensch bestätigt -> Score 1.0) if getattr(processor.frigate, "enabled", True) and meta.get("event_id"): processor.frigate.set_sub_label(meta["event_id"], name, 1.0) + if n == 0: + # Kein Gesicht gelandet heisst: veraltete IDs, leere Auswahl, oder ein + # zweiter Tab war schneller. Das war bisher ein HTTP 200 mit + # {"assigned": 0} — die Oberflaeche meldete "0 face(s) → Name" und sah aus + # wie Erfolg. Wurde die Person fuer genau diesen Aufruf angelegt, gehoert + # sie wieder weg: sonst haeuft jeder Fehlversuch ueber "Track as new" ein + # leeres Unnamed-xxxxxx in der Galerie an, das niemand zuordnen kann. + # Eine vorhandene Person wird nie geloescht. + if created_now: + gallery.delete_person(slug) + raise HTTPException(409, f"no face was assigned — {name} was not created") + raise HTTPException(409, "no face was assigned — the queue may have changed") gallery.refresh_guesses() return {"assigned": n, "slug": slug} diff --git a/faceid-addon/CHANGELOG.md b/faceid-addon/CHANGELOG.md index e2c831b..8555a79 100644 --- a/faceid-addon/CHANGELOG.md +++ b/faceid-addon/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to FaceID. The Home Assistant app shows this file in the update dialog; standalone users can watch GitHub releases. +## 0.25.1 — 2026-10-06 + +- **A failed assignment no longer leaves an empty person behind.** Assigning faces created + the person first and assigned afterwards, so when nothing could be assigned — stale ids, an + empty selection, or a second tab that got there first — the answer was HTTP 200 with + `{"assigned": 0}` and the person stayed. Through "Track as new" that meant an empty + `Unnamed-xxxxxx` in the gallery on every failed attempt, while the UI reported + `0 face(s) → Unnamed-ab12cd` and looked like success. It now answers 409 and removes a + person that was created for that very call; an existing person is never deleted, including + when its display name or a different casing was typed. +- **The action buttons show what they are doing.** ASSIGN, ASSIGN ALL and "Track as new" had + no hover, pressed or keyboard-focus style at all. Measured: 1.66:1 from rest to hover, + 1.50:1 from hover to pressed, with the label staying above 4.5:1 on both, and a focus ring + at 13:1 against the panel. Pressing also shifts by 1px, which `prefers-reduced-motion` + turns off. +- **Failures are visible.** A click whose request failed used to do nothing and say nothing: + the request helper is awaited in 25 handlers, most without their own error handling. A + single listener now reports every one of them, and still logs the original error to the + console so a programming mistake keeps its stack trace. + ## 0.25.0 — 2026-10-06 - **Folder mode is available in the Home Assistant app.** The directory watcher has existed diff --git a/faceid-addon/app/webui.py b/faceid-addon/app/webui.py index 700fa41..ae3f210 100644 --- a/faceid-addon/app/webui.py +++ b/faceid-addon/app/webui.py @@ -275,6 +275,11 @@ def unknowns(): def assign(body: AssignBody): persons_now = gallery.persons() slug = body.person if body.person in persons_now else gallery.create_person(body.person) + # Am aufgeloesten Slug entscheiden, nicht an der Eingabe: create_person ist + # absichtlich idempotent und gibt zu "Juli", "juli " oder einem zweiten Tab + # denselben vorhandenen Slug zurueck. Wer hier die Eingabe prueft, loescht + # bei null Treffern eine bestehende Person samt ihrer Gesichter. + created_now = slug not in persons_now name = gallery.persons()[slug]["name"] n = 0 for uid in body.ids: @@ -290,6 +295,18 @@ def assign(body: AssignBody): # Zuordnung ans Original-Event zurückspielen (Mensch bestätigt -> Score 1.0) if getattr(processor.frigate, "enabled", True) and meta.get("event_id"): processor.frigate.set_sub_label(meta["event_id"], name, 1.0) + if n == 0: + # Kein Gesicht gelandet heisst: veraltete IDs, leere Auswahl, oder ein + # zweiter Tab war schneller. Das war bisher ein HTTP 200 mit + # {"assigned": 0} — die Oberflaeche meldete "0 face(s) → Name" und sah aus + # wie Erfolg. Wurde die Person fuer genau diesen Aufruf angelegt, gehoert + # sie wieder weg: sonst haeuft jeder Fehlversuch ueber "Track as new" ein + # leeres Unnamed-xxxxxx in der Galerie an, das niemand zuordnen kann. + # Eine vorhandene Person wird nie geloescht. + if created_now: + gallery.delete_person(slug) + raise HTTPException(409, f"no face was assigned — {name} was not created") + raise HTTPException(409, "no face was assigned — the queue may have changed") gallery.refresh_guesses() return {"assigned": n, "slug": slug} diff --git a/faceid-addon/config.yaml b/faceid-addon/config.yaml index 44e3086..2b9c9aa 100644 --- a/faceid-addon/config.yaml +++ b/faceid-addon/config.yaml @@ -1,5 +1,5 @@ name: FaceID -version: "0.25.0" +version: "0.25.1" slug: faceid description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors url: https://github.com/SkyTechNerds/faceid diff --git a/faceid-addon/static/index.html b/faceid-addon/static/index.html index 0a8dc4a..020ce8a 100644 --- a/faceid-addon/static/index.html +++ b/faceid-addon/static/index.html @@ -82,7 +82,23 @@ padding:5px 12px;cursor:pointer;display:inline-block;line-height:1.35;text-decoration:none;vertical-align:middle} .ghost,button.ghost{background:none;border:1px solid var(--line);color:var(--dim);font:12px var(--mono); padding:4px 10px;cursor:pointer;display:inline-block;line-height:1.35;text-decoration:none;vertical-align:middle} -.ghost:hover,button.ghost:hover{color:var(--fg)} +.ghost:hover,button.ghost:hover{color:var(--fg);border-color:var(--fg)} +/* Zustaende der Handlungsknoepfe. Vorher hatte .act keinen einzigen: Zeigen, + Druecken und Tastaturfokus sahen identisch aus, obwohl ASSIGN und "Track as new" + Personen anlegen. Gemessen statt geschaetzt: Grundzustand zu Hover 1.66:1, Hover zu + Active 1.50:1 — unter etwa 1.5:1 sieht man den Wechsel nicht. Der dunkle Text bleibt + dabei ueber 4.5:1 (7.3:1 auf Hover, 4.9:1 auf Active). Der Fokusring liegt dank + outline-offset auf dem Panel und steht dort mit 13:1. */ +.act:hover,button.act:hover{background:oklch(70% .25 130)} +.act:active,button.act:active{background:oklch(60% .20 130);transform:translateY(1px)} +.ghost:active,button.ghost:active{background:color-mix(in oklab,var(--fg) 12%,transparent)} +.act:focus-visible,button.act:focus-visible,.ghost:focus-visible,button.ghost:focus-visible{ + outline:2px solid var(--fg);outline-offset:2px} +.act[disabled],button.act[disabled],.ghost[disabled],button.ghost[disabled]{ + opacity:.45;cursor:not-allowed} +/* Die einzige Bewegung hier ist der 1px-Versatz beim Druecken — genau der gehoert + abgeschaltet. 'transition:none' waere wirkungslos gewesen: keine deklariert. */ +@media (prefers-reduced-motion:reduce){.act:active,button.act:active{transform:none}} /* Regler in der Akzentfarbe statt im Browser-Blau — das Blau war die einzige Farbe der Oberflaeche, die nicht aus der Palette stammte, und stand direkt neben den gruenen Zahlenwerten. Gilt zugleich fuer Ankreuzfelder. */ @@ -151,6 +167,18 @@