digest-engine grows from a single household digest into four per-person
sections, and gains the memory and inputs to make them worth reading.
Per-person sections. identity owns a digest_sections column per person,
edited in the admin panel and read by digest-engine at the start of every
run (GET /digest-preferences). A run generates the union the household
asked for: a section nobody wants costs no LLM call and no ingestion of
its sources. Each surface then filters to the person HA resolved. The
display half is a filter, not an access control, and says so.
Network is its own section, split out of household so the two can be
wanted separately.
Political section rebuilt around four questions (global class struggle,
organising in Vorarlberg, mid-term consequences, the International and
comrades' reports). ~30 international feeds added, each carrying owner
and bias, with a symmetric ownership analysis rather than a
reliability ranking; Zionist outlets get an explicit zero-trust rule that
is not inversion. RCI social/podcast ingestion (YouTube Atom, podcast RSS,
public Telegram via the existing session) feeds a watch-later window.
Globe markers carry summaries with fold-out sources; counter_run drops
citations whose URLs are not in the context.
Meeting agendas: a Tagesordnung arriving by mail or WhatsApp is matched to
its calendar event, read with pypdf, and its points extracted
mechanically. The political section owns the contents and derives
"Political todos"; the household section is told only that an agenda
exists, enforced structurally.
The archive keeps every ingested item and measured number across runs, so
trends may finally be stated with figures and dates attached.
ntfy push after each run, assembled from existing narrations, gated by the
same per-person sections.
identity gains GET /speaker: automatic recognition for the voice path from
BLE plus recent face sightings. Unresolved means show less, never ask, and
nothing displays a digest because someone walked past a screen.
OPNsense credentials move into CoreSystemConfig.json; thin clients gain a
Display switch (HDMI-CEC, DPMS fallback) so an empty room stops powering a
TV.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpKsMV1Q2EHJ5cZVoLvK9M
Closes the oldest deferred item in the plan (open decision #22). /presence has
reported a best-effort `room` since Phase 6, but there was nothing to plot it
on: no floor plan, no room list, no coordinate format. The reason it stayed
deferred was sound — nothing here knows the shape of this flat, and inventing a
coordinate format against a guess would have been the wrong kind of work. So the
plan is DRAWN, by hand, in a new Floorplan tab.
Levels hold rooms; rooms are polygons (real rooms aren't rectangles) with an
optional background image to trace over. Click corners to draw, drag handles to
adjust. Each room maps to the HA area string /presence already reported, and the
editor offers a pick-list of the areas HA is actually reporting rather than
asking anyone to retype an area_id from Developer Tools — same anti-typo
reasoning as CoreSystemConfig.json. Tick "Show who's home" and occupied rooms
light up.
Coordinates are normalised 0-1 rather than pixels: the plan has to render on a
laptop now and possibly a wall panel later, and pixels would be right on exactly
one of them. The tradeoff is documented — replacing a background with one of a
different aspect ratio distorts existing rooms.
Two choices about failure worth calling out. A room with no HA area is legal and
labelled as such on the plan, because drawing the flat and wiring up presence are
separate jobs. And anyone who can't be placed — home with no room resolved, or
reporting an area no room claims — is listed under the plan, with unclaimed areas
named so you know what's left to draw. A floor plan that quietly lost people
would be worse than no floor plan.
The whole feature is still gated on AREA_ATTRIBUTE being right, which remains an
unconfirmed guess at what Bermuda publishes; every room can be drawn correctly
and still never light up. The editor makes that diagnosable rather than
mysterious, but can't fix it. Added to identity's verification list.
38 backend checks: level and room CRUD, polygon validation (too few points, out
of range, non-numeric, over the vertex cap), duplicate-area refusal, image
round-trip, the presence join including both unplaceable cases, area suggestions
degrading when HA is down, and cascade on level delete. The editor UI itself has
not been opened in a browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rides the same arrival transition the visit log is built from, so "arrived" has
exactly one definition in this service rather than two that could disagree.
- notify_on_arrival (default off) is the "if enabled" half: push me when someone
gets home. announce_arrivals (default ON) is a separate per-person opt-OUT of
being announced, for anyone who doesn't want their comings and goings
broadcast. The asymmetry is deliberate — if both defaulted off, ticking
"notify me" would look broken until everyone else opted in too.
- Per-person notify_topic falling back to NTFY_DEFAULT_TOPIC. Topics are
deduplicated, or a five-person household sharing one topic would get five
identical pushes per arrival. The arriver is never told about themselves;
subscribers who are away still are, since that's most of the point.
- The first sample after startup notifies nobody and establishes a baseline
instead — otherwise a restart after a gap would fire "X just got home" for
everyone already on the sofa. Costs one missed notification in that window.
- Face-recognition arrivals say "was just recognised at home", not "just got
home" — the signals aren't equally reliable and the reader should know which.
- Pushes are sent outside _db_lock and isolated from each other, so a hanging
ntfy can't stall request handling and one bad push can't swallow the rest.
- POST /people/<id>/test-notification, because the alternative way to find a
typo'd topic is to wait for someone to walk in and notice nothing happened.
identity itself never touches the WAN — it POSTs to the ntfy already in this
stack. Whether the push reaches a phone that's AWAY is a network question, and
docs/network-integration.md's existing answer (WireGuard in, never a
port-forward) applies unchanged; iOS can't do this locally at all, since ntfy's
iOS app needs APNs and therefore an upstream relay. Documented as a table.
Also fixes two pre-existing bugs found while wiring this up: chores' template
pointed IDENTITY_URL and NTFY_URL at 127.0.0.1, which inside that container is
the container itself. chores would have reached neither identity nor ntfy, and
done nothing at all — silently, since both paths fail soft by design. Deployed
chores.env files still carry the old values and need editing by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends Phase 6's identity rather than adding a service — all of this is facts
about who someone is, already identity's job.
- admin.html/admin.js: manage people/guests, edit every field, prune, history,
access log. Deliberately not kiosk-shaped and not linked from any wall panel —
a panel anyone can walk up to shouldn't carry a "prune these people" button.
- Nicknames are an input alias only. /resolve accepts them; every payload also
carries speak_name (always the real name) and chores/ uses it for reminders.
A nickname colliding with anyone's name or nickname is refused; a spoken string
matching two people refuses with ambiguous_name rather than picking one.
- Visit history sampled from identity's own /presence. home:null writes nothing,
ever — an HA outage must not record a departure that didn't happen. Departures
need sustained absence (BLE flaps) and are stamped at the last time actually
seen. "With whom" is an interval-overlap query, not a second table.
- Pruning splits selecting from deleting: the filter fills the checkboxes, the
delete goes by explicit id, and the filter is never re-run at delete time.
- Device rights are an answer, never an action — identity stores grants and
answers GET /device-access; HA asks and HA acts. Deny is the default and the
only fallback. Grants can expire; every check is logged, denials included.
- Chore assignment prefers an assignee who's home but doesn't let an absent one
block the chore (CHORE_ASSIGNMENT_STRICT flips that). Litter still ignores
both exemption and assignment.
- Admin-added identifiers still enforce TRUSTED_ENTITY_PREFIXES; only the
"in range right now" check is relaxed, never the allowlist.
Resolves open decision #26; adds #34-37 for the new unknowns (DEPARTURE_GRACE_
SECONDS is untuned, the panel has never been opened in a browser).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>