14 Commits

Author SHA1 Message Date
Tony C
e40521bd1c Rebalance EntityGroupType + hide delegates from edit sidebar (#367) (#369)
* Rebalance EntityGroupType buckets and close 18-type coverage gap (#367)

The prior bucket layout silently routed 15% of EntityTypes (FREEZER,
GARAGE_DOOR, LEAK_SENSOR, all pool types, etc.) through the OTHER
fallback because they had no explicit assignment, and conflated
heterogeneous types in OUTDOORS/UTILITIES/LIGHTS_SWITCHES. New layout
is 15 buckets organized by how a homeowner partitions their LocationView:
AUTOMATION (controllable end-points), SECURITY, SENSORS (new),
APPLIANCES (now subsumes HVAC), POOL (new), GENERAL (new — catchall
distinct from the silent OTHER fallback), and friends. Every EntityType
now has an explicit bucket assignment, pinned by a new invariant test.

* Hide delegate entities from edit-mode sidebar entity list (#367)

Placing a principal entity in a LocationView or Collection automatically
pulls its delegates along, and unplacing the principal removes them.
Showing the delegates as separately-toggleable rows in the same edit
sidebar invited confusing partial states (an Area added without its
proxy sensor, etc.).

EntityManager.create_location_entity_view_group_list and
CollectionManager.create_entity_collection_group_list now accept an
exclude_delegates kwarg; the two manage-items sidebars opt in. The
entity-pairing modal keeps the default and continues to surface
delegates so the relationship can be broken there.

* Refine EntityGroupType: drop OTHER, fold EntityType.OTHER into GENERAL (#367)

Three follow-up adjustments to the rebalance:
  - AREA moves from GENERAL to STRUCTURAL — areas are spatial
    regions of the home, structural by nature.
  - PUMP, MOTOR, SUMP_PUMP move from GENERAL to ELECTRICAL —
    they're electrically driven and don't fit a domain bucket.
  - EntityGroupType.OTHER is removed; EntityType.OTHER lives
    in GENERAL alongside AUTOMOBILE and CONSUMABLE. There's no
    longer a separate silent-fallback bucket — GENERAL is the
    explicit catchall, and the full-coverage invariant test
    ensures every EntityType has an assignment.
2026-05-26 07:43:52 -05:00
Anthony Cassandra
3c418653ca Entity state panels doc: clarify the purpose of the extras mechanism
The existing "Display contract" framing risked an unintended reading
— that omitting a role from the declared set is a valid way to opt
into the framework's extras rendering for "free". Add a focused note
that names the actual purpose: extras is a safety net for unusual
EntityType assignments (EntityType is user-adjustable, so an entity
can carry roles its type wouldn't normally expect). Declarations
should cover the roles the EntityType reasonably carries; the
fallback/state_row.html include is the escape hatch for panels that
want the standard rendering for a declared role.
2026-05-21 09:15:19 -05:00
Tony C
a878fb15f1 Conform existing state panels to ROW/TILE size budgets (#343)
* Phase 1: Fix thermostat CSS typos, defer budgets to CSS, enlarge steppers

Three small fixes ahead of the per-panel ROW redesigns:

- thermostat.css:119, :208: tile-template-columns -> grid-template-columns.
  The substitution was introduced inadvertently by the #339 Phase 2
  grid-temp -> tile-temp rename (which matched grid-template-columns as
  a substring). Browsers silently dropped the unknown property, so the
  thermostat modal modes-grid (intended 2-column layout) and the
  thermostat row dial-meta grid (intended 96px 1fr layout) have been
  rendering as default block layouts since #339 merged.

- Framework guide stops duplicating the panel size-budget numbers.
  The doc now references the --hi-panel-tile-* and --hi-panel-row-*
  CSS variables in main.css as the source of truth, keeps the
  qualitative shape and aspect-ratio contracts that aren't in the CSS,
  and adds a note that the height side of the TILE budget is a target
  rather than a clamp (camera live feeds being the canonical case).

- Thermostat modal stepper buttons bumped from 32x32 to 44x44 to meet
  the touch-target rule documented in the framework guide.

* Phase 2: Thermostat ROW redesign with setpoint controls

Replaces the old row layout (96x96 SVG dial + vertical-stacked text
metadata, carried over from the pre-#339 list.html) with a horizontal
strip designed for the ROW context: entity name + mode-status label
on the left, color-tinted current temperature, setpoint stepper(s),
and mode / fan / preset selects across the row width. The dial is
retained in tile and modal contexts.

Antinode's div[data-async] click handler now skips when the click
originated inside an interactive descendant (a, button, input, select,
textarea, [role="button"]). Previously, the wrapping card click
handler fired before any panel-author stopPropagation could
intervene, opening the modal on every stepper or select interaction.
This factoring also retires the "controllers must stop propagation"
contract from the framework guide — panel authors no longer need to
think about it; antinode bows out automatically when the click hit an
inner control.

The pre-existing controller-stopPropagation calls in thermostat.js
were removed as redundant; the corresponding pitfall in the framework
doc was replaced with the touch-target-sizing reminder (still
relevant) and the new "antinode handles it" model.

Progressive disclosure via container queries (@container collection-row)
on .collection-row-list: as the actual content width narrows, hide
right-most lower-priority controllers — preset first, then fan, then
mode select, then the mode-status label. Container queries (not @media
viewport queries) so the breakpoints work correctly regardless of
sidebar / layout-chrome width.

* Phase 3: Smoke detector ROW horizontal-strip layout

Replaces the prior 3-column-grid-with-vertical-meta-stack (badge | meta
column | aux) with a single horizontal flex row. The badge sits left,
the entity name takes the flexible middle, the status label sits next
to the name, and the "Triggered X ago" timestamp + optional battery
readout right-align via margin-left:auto. Status-driven visibility of
the status-label variants and the last-event line is preserved through
the existing CSS rules; the template change is purely structural.

Badge shrunk from 60x60 to 44x44 so the row fits the budget cleanly
(~56px tall with padding).

* Phase 4: Camera ROW redesign without live feed

Drops the 160x90 stream embed in the camera row context — a tiny live
feed is not useful, and the modal already carries the full-size live
view. Replaces it with a status-only horizontal strip: 80x45 static
snapshot thumbnail (or the camera entity icon when no snapshot is
available), entity name, optional motion chip, and an optional
"Motion N min ago" timestamp right-aligned.

The snapshot thumbnail uses the existing entity_video_snapshot
template tag and cache_bust_url helper for cache freshness. Renders
the snapshot as a static <img> only — no polling, no synthetic
streaming markers — because the row is for at-a-glance status, not
live monitoring.

The motion chip's CSS is overridden in ROW context to flow inline
(position: static; flex-shrink: 0) since there's no stream to overlay.
TILE and MODAL contexts keep the absolute-positioned overlay
behavior.

* Phase 5 part 1: Card-fill + ROW status-tint conformance

- .entity-card--tile and --row become ``display: grid`` so the single
  panel-root child auto-stretches to fill the card. Previously the
  card's min-height didn't establish a resolvable parent height for
  the panel's ``height: 100%``, leaving the card's white background
  visible around the colored panel content.

- Smoke detector and thermostat ROW templates now carry the same
  status-tinted backgrounds as their TILE counterparts (smoke decay
  palette: idle / active / recent / past; thermostat HVAC palette:
  heating / cooling). Camera ROW intentionally has no tint — the
  snapshot is the visual.

* Phase 5 part 2: Fallback ROW/TILE redesign + status-aware icon

The fallback row/tile previously rendered the entity icon + name
plus the full per-state list using state_row.html (Bootstrap card
chrome per state). Inside the new .entity-card wrapper this produced
card-within-card-within-card chrome, and entities with many states
overflowed any compact container.

Replaces with a compact-by-design layout:

- ROW: icon + name on the left, then up to N state widgets stacked
  horizontally, growing to fill available row width (capped at
  280px each). flex: 1 1 0 so widgets share remaining space.

- TILE: icon centered atop name, then up to N state widgets stacked
  vertically.

- Per-state widget uses controller_data.html directly (without the
  surrounding row chrome / history button) so sliders, selects,
  switches, etc. remain functional in compact contexts. Sense-only
  states fall back to latest_display_label text.

- N is bounded by Django's |slice filter (currently :3 for ROW,
  :2 for TILE). Easy to tune: search for "|slice:" in the two
  templates. No "more states" indicator — fallback's "show a useful
  subset" matches what typed panels do, and the modal carries the
  full state list.

Status-aware icon (entity_status_svg_icon.html, new):

- Standalone polling-hooked SVG icon template. Renders its own <svg>
  wrapper plus an inner <g> that carries data-state-id / data-status /
  status="..." when a primary_state_data is passed. The <g>-wrap is
  deliberate — the project's existing g[status="..."] CSS rules in
  main.css (~line 720+) author for the LocationView's <g>-wrapped
  icons; placing the polling attrs on the <g> makes the fallback icon
  participate in those rules without needing duplicate svg[status=...]
  selectors.

- Fallback row/tile use the new template, passing the first state of
  state_status_data_list (role-ordered primary) as primary_state_data.
  Stateless entities get no polling attrs (graceful degrade — icon
  still renders, just doesn't animate from polling).

- entity_display_only_svg_icon.html is unchanged; it remains the
  truly-display-only variant for legend / example renderings.

Antinode interactive-descendant skip (asyncClickHandler):

- Added ``label`` to the list of interactive selectors. Toggle-style
  on/off controllers use a <label> wrapping <input type=checkbox> +
  <span class=switch-slider>; the user clicks the visible span,
  which has no other interactive ancestor — closest() needs to find
  the wrapping label to recognize the click as controller-targeted.
2026-05-18 15:21:51 -05:00
Tony C
d7d2d0e67c CollectionView interaction-intent model + panel ROW/TILE rename (#341)
* Phase 1: Spec doc for ROW/TILE rename and CollectionView integration

Update entity-state-panels.md to describe the new framework shape before any
code lands. Renames the LIST/GRID DisplayContext vocabulary to ROW/TILE,
documents per-context size budgets and CSS variables, and adds a
CollectionView integration section covering the four CollectionViewType
values, the whole-card click contract, and the controller pointer-event
convention. Acts as the spec the implementation commits in later phases
will target.

* Phase 2: Rename DisplayContext LIST/GRID to ROW/TILE

Pure rename, zero functional change. Updates the panel-framework
vocabulary to match what authors actually design for (shape), not what
consumers do with the result (layout):

  DisplayContext.LIST  -> DisplayContext.ROW
  DisplayContext.GRID  -> DisplayContext.TILE

Cascade:

- Enum values in entity/enums.py.
- All panel.py declarations (fallback, camera, smoke_detector, thermostat):
  panel object names (*_list -> *_row, *_grid -> *_tile), display_contexts,
  template_name paths.
- Template file renames via git mv: list.html -> row.html,
  grid.html -> tile.html across all four panel directories.
- CSS class hooks and content classes per panel: --list -> --row,
  --grid -> --tile, list-meta -> row-meta, grid-overlay -> tile-overlay,
  grid-temp -> tile-temp.
- Interim mapping in collection_manager (still binary is_grid vs is_list;
  Phase 3 expands to four-value dispatch).
- Test fixture maps in test_state_panel_framework.py.

All 2974 tests pass; lint clean.

* Phase 3: Expand CollectionViewType to four values; rewire camera bypass

Adds DEFAULT and SECURITY values to CollectionViewType (in addition to
existing GRID and LIST), placing DEFAULT first so it becomes the default
for new collections. CollectionType is untouched.

The wrapper-template camera bypass (entity_card_list.html /
entity_card_grid.html) and the grid_css_class branch in transient_models
are rewired from collection.collection_type.is_cameras to
collection.collection_view_type.is_security. Functional behavior is
unchanged at this phase; the trigger shifts from CollectionType-driven
to ViewType-driven, and Phase 4 will retire the bypass altogether in
favor of the panel framework path.

Migration 0003 is data-only: any existing CAMERAS-typed collection has
its collection_view_type_str set to 'SECURITY' to preserve the camera-
montage rendering through the new SECURITY-driven bypass.

Tests cover the four-value classification (every value classifies as
exactly one of is_default/is_grid/is_list/is_security) and persistence
through the model property accessor for DEFAULT and SECURITY.

All 2976 tests pass; lint clean.

* Phase 4: CollectionView wrapper rewrite + adaptive CSS + GRID_LARGE

Replaces the old GRID/LIST card wrappers with four per-CollectionViewType
templates, drops the is_security camera bypass entirely, and switches to
adaptive CSS column counts driven by panel size budgets.

CollectionViewType expands to five values: DEFAULT (icon+name index, no
panel), GRID (adaptive tiles), GRID_LARGE (bigger tiles for camera
montages and dial-heavy panels — 2 cols on tablet, 3 on standard desktop,
4 on wide desktop), LIST (full-width rows), SECURITY (aspirational
placeholder, renders like GRID today).

Whole-card click model: each wrapper is a <div data-async data-href>.
Antinode picks up the click via div[data-async] (extended to read URL
from data-href when href is not valid HTML5 on the element). Inner
antinode-managed links (camera live view) stop propagation naturally,
so the outer card click does not fire when the user clicks an inner
action target. Non-antinode interactive elements (controllers) must
stop propagation per the documented panel-author convention.

Edit mode disables inner interactivity globally via a CSS rule
([hi-edit="True"] .entity-card * { pointer-events: none }) so card
clicks always reach the outer wrapper and open the edit pane —
panel authors do not need to gate their own handlers on edit state.

Fallback panel TILE/ROW templates now render entity icon + name + the
state list, making them self-contained. Stateless entities no longer
produce blank cards.

Migration of CAMERAS-typed collections to SECURITY (from Phase 3)
stands; users who want the original 2-col montage visual can opt into
GRID_LARGE on a per-collection basis.
2026-05-18 12:52:14 -05:00
Tony C
4a7d6d9507 Complete the three initial EntityStatusPanel implementations (#340)
* Phase 2: Entity status panel framework spec

Rewrite docs/dev/frontend/entity-status-panels.md to describe the new
panel framework before any code lands. The spec is the target the
Phase 3 implementation commits will produce. Key concepts:

- Panels are Python declarations under
  src/hi/apps/entity/state_panels/<name>/panel.py with templates and
  static assets in mirrored directory trees. Autodiscovery at app-ready
  time; adding the panel.py file is the entire registration.
- Each declaration carries name, entity_type, display_contexts (Set),
  priority (required int), required_roles + optional_roles (disjoint
  Sets of EntityStateRole), and template_name. No filename convention
  on templates.
- Dispatch is a view-layer concern: a resolver helper selects the
  matching panel from (EntityType, DisplayContext, EntityStateRoles
  present), ordered by priority. Multiple panels per EntityType are
  the supported way to express layout variants; templates stay
  context-agnostic.
- Display contract: declared roles must be displayed by the panel;
  roles outside the declared set are "extras" owned by the framework.
- Modal context auto-appends an expandable "Other states" section for
  extras, giving the user a framework-level invariant that every
  EntityState is reachable in the modal view. Grid and list ignore
  extras silently.
- Required-role access in templates needs no defensive {% if %}.

Also extend entity-status-display.md with one paragraph clarifying
that the polling apply pass tolerates state-set changes across polls
(missing ids are no-ops; new ids picked up on subsequent ticks),
formalizing the contract panel JS handlers already assume.

No code change in this commit. The render_entity_status_panel
template tag, today's three monolithic panels, and the current
fallback-chain dispatch remain in place until Phase 3 lands.

* Tweaks to Claude /pickup command.

* Phase 3a: Entity status panel framework core

Lands the new framework's Python primitives. No callers wired yet: the
existing render_entity_status_panel template tag (and its
resolve_panel_template helper) stay in place until Phase 3b removes
them and migrates the wrapper views.

- DisplayContext enum (MODAL / LIST / GRID) added to entity.enums.
- EntityStatusPanel as a plain @dataclass with __post_init__ validation
  only. No registry coupling; required vs optional fields are
  expressed by dataclass defaults.
- EntityStatusPanelRegistry singleton: discover() walks
  state_panels/<name>/panel.py and registers every EntityStatusPanel
  instance found at module scope (one panel per module is typical;
  multiple are supported). Identity-based dedup in register().
- resolve_panel(entity_type, display_context, present_roles) returns a
  PanelResolution carrying the chosen panel, the computed extras
  (present_roles - declared_roles), and a trace for ?debug_panel=1.
  Type-specific matches win over entity_type=None fallback panels;
  within either group, lower priority wins, name is the alphabetical
  tiebreaker.
- entity.apps.DeviceConfig.ready() triggers discovery.
- 18 unit tests cover declaration validation, registry register/dedup,
  and the full dispatch decision tree (typed wins, priority ordering,
  alphabetical tiebreak, missing-required disqualification, context
  mismatch, extras computation, no-fallback raise).

Doc: rewrite docs/dev/frontend/entity-status-panels.md so the
authoring contract matches the implementation (dataclass instance in
panel.py, not subclass; module-scope scan, not __init_subclass__).

* Phase 3b: retrofit panels onto new framework + wire view layer

Cuts the entity status modal and the collection card wrappers over to
the new EntityStatusPanel framework. Each existing panel becomes a
set of EntityStatusPanel declarations (one per DisplayContext, since
templates are context-agnostic under the new framework), and the
view layer resolves which panel to render before the template runs.

Panel declarations under hi/apps/entity/state_panels/<name>/panel.py:
- thermostat_{modal,list,grid}: required THERMOSTAT_CURRENT_TEMPERATURE,
  optional THERMOSTAT_TARGET_TEMPERATURE + HVAC_ACTION. An entity
  marked THERMOSTAT without a current-temperature state now falls
  back to the flat state list rather than rendering a sparse dial.
- smoke_detector_{modal,list,grid}: required SMOKE, optional
  BATTERY_LEVEL.
- camera_{modal,list,grid}: no required roles (panel renders against
  has_live_view).
- fallback_{modal,list,grid}: entity_type=None catch-all.

Dispatch layer (state_panel_dispatch.py) gains EntityPanelData — a
single dataclass that encapsulates everything a wrapper template
needs (entity_status_data, panel_template, panel_context, trace, plus
forwarded entity / display_category / display_only_svg_icon_item /
extras_state_data_list). build_entity_panel_data(entity_display_data,
display_context) is the single helper both EntityStatusView and
CollectionManager call; both flow the same object into the same
{% include_panel entity_panel_data %} tag. The old per-panel
{% render_entity_status_panel %} dispatch tag and its supporting
legacy resolver are gone.

build_panel_context filters state_status_data_list and
state_status_data_by_role to the panel's declared (required +
optional) roles. Roles outside that set are extras; the modal wrapper
auto-appends an "Other states" expandable section for them, grid and
list ignore them silently. Fallback panels (entity_type=None) see the
unfiltered state list, so the fallback path behaves like today.

Dead {% if current_data %} / {% if smoke_data %} guards on required
roles are removed from the affected panel templates — the framework
guarantees those roles are present whenever the panel renders.

Append ?debug_panel=1 to the entity status modal URL to see the
dispatch trace as an HTML comment.

Test isolation: EntityStatusPanelRegistry gains snapshot_for_tests /
restore_for_tests so framework tests can spin up an isolated registry
and restore production state in teardown.

* Refactor entity state panel naming + dispatch class

Two coupled refactors on the new panel framework, no behavior changes.

1. Wrap the panel dispatch module-level functions in a
   ``StatePanelDispatcher`` class with classmethods, matching the
   codebase convention of namespacing related operations on a class
   rather than at module scope. ``PanelResolution`` and the renamed
   ``EntityStatePanelData`` stay module-level as result types.

2. Disambiguate "Status" vs "State" in the framework's class names.
   The framework renders only the *state* subset of an
   ``EntityStatusView``; the directory / module / dispatcher names
   already use "state_panel," but the class names drifted back to
   "Status." The classes now match the file layout:

   - ``EntityStatusPanel``         -> ``EntityStatePanel``
   - ``EntityStatusPanelRegistry`` -> ``EntityStatePanelRegistry``
   - ``EntityPanelData``           -> ``EntityStatePanelData``
   - method ``build_entity_panel_data`` -> ``build_state_panel_data``
   - variable ``entity_panel_data`` / ``entity_panel_data_list``
     -> ``state_panel_data`` / ``state_panel_data_list``
   - doc ``entity-status-panels.md`` -> ``entity-state-panels.md``

   ``EntityStatusView`` / ``EntityStatusData`` keep their names: those
   genuinely scope to the broader status view that *contains* the
   state panel.

* Address pre-Phase-4 review feedback on state panel framework

Fixes a real bug and applies a batch of framework hardening + cleanup
items surfaced by the four-agent review of the state panel work.

Bug fix:
- ``CollectionViewType.is_grid`` / ``is_list`` were methods, not
  properties. The Phase 3b dispatcher read ``.is_grid`` without
  calling it (truthy bound method), so collections always resolved
  through the GRID context regardless of view type. Convert to
  ``@property`` for consistency with ``CollectionType.is_cameras``;
  templates auto-call either form.

Framework hardening:
- ``EntityStatePanelRegistry.discover()`` catches ``Exception``
  per-module and sets ``_discovered`` only after a successful pass,
  so a malformed third-party panel can't poison Django startup.
- ``{% include_panel %}`` tag passes ``request=`` to ``Template.render``
  so the inner panel renders under a fresh ``RequestContext``,
  re-running context processors.
- ``StatePanelDispatcher.resolve_panel`` gains a ``debug`` parameter;
  trace string formatting is skipped unless ``debug=True`` or
  DEBUG-level logging is enabled. The collection hot path (one
  dispatch per card) no longer pays the trace cost.
- ``EntityStatusView`` opts into trace construction only when
  ``?debug_panel=1`` is set.

API and naming:
- Rename ``EntityStatePanelData.entity_status_data`` field to
  ``entity_display_data`` so it matches the ``EntityDisplayData``
  type it holds; collection card wrappers updated accordingly.
- Drop ``TYPE_CHECKING`` forward-reference; ``EntityDisplayData`` is
  imported directly. Type-annotate ``entity_display_data`` parameters
  on the dispatcher methods.
- Forward ``entity_for_video`` on ``EntityStatePanelData`` to match
  the other entity-level forwards.
- Promote ``extras_state_data_list`` to a typed dataclass field
  (was derived through a dict-key lookup on ``panel_context``).
- Extract role-filtering to ``EntityDisplayData.filter_to_roles``;
  the dispatcher's ``build_state_panel_data`` is now pure assembly.

Cleanup:
- Delete ``EntityDisplayData.to_template_context``; production paths
  flow through the dispatcher, and the two tests that referenced it
  now assert on the dataclass's properties directly.
- Drop ``EntityStatePanelRegistry.reset_for_tests``; ``snapshot``/
  ``restore`` with an empty snapshot covers the same need with one
  fewer public method reaching into private state.
- Sweep ``EntityStatusPanel`` stragglers in JS / CSS comments.
- Drop dead ``{% with sensor_response %}`` in
  ``fallback/state_row.html`` and ``{% if state_status_data_list %}``
  in ``camera/modal.html`` (camera declares no roles, so its
  filtered state list is always empty).
- Add NaN guard in ``thermostat.js`` so an empty ``data-temp-value``
  hides the marker rather than parking it at the dial's top.
- Standardize validation error prefixes on
  ``EntityStatePanel(<name>): ...``.

Tests added:
- ``EntityStatusView`` falls back to the framework fallback panel
  when a typed entity is missing its required roles (integration).
- ``discover()`` skips a broken panel module and continues
  registering the others.
- ``{% include_panel %}`` merges ``panel_context`` over the parent
  context (panel values win on key collision).

* Phase 4a: thermostat panel completion + role-data template aliases

Brings the thermostat EntityStatePanel up to the original mock and adds
a framework convenience for panels with many role lookups.

Framework extension — role_data_template_aliases:
  EntityStatePanel gains an optional ``role_data_template_aliases``
  declaration: a dict mapping template-context variable names to
  declared EntityStateRoles. The dispatcher resolves each alias against
  the entity's by-role map at render time and seeds ``panel_context``
  with the named variables, so templates use ``{{ current_data.X }}``
  directly instead of chaining ``{% with %}`` blocks (which Django
  cannot parse across newlines, making chains of more than a few
  bindings unwieldy). Validated at construction: every aliased role
  must be in ``required_roles | optional_roles``; values must be
  ``EntityStateRole`` members. Recommended threshold is 5+ named
  roles per panel — smaller panels keep the lighter ``{% with %}``
  form.

Thermostat panel completion:
- Required role unchanged: THERMOSTAT_CURRENT_TEMPERATURE.
- Optional roles expanded to cover dual setpoint
  (THERMOSTAT_TARGET_TEMPERATURE_LOW + _HIGH), HVAC_MODE, FAN_MODE,
  PRESET_MODE, HUMIDITY (plus the prior TARGET / HVAC_ACTION).
  SWING_MODE intentionally excluded — not exposed by the HA simulator.
- Nine role_data_template_aliases declared so the modal / list / grid
  templates read at the top level (current_data, target_data, low_data,
  high_data, action_data, mode_data, fan_data, preset_data,
  humidity_data). Each template opens with a comment listing the
  aliases.
- Modal template gains a controls grid with a setpoint stepper
  (single-row or dual-row depending on which setpoint roles are
  present) plus Mode / Fan / Preset rows that delegate to the existing
  controller-widget machinery when controllable and fall back to
  read-only labels otherwise. Humidity surfaces as a secondary
  readout. Custom −/+ stepper widget posts each adjustment through
  the same async-form mechanism the slider uses, but routes the
  response into a hidden sink so the buttons survive across clicks;
  polling refreshes both the visible label and the hidden form value.
- List card shows dual-or-single setpoint summary + humidity line.
- Grid card shows dual-or-single setpoint in the mode summary line.
- CSS extended for the modal controls grid, stepper widget, and
  secondary readout row.
- JS gains a stepper click handler that bumps the form's hidden
  value by the button's data-stepper-delta and submits.

Sense-vs-controller asymmetry: each interactive widget checks
``<role_data>.controller_data_list`` to decide whether to render the
controllable variant or a plain readout, so a future integration
exposing thermostat target as sense-only still renders correctly.

Spec doc: ``role_data_template_aliases`` added to the declaration
table and a two-paths section explains when to prefer the alias form
over a single ``{% with %}``.

* Phase 4a polish: thermostat dynamic UI + unified status-apply path

Wraps up the thermostat panel work with the runtime polish that
emerged from exercising it in the browser, plus a framework-level
refactor that benefits every panel.

Thermostat polish:

- Mode-driven single/dual setpoint visibility. HA exposes target /
  target_low / target_high simultaneously, so role-presence alone
  can't pick the active setpoint shape. The panel root carries
  ``data-hvac-mode`` (initial value rendered server-side, kept in
  sync at runtime by the JS update handler against the polled mode
  state); CSS rules at end-of-file show the dual block under
  ``heat_cool`` / ``auto`` and the single block otherwise. Both
  blocks render so the toggle is a CSS swap, not a re-render.

- Layout stability across the toggle. Setpoints occupy their own
  centered row; Mode / Fan / Preset live in an independent 2-col
  grid below. Setpoint count no longer reflows the modes row.

- Custom thermostat select widget that bypasses antinode. The
  standard ``controller_data.html`` form-submit replaces the entire
  widget with the server response on every change, which conflicts
  with the dial / panel-root state the thermostat owns. The
  thermostat-local ``role_control.html`` renders the same
  ``.discrete-select`` widget (preserving the global styling) but
  drops ``onchange-async`` / ``data-async`` and lets the panel's
  own JS handler POST to the controller endpoint.

- Single-line ``{% with %}`` everywhere. Django's tag lexer doesn't
  span newlines despite the ``re.DOTALL`` apparent suggestion;
  multi-line ``{% with %}`` silently parses as text and orphans
  ``{% endwith %}``.

Framework: unified status-apply path
(``src/hi/static/js/entity_state_status.js``)

- Generic ``change`` listener on every
  ``[data-state-id][data-controller-value]`` element synthesizes a
  one-entry statusMap from the new value and runs it through the
  existing ``Hi.entityStateStatus.apply`` pipeline. All dependent
  DOM (display labels, marker positioning, panel-root attributes
  like ``data-hvac-mode``) updates immediately on user interaction,
  not on the next polling tick. Polling reconciles or corrects.

- The server-bound submit path (antinode's ``onchange-async``, or a
  panel's custom ``$.post``) is unchanged and runs independently of
  this handler — error responses still surface through the normal
  mechanism; rejected changes are reverted by the next poll.

- Optimistic format preservation. Numeric synthesis reads the
  decimal precision and unit suffix from the paired
  ``[data-display-text]`` element and applies them to the new value,
  so an optimistic ``73.0°F`` matches the canonical ``72.0°F``
  formatting until the next poll. Heuristic, but covers the common
  decimal + unit case; a follow-up issue should add a server-driven
  format spec to the polling payload for the general case.

Collection cards:

- The entity-name link in collection cards now opens the entity
  status modal in non-edit mode (matching LocationView click
  behavior). Now that cards render a per-type panel rather than the
  full flat status list, the modal is where complete status lives.

* Phase 4b: smoke detector panel completion + low-battery framework status

Brings the smoke detector EntityStatePanel up to the mock and adds
a framework-level low-battery status derivation that any panel
exposing a BATTERY_LEVEL state can opt into.

Smoke detector panel:

- All three contexts (modal / list / grid) restructured to the
  thermostat-era patterns: ``role_data_template_aliases``
  (smoke_data, battery_data), top-of-template alias comment, and
  ``svg_status_style.status_value`` for initial render so the
  decay-aware token (``idle`` / ``active`` / ``recent`` / ``past``)
  matches the vocabulary the polling pipeline emits — no dual
  ``smoke_clear`` / ``smoke_detected`` CSS rule set anymore.
- Four-state badge hero per mock: shield-check (idle), exclamation
  (active), and two clock variants (recent / past). Color tints
  follow the existing decay-color palette. Active-state pulses
  via CSS keyframes.
- Grid card gains a tinted background per state and a dedicated
  ``Low Battery`` warning shown only in low-battery state.
- The status detail line (``Triggered just now`` / ``Alarm 14m
  ago`` / ``No events in the last 30 days``) is intentionally
  deferred — those text variants need recent-sensor-history data
  that the framework doesn't yet expose. Captured for a follow-up
  framework discussion after Phase 4c.

Framework: BATTERY_LEVEL status derivation:

- ``EntityStateDisplayData._get_svg_status_style`` now dispatches
  to ``_get_battery_level_status_style`` for BATTERY_LEVEL states,
  emitting ``StatusStyle.BatteryLow`` when the value is below
  ``BATTERY_LOW_THRESHOLD_PCT`` (20%) and ``StatusStyle.BatteryOk``
  otherwise. The polling pipeline carries the token through the
  existing ``data-status`` opt-in, so any panel can react with
  CSS rules on ``[status="low"]``. Threshold lives on the class so
  it's adjustable in one place.

Simulator:

- New ``HassSmokeDetectorWithBatteryFields`` combo entity (smoke +
  battery states), modeled on the existing leak-sensor +
  battery shape. Zoo profile gets a second smoke detector entry
  named ``Zoo Smoke Detector (Battery)`` to exercise the
  battery aux readout and low-battery escalation paths.

* Phase 4c: camera panel completion + motion chip + extras inline

Brings the camera EntityStatePanel up to the polish bar set by the
earlier panels, and adjusts the framework extras section so its
modal presentation matches the "every state reachable" invariant.

Camera panel:

- One declaration per DisplayContext (no role-based variant split —
  the motion treatment is a small visual enhancement, not a layout
  fork, so the optional-role pattern from the smoke detector
  applies). MOVEMENT is declared optional with a ``motion_data``
  alias; the template guards the chip with ``{% if motion_data %}``.
- Motion chip overlay on the stream corner when the camera entity
  carries a MOVEMENT role. Uses the framework's decay-aware status
  tokens (active / recent / past / idle) so the chip color
  matches the rest of the app's motion treatment, including the
  ``--on-status-*-color`` companion tokens for foreground contrast.
  Pulses on ``active``. 36px in modal, 24px in compact contexts.
- Per the user's redirection of the mock, the live feed continues
  to flow through the existing ``entity_live_view_w_link.html``
  template — the panel handles sizing and the chip overlay, not
  the complications of stream rendering / offline placeholders.
- Connectivity, "Last seen X min ago", and ZM-Function-specific
  layout from the mock intentionally deferred: per-camera
  connectivity isn't tracked anywhere today, recent-history
  timestamps require framework work that hasn't landed, and the
  ZM Function controller falls cleanly into the framework's
  extras section without bespoke handling.

Framework: extras section inline:

- ``extras_section.html`` no longer wraps content in
  ``<details><summary>Other states</summary>``. The collapsible
  predated the framework decision that modal must surface every
  state unconditionally; with that invariant in place, the
  collapsible defeats the contract. The "Other states" heading
  is also dropped — "state" isn't user-facing nomenclature, and a
  section label adds nothing of value.

* Extract shared partials for smoke detector badge and camera motion chip

The badge SVG icons (idle/active/recent/past) and the camera motion chip
markup were duplicated across modal/list/grid contexts. Move each into a
single included partial so the icon/chip definitions live in one place
and the per-context templates only carry their layout-specific wrapping.

* Phase 5+6: Expose recent state-value history to panels; adopt in smoke detector

SensorResponseManager already caches the last 5 deduped sensor values
per state, and EntityStateDisplayData already holds that list. Promote
it from internal threshold input to a first-class, display-ready
accessor — recent_state_value_summary — so panels can surface "how
recent" detail without DB queries.

Each cached entry's value flows through the same ConsoleConverterHelper
pipeline as latest_display_label, so history rows render in the user's
preferred unit. The summary is server-render-only (deliberately not in
the polling payload) and makes no completeness claims about the cache
window.

Smoke detector modal and list contexts now render "Triggered N minutes
ago" when status decays to recent or past; idle and active suppress.
Grid is intentionally left tight — the badge color carries the in-glance
signal and the modal carries the detail.
2026-05-17 21:19:30 -05:00
Tony C
3c33cf29cf EntityStatusPanel framework, dispatcher, and three pilot panels (#328)
* Add EntityStatusPanel framework: dispatch tag, presentation helper, JS hook

Lays the foundation for per-EntityType panels in the three display contexts
(modal, list, grid). No panels yet — just the plumbing the panels will use.

The render_entity_status_panel template tag resolves a panel along a three-
step chain: panel's context-specific template, panel's required default
(modal.html), framework fallback. Framework fallbacks are guaranteed to
exist and currently delegate to the existing flat-list / EntityStatus
default partials so behavior is preserved for every EntityType without a
panel.

The panel_state template tag looks up an EntityState by EntityStateRole
for panel templates that need to reach a specific state by semantic
purpose (e.g., a thermostat panel reaching for its current-temperature
state vs. the setpoint). Walks the entity's own states then delegations;
own-states win on role collision.

The JS dispatcher in entity_state_status.js gains two small additions:

  - The status forwarding selector relaxes from div[status] to [status]
    so panels can use any element tag for the status carrier.
  - After the existing apply pass, registered EntityStatusPanel handlers
    are notified with the statusMap. Panels that need to react beyond
    CSS-driven variant switching register a handler via
    Hi.statePanels.register; panels that don't pay nothing.

Dispatch is wired into the three call sites: EntityStatusView modal body,
collection list card, collection grid card. The legacy entity_status_<type>
modal templates and the collection list/grid is_cameras branches are no
longer in the new resolution chain but stay in place — their migration is
deferred to Phase 3 and Phase 6 respectively.

Tests cover the dispatch fallback chain (per-context resolution, panel
overrides, modal-as-default for list/grid, unsupported context errors)
and the panel_state role-lookup (case-insensitive match, missing role,
delegated states, own-state wins on collision).

* Add smoke detector EntityStatusPanel; rework dispatch tag to take entity_status_data

The smoke detector panel is the CSS-only reference panel. Templates emit
the four visual variants (clear / alarm / recent / past) as separate
elements with class-modifier suffixes; CSS keyed on the panel root's
``status`` attribute shows the one matching the current state. Two
status vocabularies land on the root over a panel's lifetime — the raw
sensor value (smoke_clear / smoke_detected) on initial server render,
and the decay-aware value (idle / active / recent / past) once the first
polling refresh lands. CSS comma-groups them so both render the right
variant.

The dispatch tag's signature changes to take ``entity_status_data``
rather than ``entity``. The tag calls ``entity_status_data.to_template_
context()`` and merges those fields into the panel template's context,
so panels use top-level names (entity, state_status_data_list,
state_status_data_by_role, etc.) regardless of whether the dispatch fires
from the modal, list card, or grid card call site. This resolves the
prior asymmetry where the modal context had fields unpacked at top level
but collection card contexts only had ``entity_status_data`` as a single
variable.

EntityStatusData gains a ``state_status_data_by_role`` property — the
role-keyed lookup panel templates use to pull a specific state by
semantic role (e.g., ``state_status_data_by_role.smoke`` on a smoke
detector, ``state_status_data_by_role.battery_level`` for the battery
auxiliary). The dispatch tag exposes it via ``to_template_context()``,
which now also self-references ``entity_status_data: self`` so the
modal's own template can pass the dataclass to the dispatch tag.

Pipeline registers the smoke detector CSS in the ``css_head`` bundle.
Per-panel asset auto-loading is deferred per the v1 spec.

* Add camera EntityStatusPanel and framework-level identifying chrome

Camera panel composes existing video-stream and state-status partials:
the existing entity_video_stream_w_link.html for the live frame
(click-through to the full-page video pane preserved), and the existing
entity_state_status_list.html for any controllers/sensors the entity
exposes (so ZoneMinder Function dropdown and the like render through
the established controller_data machinery). No new JS — connection
management, MJPEG rendering, and integration-gateway lookup all stay
where they live. The is_cameras collection branches are unrelated
(montage view) and stay in place.

EntityStatus modal chrome reworked to provide identifying information
at the framework level rather than each panel template repeating it.
Modal title carries the entity name. A new subheader strip inside the
modal body shows entity-type icon + label on the left, integration
logo centered (when present), and the "Status" modal-type label on
the right. Panels render their body below this strip and don't
duplicate any of this identifying chrome. The smoke detector and
camera panel templates lose their per-panel headers accordingly.

* Tighten polling-update scope and make the flat-list fallback a panel

Polling-update dispatch in entity_state_status.js previously expanded its
scope from the matched CSS-class element to its descendants when looking
for ``display-text`` / ``[status]`` markers. That worked for the flat-
list partials' wrapper-with-class + child-with-attr structure but bled
across state boundaries when a state's CSS class anchored a panel root
containing other states' display elements.

Tightens the contract: an element receives a polling update only when
it has BOTH the matching CSS class AND the marker attribute (``status``,
``display-text``, etc.) on the same element. No descendant propagation.
``filter('[attr]')`` replaces ``find('[attr]').addBack(...)``; the
``else if (attrName == 'status') { find('div[status]') }`` legacy path
is gone.

The sensor flat-list templates that previously wrapped the value div in
a class-bearing wrapper are restructured: the value div carries the
class itself, and the outer wrapper is removed.

entity_state_status.js absorbs the universally-useful pieces of the old
controllers.js (active-slider tracking, value-to-element dispatch,
slider-display sync, checkbox value coercion, slider drag input mirror)
and combines attribute / display / controller-value application into
one pass per CSS class — the prior two-stage apply with a separate
``Hi.controllers.applyValueMap`` callout dropped.

The flat-list-specific bits that remained in controllers.js — the
``.on-off-control`` / ``.status-text`` caption mirror and the
continuous-slider preset-button click handler — move into a new
``state_panels/fallback/fallback.js`` and register with the post-apply
panel hook like any other panel JS. The fallback is now its own panel
(matching the existing fallback templates in state_panels/fallback/).

controllers.js deleted; pipeline updated accordingly (entity_state_
status.js promoted into the always-loaded ``js_before_content`` bundle
so the fallback panel's ``Hi.statePanels.register`` call has the
framework's panel-handler registry in scope).

* Self-describing polling dispatcher and thermostat panel

Dispatcher rewritten around a data-state-id anchor plus per-element
declaration attributes (data-status, data-display-text/magnitude/unit,
data-controller-value, data-svg-style). Replaces the class-based
join that forced authors to fuse a CSS class and an update marker
onto the same element and shared the status attribute namespace with
the global div[status] paint. Each element now opts in directly;
no descendant traversal, no propagation leaks.

Server-side statusMap keyed by entity_state.id with nested
{status, controller, display, svg_style}. SVG icons opt in via
data-status (singular push); SVG paths use data-svg-style (full
bundle) so each element only receives the attributes it rendered.

Global div[status] rule rescoped to .hi-status-display[status],
freeing the status attribute for panel use. EntityStateDisplayData
delegates raw accessors via __getattr__ so mainline templates that
took raw status data also accept the display wrapper.

Panel framework adds registerInit alongside registerUpdate. Init
handlers fire at jQuery ready and after async content insertion;
antinode gets a dedicated afterModalRender hook so JSON-delivered
modals get their init pass once the modal is in the DOM (the
existing afterAsyncRender stays at its earlier position to keep
scroll-bar restore ahead of resetScrollbar/scrollTo).

Adds thermostat EntityStatusPanel (modal/list/grid) with SVG dial
markers positioned via the registered panel JS.

Renames monitor/transient_models.py to status_data.py and
status_display_data.py to display_data.py for symmetric naming;
EntityDisplayData wraps raw EntityStatusData with display
projections (CollectionData and EntityStatusView adopt it).

* Document the icon vs path polling-update asymmetry

SVG icons opt into polling updates via data-status while paths use
data-svg-style; an existing bug fix made the choice mechanical but
the rationale lived only in conversation. Adds a short subsection
under Client-Server Status Updates explaining why icons can't
safely receive the full attribute bundle (SVG inheritance fights
child-specific styling) and why paths can (single element with a
Python-parameterized palette).

* Consolidate fallback panel templates into the panel directory

The EntityStatusPanel fallback was structured as three thin
wrappers that included historical pre-framework templates living
elsewhere. The asymmetry forced new readers to chase indirection
to understand what the fallback actually renders, and left
chrome-rendering duplicated between the legacy default body and
the modal framework subheader.

Inlines the fallback's modal/list/grid templates and moves the
flat-list partials (now state_list.html and state_row.html) into
state_panels/fallback/, since fallback is where those partials
were first authored. Camera and thermostat panels — which embed
the flat list inside their own modal layouts — reference the
partials from their new home.

Drops the duplicated identifying chrome (entity name, integration
logo, type label) from the fallback modal body; the framework's
subheader block in entity_status.html already renders it.

Deletes orphan legacy templates: entity_status_default.html, the
unused per-type wrappers entity_status_thermostat.html and
entity_status_ceiling_fan.html, and the collection-card list/row
pair (collection/panes/entity_state_{list,row}.html) which had
diverged from entity/panes/entity_state_status_{list,row}.html on
short-name vs full-name with no real justification — both contexts
display the entity name in surrounding chrome, so the short-name
form is correct in both.

* Remove dead in_modal_context flag from controller form rendering

``in_modal_context`` was originally a hint to controller_data.html
about whether to emit ``data-stay-in-modal="true"`` (which keeps
the entity status modal open across control submissions) and a
hidden ``response_context`` input the server read back to preserve
the flag across the form-replace round trip.

The configurability was already dead: ``controller_data_row.html``
hardcoded ``in_modal_context=True`` at the only template boundary
where the value reached ``controller_data.html``, so every
controller form already emitted both signals. Outside a modal,
``data-stay-in-modal`` is a no-op because antinode's modal-close
check gracefully handles the no-modal case.

Drops the flag everywhere: hardcodes ``data-stay-in-modal="true"``
on the controller form, removes the hidden ``response_context``
input and its server-side parse, drops ``in_modal_context`` from
``controller_data_response``'s signature, and strips the unused
``in_modal_context=True`` from the panel templates that propagated
it (camera/modal, thermostat/modal, fallback/{modal,list,grid}).
No observable behavior change.

* Reorganize and reconcile entity display frontend docs

The three existing docs (entity-status-display, entity-status-panels,
entity-visual-configuration) overlapped on the polling-update
contract while none gave a top-level mental model. The two older
docs also contained fictional code samples that pre-dated the
data-state-id dispatcher refactor and actively misled readers.

Adds entity-display-overview as a one-page architecture entry
point: the sensor-to-pixel flow, the four display surfaces
(LocationView SVG icons, paths, status modal, collection cards),
the polling contract as connective tissue, and a topic-keyed
index into the focused docs.

Rewrites entity-status-display as the polling-update contract
reference: server payload shape, the data-state-id anchor and
declaration grammar, status-value vocabulary, color palette, and
the icon-vs-path asymmetry. Removes the fictional CSS-class
mapping examples and the fictional thermostat workflow that
pre-dated the dispatcher refactor.

Trims entity-status-panels' polling-contract section to defer to
the contract doc rather than restating it.

Slims entity-visual-configuration to its accurate core: SVG icon
and path asset authoring, EntityStyle registration, and per-type
StatusStyle binding. Removes the fictional Template Integration
section and the two fictional workflow walkthroughs that invoked
methods (get_status_css_classes, get_path_styling) that don't
exist.

Each doc now cross-links to all three others.

* Surface integration health and improve controller error chrome

Adds two failure-mode UX improvements to the entity status modal:

1. Integration health banner. When an entity's owning integration
   reports WARNING / ERROR / DISABLED, the modal renders an alert at
   the top of its body indicating the integration's status and that
   displayed values may be stale and controls may not respond. The
   banner is a snapshot at modal-open time (no live tracking); it
   renders nothing for healthy integrations.

   Implemented as a reusable inclusion tag
   ``integration_health_banner`` in the integrations app, so any
   surface that depends on an integration's data can drop in the
   banner with a single template line plus an optional context
   message describing how the degraded state affects that surface.

2. Controller error chrome. Replaces the bullet-list red-text error
   block on failed controller actions with a Bootstrap alert-danger
   that uses an icon, a compact small-text body, and one line per
   error message. The styling matches the rest of the application's
   error UI; the message content channel is unchanged.

* Use exclamation-circle for critical health status icons

Aligns the icon for FAILING / critical health status with the
existing icon for ERROR / UNAVAILABLE — both states are attention
signals, and a shared exclamation-circle glyph reads more
consistently across the health-status UI than the previous
times-circle. Updates ApiHealthStatus.status_icon,
HealthStatus.status_icon, and the test-UI page that mirrors the
critical-state icon.

* Fix Firefox video stream connection leaks via service worker and lifecycle hooks

Three reinforcing changes that together resolve Firefox failures when
loading pages with multiple MJPEG video streams (live camera streams
and event-video playback). Chrome was unaffected; Firefox's service
worker handling of concurrent multipart/x-mixed-replace responses
combined with no explicit teardown on async content swap caused
connections to leak across page navigations and exhaust the per-host
pool.

Service worker scope. The fetch handler now intercepts only the
pre-cached static assets list, returning early for everything else.
MJPEG streams, polling, and dynamic pages flow through the browser's
native fetch path, avoiding the multipart-via-SW edge that Firefox
mishandles when multiple streams are concurrent.

Antinode before-render hook. Adds addBeforeAsyncRenderFunction
mirroring the existing after-render hook. Antinode fires it with the
outgoing $target before each HTML content swap, giving subscribers
the chance to clean up resources tied to elements about to be
orphaned. Generic; antinode no longer knows about video.

Video connection manager rewrite. Replaces the single-current +
small-previous-cache design (which used querySelector and could only
ever see one element) with a Set tracking every <img> opted in via
data-video-stream. Registers a beforeAsyncRender callback that
force-closes streams in the outgoing subtree by swapping src to a
transparent GIF, terminating the fetch before the browser orphans
the element. Reconciles on each afterAsyncRender / afterModalRender
to keep the set honest. Adds the marker to all six stream <img>
templates: live stream, event playback, alert pane and modal video
elements, sensor history modal.

* Address post-review feedback: lifecycle, caching, SW scope, hook isolation

Four follow-up fixes from the pre-PR review pass:

1. Modal-dismiss video stream leak. Renamed antinode's
   ``addBeforeAsyncRenderFunction`` to ``addBeforeContentRemovalFunction``
   so the name reflects what it really fires for — any subtree about to
   be detached from the DOM by an antinode-driven operation. Added a
   second call site in ``handleModalHiddenEvent`` before the modal is
   removed, so VideoConnectionManager force-closes streams on modal
   dismissal in addition to HTML content swap. Antinode has no direct
   reference to the manager; both call sites just invoke registered
   callbacks.

2. Antinode hook isolation. The three lifecycle loops
   (``beforeContentRemoval``, ``afterAsyncRender``, ``afterModalRender``)
   now wrap each callback invocation in try/catch and log on error.
   Matches the pattern already in place in entity_state_status.js'
   panel-handler loops. A throwing handler logs and yields rather than
   skipping siblings or bubbling into antinode's caller.

3. EntityDisplayData per-state wrapping memoization. Adds a
   ``state_display_data_map`` field initialized in ``__post_init__``
   that wraps each contained state in ``EntityStateDisplayData``
   exactly once. The two projection properties
   (``state_status_data_list``, ``state_status_data_by_role``) now
   look up wrappers from the map. Each per-state cost (the
   ConsoleConverterHelper lookup and ``_get_svg_status_style`` dispatch)
   is paid once per render even when both projections are touched
   (the typical ``to_template_context()`` path).

4. Service worker rewrite. The fetch handler now intercepts only
   paths under Django's STATIC_URL prefix; the enumerated
   STATIC_ASSETS list goes away entirely so the SW doesn't need
   manual maintenance as the static asset surface grows. Cache-first
   with on-demand population on miss; bumped CACHE_VERSION to wipe
   any stale state from the previous SW. STATIC_URL is exposed to all
   templates via the existing constants_context processor.
2026-05-14 20:27:56 -05:00
Tony C
900f85394c HA thermostat support + temperature unit handling (#299 / #308) (#309)
* Issue #299 Phases 1+2: HA climate substate decomposition and thermostat simulator entity

Climate is inherently multi-axis, so apply the substate-decomposition pattern from #295/#298. A climate entity that declares hvac_modes always decomposes into substates: current_temperature (sensor), hvac_mode (controllable from declared modes), hvac_action (sensor with HA's standard action choices), plus a setpoint substate set chosen from the supported modes — single target_temperature for any single-mode operation, low+high pair for heat_cool. Climate entities lacking hvac_modes fall through to the existing single-state TEMPERATURE mapping for backward compatibility.

Per-domain composer methods _climate_substate_specs and _climate_substate_value join the existing _light/_fan ones. _climate_to_sensor_value_map handles the multi-axis decomposition; the dispatcher routes climate to it. A small _numeric_attr_as_str helper centralizes the recurring float-coercion-with-graceful-fallback pattern used across temperature attributes.

Simulator side adds HassThermostatFields with operator-configurable hvac_modes list and temperature_unit, paired with six SimState classes (current_temperature, target_temperature, target_temp_low, target_temp_high, hvac_mode, hvac_action). The new HassApiComposer._thermostat collapses them into one HA climate.x entity, picking the setpoint shape (single temperature vs target_temp_low+high) based on the active hvac_mode — mirroring real HA's per-mode shape change. The HassServiceDispatcher._thermostat handler routes set_temperature (single or low/high) and set_hvac_mode to the matching SimStates. The hass-zoo profile gains two thermostats: Zoo Thermostat (full-feature heat/cool/heat_cool/off, °F) and Zoo Heater (heat-only, °C) — exercising both single-mode and multi-mode setpoint shapes plus both temperature units.

* Issue #299 Phases 3+4: outbound dispatch for climate substates plus fan_mode and current_humidity axes

Phase 3 — outbound dispatch

_substate_service_call extended for climate suffixes: target_temperature routes to set_temperature with single-value payload; the target_temp_low / target_temp_high pair routes to set_temperature with the dual-value payload, using a companion-substate cache lookup that mirrors the hue/saturation pattern (so adjusting one bound carries the other along, with a safe ordering fallback when the partner value isn't yet cached); hvac_mode routes to set_hvac_mode. New HassServiceComposer methods for_temperature_range and for_hvac_mode encapsulate the wire shapes. New controller_temperature.html slider widget reads min/max from the EntityState's value_range_dict (set per-temperature_unit at Phase 1 substate creation time) so the slider bounds adapt to °F vs °C; unit-suffix display is a placeholder pending #308's full unit-handling work.

Phase 4 — stretch axes

Climate substate spec composer extended for fan_mode (DISCRETE controllable, choices from the per-thermostat fan_modes list when reported) and current_humidity (HUMIDITY sensor-only when reported). _climate_substate_value reads fan_mode and current_humidity attributes. New HassServiceComposer.for_fan_mode and HassApi.SET_FAN_MODE_SERVICE constant. The simulator's HassThermostatFields gains a fan_modes field (default fan-aware; Zoo Heater overrides to []) and two new SimStates: HassThermostatFanModeState (DISCRETE per-instance choices) and HassThermostatCurrentHumidityState (CONTINUOUS sensor-driven). The thermostat composer emits fan_mode / fan_modes / current_humidity attributes when the underlying SimStates supply them; the service dispatcher routes set_fan_mode to the matching SimState.

* Issue #299 Phase 5: tests for HA climate converter and new HassServiceComposer methods

- New test_hass_converter_climate.py: substate spec composition (no hvac_modes -> no decomposition; full vs heat-only vs heat_cool-only mode sets; per-unit value_range; choices from hvac_modes / fan_modes; fan_mode omitted when fan_modes absent); inbound translation for full-feature and single-mode thermostats including missing-attribute graceful drop and non-numeric temperature graceful empty; end-to-end import for both flavors verifying right EntityState count and per-substate types; outbound dispatch for target_temperature, hvac_mode, fan_mode, and the target_temp_low/high pair (companion-substate cache lookup with cached and uncached partner).
- TestClimateAxisComposers in test_hass_service_composer: for_temperature_range / for_hvac_mode / for_fan_mode happy paths plus the ValueError-raising validation branches (low > high; empty mode strings).

* Fix UPDATE button hidden on config settings page

The action bar in edit_content_body.html gated UPDATE-button visibility
(and its dirty/status message stack) on attr_item_context.can_add_custom_attributes.
That flag did double duty for "page is editable" and "new attributes can
be added" — and attr_item_context isn't part of the top-level template
context in multi-edit (config settings) mode, so the gate silently
evaluated false and the UPDATE button disappeared, blocking edits to
user/system settings.

Split the conflated concept and move both flags up to the page-level
context so they work for single- and multi-edit surfaces alike:

- allow_edits (new): drives UPDATE button + dirty/status visibility
- can_add_custom_attributes: drives Add File / Add Info visibility only

Entity overrides allow_edits to track its existing externally-managed
flag (HomeBox-imported entities remain fully read-only). Subsystem
contexts override can_add_custom_attributes to False (config exposes
only system-defined attributes) while inheriting allow_edits=True.

Also tightened the per-attribute Restore-from-history gate to require
both page-level allow_edits AND the attribute's own is_editable, so
predefined non-editable attributes correctly hide Restore even on an
otherwise editable page.

* Issue #308: HA temperature unit handling end-to-end

Per-EntityState unit storage with conversion at all boundaries
(HA wire ↔ HI canonical, HI canonical ↔ user display unit, UI
input ↔ stored unit). HI's canonical temperature unit is declared
once in the climate substate spec; downstream code consults
EntityState.units / payload metadata so the canonical choice is
data-driven, not code-distributed.

Cross-cutting infrastructure:

- IntegrationMetadataCache: process-wide, lazy-warmed cache of
  EntityState metadata (sync/async parallel APIs) so polling-loop
  unit lookups don't multiply DB queries inside the HA monitor's
  tight loop. Bulk warmup on first read; lazy fill for entities
  added later. Keyed by IntegrationKey; entry shape is a small
  dict so future metadata can be added without API churn.

- IntegrationConverterHelper / ConsoleConverterHelper: symmetric
  to_/from_entity_state_value pairs for the integration and UI
  boundaries respectively. ConsoleConverterHelper returns a
  DisplayValue dataclass so magnitude-only consumers (slider
  numeric value) and combined-text consumers (status text, modal)
  share one helper output; templates use a new to_display filter
  that replaces the verbose as_quantity | format_magnitude chain.

- ControlViewMixin.to_entity_state_value delegates to the helper.

- StatusDisplayData.latest_display_value routes async UI poll
  refreshes through the same helper, fixing anomalies where
  canonical magnitudes leaked through unconverted (post-poll
  display reverting to raw °C when initial render correctly
  showed °F).

HA simulator additions for end-to-end verification:

- SimulatorRuntimeSettings: process-wide in-memory temperature
  unit override (Default / °F / °C) selectable from a header
  dropdown with antinode swap-in-place to preserve the active
  integration tab. Composer and dispatcher convert at the wire
  boundary; SimStates store in profile unit so the override is
  purely a wire-format toggle.

- UnitTranslationHelper (HA-specific): SimTemperatureUnit → HA
  wire format mapping plus F↔C conversion at composer/dispatcher
  boundaries.

- Thermostat seeds: friendly_name set on the emitted attributes
  so HI shows pretty entity names. Temperature SimState defaults
  and slider bounds are now unit-aware so °C-native profiles
  start at sensible Celsius values rather than 70°C-as-room-temp.
  Per-control unit suffix on the simulator's continuous slider;
  slider value displays use floatformat to avoid Pint float reprs
  leaking many decimals into the UI.

UI:

- continuous_slider_with_units.html (new): unit-aware slider
  routing through the helper. Slider step aligned with 1-decimal
  display precision so HTML range step coercion doesn't mismatch
  the template-rendered value on async polling refreshes.

- sensor_response_value_temperature.html (new): per-EntityStateType
  render for TEMPERATURE.

- controller_temperature.html updated to use the unit-aware slider.

Phase 1's hi_temperature_unit field is removed from
integration_payload — the cache provides EntityState.units to
outbound dispatch directly, eliminating the redundancy.

Trace-output column widths adjusted to better fit the longer
substate-suffixed integration_name strings.

Tests: 2788 passing.

* Skip controller poll updates for actively-dragged sliders

Polling-driven controller value updates could replace a slider's
value mid-drag, yanking the thumb out from under the operator's
fingers. Track active drag state via a WeakSet (mousedown /
touchstart add; mouseup / touchend / change / blur remove); the
polling-update path skips range inputs in that set. The slider's
``change`` event clears the flag on release and antinode's
onchange-async handler submits the new value, so values still
reconcile correctly — only the in-flight redraw is suppressed.

* Tests for unit translation infrastructure (#299 / #308)

High-value coverage for the unit-translation classes added with
the thermostat / temperature unit work:

- IntegrationMetadataCache: warmup loads units from Sensors,
  dedupes Sensor/Controller pairs sharing a key, lazy fill for
  entities created post-warmup, miss caching to avoid re-querying
  the DB, divergent Sensor/Controller pair resolution (Sensor
  wins), async variant returns same as sync, warm-cache async
  short-circuits the sync_to_async hop.

- IntegrationConverterHelper.to_/from_entity_state_value: F→C and
  C→F conversion via cache lookup, passthrough when units match
  or are absent, round-trip preservation, sync/async variant
  equivalence.

- ConsoleConverterHelper / DisplayValue: combined-text
  formatting via __str__, inbound conversion at the HTML/JS
  boundary, outbound rendering as DisplayValue, passthrough on
  no-units / non-numeric / None inputs, full round-trip via the
  to_/from pair.

- UnitTranslationHelper (HA simulator boundary): emitted unit
  selection respects the runtime override, F↔C conversion at
  notable points (freezing, boiling, room temp), defensive
  passthrough on missing units / non-numeric / None, round-trip
  precision.

Tests follow the codebase convention of disabling logging at
module load (assertions are behavior-based, not log-based).
Async paths use AsyncTaskTestCase so the event-loop lifecycle is
managed and DB-locking deadlocks are avoided.

* Document unit-conversion conventions and helper classes

Frontend guidelines gain a "Unit-Bearing Values: Server ↔ UI
Translation" section covering the canonical-storage convention,
the ConsoleConverterHelper to_/from pair, the DisplayValue
return shape, the to_display template filter, and the contract
that initial render and polling refresh share the same helper.

Integration guidelines gain a "Unit conversion at the integration
boundary" section covering IntegrationMetadataCache and
IntegrationConverterHelper.to_/from_entity_state_value, with a
pointer to the HA converter as a worked example. Cross-links the
parallel UI-boundary section in the frontend guidelines.

* Review feedback: hoist canonical, Singleton cache, JS drag-protect

Address review findings on the unit-translation infrastructure
before PR:

- Hoist ``CANONICAL_TEMPERATURE_UNIT`` out of ``HassConverter`` to
  ``hi/units.py`` so other integrations adopting the canonical-at-
  boundary pattern import the choice cross-cuttingly rather than
  duplicating or reaching into HA-specific code. The climate-
  specific slider bounds stay on ``HassConverter`` but are renamed
  ``_SETPOINT_MIN_CANONICAL`` / ``_SETPOINT_MAX_CANONICAL`` to make
  their narrower scope clear.

- Convert ``IntegrationMetadataCache`` to use the project's
  ``Singleton`` base class for consistency with
  ``SimulatorRuntimeSettings`` and the rest of the codebase. Cache
  state moves to instance attrs; callers updated to
  ``IntegrationMetadataCache().method(...)`` form.

- JS slider drag-protect: add Pointer Events (``pointerdown`` /
  ``pointerup`` / ``pointercancel``) for modern stylus / touch
  interactions, and move release-side handlers from ``body`` to
  ``document`` so off-element / off-window releases still clear
  the active-drag flag.

- Rename the ``to_display`` template filter to ``as_display_value``
  for clearer return-type signaling (filter outputs a
  ``DisplayValue``).
2026-05-10 12:49:12 -05:00
Tony C
06bddccc6a Updated docs, claude agents and commands (#193)
* Working on improving AI agent docs.

* Update dev/docs and claude agent configs.

* Updated claude commands.
2025-09-17 17:19:51 +00:00
Tony C
f5032b72e0 Docs updates for new first-time user flows (#191)
* New Getting Started page, moved and revised content into Editing page.
2025-09-17 00:09:29 +00:00
Tony C
cf8a02c9c8 Improvement to EntityType icons (#185)
* Added 18+ new entity type items, svg and visual display configs.
* Added EntityType visual browser and improved some SVGs.
* Added small appliance entity type. Location edit mode button tweak.
* Updated initial profiles. A few more UI tweaks.
2025-09-15 01:23:04 +00:00
cassandra-ai-agent
6fc76e3da2 Add JavaScript unit tests for auto-view.js using QUnit (#160)
* Add JavaScript unit tests for auto-view.js using QUnit

- Set up QUnit testing framework with local vendoring (no CDN dependency)
- Create comprehensive test suite for auto-view.js core functions:
  * throttle() - timing behavior and edge cases
  * shouldAutoSwitch() - idle timeout decision logic
  * isPassiveEventSupported() - feature detection and caching
  * recordInteraction() and state management functions
- Add test runner HTML file that works offline
- Document testing approach and usage in README.md
- All tests focus on business logic, avoid testing framework internals
- Establishes reusable pattern for testing other JS modules

* Fix test assertion for shouldAutoSwitch 60s threshold behavior

The shouldAutoSwitch function uses >= comparison, so at exactly 60 seconds
it returns true (should auto-switch). Updated test to match actual behavior.

Also added Node.js test runner for command-line testing capability.

* Add master test runner for all JavaScript tests

- Created test-all.html that runs all JavaScript module tests in one page
- Updated README.md with improved workflow for single-URL testing
- Added instructions for adding future JavaScript modules to master runner
- Maintains individual test runners for focused debugging

This addresses the practical concern of needing to visit multiple URLs
as JavaScript test coverage grows.

* Organize JavaScript testing documentation

- Created high-level documentation at docs/dev/frontend/javascript-testing.md
- Covers testing philosophy, framework choice, and best practices
- Uses auto-view.js as reference implementation example
- Streamlined tests/README.md to focus on practical usage
- Follows project documentation organization patterns
- Maintains concise, maintainable approach with code references

* Remove redundant README.md, consolidate all JS testing docs

- Removed src/hi/static/tests/README.md (duplicate content)
- Made docs/dev/frontend/javascript-testing.md completely self-contained
- Added detailed code examples for adding new tests
- Single source of truth for JavaScript testing documentation

* Add comprehensive JavaScript unit tests for high-value modules

- svg-utils.js: 150+ test cases for SVG transform string parsing with regex edge cases
- main.js: Tests for generateUniqueId, cookie parsing/formatting with URL encoding
- video-timeline.js: Tests for VideoConnectionManager array logic and caching algorithms
- watchdog.js: Tests for timer management and restart logic simulation
- Updated test-all.html with proper module loading order (main.js first)
- Fixed svg-utils test assertions to match actual regex behavior
- Enhanced Node.js test runner with better mocking (though some modules too DOM-heavy)

All JavaScript tests now pass in browser - establishes comprehensive testing
coverage for business logic functions across multiple modules.

* Add JavaScript testing documentation links

- Added link to javascript-testing.md in frontend-guidelines.md
- Added link to javascript-testing.md in testing-guidelines.md
- Ensures JavaScript testing approach is discoverable from main guideline docs
2025-09-09 20:46:18 +00:00
Tony C
49331dbf8e [Feature] Integration Settings Redesign Implementation (#144)
* Checkpoint: basic integration attribute editing integrated.

* Changed the integrations manage page layout for new editing.

* Improved integrations first time use flows/views.

* Added modal attribute editing for enabling integration.

* Fixed extraneous config page section.

* Added attribute editing options for integartion enable modal.

* Tweaks to integrations editing pages.

* Added left column option for main attribute edit content pane.

* Better styling for vertical tabs: settings and integration pages.

* Removed code made obsolete with new attribute editing framework.

* More obsolete code removal.

* Added missing log suppression for many test files.

* Added first time edit message. CSS file re-org too.

* Improved the editing help modal styling/layout. Added more icons.
2025-09-06 11:48:42 -05:00
Tony C
193c86eb65 Entity View/Edit Modal Redesign (#136)
* Document design workflow process and update CLAUDE.md

- Add comprehensive design workflow documentation in docs/dev/workflow/design-workflow.md
- Update CLAUDE.md with design work documentation workflow section
- Establishes data/design/issue-{number}/ pattern for local work
- Documents GitHub issue attachment vs comment organization
- Provides reusable process for future design-focused issues
- Maintains repository cleanliness while enabling design iteration

* Snapshpt fo semi-working new entity and attribute edit modal.

* Refactored new forms to remove duplication. Fixed some textarea bugs.

* Fixed textarea attributes to prevent data corruption.

* Fixed new attribute validation error handling.

* Moved Entity attribute formset non-file filtering closer to def.

* Styling impovements.

* Added attribute value history browsing and restoring.

* Fixed styling on new entity edit modal header area.

* Refactored the messy and confusing new entity edit modal.

* Fixed icon in new attruibute form.

* Attribute Edit style changes.

* Fixed bad claude factoring. Fixed CSRF file upload issue.

* Added scroll-to in antinode.js whic helped fix file upload UX issue.

* Fixed styling of the add new attribute card for the 'mark as secret'.

* Added modified field forms styling for new entity edit modal.

* Fixed bug in secret attribute readonly editing logic.

* Added file attribute value editing to new entity edit modal.

* Removed legacy EntityEditView and related code.

* Refactor to remove EntityEditData.

* Refactor to rename: EntityDetailsData -> EntityEditModeData

* More refactoring for name change from "details" to "edit mode"

* Removed debug log messages. Doc typo fix.

* Refactored entity views to add helper classes.

* Coded cleanup and test fixes.

* Refactored to replace use of hardcoded DOM ids, classes, selectors.

* Refactorings: better naming removed debug messages.

* Renamed "property" to "attribute".

* Fixed unit test gaps.

* Replaced hardcoded form field prefixes with common access.

* Added EntityTransitionType to replace brittle "magic" strings.

* Tweaks on display names for entity edit modal.

* Added missing __init__.py to nested test dirs. (New failures found.)

* Working on fixing unit tests: checkpoint. WIP

* Fixed units tests

* Removed mocking from soem unit tests.

* Removed partial V2 implementation mistake from entity edit work.

* Added testing doc about lessons learned.

* Added a bunch of testing-related documentation.

* Fixed test expectation for LocationAttributeUploadView invalid upload

Updated test to expect 200 status with modal form response instead of 400 error, aligning with new form error handling behavior from entity attribute redesign.
2025-08-31 13:21:22 -05:00
Tony C
39c7266e77 Refactor developer documentation for role-based use (#126)
* Refactored dev documentation extensivekly.

* Style tweaks and link removals.
2025-08-26 23:17:26 +00:00