Skip to content

Commit 5187d50

Browse files
committed
add a LOG tab to the web UI
App users have Home Assistant's log tab; standalone and container users had to open a terminal - precisely when they are in the UI wondering why nothing is recognised. Shows the last 500 lines with a warnings filter and a copy button, with inference-library noise filtered out.
1 parent aa2f99a commit 5187d50

12 files changed

Lines changed: 245 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
All notable changes to FaceID. The Home Assistant app shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.6.11 — 2026-07-26
7+
8+
- **New LOG tab.** The service log is now visible in the web UI — the last 500 lines,
9+
refreshing every 5 seconds, with a warnings-only filter and a copy button for pasting
10+
into a bug report. Home Assistant app users had the app's log tab; standalone and
11+
container users had to reach for `journalctl` or `docker logs`, which is exactly the
12+
wrong moment to switch to a terminal when you are trying to find out why nothing is
13+
being recognised. Noise from the inference library is filtered out.
14+
615
## 0.6.10 — 2026-07-26
716

817
- **Fixed: polled events could be processed twice.** The finalizer clears an event from

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@ being unusual, it's automatically kept.
331331
**Full details:** [docs/trimming.md](docs/trimming.md) explains the why, the exact
332332
selection rule (with numbers), and how to restore or curate set-aside photos.
333333

334+
## Seeing what it is doing
335+
336+
The **LOG tab** shows the last 500 log lines straight in the UI — including the quiet
337+
cases that decide whether a setup works: whether Frigate answers at startup, which
338+
cameras were announced to Home Assistant, and for every event whether a face was found
339+
at all. If nothing is ever recognised, that tab usually says why within a few lines.
340+
There is a warnings-only filter and a copy button for pasting into an issue.
341+
334342
## Backup & restore
335343

336344
Your gallery (enrolled persons + ignore anchors) is the one thing you can't regenerate —

app/logbuffer.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Die letzten Logzeilen im Speicher halten, damit die Web-UI sie zeigen kann.
2+
3+
Wer FaceID als Home-Assistant-App betreibt, hat den Log-Tab der App. Standalone und
4+
im Container liegt das Log dagegen in journalctl bzw. `docker logs` — beides sieht
5+
niemand, der gerade in der Oberfläche nach dem Grund sucht, warum nichts erkannt wird.
6+
"""
7+
import logging
8+
import re
9+
from collections import deque
10+
11+
# Rauschen der Inferenz-Bibliothek, das im UI-Log nur ablenkt.
12+
_NOISE = re.compile(r"pthread_setaffinity_np|Applied providers|find model:|model ignore:|"
13+
r"set det-size|FutureWarning|tform\.estimate")
14+
15+
16+
class RingBufferHandler(logging.Handler):
17+
def __init__(self, capacity: int = 500):
18+
super().__init__()
19+
self.records: deque = deque(maxlen=capacity)
20+
21+
def emit(self, record: logging.LogRecord):
22+
try:
23+
msg = record.getMessage()
24+
if _NOISE.search(msg):
25+
return
26+
self.records.append({
27+
"ts": record.created,
28+
"level": record.levelname,
29+
"logger": record.name.removeprefix("faceid."),
30+
"msg": msg if len(msg) <= 2000 else msg[:2000] + " …",
31+
})
32+
except Exception: # ein kaputter Logeintrag darf nichts umbringen
33+
pass
34+
35+
def tail(self, limit: int = 300, level: str | None = None):
36+
items = list(self.records)
37+
if level in ("WARNING", "ERROR"):
38+
wanted = {"WARNING", "ERROR", "CRITICAL"} if level == "WARNING" else {"ERROR", "CRITICAL"}
39+
items = [r for r in items if r["level"] in wanted]
40+
return items[-limit:]
41+
42+
43+
_handler: RingBufferHandler | None = None
44+
45+
46+
def install(capacity: int = 500) -> RingBufferHandler:
47+
"""Am Root-Logger einhängen; mehrfacher Aufruf liefert denselben Puffer."""
48+
global _handler
49+
if _handler is None:
50+
_handler = RingBufferHandler(capacity)
51+
_handler.setLevel(logging.INFO)
52+
logging.getLogger().addHandler(_handler)
53+
return _handler
54+
55+
56+
def buffer() -> RingBufferHandler | None:
57+
return _handler

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uvicorn
77
import yaml
88

9+
from . import logbuffer
910
from .engine import FaceEngine
1011
from .frigate_api import FrigateAPI
1112
from .gallery import Gallery
@@ -16,6 +17,7 @@
1617
BASE = Path(__file__).resolve().parent.parent
1718

1819
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
20+
logbuffer.install() # damit die Weboberflaeche das Log zeigen kann
1921
log = logging.getLogger("faceid")
2022

2123

app/webui.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from fastapi.staticfiles import StaticFiles
1717
from pydantic import BaseModel
1818

19+
from . import logbuffer
1920
from .engine import FaceEngine, crop_face, find_face_padded
2021
from .backup_util import build_backup_gz, write_backup_file, prune_backups
2122
from pathlib import Path as _P
@@ -420,6 +421,15 @@ async def restore(file: UploadFile, merge: bool = False):
420421
return {"restored_files": added, "mode": "merge" if merge else "replace",
421422
"persons": len(gallery.persons())}
422423

424+
@app.get("/api/logs")
425+
def logs(limit: int = 300, level: str | None = None):
426+
"""Die letzten Logzeilen — im Container und standalone sonst nur per Terminal
427+
einsehbar, genau wenn man wissen will warum nichts erkannt wird."""
428+
buf = logbuffer.buffer()
429+
if buf is None:
430+
return {"lines": [], "note": "Log-Puffer nicht aktiv"}
431+
return {"lines": buf.tail(max(1, min(limit, 500)), level)}
432+
423433
@app.get("/api/health")
424434
def health():
425435
# "queue" ist die Review-Queue — das ist es, was der Header zeigt. Die interne

faceid-addon/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
All notable changes to FaceID. The Home Assistant app shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.6.11 — 2026-07-26
7+
8+
- **New LOG tab.** The service log is now visible in the web UI — the last 500 lines,
9+
refreshing every 5 seconds, with a warnings-only filter and a copy button for pasting
10+
into a bug report. Home Assistant app users had the app's log tab; standalone and
11+
container users had to reach for `journalctl` or `docker logs`, which is exactly the
12+
wrong moment to switch to a terminal when you are trying to find out why nothing is
13+
being recognised. Noise from the inference library is filtered out.
14+
615
## 0.6.10 — 2026-07-26
716

817
- **Fixed: polled events could be processed twice.** The finalizer clears an event from

faceid-addon/app/logbuffer.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Die letzten Logzeilen im Speicher halten, damit die Web-UI sie zeigen kann.
2+
3+
Wer FaceID als Home-Assistant-App betreibt, hat den Log-Tab der App. Standalone und
4+
im Container liegt das Log dagegen in journalctl bzw. `docker logs` — beides sieht
5+
niemand, der gerade in der Oberfläche nach dem Grund sucht, warum nichts erkannt wird.
6+
"""
7+
import logging
8+
import re
9+
from collections import deque
10+
11+
# Rauschen der Inferenz-Bibliothek, das im UI-Log nur ablenkt.
12+
_NOISE = re.compile(r"pthread_setaffinity_np|Applied providers|find model:|model ignore:|"
13+
r"set det-size|FutureWarning|tform\.estimate")
14+
15+
16+
class RingBufferHandler(logging.Handler):
17+
def __init__(self, capacity: int = 500):
18+
super().__init__()
19+
self.records: deque = deque(maxlen=capacity)
20+
21+
def emit(self, record: logging.LogRecord):
22+
try:
23+
msg = record.getMessage()
24+
if _NOISE.search(msg):
25+
return
26+
self.records.append({
27+
"ts": record.created,
28+
"level": record.levelname,
29+
"logger": record.name.removeprefix("faceid."),
30+
"msg": msg if len(msg) <= 2000 else msg[:2000] + " …",
31+
})
32+
except Exception: # ein kaputter Logeintrag darf nichts umbringen
33+
pass
34+
35+
def tail(self, limit: int = 300, level: str | None = None):
36+
items = list(self.records)
37+
if level in ("WARNING", "ERROR"):
38+
wanted = {"WARNING", "ERROR", "CRITICAL"} if level == "WARNING" else {"ERROR", "CRITICAL"}
39+
items = [r for r in items if r["level"] in wanted]
40+
return items[-limit:]
41+
42+
43+
_handler: RingBufferHandler | None = None
44+
45+
46+
def install(capacity: int = 500) -> RingBufferHandler:
47+
"""Am Root-Logger einhängen; mehrfacher Aufruf liefert denselben Puffer."""
48+
global _handler
49+
if _handler is None:
50+
_handler = RingBufferHandler(capacity)
51+
_handler.setLevel(logging.INFO)
52+
logging.getLogger().addHandler(_handler)
53+
return _handler
54+
55+
56+
def buffer() -> RingBufferHandler | None:
57+
return _handler

faceid-addon/app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uvicorn
77
import yaml
88

9+
from . import logbuffer
910
from .engine import FaceEngine
1011
from .frigate_api import FrigateAPI
1112
from .gallery import Gallery
@@ -16,6 +17,7 @@
1617
BASE = Path(__file__).resolve().parent.parent
1718

1819
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
20+
logbuffer.install() # damit die Weboberflaeche das Log zeigen kann
1921
log = logging.getLogger("faceid")
2022

2123

faceid-addon/app/webui.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from fastapi.staticfiles import StaticFiles
1717
from pydantic import BaseModel
1818

19+
from . import logbuffer
1920
from .engine import FaceEngine, crop_face, find_face_padded
2021
from .backup_util import build_backup_gz, write_backup_file, prune_backups
2122
from pathlib import Path as _P
@@ -420,6 +421,15 @@ async def restore(file: UploadFile, merge: bool = False):
420421
return {"restored_files": added, "mode": "merge" if merge else "replace",
421422
"persons": len(gallery.persons())}
422423

424+
@app.get("/api/logs")
425+
def logs(limit: int = 300, level: str | None = None):
426+
"""Die letzten Logzeilen — im Container und standalone sonst nur per Terminal
427+
einsehbar, genau wenn man wissen will warum nichts erkannt wird."""
428+
buf = logbuffer.buffer()
429+
if buf is None:
430+
return {"lines": [], "note": "Log-Puffer nicht aktiv"}
431+
return {"lines": buf.tail(max(1, min(limit, 500)), level)}
432+
423433
@app.get("/api/health")
424434
def health():
425435
# "queue" ist die Review-Queue — das ist es, was der Header zeigt. Die interne

faceid-addon/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: FaceID
2-
version: "0.6.10"
2+
version: "0.6.11"
33
slug: faceid
44
description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors
55
url: https://github.com/SkyTechNerds/faceid

0 commit comments

Comments
 (0)