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
2 changes: 1 addition & 1 deletion docs/COMPONENT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ is not. The web admin console doubles as the desktop's management surface
| Settings | server settings + users/RBAC | settings + embedded `/admin` | `settings/` | `Settings/` |
| Notifications | rules + history CRUD | toasts | poll + local notifications | `Settings/` |
| Update notice (issue #7) | Server settings toggle + status/"Check now" (the console's own update IS the server update) | Shipped: update banner + check controller + About panel (`apps/desktop-flutter/lib/ui/updates/`, `docs/UPDATE-SYSTEM-PLAN.md` §7 C2) | Shipped: `feature/update/` (`UpdateBanner.kt`, `UpdateViewModel.kt`, `SemVer.kt`) (§7 C3) | Models only (`Models/UpdateModels.swift`), no banner yet; iOS lowest priority per D5 (§7 C4) |
| Home Assistant overlay (`docs/DECISIONS.md` 2026-07-10; backend: `services/api/src/ha.rs` + `services/common/src/ha.rs`, `ha_config`/`camera_ha_links` migrations, `GET/PUT /config/ha` admin + `POST /config/ha/test` + `GET /ha/entities`/`/ha/states` + `GET/PUT /cameras/:id/ha/links` + `PUT /cameras/:id/ha/links/:link_id/placement`; token write-only; env fallback `HA_BASE_URL`/`HA_TOKEN`/`HA_TOKEN_FILE`) — **whole-object REPLACE, NOT a merge** (`docs/DECISIONS.md` 2026-08-07, issue #552): a body-level `null` clears the placement, an omitted style key means exactly what an explicit `null` means (that override is unset), and `x`/`y` are required so a style-only body 422s. Deliberately the opposite of `PUT /config/server`'s merge convention (#472/#533), because these columns have no `""` state to clear with; every writer must send the badge's CURRENT values forward, not just a delta. `label` is the one exception, riding the `PUT /config/ha` token convention (omitted ⇒ unchanged, `""` ⇒ clear). Pinned by `services/api/tests/ha_placement_replace_semantics.rs`| **Settings → Home Assistant** (dedicated hub, issue #441 — was split across Detection & clips + the camera editor, now one coherent area): connect (base URL + long-lived token, relocated verbatim from Detection & clips); global **links overview** across every camera (camera, entity_id, role, device_class, label, live state), aggregated client-side from the per-camera links endpoint (no new bulk endpoint), with **orphaned-link** detection (a link's `entity_id` absent from `GET /ha/states` ⇒ no longer exists in HA) and a filter/orphans-only toggle; per-camera link editor stays in the camera's Motion tab, reachable from either page: add/remove links, edit role/device_class/label (#453); per-link **icon & style** authoring (icon from the closed `CANONICAL_ICON_SLUGS` vocabulary, shape, color, bg_color, size, opacity via `PUT .../placement`, issue #439) with a static badge-mockup preview (no drag-to-position here, an unplaced badge saves centered until repositioned in a live view) | Entity **badges** on live video (`apps/desktop-flutter/lib/ui/ha_overlay/`: palette, badge-style editor, overlay layer/controller); OFF until configured | Read-only entity **badges** on live video + per-camera entity sheet (`feature/live/HaBadgeOverlay.kt`, `HaEntitiesSheet.kt`); polls `/ha/states`. Global **hide-overlays** quick-toggle (eye button in the fullscreen controls; app-wide `feature/live/HaOverlayVisibility.kt` over `SecureStore.showHaOverlays`, persisted): display-only, suppresses every on-video badge (and pauses the states poll when nothing else needs it) while the entities sheet and its popups stay reachable. **Deferred:** desktop/iOS equivalents | Read-only entity **badges** on live video (letterbox-positioned) + per-camera entity sheet + tap detail card (`apps/ios/Crumb/Features/HomeAssistant/`); polls `/ha/states` (3s), state-honesty grey on unknown/stale; no admin editor (link/place via console/desktop) |
| Home Assistant overlay (`docs/DECISIONS.md` 2026-07-10; backend: `services/api/src/ha.rs` + `services/common/src/ha.rs`, `ha_config`/`camera_ha_links` migrations, `GET/PUT /config/ha` admin + `POST /config/ha/test` + `GET /ha/entities`/`/ha/states` + `GET/PUT /cameras/:id/ha/links` + `PUT /cameras/:id/ha/links/:link_id/placement`; token write-only; env fallback `HA_BASE_URL`/`HA_TOKEN`/`HA_TOKEN_FILE`) — **whole-object REPLACE, NOT a merge** (`docs/DECISIONS.md` 2026-08-07, issue #552): a body-level `null` clears the placement, an omitted style key means exactly what an explicit `null` means (that override is unset), and `x`/`y` are required so a style-only body 422s. Deliberately the opposite of `PUT /config/server`'s merge convention (#472/#533), because these columns have no `""` state to clear with; every writer must send the badge's CURRENT values forward, not just a delta. `label` is the one exception, riding the `PUT /config/ha` token convention (omitted ⇒ unchanged, `""` ⇒ clear). Pinned by `services/api/tests/ha_placement_replace_semantics.rs`| **Settings → Home Assistant** (dedicated hub, issue #441 — was split across Detection & clips + the camera editor, now one coherent area): connect (base URL + long-lived token, relocated verbatim from Detection & clips); global **links overview** across every camera (camera, entity_id, role, device_class, label, live state), aggregated client-side from the per-camera links endpoint (no new bulk endpoint), with **orphaned-link** detection (a link's `entity_id` absent from `GET /ha/states` ⇒ no longer exists in HA) and a filter/orphans-only toggle; per-camera link editor stays in the camera's Motion tab, reachable from either page: add/remove links, edit role/device_class/label (#453). Rows are grouped by role and added via one **+ Add Home Assistant entity** action whose picker carries a Motion/Reading/Control segment toggle; role labels are **mode-aware** (a Continuous-mode camera never shows "triggers recording" copy — only a Motion-mode camera does, though the motion role feeds the additive motion source either way); the `device_class` field lives inside the **icon & style** panel (it only sets `binary_sensor` glyphs) but still saves via the whole-list `PUT .../ha/links`, which shows an **Unsaved changes** indicator until saved; per-link **icon & style** authoring (icon from the closed `CANONICAL_ICON_SLUGS` vocabulary, shape, color, bg_color, size, opacity via `PUT .../placement`, issue #439) with a static badge-mockup preview (no drag-to-position here, an unplaced badge saves centered until repositioned in a live view) | Entity **badges** on live video (`apps/desktop-flutter/lib/ui/ha_overlay/`: palette, badge-style editor, overlay layer/controller); OFF until configured | Read-only entity **badges** on live video + per-camera entity sheet (`feature/live/HaBadgeOverlay.kt`, `HaEntitiesSheet.kt`); polls `/ha/states`. Global **hide-overlays** quick-toggle (eye button in the fullscreen controls; app-wide `feature/live/HaOverlayVisibility.kt` over `SecureStore.showHaOverlays`, persisted): display-only, suppresses every on-video badge (and pauses the states poll when nothing else needs it) while the entities sheet and its popups stay reachable. **Deferred:** desktop/iOS equivalents | Read-only entity **badges** on live video (letterbox-positioned) + per-camera entity sheet + tap detail card (`apps/ios/Crumb/Features/HomeAssistant/`); polls `/ha/states` (3s), state-honesty grey on unknown/stale; no admin editor (link/place via console/desktop) |
| Per-state HA badge background (badge-customization rework **wave A, server contract only**; `docs/DECISIONS.md` 2026-08-06 per-state-background entry; backend: `overlay_bg_color_on` column migration `0076` with a `'#RRGGBB'` CHECK, `CameraHaLink.overlay_bg_color_on` in `services/common/src/types.rs`, `HaOverlayPlacement.bg_color_on` + every SELECT/RETURNING/carry-over path in `services/common/src/db.rs`, `HaLinkDto.overlay_bg_color_on` + `PlacementInput.bg_color_on` validated by `valid_overlay_color` in `services/api/src/ha.rs`. `overlay_bg_color` is the BASE (off + indeterminate/stale), `overlay_bg_color_on` overrides only while `edge_on`; resolution in every renderer is on ⇒ `bg_color_on ?? bg_color ?? #17171B`, otherwise ⇒ `bg_color ?? default`. Null everywhere ⇒ today's rendering, unchanged) | Wave B shipped (#490): second colour well + "Auto (follows Off)" clear in the HA link icon & style panel (`admin.html`) | Wave B shipped (#491): `apps/desktop-flutter/lib/ui/ha_overlay/ha_badge_popover.dart`, `ha_api.dart`, `ha_models.dart` | Wave B shipped (#489): `feature/live/HaVisual.kt` / `HaModels.kt`, tests in `HaVisualActiveTest.kt` | Wave B shipped (#492): `Features/HomeAssistant/HomeAssistant.swift` + `Models.swift`, tests in `HaBadgeVisualTests.swift` |
| HA badge PILL LAYOUT (issue #497; `docs/DECISIONS.md` 2026-08-06 pill-layout entry; backend: `overlay_pill_width` + `overlay_text_align` columns migration `0078` with value CHECKs, `CameraHaLink.*` in `services/common/src/types.rs`, `HaOverlayPlacement.pill_width`/`.text_align` + every SELECT/RETURNING/carry-over path in `services/common/src/db.rs`, `HaLinkDto.overlay_pill_width`/`overlay_text_align` + `PlacementInput.pill_width`/`text_align` validated against `HA_PILL_WIDTH_MODES`/`HA_TEXT_ALIGNS` in `services/api/src/ha.rs`. **The vocabulary is the contract and must be byte-identical in all five places**: `auto`/`narrow`/`medium`/`wide` and `start`/`center`/`end`. Width resolution in every renderer is `auto`/null/unrecognized ⇒ measured content width, else `4x`/`6x`/`8x` the badge HEIGHT (exact, not a minimum — an over-long label ellipsizes); align resolution is `start`/null/unrecognized ⇒ leading, `center`, `end`. Pill only; a dot ignores both. Null everywhere ⇒ today's rendering, unchanged) | Pill width + Text align selects in the HA link style panel (`admin.html` `haStylePanel`, `HA_PILL_WIDTH_FACTORS`/`HA_TEXT_ALIGN_JUSTIFY`, mock preview honors both) | Width + Align rows in the badge popover, pill-only (`ha_badge_popover.dart`); `haPillWidthFactor`/`haPillAlignment` + `HaOverlayBadgeItem.pillBaseWidth(widthMode:)` in `ha_overlay_layer.dart`/`ha_overlay_controller.dart`; `pill_width`/`text_align` on the placement PUT (`ha_api.dart`) | `HaBadgeMetrics.pillWidthFactor`/`pillWidth(..., widthMode)` + Row `horizontalArrangement` in `feature/live/HaBadgeOverlay.kt`, `HaLinkDto` fields in `data/HaModels.kt` | `HA.pillWidthFactor`/`HA.pillAlignment` + the pill `.frame(width:height:alignment:)` in `Features/HomeAssistant/HomeAssistant.swift`, `HaLink` fields in `Models/Models.swift` |
| Home Assistant control (Phase 2, #187 + interaction refinement #428 + value controls #442 Slice 1, `docs/DECISIONS.md` 2026-08-01 (two entries); backend: `POST /cameras/:id/ha/action` `{link_id, action, value?}` in `services/api/src/ha.rs`, strict per-domain action allowlist as a structured spec table `HaActionSpec {action, service, value: Option<ValueKind>}` (`light`/`switch`/`fan`/`siren` on/off/toggle, `cover` open/stop/close, `lock` lock/unlock, `button` press, `scene`/`script` turn_on, plus percent-kind **value actions** `light.set_brightness`→turn_on, `cover.set_position`→set_cover_position, `fan.set_speed`→set_percentage; word≠service now) built server-side from the stored entity, `HaClient::call_service`/`call_service_with`; value validated 0..100 server-side (`validate_value`) before HA, rounded to a JSON int; client capability discovery = optional `control` descriptor on `GET /ha/states` entries (action/kind/value/min/max/step/unit, projected from cached attributes, no HA round-trip); per-link `require_confirm`/`allowed_actions` control config (migration `0075`, issue #440, `null`⇒all actions incl. value words) enforced in `post_action`; new role capability `actuators` (default-off, admin implies, migration `0074`) surfaced at `/auth/me` `capabilities.actuators`; `ha_actuation` audit row per attempt (incl. parsed value); climate `set_temperature` deferred to Slice 2; validated live end-to-end on real HA) | roles editor **Control linked devices** capability checkbox (`admin.html`); per-**actuator**-link **Controls** panel in the HA link editor authors `require_confirm` + an `allowed_actions` restriction (checkboxes seeded from the domain's action set, incl. the value words "Set brightness"/"Set position"/"Set speed"; no per-link value clamp in v1), saved via `PUT .../ha/links` (issue #439); no live wall so no actuation surface | Single-**click** actuates a badge directly (simple domains), card + confirm for `cover`/`lock`, hover shows state; **value slider** (brightness/position/speed) shipped for dimmable lights/covers/fans (#461, card icon/rows #466) (`ui/ha_overlay/ha_actions.dart`, `ha_overlay_layer.dart`) | Single-**tap** actuates directly, long-press for detail, `cover`/`lock` confirm dialog; **value slider** (brightness/position/speed) shipped (#463, #468) (`feature/live/HaBadgeOverlay.kt`, `HaEntitiesSheet.kt`) | Single-**tap** actuates directly, long-press for detail, `cover`/`lock` confirm; **value slider** (brightness/position/speed) shipped (#462, badge-color match #467) (`Features/HomeAssistant/HomeAssistant.swift`) |
Expand Down
38 changes: 38 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ revisit.

---

## 2026-08-08, Home Assistant entity role labels in the camera editor are mode-aware (copy only)

**Context.** The per-camera HA link editor (`admin.html`, `renderHaLinks` &c.)
labeled the `motion` role "Motion (triggers recording)". That is only true on a
camera whose recording policy is Motion-triggered. On a Continuous (24/7) camera
a linked motion binary sensor never starts a recording — it feeds the additive
multi-source motion stream (timeline events + notifications) only. The static
label read as a contradiction on a 24/7 camera and confused the maintainer about
how to add an HA entity at all.

**Decision.**

- The `motion` role's label and the picker/helper copy are derived from the open
camera's effective policy mode (`profileById(cam.policy_id) || cam.policy`,
the same source the storage tab and banner use), surfaced as
`HA_CAM_MOTION_MODE`. Motion-mode ⇒ "Motion sensor (triggers recording + marks
the timeline)"; Continuous ⇒ "Motion sensor (marks the timeline, can notify)".
The `sensor`/`actuator` labels are mode-independent.
- This is **presentation only**. The stored roles remain `motion|sensor|actuator`,
`device_class` and the whole-list `PUT /cameras/:id/ha/links` contract are
unchanged; no migration, no schema, no server change. The same rework moved the
`device_class` box into the icon & style panel, replaced the three add buttons
with one "+ Add Home Assistant entity" + a role segment toggle, grouped rows by
role, and added a Save-links "Unsaved changes" indicator.

**Rejected.** A single static label for all cameras (the status quo) — rejected
because "triggers recording" is factually wrong on a Continuous camera and was
the reported source of confusion. Encoding the behavior difference in the schema
(e.g. a per-link "notify" flag) — rejected as out of scope; it is genuinely new
data, flagged in the PR for a future decision, not built here.

**Revisit if.** The motion role's server-side behavior stops depending on the
camera's record mode (e.g. motion links always trigger recording, or never do),
or a per-link notify/behavior toggle is added — at which point the copy should
follow the new behavior and this entry is superseded.

---

## 2026-08-07, Timeline motion-intensity is bucketed in SQL (GROUP BY over a `generate_series`-expanded range), not by fetching every segment to Rust

**Context.** The desktop Playback/clip timeline "intensity ribbon" took roughly
Expand Down
Loading
Loading