From 76d1165dfaf4f2a356ac7e140488c92738016c20 Mon Sep 17 00:00:00 2001 From: Nikolay <110602332+NikaSir@users.noreply.github.com> Date: Wed, 9 Sep 2026 05:58:45 +0100 Subject: [PATCH] Sync organization mirror with canonical UI contracts --- .nikas-ui-standard.json | 164 ++++- NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md | 82 ++- README.md | 8 +- ...IALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md | 23 +- SPECIALIZED_PANEL_UI_STANDARD.md | 4 +- docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md | 298 ++++++++ docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md | 667 ++++++++++++++++++ docs/NIKAS_HACS_PUBLICATION_CONTRACT.md | 95 +++ docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md | 77 ++ docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md | 4 +- docs/NIKAS_REFRESH_ACTION_CONTRACT.md | 111 +++ scripts/check_nikas_ui_standard.py | 94 ++- 12 files changed, 1591 insertions(+), 36 deletions(-) create mode 100644 docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md create mode 100644 docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md create mode 100644 docs/NIKAS_HACS_PUBLICATION_CONTRACT.md create mode 100644 docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md create mode 100644 docs/NIKAS_REFRESH_ACTION_CONTRACT.md diff --git a/.nikas-ui-standard.json b/.nikas-ui-standard.json index 9e2aa05..f5efc8e 100644 --- a/.nikas-ui-standard.json +++ b/.nikas-ui-standard.json @@ -2,10 +2,11 @@ "version": "2.2", "navigation_contract_version": "1.2", "canonical_repository": "NikaSir/ha-contract-generated-ui", + "canonical_revision": "4d229f38e2f75009263e904b52462cc232eefaba", "standard_path": "NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md", - "standard_sha256": "3b6cc750b08aa0d2a375d1430ea04ac68c90525a527d197b014abd96728d23d1", + "standard_sha256": "2a15e5c2483f0fa959faff54cd29144ddb8c392e6da4546bd4c5034bf652c2c7", "navigation_contract_path": "docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md", - "navigation_contract_sha256": "d495eca80345b96976c168029a96146803f3d8195b6f6fc723827b601ffb578e", + "navigation_contract_sha256": "79923d2de82ef59ab76e37491f75ba7eb7e7c4c23e62d75142744159cae64229", "role": "mirror", "shell_contract": { "version": "2.1", @@ -55,11 +56,10 @@ "connection_plaque_reference": { "implementation": "S8 OMNI", "implementation_source": "NikaSir/ha-s8-omni", - "min_height_px": 58, - "padding_px": "12 14", + "padding_px": "11 12", "radius_px": 18, "lamp_px": 10, - "column_gap_px": 11, + "column_gap_px": 9, "main_font": "16px/700", "freshness_font": "13px/600", "success_background_mix_percent": 11, @@ -67,7 +67,19 @@ "neutral_background_mix_percent": 8, "status_border_mix_percent": 30, "neutral_border_mix_percent": 28, - "shadow": "0 4px 14px rgba(0,0,0,.055)" + "shadow": "0 4px 14px rgba(0,0,0,.055)", + "contract_version": "1.1", + "width_px": 168, + "height_px": 58, + "box_sizing": "border-box", + "top_px": 13, + "right_px": 13, + "coordinate_origin": "card_inner_border_edge", + "font_family": "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Arial,sans-serif", + "main_line_height_px": 17, + "freshness_line_height_px": 14, + "text_gap_px": 3, + "geometry_overrides_allowed": false }, "peer_device_status_lamp_reference": { "implementation": "Stark SolarPower", @@ -90,5 +102,143 @@ "update_mode": "point-patch", "accessible_status_required": true }, - "frontend_delivery_path": "SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md" + "refresh_action_feedback": { + "version": "1.1", + "status": "required", + "applies_when": "refresh_action_present", + "path": "docs/NIKAS_REFRESH_ACTION_CONTRACT.md", + "sha256": "69f73e9d5949a2564f2bcef3059039cc339c12c9e8dfe71cfa9f24a45b78d2a8", + "busy_animation": "rotate", + "minimum_visible_ms": 900, + "busy_until": "request_settled_and_minimum_elapsed", + "duplicate_activation": "blocked", + "aria_busy": true, + "failure_notification": true, + "reduced_motion": "static_busy_surface", + "execution_scope": "read_only_telemetry", + "freshness": "accepted_sample_only", + "update_mode": "point-patch", + "preserve": [ + "header", + "active_tab", + "peer", + "work_viewport", + "scroll", + "zoom", + "editor_draft" + ], + "test_source": "production_entrypoint", + "required_cases": [ + "activation", + "fast_success", + "slow_success", + "duplicate_activation", + "failure_cleanup", + "unavailable_targets", + "render_stability", + "context_preservation", + "reduced_motion", + "truthful_freshness", + "result_presentation", + "retry_during_result", + "partial_failure", + "disconnect_cleanup" + ], + "browser_acceptance_required": true, + "completion_feedback": { + "visible_ms": 1400, + "success_icon": "mdi:check", + "success_color": "#43a047", + "error_icon": "mdi:alert-circle-outline", + "error_color": "#e53935", + "idle_icon": "mdi:refresh", + "idle_color": "var(--primary-color)", + "success_semantics": "all_required_requests_explicitly_succeeded", + "retry_during_result": true, + "cancel_previous_timer_on_retry": true, + "preserve_result_deadline_on_render": true, + "disconnect_cleanup": true, + "geometry_unchanged": true, + "accessible_result_name": true + } + }, + "hacs_publication": { + "version": "1.0", + "status": "required", + "applies_when": "hacs_distributed_integration", + "path": "docs/NIKAS_HACS_PUBLICATION_CONTRACT.md", + "sha256": "d9d42be0fa3fab5915cb76399a0a451a4fcf740ec6da6137ee560c7ec5f3774a", + "version_source": "custom_components//manifest.json", + "delivery_boundary": "hacs_visible_release_when_release_driven", + "main_is_delivery": false, + "release_version": "exact_manifest_version", + "release_target": "merged_main_commit_with_manifest_version", + "idempotent_publish": true, + "default_custom_assets": false, + "publication_permission": "contents_write_publication_workflow_only", + "unsupported_branch_install_after_rejection": true, + "states": { + "merged_main": "code_accepted", + "matching_release": "published_to_hacs_channel", + "hacs_installable": "delivered", + "device_acceptance": "accepted_in_operation" + }, + "required_cases": [ + "manifest_version", + "release_exact_match", + "release_target", + "idempotent_publish", + "hacs_validation", + "no_release_assets_by_default", + "delivery_acceptance" + ] + }, + "knowledge_base_path": "docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md", + "frontend_delivery_path": "SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md", + "panel_lifecycle": { + "version": "1.0", + "status": "required", + "path": "docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md", + "sha256": "939b58f7451d0819ad155975466162383e74c99c78040107f88cf667673ea32a", + "route_owner": "installed_configuration", + "registration_before_device_io": true, + "initial_failure": "panel_remains_registered", + "unavailable_rendering": "fail_closed", + "recovery": "coordinator_or_config_entry_retry", + "required_cases": [ + "registration_before_refresh", + "initial_failure_preserves_route", + "offline_bootstrap", + "retry_recovery", + "route_collision", + "unload_ownership", + "generated_manifest_ownership" + ] + }, + "connection_decoration_contract": { + "version": "1.1", + "status": "required_when_present", + "path": "docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md", + "sha256": "0cbc29e20fd811af3e14821ca59adbe737d2e22a9a8a6822472d52cc8ed83036", + "production_browser_acceptance_required": true, + "state_layout_delta_px": 0, + "measurement_noise_px": 0.1, + "geometry_tolerance_px": 1 + }, + "blue_corner_reference": { + "width_px": 205, + "height_px": 205, + "top_px": -92, + "right_px": -70, + "base_color": "#03A9D9", + "background": "rgba(3,169,217,0.07)", + "opacity": 1, + "radius": "50%", + "coordinate_origin": "card_inner_border_edge", + "clip": "persistent_card_decoration_layer", + "theme_primary_dependent": false, + "geometry_overrides_allowed": false, + "pointer_events": "none", + "aria_hidden": true + } } diff --git a/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md b/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md index e60fcb2..3947c6c 100644 --- a/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md +++ b/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md @@ -93,13 +93,33 @@ The complete upper application menu copies the S8 OMNI Header, not merely its ti - Primary title: `23px`, weight `800`, one line; `21px` on very narrow phones. - Secondary/version line: `14px`, weight approximately `560`, `var(--secondary-text-color)`; `13px` on very narrow phones. - The permanent left action is only Home Assistant system menu `mdi:menu`; it dispatches bubbling/composed `hass-toggle-menu`. -- At most one panel-global action occupies the right rail. Refresh uses `mdi:refresh`. +- At most one panel-global action occupies the right rail. Refresh uses `mdi:refresh` in idle/busy state and the completion glyphs defined below. - Menu and refresh copy the S8 OMNI side plaques exactly: `44px × 44px`, `16px` radius, `1px solid color-mix(in srgb,var(--divider-color) 72%,transparent)` border, `var(--card-background-color)` background, `0 7px 20px rgba(23,45,76,.08)` shadow and a `25px` `ha-icon` glyph. -- Menu glyph uses `var(--primary-text-color)`; refresh uses `var(--primary-color)`. +- Menu glyph uses `var(--primary-text-color)`; refresh uses `var(--primary-color)` in idle/busy state. - Disabled global action may reduce opacity, but its plaque geometry and reserved rail remain unchanged so title centering never moves. - A transparent refresh rail, a borderless side action, a locally selected integration color or mismatched menu/refresh geometry is non-conforming. - Back, an integration drawer, a device command or a decorative brand icon is prohibited in the permanent left rail. +### Refresh — busy, success and error + +[NikaS Refresh Action Contract v1.1](NIKAS_REFRESH_ACTION_CONTRACT.md) is required +when a refresh action is present. The same mounted button has four states: +idle arrow → busy rotation → success check or error glyph → idle arrow. + +- Start the permitted read-only refresh immediately; rotate for at least 900 ms + and until the request settles, with duplicate activation blocked throughout. +- After explicit success, stop rotation and show green `mdi:check` (`#43a047`) + for 1400 ms. After failure, show red `mdi:alert-circle-outline` (`#e53935`) + for 1400 ms with a visible error message. Then restore the idle arrow/color. +- Keep the `44px × 44px` plaque, `25px` glyph and Header coordinates unchanged. + Update accessible names for busy/success/error; color alone is insufficient. +- A deliberate retry is available during the result interval. Cancel the previous + timer; neither it nor a late callback may overwrite the new request. +- Telemetry and tab changes preserve the button, result and original deadline. + Reduced motion removes rotation but retains visible busy and result states. +- The check confirms the declared HA/API request result only. It never fabricates + a device acknowledgement, sample timestamp, healthy state or fresh telemetry. + ### Center title plaque — return to the source NikaS base panel - The geometrically centered two-line title is a persistent clickable plaque and the sole standard return control from a specialized panel to the NikaS base interface. @@ -212,17 +232,30 @@ The two-level indicator is introduced only by an explicit product request. It is - Transport and freshness are independent. For example, `Облако · Данные устарели` is valid; stale data must not be relabelled as a transport outage without evidence. - A failed current poll makes preserved telemetry `Данные устарели`. Unless a domain documents another justified threshold, a sample also becomes stale after three normal polling intervals; it becomes current again only after a new successful sample is accepted. -### Placement and geometry - -- The canonical placement copies S8 OMNI: upper-right of the first operational Hero/card, in the same heading row as the current-state title. The plaque belongs to the work viewport, not the fixed Header, and therefore scales with work content. -- Use a two-column heading row with the state/title in `minmax(0,1fr)` and the plaque in an intrinsic right column. On normal phone widths the plaque receives enough room for its longest label; the S8 OMNI reference reserves approximately `minmax(168px,44%)`. -- At extremely narrow widths where the title and plaque cannot remain readable, stack the row and align the plaque to the start. Shrinking required text below the typography envelope or allowing overlap is non-conforming. -- Surface layout: `display:grid`, columns `10px minmax(0,1fr)`, vertically centered, `11px` column gap. -- Minimum height: `58px`; padding: `12px 14px`; radius: `18px`; `white-space:nowrap`; `max-width:100%`. -- Default surface before state coloring: `var(--card-background-color)` background, `1px solid color-mix(in srgb,var(--divider-color) 72%,transparent)` border and `0 4px 14px rgba(0,0,0,.055)` shadow. -- Status lamp: `10px × 10px`, fully inside the plaque, circular, never moved outside the rounded surface. -- Text block is a stable vertical flex column with `3px` gap. Main line: `16px/700`, line-height approximately `1.05`. Freshness line: `13px/600`, line-height approximately `1.05`. -- The plaque is sized from the longest permitted transport/freshness pair. It must not change width, height or alignment when state changes. +### Placement and geometry — locked contract + +The mandatory companion is [NikaS Connection Plaque and Blue Corner Contract +v1.1](NIKAS_CONNECTION_DECORATION_CONTRACT.md). It restores the compact S8 +width and phone insets, with an exact `58px` height instead of the former +minimum-only height. It supersedes v1.0's larger `200px × 60px` plaque. + +- The plaque is exactly `168px × 58px` border-box, radius `18px`, at `top:13px; + right:13px` from the operational card's inner border edge. +- Use `11px 12px` padding, a `1px` border, internal columns `10px minmax(0,1fr)`, + `9px` column gap and a circular `10px × 10px` lamp. +- Main text is `16px/700`, line-height `17px`; freshness is `13px/600`, line-height + `14px`; text gap is `3px`. Both use the companion's exact shared font stack. +- The title occupies its own reserved area. Below `360px` outer card width it + moves below the plaque; the plaque retains its top/right position and size. +- Both elements belong to the work viewport and scale with it. Status text, + image size/loading, peer switches and telemetry must not move their anchors. +- The separate upper-right background circle is exactly `205px × 205px`, + `top:-92px; right:-70px`, radius `50%`, fill `rgba(3,169,217,0.07)`. + It is clipped by a persistent card decoration layer and never recolored by + `--primary-color`, state or device mode. It does not represent Refresh. +- No per-panel or responsive override may change these tokens. Validate actual + production computed styles and rectangles using the companion's scenarios; + merely copying the latest S8 CSS is not evidence of conformance. ### State surfaces and colors @@ -301,7 +334,7 @@ Repository tests or static checks must verify: 9. brand `icon.png` exists in the shipped integration package; 10. meaningful typography stays within `12–25px`, subject only to the documented schematic exception; 11. routine telemetry cannot replace the shell, viewport, canvas, background or Bottom Tab Bar; -12. an optional connection indicator, when requested, uses the canonical transport/freshness vocabulary, S8 OMNI geometry and exact state-tinted surface percentages; +12. a requested connection indicator and enabled blue corner satisfy `NIKAS_CONNECTION_DECORATION_CONTRACT.md` v1.1, including exact tokens, all label lengths, stable DOM and zero state-caused geometry movement; transport/freshness vocabulary and state-tinted surface percentages remain canonical; 13. the center title is a two-line, exactly `52px` high semantic button, contains no arrow or separate Back label and retains geometric centering; 14. every reset path normalizes and persists `{scale:1,x:0,y:0}` and native scroll origin; 15. source-route capture follows `NIKAS_PANEL_NAVIGATION_CONTRACT.md`, uses the four canonical base entry routes, writes the common session hand-off at outbound click/keyboard time, consumes it once, performs explicit HA navigation and contains no `history.back()`; @@ -318,6 +351,8 @@ Repository tests or static checks must verify: 26. every Bottom Tab Bar label is fully visible, including Cyrillic descenders, with the sidebar expanded and collapsed and in every mandatory viewport. 27. the non-passive touch boundary guard blocks Home Assistant pull-to-refresh and outer scrolling at both work-viewport edges without replacing native interior scrolling or two-finger zoom. 28. a peer-device selector, when present, keeps one persistent accessible status lamp per device, preserves selection styling independently, applies the green/orange/red/gray fail-closed state contract and updates lamps without replacing selector DOM. +29. an owned panel route is registered before fallible device I/O, remains present after initial failure and is removed only by its exact owner, as required by `NIKAS_PANEL_LIFECYCLE_CONTRACT.md`. +30. a refresh action satisfies `NIKAS_REFRESH_ACTION_CONTRACT.md` v1.1, including the 1400 ms green check/red error state, truthful completion, retry/timer isolation and stable geometry. Each repository also maintains `docs/NIKAS_SPECIALIZED_PANEL_COMPLIANCE.md` (or an equivalent explicit record). Unimplemented runtime behavior is recorded as `GAP`, never assumed to pass from documentation alone. @@ -348,7 +383,7 @@ For every matrix entry, compare the measured Header, title plaque, work viewport - the centered title plaque shows the panel name and exact `UI vX.Y.Z`, returns to each of the four originating NikaS base panels and uses the configured safe fallback after a direct open; - Bottom icons and labels match the Stark SolarPower visual scale; - integration/repository icon is present and recognizable in installed/distribution surfaces. -- a requested connection indicator visually matches S8 OMNI: `58px` minimum height, `18px` radius, internal `10px` lamp, stable two-line text and state-specific surface without geometry movement; +- a requested connection indicator has the exact `168px × 58px` box, `13px` top/right inset, `18px` radius, internal `10px` lamp and fixed `16/700` + `13/600` text; an enabled blue corner has the exact `205px` circle and fixed fill/anchor from `NIKAS_CONNECTION_DECORATION_CONTRACT.md`; - repeated telemetry, indicator transitions, tab changes and upward/downward scroll produce no full-screen flash or white frame; - scrolling the work area never moves Header, peer selector or Bottom Tab Bar; - pulling downward at the top of any tab never displays the Home Assistant refresh spinner or splash screen; dragging upward at the bottom never moves the complete panel or leaves a blank field below it; @@ -369,3 +404,20 @@ For every matrix entry, compare the measured Header, title plaque, work viewport - GitHub Releases are not used. - Automatic release tags are not used as a publication gate or update channel. An internal integration/UI version does not require a Git tag. - A pull request remains draft until automated checks pass and the complete viewport matrix above is ready for user verification. + +## 16. Panel lifecycle and availability + +[NikaS Panel Lifecycle Contract v1.0](NIKAS_PANEL_LIFECYCLE_CONTRACT.md) is a +required companion to this standard. + +- A configured panel route is application infrastructure, not telemetry state. +- The owner registers its route before fallible first device/cloud/coordinator I/O, + or uses integration-wide registration when the route is not entry-specific. +- `async_config_entry_first_refresh()` and successful device discovery must never + be prerequisites for route existence. +- Initial failure mounts explicit unavailable/no-data content and preserves Header, + navigation and sidebar entry. +- Recovery uses the normal coordinator/config-entry retry lifecycle and patches the + existing panel. +- Generated panel existence follows enabled manifest/configuration state; entity + availability affects content only. diff --git a/README.md b/README.md index 99ebd1f..6cd0ab7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains the default community health and engineering-standard files used across public repositories owned by `NikaSir`. -The current required specialized-panel contract is `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v2.2 together with `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md`. It includes the exact LIDER center return plaque, deterministic source-route hand-off, UPS-reference fixed Header actions, native scrolling at 100%, overflow-axis pan only above 100%, stable-DOM anti-flicker rules, the opt-in two-level connection/freshness indicator, the 12–25px typography range, the 26px Bottom Tab Bar icon rule and mandatory integration brand assets. +The current required specialized-panel contract is `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v2.2 together with `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md` v1.2 and its declared companion contracts. `.nikas-ui-standard.json` pins the reviewed canonical revision in `NikaSir/ha-contract-generated-ui`; the local checker verifies the mirrored documents, hashes, lifecycle rules and locked connection geometry against that declaration. ## Purpose @@ -23,6 +23,12 @@ The goal is to keep contribution, support, security, issue, pull-request, reposi - `.github/ISSUE_TEMPLATE/` — structured bug and feature request forms. - `REPOSITORY_STANDARD.md` — common repository structure and publication expectations. - `SPECIALIZED_PANEL_UI_STANDARD.md` — mandatory mobile-first navigation/application-shell contract for integration-owned Home Assistant panels. +- `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md` — canonical route ownership and source-route hand-off rules. +- `docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md` — registration, unavailable-state and recovery requirements. +- `docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md` — locked connection-plaque and blue-corner geometry. +- `docs/NIKAS_REFRESH_ACTION_CONTRACT.md` — truthful refresh feedback and stable-render behavior. +- `docs/NIKAS_HACS_PUBLICATION_CONTRACT.md` — HACS delivery and release identity requirements. +- `docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md` — reviewed implementation lessons bound to the normative baseline. - `NIKAS_INTEGRATION_PANEL_TEMPLATE.md` — mandatory common shell, visual primitives, geometry and information-presentation template for specialized panels. - `SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md` — mandatory self-contained production-bundle and cold-load reliability contract for specialized Home Assistant panels. - `reference/integration-panel-template/` — development-time reference implementation of the shared panel shell and primitives; it is copied/vendored into integration source and must never be a cross-repository runtime dependency. diff --git a/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md b/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md index 200dfb4..d13eed9 100644 --- a/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md +++ b/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md @@ -1,7 +1,8 @@ -# NikaS Specialized Panel Frontend Delivery Standard v1.7 +# NikaS Specialized Panel Frontend Delivery Standard v1.8 **Status:** required for every integration-owned Home Assistant specialized panel **UI authority:** [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md) v2.2 +**HACS publication companion:** [`NIKAS_HACS_PUBLICATION_CONTRACT.md`](NIKAS_HACS_PUBLICATION_CONTRACT.md) v1.0 ## Production artifact @@ -39,4 +40,22 @@ Before merge, verify: ## Publication workflow -Changes receive an explicit UI/integration version where applicable, a changelog entry and automated checks. NikaS work is published through commits, branches, pull requests and the accepted `main` state. GitHub Releases and automatic release tags are not created. +Changes receive an explicit UI/integration version where applicable, a changelog entry and automated checks. NikaS work is reviewed and accepted through commits, branches, pull requests and the authoritative `main` state. + +`main` is the code authority, but **it is not automatically the HACS delivery boundary**. For every integration distributed through HACS, the repository declares and tests its actual delivery mechanism. + +For a release-driven HACS integration: + +1. merge only after required PR checks pass; +2. read the integration version only from `custom_components//manifest.json`; +3. after merge, create an exact matching GitHub Release/tag if one does not already exist; +4. the release targets the merged `main` commit containing that manifest version; +5. publication is idempotent and uses no custom release assets unless the repository explicitly requires them; +6. verify HACS validation and confirm that repository refresh exposes the same version as installable; +7. after installation/restart, verify the Home Assistant integration version and matching visible panel UI version. + +A merged version without its required HACS-visible Release is **accepted code but not delivered software**. Do not report it to the user as available for update. + +If HACS rejects a branch/commit installation path such as `version: main`, that path is considered unsupported for that repository until separately verified; repair the supported publication mechanism instead of asking the user to repeat the failed action. + +The exact required behavior, failure modes, permissions and regression cases are defined by [NikaS HACS Publication Contract v1.0](NIKAS_HACS_PUBLICATION_CONTRACT.md), which supersedes the older blanket rule that GitHub Releases are never used for HACS-delivered integrations. diff --git a/SPECIALIZED_PANEL_UI_STANDARD.md b/SPECIALIZED_PANEL_UI_STANDARD.md index 5c67326..fdc56fc 100644 --- a/SPECIALIZED_PANEL_UI_STANDARD.md +++ b/SPECIALIZED_PANEL_UI_STANDARD.md @@ -2,6 +2,6 @@ This filename is retained for existing repository links only. -The sole current authority is [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md), version 2.2, together with its required [navigation contract](docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md) v1.2. The authoritative owner is [`NikaSir/ha-contract-generated-ui`](https://github.com/NikaSir/ha-contract-generated-ui). +The sole current authority is [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md), version 2.0. It replaces every earlier rule for Home Assistant host binding, Header navigation, Back, safe areas, zoom, scrolling, panning, rendering, typography, connection/freshness indicators, Bottom Tab Bar geometry and repository/integration icons. -Implementations must not combine historical rules with v2.2. The permanent left Header action is the Home Assistant system menu, the centered title plaque is the sole return control, Bottom Tab Bar icons are 26px, and GitHub Releases or automatic release tags are not part of the NikaS panel publication workflow. +Implementations must not combine historical rules with v2.2. In particular, the permanent left Header action is the Home Assistant system menu, not Back; GitHub Releases and automatic release tags are not part of the NikaS panel publication workflow. diff --git a/docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md b/docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md new file mode 100644 index 0000000..2656f44 --- /dev/null +++ b/docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md @@ -0,0 +1,298 @@ +# NikaS Connection Plaque and Blue Corner Contract v1.1 + +**Status:** REQUIRED where the respective element is enabled. + +**Authority:** `NikaSir/ha-contract-generated-ui`, NikaS UI Standard v2.2. + +**Decision date:** 2026-09-08. + +**Visual lineage:** S8 OMNI UI v1.0.4, compact phone layout. Revision 1.1 restores +its `168px` width and `13px` top/right inset at the user's request. The `58px` +height is now an exact compact specification; the source used only +`min-height:58px` and its full text stack could make the box about `60.45px` high. +The numbers below are normative across all supported widths; copying a moving +product implementation is not an acceptance criterion. + +## 1. Scope and ownership + +This contract fixes the two-line connection/freshness plaque («Связь») and the +separate pale-blue circle clipped by the upper-right corner of the operational +card. The circle is a background decoration with no status meaning or action. +The Header refresh button remains governed by `NIKAS_REFRESH_ACTION_CONTRACT.md`. + +Apply this geometry to every requested instance, initially S8 OMNI, Climate and +HO-SC-8W. This does not add a connection plaque to StarLine or «Дом сейчас», or +introduce decoration into panels where it has not been requested. + +All dimensions are CSS pixels at work scale 100%. Both elements belong to the +work card, scroll with it and scale together with the work area at 75–200%. +They are never independently scaled or fixed to the browser window. The Header, +peer selector and Bottom Tab Bar retain their existing native-scale contract. + +## 2. One coordinate system + +The containing operational card has `position:relative`, `isolation:isolate`, +`box-sizing:border-box`, a `1px` border and `16px` internal padding. Its outer +radius and approved domain composition remain owned by the panel. Clip only +the decoration layer to that same numeric radius; do not clip controls or focus rings. +The inset-zero layer starts inside the 1px border and inherits the outer radius +numerically, exactly as in §5; do not subtract 1px in individual panels. + +The origin for offsets is the card's **inner border edge** (CSS absolute +positioning containing block). Let `C` be the card's outer border rectangle. +At 100%, with the required 1px card border: + +| Element | Exact rectangle / anchor | +|---|---| +| Connection plaque | width `168px`, height `58px`, top `13px`, right `13px` | +| Plaque outer coordinates | top `C.top + 14px`; right `C.right − 14px` | +| Blue circle, before clipping | width `205px`, height `205px`, top `−92px`, right `−70px` | +| Circle outer coordinates | top `C.top + 1px − 92px`; right `C.right − 1px + 70px` | + +The plaque is anchored to the card, not vertically centered against an image, +title, state text, remaining time or metrics. The card's top origin for a given +route/layout cannot depend on telemetry, image loading or status copy. Dynamic +messages appear below its reserved heading area, without shifting the card. +Different shell rows (for example an enabled peer selector) naturally change +the screen coordinate; compare the same host, route, scroll and work scale. + +Reserve `177px` on the right of the state-heading area and at least `58px` +heading height. The card keeps its `16px` internal padding, while the plaque +uses a `13px` inset: `168 + 12 + 13 − 16 = 177px` leaves exactly `12px` +between the title column and the plaque. The state/title may wrap in its own column. +If the card's outer width is below `360px`, keep the plaque at the **same +top/right anchor**, reserve `67px` above the title and let the title span the +width below it. Relative to the padded content, `13 + 58 + 12 − 16 = 67px` +leaves exactly `12px` below the plaque. This one breakpoint depends on card width, never on status or +text length. It moves the title only; the plaque does not move, shrink or grow. +Keep a `12px` gap from the complete heading area to the separate image block. +Operational text and the plaque must not overlay the device image. + +## 3. Exact connection plaque tokens + +| Property | Required value | +|---|---| +| Box model | `border-box`; width/min-width/max-width `168px`; height/min-height/max-height `58px` | +| Placement | absolute; top `13px`; right `13px`; margin `0`; `z-index:2` | +| Padding / border / radius | `11px 12px` / `1px solid` / `18px` | +| Internal grid | `10px minmax(0,1fr)`; column gap `9px`; `align-items:center` | +| Lamp | `10px × 10px`; circle; no flex shrink; no extra halo | +| Text stack | vertical flex; gap `3px`; left aligned; no wrapping | +| Shared font stack | `-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif` | +| Main line | font size `16px`; weight `700`; line-height `17px` | +| Freshness line | font size `13px`; weight `600`; line-height `14px` | +| Typography reset | margin/padding `0`; letter-spacing `0`; font-style `normal`; text-transform `none` | +| Shadow | `0 4px 14px rgba(0,0,0,.055)` | + +The text stack is exactly `34px` high (`17 + 3 + 14`); padding and border add +`24px`, producing the explicit `58px` border box. The text column is `123px` +wide. All canonical Russian labels must fit in full; ellipsis, clipping and +font shrinking are failures. Use the same font stack in every panel, without +domain-specific or HA-theme font overrides. Platform font selection is allowed +only through this common stack; physical glyph rasterization may vary by OS. + +Revision 1.1 supersedes the v1.0 `200px × 60px` box and `16px` top/right +inset. It retains fixed sizing instead of restoring the source's proportional +width, minimum-only height or left-stacked narrow-screen variants. Horizontal +padding `12px` and column gap `9px` follow the compact S8 source; vertical +padding `11px` makes the exact `58px` height fit the unchanged `17px`/`14px` +line heights and `3px` text gap. Do not restore `12px` vertical padding inside +this smaller box: it would require `60px` and conflict with the fixed height. + +Transport/freshness semantics and state surface colors remain those of +UI Standard v2.2 §8, including no false green for unknown/untrusted data. +State changes may update text, accessible descriptions and the state palette; +they may not change any geometry or typography token above. + +## 4. Exact blue decoration tokens + +| Property | Required value | +|---|---| +| Shape | one circle; border radius `50%` | +| Size | `205px × 205px`; no min/max or aspect-dependent alternatives | +| Anchor | absolute; top `−92px`; right `−70px` from the card's inner border edge | +| Base blue | `#03A9D9` (RGB `3,169,217`) | +| Fill | `rgba(3,169,217,0.07)`; element opacity `1` | +| Effects | border `0`; shadow `none`; filter `none`; transform `none`; animation `none` | +| Clipping | persistent inset-zero decoration layer, same outer radius as the card, `overflow:hidden` | +| Layer | decoration `z-index:0`; normal content `1`; plaque `2` | +| Interaction | `pointer-events:none`; `aria-hidden="true"` on the decoration layer; no focus target | + +Do not use `var(--primary-color)` for this decoration: an integration or theme +accent must not turn it another color. Its specified translucent fill is the +same in light and dark themes; the final composited pixel naturally depends on +the card background. Do not apply opacity a second time to the containing +layer. No gradient, image asset, state tint, pulse, mobile resizing, orientation +override or per-panel offset is allowed. The persistent clipping layer must +not take layout space or create horizontal scroll overflow. + +## 5. Reference geometry CSS + +This is a geometry reference, not a panel replacement. Use exact component +selectors in the panel's own shadow root; preserve its factual state model. +The ancestor containing `.nikas-status-heading` establishes the named `nikas-card` +inline-size container on the card. CSS content width is outer width minus +`34px` (two borders and two 16px paddings), so `326px` corresponds to the +`360px` outer-card breakpoint. + +```css +.nikas-status-card { + position: relative; + isolation: isolate; + box-sizing: border-box; + border-width: 1px; + border-style: solid; + padding: 16px; + container: nikas-card / inline-size; +} +.nikas-card-decoration { + position: absolute; + inset: 0; + border-radius: inherit; + overflow: hidden; + z-index: 0; + pointer-events: none; +} +.nikas-card-decoration::before { + content: ""; + position: absolute; + top: -92px; + right: -70px; + width: 205px; + height: 205px; + border: 0; + border-radius: 50%; + background: rgba(3,169,217,0.07); + opacity: 1; + box-shadow: none; + filter: none; + transform: none; + animation: none; +} +.nikas-status-content { + position: relative; + z-index: 1; + min-width: 0; +} +.nikas-status-heading { + box-sizing: border-box; + min-height: 58px; + padding-right: 177px; +} +.nikas-status-image { + margin-top: 12px; +} +.nikas-connection-plaque { + position: absolute; + top: 13px; + right: 13px; + z-index: 2; + box-sizing: border-box; + width: 168px; + min-width: 168px; + max-width: 168px; + height: 58px; + min-height: 58px; + max-height: 58px; + margin: 0; + padding: 11px 12px; + border: 1px solid var(--nikas-connection-border); + border-radius: 18px; + display: grid; + grid-template-columns: 10px minmax(0,1fr); + column-gap: 9px; + align-items: center; + background: var(--nikas-connection-background); + box-shadow: 0 4px 14px rgba(0,0,0,.055); + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; + font-style: normal; + letter-spacing: 0; + text-transform: none; + text-align: left; + white-space: nowrap; + transition: none; + animation: none; +} +.nikas-connection-lamp { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--nikas-connection-main-color); +} +.nikas-connection-text { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} +.nikas-connection-main { + margin: 0; + padding: 0; + font: 700 16px/17px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; + color: var(--nikas-connection-main-color); +} +.nikas-connection-freshness { + margin: 0; + padding: 0; + font: 600 13px/14px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; + color: var(--nikas-connection-freshness-color); +} +@container nikas-card (width < 326px) { + .nikas-status-heading { + padding-right: 0; + padding-top: 67px; + } +} +``` + +The decoration layer, the content layer and the plaque are direct children of +the card. Set the four state color variables on the plaque from §8; these are +the only appearance inputs permitted to vary with connection/freshness state. +`aria-hidden="true"` belongs on the decoration layer only. Expose both plaque +lines to assistive technology; do not hide operational state with the decoration. + +## 6. Forbidden sources of drift + +- `fit-content`, content-measured sizing, percentage widths, `flex-shrink`, + `align-items:center` against the Hero image, or a left-aligned mobile plaque. +- `clamp()`, viewport/container font units, status-specific fonts, implicit + `normal` line-height, UA `strong`/`small` styling or inherited product fonts. +- Wildcard overrides such as `[class*="connection"]` or broad `strong`, `small`, + `span`, `ha-icon` rules that alter internal geometry. One historical wildcard + turned the circular lamp into a horizontal bar. +- Appending another CSS patch to override earlier conflicting rules. Replace + the old component rule and remove obsolete responsive variants instead. +- Remounting either element, retinting/repositioning the circle on status + change, animated layout or using image dimensions as a positioning input. + +## 7. Required acceptance evidence + +Check the **actual production entrypoint**, not an obsolete imported ancestor +or a standalone drawing alone. Record commit SHA, UI version, environment, +viewport/host/card widths, work scale, computed styles and measured rectangles. + +1. At 100%, both rectangles and every typography token match §§2–4. Computed + numeric tokens must be exact; allow at most `1 CSS px` measurement rounding + for rendered coordinates. A size/position delta caused by a state-only + update must be `0` (at most `0.1 CSS px` numeric measurement noise). +2. Run all 15 transport/freshness label combinations as a **layout-only fixture**; + this does not make impossible combinations valid domain states. Show the + longest labels fully, with no overlap, ellipsis or lamp deformation. +3. Exercise factual local/cloud/reserve, outage, stale, no-data and recovery + transitions. The same plaque and decoration DOM nodes remain mounted. + Refresh busy/success/error and ten peer/tab changes must not alter anchors. +4. Vary adjacent title length, line count, remaining-time copy and image loading. + Neither element moves. Test card outer widths `359`, `360`, `361px` and the + narrow phone widths `320`, `360px`; the title alone reflows at the breakpoint. +5. Run the standard phone/tablet/desktop matrix with the HA sidebar open/closed; + compare card-relative coordinates. Scroll, resize, orientation and zoom + `75/100/150/200%`, then two-finger reset, must preserve the same local tokens + without horizontal overflow at 100% or an extra scroll/zoom owner. +6. In light/dark themes and with a non-blue HA primary color, the circle keeps + `rgba(3,169,217,0.07)` and its exact clip/position. Verify the clipping layer + does not intercept input. Confirm iOS/HA font rendering separately. + +Any unexplained shift, local token override or missing production evidence is +a blocking UI defect (`GAP` / `not_verified` until checked). A passing registry +hash/token check validates the specification only. It does not certify a panel +as conforming or claim that this documentation update changes installed UI. diff --git a/docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md b/docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md new file mode 100644 index 0000000..71bd61d --- /dev/null +++ b/docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md @@ -0,0 +1,667 @@ +# NikaS Engineering Knowledge Base + +**Status:** LIVING DOCUMENT +**Scope:** Home Assistant custom integrations, integration-owned specialized panels, generated/base panels +**Normative baseline:** NikaS Specialized Panel UI Standard v2.2 + NikaS Panel Navigation Contract +**Purpose:** preserve engineering experience, failure modes, proven practices and acceptance criteria so new work starts from accumulated knowledge rather than from previous implementations. + +This file is intentionally broader than the UI standard. The standard defines mandatory behavior. This knowledge base records *why* those rules exist, what repeatedly failed in real devices, and how integration/backend, frontend, history/statistics and release work should be organized. + +--- + +## 1. Core engineering principle + +A NikaS integration/panel is not a page that periodically redraws. It is a small stateful application inside Home Assistant with four independent concerns: + +1. **truth acquisition** — reliable states, registries, coordinator/API results; +2. **domain model** — explicit mapping, freshness, availability, derived values and write capabilities; +3. **persistent UI shell** — Header, one work viewport, Bottom Tab Bar, optional peer selector; +4. **incremental rendering** — patch only the values that changed. + +Most historical visual defects, flicker, slow startup, broken scrolling and inconsistent state were caused by crossing these boundaries. + +### Rule + +Before UI work begins, define the data contract and state lifecycle. Before styling begins, create the persistent shell and rendering model. Before adding history, define its request lifecycle and cache. Before publishing, prove version/build parity and real-device behavior. + +--- + +## 2. Lessons learned from previous panels + +### 2.1 Stable shell beats repeated rendering + +Repeated full `shadowRoot.innerHTML` replacement caused: + +- white frames during tab changes; +- background/image re-decoding; +- Header and Bottom Tab Bar movement; +- scroll position jumps; +- gesture state loss; +- flicker during telemetry updates; +- expensive compositor reconstruction in iOS WebView. + +**Correct model:** mount the application shell once; update existing text nodes, classes, attributes and CSS variables. Cache visited work-view subtrees and reattach them instead of rebuilding the shell. + +### 2.2 The work viewport must be the only scroll owner + +Panels that relied on the outer Home Assistant document or independent `position: fixed` layers showed mobile failures: Header could be pulled upward, the bottom menu followed short pages, inertia propagated into HA, and safe-area calculations accumulated. + +**Correct model:** height-locked phone shell with exactly one middle work viewport. Header and Bottom Tab Bar occupy fixed grid rows outside it. Prevent scroll chaining. + +### 2.3 One zoom engine only + +Multiple wrappers, nested transforms or mixing browser zoom, transform pan and native scroll produced jump-to-max zoom, clipped cards and unpredictable panning. + +**Correct model:** one zoom viewport. At 100% use native vertical scrolling and no transform pan. Transform panning exists only above 100%, with factual clamping to content edges. Pinch range 75–200%; 97–103% snaps to 100%; two-finger double tap resets scale/translation/native scroll. + +### 2.4 Do not shrink typography to solve layout + +Unreadable secondary pages and clipped labels were repeatedly caused by compressing text instead of recomposing cards. + +**Correct model:** meaningful text stays within 12–25 px. If content does not fit, change geometry, grouping, line breaks or card structure; do not solve it with 9–10 px operational text. + +### 2.5 Status transport and freshness are separate facts + +A single “Online” indicator hid whether data came locally or from cloud and could incorrectly suggest healthy fresh data. + +**Correct model:** when explicitly requested, use two independent dimensions: + +- transport: `Локально / Облако / Резерв / Нет связи / Нет данных`; +- freshness: `Данные актуальны / Данные устарели / Нет данных`. + +Do not infer outage from stale data. Preserve last-known telemetry only with a stale state. + +### 2.6 State-only changes must not alter topology + +Flicker appeared after introducing indicators or conditional cards because the changing state controlled element creation/removal. + +**Correct model:** stable placeholders; state controls content/classes, not shell topology. Do not use telemetry age, status strings or current values as structural keys. + +### 2.7 Images must be persistent assets + +Large PNG backgrounds slowed first render and re-render. Reassigning unchanged `src` or rebuilding the image layer made the problem visible. + +**Correct model:** optimize assets (prefer WebP where appropriate), mount once, reuse, avoid writing the same `src`, and keep persistent compositor layers stable during scroll and telemetry updates. + +### 2.8 Phone acceptance is a separate engineering stage + +Desktop/static screenshots did not reveal inertial scroll, iOS safe-area, synthetic click after pinch, WebView flicker or shell movement. + +**Correct model:** automated checks are necessary but not sufficient. iPhone Pro Max portrait is the primary acceptance viewport for specialized panels. Regression scenarios are defined before merge. + +### 2.9 A common idea is not a common shell + +A cross-panel review on phone, tablet landscape and desktop with the Home Assistant sidebar open showed that independently implemented “fixed Header / viewport / Bottom Nav” shells still diverged in top origin, available width, content frame, title centering and bottom-bar attachment. The prose requirements were correct, but phone-first acceptance and approximate geometry allowed every repository to choose a different coordinate system. + +**Correct model:** NikaS UI v2.2 binds the shell to the real Home Assistant panel host, defines numeric row sizes and one canonical content frame, and requires the same rectangle checks across phone, tablet and desktop with the Home Assistant menu open and closed. Screenshots of S8, Stark or another panel are visual lineage, not a substitute for the numeric contract. + +Five internal destinations are valid. The Keenetic panel has five Bottom Tab destinations and was not the source of this defect; its review finding concerned shell consistency, not tab count. + +### 2.10 Peer status and selection are different facts + +A selector button answers which peer device is open. Its status lamp answers whether that device is healthy. Recoloring the whole selected button from telemetry mixed these meanings and made selection unstable. + +**Correct model:** retain one persistent 9px lamp per peer device with a subtle 3px halo. Green means healthy and current, orange means a documented warning or degraded/reserve state, red means confirmed fault/offline, and gray means unknown or incomplete data. Classification fails closed in the priority fault → warning → healthy → unknown. Selection styling remains independent. Updates patch only lamp color and accessible status text without rebuilding the selector or shell. + + +### 2.11 Peer selector geometry has one visual reference + +Different panels implemented the same peer-device selector as a shared segmented pill, independent cards, or compressed labels. Even when all variants remained usable, the changing frame made the NikaS shell look inconsistent and encouraged selection styling to absorb device health. + +**Proven reference:** StarLine UI v0.6.8. + +**Correct model for two peers:** + +- one 52px selector row immediately below Header and outside the work viewport; +- the row itself has no shared card surface, border, radius or shadow; +- horizontal inset is at least 12px plus the relevant safe-area inset; +- two equal-width independent buttons separated by an 8px gap; +- each button is 44px high, has a 1px border, a 15px radius and the ordinary card surface; +- content is left aligned: persistent 9px status lamp with a subtle 3px halo, then one-line peer name; +- the selected button uses primary-colored text, a primary-color border at about 65% strength and a primary-color surface at about 10% strength; +- device health never recolors the selected surface: health changes only the lamp and accessible status text; +- long peer names use one-line ellipsis and must not change button height or selector topology. + +More than two peers may use another explicitly approved adaptive composition only when the 44px touch target and legible names remain intact. Do not squeeze unreadable labels into the two-peer reference geometry. + +--- + +### 2.12 Refresh needs a visible completion result + +The user accepted the green completion check in Climate UI 1.4.17, then reported +its absence in the vacuum and irrigation panels. Rotation alone shows that a +request is running; an immediate return to the arrow makes its outcome unclear. +Another S8 failure showed that a generic loading CSS class could distort the +Header button. Use a button-specific state class and preserve plaque geometry. + +**Correct model:** one mounted button, four states: +`idle → busy → success/error → idle`. Follow +[Refresh Action Contract v1.1](NIKAS_REFRESH_ACTION_CONTRACT.md): + +- busy begins immediately, lasts at least 900 ms and until the request settles; +- explicit success shows green `mdi:check` for 1400 ms; +- failure shows red `mdi:alert-circle-outline` for 1400 ms and an error message; +- the result interval ends by restoring the arrow, with no layout shift; +- duplicate requests are blocked while busy; retry during a result clears its old + timer so it cannot overwrite the new request; +- HA updates and tab changes preserve the result and its original deadline; +- accessible names and static reduced-motion states explain progress/outcome; +- only an accepted factual sample changes telemetry freshness. A resolved promise + carrying `false`, a swallowed exception or partial failure is not success. + +Completion presentation was added in +[S8 OMNI 1.0.4, PR #129](https://github.com/NikaSir/ha-s8-omni/pull/129) and +[HO-SC-8W 1.0.1, PR #175](https://github.com/NikaSir/ha-ho-sc-8w/pull/175). +Product tests and browser checks are evidence for the tested cases only; these +references do not certify the entire contract or replace physical acceptance. + +--- + +### 2.13 Connection plaque and blue corner must use locked tokens + +On 2026-09-08 the user again reported that «Связь» changes position, size and +font, and requested the same strict treatment for the upper-right blue element. +The source comparison found concrete drift: + +| Source inspected | Plaque drift | Blue circle drift | +|---|---|---| +| [S8 OMNI `2ed8bac`](https://github.com/NikaSir/ha-s8-omni/tree/2ed8bacb5d3def6141aa118bd6994b036c9f2610) | Proportional width; `min-height:58px`; mobile stacking; text gap 4px | 205px; top −92px / right −70px; theme-dependent blue | +| [HO-SC-8W `dc4afe2`](https://github.com/NikaSir/ha-ho-sc-8w/tree/dc4afe23ddb14850c289c3145526043d0894441c) | Width 168px; minimum height only; vertical centering against controller image; another font stack | 200px; top −90px / right −65px; another blue | +| [Climate `b5fae6c`](https://github.com/NikaSir/ha-nikas-climate/tree/b5fae6ce16b905d1e9c1d42d98d49324ece40772) | Proportional width; 58/64px minimum heights; implicit line-height; another font stack | 220px desktop / 188px mobile; different offsets | + +A minimum height is not an exact height: the old S8 text, gap, padding and border +already required about 60.45px. “Use the S8 reference” therefore did not uniquely +specify the result. CSS appended to historical classes could also miss the +current production DOM, while a wildcard connection selector deformed the lamp. + +The subsequent user comparison selected the compact S8 phone appearance. +Revision 1.1 restores `168px` width and the phone layout's `13px` top/right +inset (`14px` from the outer card edge with its `1px` border). The agreed +`58px` compact height is fixed explicitly, not represented as a measured +height of the old runtime. Its smaller inner padding reconciles that height +with the unchanged type sizes; the blue corner tokens remain unchanged. + +**Required model:** [Connection Plaque and Blue Corner Contract v1.1](NIKAS_CONNECTION_DECORATION_CONTRACT.md). + +- «Связь»: exactly 168×58px border-box; top/right 13px from the card's inner + border edge; radius18; padding11/12; lamp10; column gap9; text gap3. +- One font stack everywhere; main16px/700 with 17px line-height; + freshness13px/600 with 14px line-height. No product font inheritance. +- Blue decoration: circle205px; top−92/right−70; fixed `rgba(3,169,217,0.07)`; + no theme-primary input, responsive alternative or status meaning. +- Preserve both DOM nodes and the anchors through every state update. On narrow + cards the title moves below the plaque; the plaque remains at the top right. +- Replace conflicting CSS at its source. Do not append another patch, shrink + text, center the plaque against the image or use wildcard internal selectors. +- Production acceptance measures rectangles/computed styles for all label + lengths, state transitions, peer/tab changes, width boundaries, zoom and themes. + +These are intentionally fixed new tokens within UI Standard v2.2. Updating the +knowledge base does not update the three installed panels. Their conformance +remains unverified until individual production changes and acceptance evidence +are recorded; a documentation/hash check is not runtime evidence. + +--- + +## 3. Integration architecture + +### 3.1 Backend owns truth; frontend owns presentation + +The frontend must consume: + +- Home Assistant state objects; +- entity/device/area registries; +- integration-owned coordinator/API data; +- explicit panel/integration mapping contracts. + +It must not invent entity IDs, raw datapoints, device capabilities, success results or “healthy” defaults. + +### 3.2 Explicit entity/capability mapping + +Entity discovery should be registry-driven where practical. Hard-coded entity IDs are allowed only when they are a deliberate, tested public contract. Generated IDs guessed from labels or device names are prohibited. + +When operational labels exist (for example `В эксплуатации`, `Резерв`, `На обслуживании`, `Требует замены`, `Выведено`), filtering must occur from the factual metadata/labels rather than by hiding arbitrary entities in the UI. + +### 3.3 Coordinator/API lifecycle + +For polled integrations: + +- one coordinator is the normal source of periodic truth; +- polling cadence is explicit; +- successful sample acceptance updates freshness timestamp; +- failed polls do not erase the last good sample, but mark it stale; +- expensive work is not duplicated by each entity or panel subscriber; +- listeners receive already-normalized domain state. + +### 3.3.1 Panel existence is application infrastructure + +A configured panel route is registered before fallible device or cloud I/O. Device +reachability changes the panel's content to an explicit unavailable/no-data state; it +does not remove the application surface. First-refresh helpers may still govern +entity setup or config-entry retry, but never route existence. Registration, +collision handling, retries and unload ownership follow +`NIKAS_PANEL_LIFECYCLE_CONTRACT.md`. + +### 3.4 Unknown and unavailable are first-class states + +Never convert missing input to zero, false, normal or green. Distinguish: + +- valid value; +- `unknown`; +- `unavailable`; +- no sample yet; +- preserved stale value; +- source/transport failure. + +Derived values fail closed when required inputs are absent. + +### 3.5 Read-only versus write-enabled integration + +A read-only panel remains read-only. Refresh means refresh telemetry, not a device command. + +A write action is allowed only through a registered integration service or a discovered HA entity capability. For every write action: + +1. validate capability and target availability; +2. collect/edit value without sending it immediately when the workflow requires confirmation; +3. expose an explicit Apply/Start/Stop action; +4. require confirmation for consequential commands according to the product policy; +5. prevent duplicate submission while busy; +6. await the HA/integration call; +7. show error if the call fails; +8. do not optimistically claim success before factual state confirms it. + +This pattern was validated for seasonal irrigation correction and is the default for settings/commands that write to equipment. + +--- + +## 4. Panel construction order + +Build a new specialized panel in this order: + +### Stage A — contract + +Define: + +- product identity and panel name; +- read-only/write policy; +- peer devices; +- factual entities/data sources; +- operational and diagnostic fields; +- derived values and their inputs; +- transport/freshness policy if requested; +- history/statistics metrics and periods; +- navigation parent/fallback routes. + +### Stage B — shell + +Implement only: + +- fixed Header; +- optional peer selector; +- one work viewport/canvas; +- fixed Bottom Tab Bar; +- safe area handling; +- navigation return plaque; +- deterministic initial loading surface. + +Do not begin rich cards until shell regression tests pass. + +### Stage C — rendering engine + +Implement: + +- one-time mount; +- stable view cache; +- coalesced state updates, max one animation frame; +- changed-value checks before DOM writes; +- tab/peer selection without shell rebuild; +- persistent image/background layers. + +### Stage D — gestures + +Add native 100% scroll first. Then add pinch/reset and enlarged pan. Verify gesture/click/hold isolation before adding device commands. + +### Stage E — operational views + +Operational first page should answer “what is happening now?” and avoid deep diagnostics. Diagnostic detail belongs on a separate tab or Infrastructure surface. + +### Stage F — diagnostics + +Diagnostics should expose all enabled state-bearing entities bound to the device/integration, raw attributes when useful, timestamps/context, and clear source status. It is primarily for service and debugging, not the first page. + +### Stage G — statistics/history + +Add only after live-state rendering is stable. History must never drive or block the shell. + +### Stage H — commands + +Add writes last. Every write path receives separate safety, busy-state and failure tests. + +--- + +## 5. UI composition principles + +### 5.1 Header + +Follow v2.2 exactly. The center title plaque is the sole standard return control. No browser `history.back()`, no separate arrow or “Назад”. Left rail is HA system menu; right rail has at most one panel-global action. All three tracks are positioned inside the Home Assistant panel host, never against the browser viewport. + +### 5.2 Bottom navigation + +3–5 equal destinations, fixed outside the work viewport, MDI icons through `ha-icon`, minimum touch target 52 px. A short page must not pull the bar upward; a long page must scroll its final control above it. + +The bar spans the panel host on phone, tablet and desktop. Five destinations are conforming; a sixth requires a secondary navigation level. An active state may change color and background only, never item size, column width or bar height. + +### 5.3 Page hierarchy + +Preferred information order: + +1. state/scene that gives immediate understanding; +2. primary live facts; +3. actionable warnings; +4. secondary operational parameters; +5. history/statistics; +6. diagnostics/raw data. + +Do not duplicate the same parameter on image overlays and cards unless each occurrence serves a distinct task. + +### 5.4 Visual scene versus data cards + +Use images/scenes when they improve comprehension of physical topology or state. Do not place redundant telemetry on the image simply because there is empty space. Prefer a clean scene with a small number of meaningful overlays and structured cards below/around it. + +### 5.5 State imagery + +For devices with materially different states (car security, vacuum cleaning/charging/return/attention, network active path), use explicit state scenes rather than tinting one generic image when state is otherwise hard to read. + +### 5.6 Controls must match semantics + +Do not mix “start now” and “automatic setting enabled” in one control group. If a vendor exposes only Stop for a running station function, represent that factual capability rather than inventing a Start action. + +--- + +## 6. Clean code requirements + +### 6.1 Separate layers + +Recommended frontend separation: + +- constants and UI version; +- route/navigation helpers; +- state/domain normalization; +- shell mount; +- view builders; +- targeted live patchers; +- gestures/viewport controller; +- history/statistics loader; +- command handlers; +- formatting helpers. + +Backend separation follows HA conventions: manifest/config flow/constants/coordinator/platforms/services/panel registration with minimal cross-module coupling. + +### 6.2 Avoid monolithic render functions + +A function that constructs the full panel HTML for every `hass` update is a design defect. Rendering should distinguish structural mount from state patch. + +### 6.3 One source of version truth + +UI/integration/contract/panel manifest/cache key must be coherent. A behavioral frontend change increments UI version and deterministic cache busting. CI must reject version drift. + +### 6.4 Generated production bundle + +Exactly one shipped autonomous JS entrypoint. Build-time composition is allowed; runtime imports/CDN dependencies are not. CI regenerates/checks the bundle deterministically and fails if tracked output is stale. + +### 6.5 No dead compatibility engines + +When shell/zoom/navigation is replaced, remove the superseded runtime implementation from the production path. Do not leave multiple active generations behind “for safety”. Historical code may exist only outside runtime or as source history. + +### 6.6 Data formatting is centralized + +Units, unknown/unavailable handling, precision and labels belong in reusable formatters. Do not scatter special-case string logic through view templates. + +### 6.7 Minimal writes + +Before setting `textContent`, class, style variable, ARIA attribute or image source, compare with the current value where practical. Avoid layout writes during scroll/gesture loops. + +--- + +## 7. Performance model + +Performance is accepted by behavior, not by subjective code compactness. + +### 7.1 Startup + +Mount Header/loading work surface/Bottom Tab Bar immediately. Large assets may load after shell mount, but the panel must not show a blank white application frame. + +### 7.2 Telemetry updates + +- coalesce to one animation frame; +- patch only active/affected elements; +- do not remount backgrounds or fixed chrome; +- do not trigger history requests from ordinary live telemetry; +- skip unchanged DOM writes. + +### 7.3 Tab switches + +Use lazy view cache. Ten consecutive tab switches are a standard regression scenario: no white frames, no shell motion, no duplicate event handlers, no lost zoom controller. + +### 7.4 Asset budget + +Prefer compressed local assets; WebP is preferred for photographic/scene backgrounds where it materially reduces size. Keep dimensions close to display needs. Do not repeatedly decode high-resolution PNGs for unchanged scenes. + +### 7.5 Expensive queries + +Recorder, registries and vendor APIs must have explicit single-flight/cache semantics. Never allow a `hass` setter to multiply asynchronous work. + +--- + +## 8. Statistics and Recorder history + +History/statistics is an asynchronous subsystem with its own lifecycle. + +### 8.1 Source of truth + +Use authenticated Home Assistant Recorder/history APIs or integration-owned statistics. Do not depend on Lovelace-only card helpers from a custom panel runtime unless their availability is explicitly guaranteed. + +### 8.2 Define grouping semantically + +Statistics groups follow physical meaning, not arbitrary entity order. Example validated for power: `До стабилизаторов` → `После стабилизаторов` → `Неотключаемая линия`; exclude generation/export when the installation has none. + +### 8.3 Periods are independent cached loads + +Each selected period owns a load object. Returning to a previously loaded/in-flight period must reuse it unless the user explicitly presses Refresh. + +### 8.4 Partition large requests + +One oversized Recorder response can block all graphs. Prefer per-graph or otherwise bounded requests. LIDER validated a concurrency limit of **2** requests, progressive graph rendering and a **60 s terminal timeout per graph**. + +### 8.5 Progressive terminal states + +Every graph must eventually show exactly one of: + +- data; +- no recorded data; +- Recorder unavailable/error. + +Indefinite “Loading…” is a defect. + +### 8.6 Telemetry isolation + +Live `hass` updates must not restart history. Period switching must not invalidate an unrelated in-flight period. Refresh invalidates only the active period. + +### 8.7 SVG/downsampling + +When autonomous charts are required, limit *rendered points* rather than silently limiting the requested time range. Preserve period semantics, then downsample for display. + +### 8.8 Acceptance sequence + +At minimum test: `24h → 7d → 24h → 7d`, then 30d/12m. Confirm reused periods do not restart, completed graphs appear progressively, and shell/zoom/live telemetry remain stable. + +--- + +## 9. Navigation experience + +Specialized panels are entered from NikaS base panels. The source route is captured explicitly on activation and consumed according to the navigation contract. Do not recalculate a return route on telemetry updates or depend on browser back history. + +Safe route precedence must follow the canonical navigation contract and reject invalid cross-origin/unknown routes. + +--- + +## 10. Diagnostics and observability + +Every integration should make failures diagnosable without modifying production code. + +Include where relevant: + +- source/transport; +- last successful update; +- last attempt/error; +- entity IDs actually bound; +- raw HA state/attributes useful for debugging; +- device identifiers/model/firmware if factual; +- polling/update interval; +- integration/UI version. + +Diagnostic rendering must not expose secrets/tokens. + +--- + +## 11. Quality gates + +A panel/integration is not “ready” because it loads. + +### 11.1 Static/CI gates + +Required checks should cover: + +- Python/JSON/YAML syntax as applicable; +- Home Assistant/Hassfest expectations; +- HACS validation; +- contract/schema checks; +- UI/integration/version parity; +- deterministic production bundle parity; +- production JS syntax; +- no runtime imports/CDN dependencies; +- one shell/viewport; +- stable-DOM guard; +- navigation contract; +- typography envelope; +- forbidden legacy patterns (`history.back()`, routine full `innerHTML`, old fixed-layer topology, etc.). +- host-bound shell geometry and the absence of `100vw` or hard-coded Home Assistant sidebar offsets; +- numeric Header/work/Bottom Nav rectangle parity across the mandatory v2.2 viewport matrix; +- unchanged chrome coordinates during work scroll, overscroll and Home Assistant sidebar toggles. + +### 11.2 Dynamic regression gates + +Automate where practical: + +- multiple repeated `hass` updates do not replace shell nodes; +- tab cache preserves visited subtrees; +- period history calls are single-flight/cached; +- max history concurrency is enforced; +- command duplicate submission is blocked; +- refresh success/error glyphs last 1400 ms, survive telemetry patches and cannot be reset by an old timer during a newer request; +- unknown/unavailable data does not become healthy; +- the two-peer selector keeps the StarLine reference geometry (52px row, 44px independent buttons, 8px gap) and patches status lamps independently of selection. + +### 11.3 Real-device acceptance + +Primary phone acceptance checks: + +- Header fixed under notch/safe area; +- Bottom Tab Bar fixed above Home Indicator; +- native and inertial scroll only inside work viewport; +- short page does not move chrome; +- long page reaches final content; +- ten tab switches without white frame; +- pinch focal behavior; +- two-finger reset; +- synthetic click suppressed after pinch; +- telemetry updates do not flicker; +- images/background do not reflash; +- peer selector geometry and selection/status separation remain identical on phone, tablet and desktop; +- history period switching does not freeze; +- write confirmation and busy/error states behave correctly. + +Phone acceptance is followed by tablet portrait/landscape and desktop checks with the Home Assistant sidebar expanded and collapsed. A panel is not shell-complete when only its phone portrait layout has passed. + +--- + +## 12. Publication policy learned from iterations + +Preferred flow: + +1. branch from current `main`; +2. implement one coherent change set; +3. run local/static/dynamic checks; +4. open PR; +5. require repository checks + integration validation + HACS/Hassfest as applicable; +6. merge only after green CI; +7. no tag/GitHub Release unless explicitly requested; +8. update/install through HACS or project deployment path; +9. perform real-device acceptance; +10. if phone acceptance reveals a gap, document it explicitly rather than declaring the whole change finished. + +Do not keep obsolete draft PRs as parallel sources of truth after replacement; close them with a clear reason. + +--- + +## 13. Anti-pattern register + +The following patterns are considered known regressions unless a new design proves otherwise: + +- full panel redraw from `set hass()`; +- Header/Bottom Tab Bar inside transformed or scrolling content; +- multiple zoom wrappers; +- horizontal scroll at 100%; +- one-finger transform panning at 100%; +- sticky chrome used as a substitute for fixed shell rows; +- meaningful operational text below 12 px; +- generic “Online” when transport/freshness distinction is required; +- status represented by color only; +- refresh silently returning to the arrow without showing its result, or displaying a green check after a failed/partial request; +- a generic page-loading CSS class applied to a Header action and changing its geometry; +- a shared outer pill around peer-device buttons, or selection styling driven by device health; +- missing/unavailable rendered green or as zero; +- guessed entity IDs; +- write controls that claim success before state confirmation; +- automatic writes on every input change for consequential settings; +- history restarted by telemetry; +- a single unbounded Recorder request for all graphs/periods; +- infinite history loading; +- repeated setting of unchanged image `src`; +- runtime import chain/CDN dependency for the production custom panel; +- unsynchronized UI version/cache key/manifest/contract; +- browser `history.back()` as return navigation; +- “fixing” mobile fit by shrinking type or hiding required content. + +--- + +## 14. Definition of Done + +A NikaS integration/panel is complete only when all of the following are true: + +- factual data contract is explicit; +- command policy is explicit; +- UI v2.2 shell is compliant across the mandatory viewport matrix; +- live updates are incremental and stable; +- startup has no blank application frame; +- mobile scroll/zoom/safe-area behavior is accepted; +- diagnostics explain source and failures; +- statistics/history has bounded requests, cache and terminal states; +- source code and generated bundle are clean and coherent; +- all automated checks are green; +- target-phone acceptance is performed for the changed behavior; +- documentation/knowledge base is updated when a new failure mode or proven pattern is discovered. + +--- + +## 15. Maintenance rule + +This is a living file. After each meaningful defect investigation or successful architectural improvement, update one or more of: + +- **Lessons learned** — what failed and why; +- **Proven pattern** — the reusable solution; +- **Anti-pattern register** — what must not return; +- **Quality gates** — how recurrence will be detected automatically; +- **Acceptance** — what must be verified on the real device. + +A lesson is not considered preserved until it changes either the design rule, the automated check, or the acceptance checklist. diff --git a/docs/NIKAS_HACS_PUBLICATION_CONTRACT.md b/docs/NIKAS_HACS_PUBLICATION_CONTRACT.md new file mode 100644 index 0000000..6d90f86 --- /dev/null +++ b/docs/NIKAS_HACS_PUBLICATION_CONTRACT.md @@ -0,0 +1,95 @@ +# NikaS HACS Publication Contract v1.0 + +**Status:** REQUIRED companion to NikaS Specialized Panel UI Standard v2.2 +**Canonical source:** `NikaSir/ha-contract-generated-ui` +**Applies to:** every NikaS custom integration distributed to the user through HACS +**Supersedes:** the sentence in NikaS UI Standard v2.2 section 15 that GitHub Releases are not used, but only for repositories whose HACS delivery depends on release versions +**Approved:** 2026-09-07 +**Machine contract:** `.nikas-ui-standard.json` → `hacs_publication` + +## Why this rule exists + +HO-SC-8W reached `main` at integration `1.0.0-b006.31` / UI `0.7.10`, while HACS still offered only `1.0.0-b006.29`. The release automation had been removed, so the latest GitHub Release remained `.29`. Installing `main` through `update.install` was then rejected by HACS for that integration. After restoring a minimal release workflow, HACS exposed `.31` correctly. + +Therefore, for a release-driven HACS repository, merge to `main` is code publication but is **not** user delivery. A version is considered delivered only when the HACS-visible release exists. + +## Mandatory publication model + +### HACS-01 — One source of version truth + +The integration version is read from `custom_components//manifest.json`. Release automation must not invent, transform or independently increment the version. + +### HACS-02 — Release is the delivery boundary + +If HACS exposes installable versions from GitHub Releases, every version intended for users must have a GitHub Release/tag with the exact same version string as `manifest.json`. `main` alone is not an installable release and must not be described as already delivered. + +A repository may use a different HACS-supported delivery mechanism only when that mechanism has been verified on the target installation and is recorded explicitly in the repository contract. + +### HACS-03 — Publication runs after merge + +The normal sequence is: + +1. PR CI passes; +2. PR is merged to `main`; +3. publication workflow reads the merged `manifest.json`; +4. if the matching release/tag does not exist, it creates exactly one release; +5. if it already exists, the workflow exits successfully without a duplicate. + +The release must target the merged `main` commit that contains the declared version. + +### HACS-04 — Minimal release surface + +For NikaS custom integrations, a HACS delivery Release is a transport marker, not a second development process. By default it contains no uploaded assets and does not modify integration code. GitHub-generated source archives are sufficient unless a repository documents another requirement. + +### HACS-05 — No skipped user versions + +A merged version may be intentionally skipped only before it is presented as delivered. If `.30` and `.31` are both merged before publication and `.31` supersedes `.30`, publishing only `.31` is allowed when `.31` contains all `.30` changes. The changelog/release notes must make that traceable. + +### HACS-06 — Verification after publication + +After release creation, verify all of the following: + +- GitHub latest Release/tag equals the version in `manifest.json`; +- the Release targets a commit containing that same manifest version; +- HACS validation passes; +- after HACS repository data refresh, the same version appears as installable; +- after install/restart, Home Assistant reports the integration version and the panel displays its matching UI version. + +Until these checks pass, publication status is `GAP` / `not delivered`, not success. + +### HACS-07 — Idempotence and permissions + +The release workflow is idempotent. Re-running it for an already published version must not create a second tag or release. `contents: write` is granted only to the publication job/workflow that needs it; normal CI remains read-only. + +### HACS-08 — HACS failure modes are explicit + +If HACS rejects a branch name such as `main` as a version, do not instruct the user to retry the same path. Record the observed failure, restore/repair the supported publication mechanism, and retest delivery end to end. + +## Required repository checks + +For a release-driven HACS integration, repository automation must verify: + +| Case | Required assertion | +|---|---| +| `manifest_version` | `manifest.json` contains a non-empty version string. | +| `release_exact_match` | Intended HACS release/tag equals the manifest version exactly. | +| `release_target` | Release/tag points to a commit containing that manifest version. | +| `idempotent_publish` | Existing release causes success/no-op, not duplicate creation. | +| `hacs_validation` | HACS repository validation succeeds. | +| `no_release_assets_by_default` | No custom assets are required unless repository policy explicitly says so. | +| `delivery_acceptance` | HACS refresh exposes the release and installed Home Assistant reports it after restart. | + +The first six can be automated in repository/GitHub CI. `delivery_acceptance` is a real HACS/Home Assistant acceptance step and cannot be replaced by a GitHub-only test. + +## Relationship to `main` + +`main` remains the authoritative code branch and merge target. Releases do not replace PR review, CI or version history. They only expose an already accepted `main` state to HACS when HACS requires release versions. + +Thus: + +- **merged to `main`** = code accepted; +- **matching Release exists** = version published to HACS channel; +- **HACS exposes and installs it** = version delivered; +- **device/phone acceptance passes** = version accepted in operation. + +These states must not be conflated. diff --git a/docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md b/docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md new file mode 100644 index 0000000..0c119a0 --- /dev/null +++ b/docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md @@ -0,0 +1,77 @@ +# NikaS Panel Lifecycle Contract v1.0 + +## Scope + +This contract applies to every NikaS route owned by an integration or generated +panel manifest. It defines panel availability; it does not change device protocols, +credentials, commands or telemetry semantics. + +## Normative rules + +### LIFECYCLE-01 — Route ownership is configuration state + +An installed and configured panel owner controls whether its route and sidebar entry +exist. Physical-device reachability, vendor-cloud availability and the presence of a +fresh telemetry sample do not control route existence. + +### LIFECYCLE-02 — Register before fallible device I/O + +Register the owned static path, websocket/bootstrap surface and panel route before +calling device, RCI, Tuya, cloud or coordinator operations that may raise +`ConfigEntryNotReady`, `ConfigEntryAuthFailed` or `UpdateFailed`. + +An integration-wide `async_setup` registration is conforming when the route is not +specific to one config entry. An entry-owned route must publish the minimal runtime +state needed by bootstrap before registration. + +### LIFECYCLE-03 — Initial failure is fail-closed content + +No sample yet, timeout, offline, authentication failure, `unknown` and +`unavailable` render as explicit non-healthy states inside the mounted panel. They +must not remove the route, hide the sidebar entry or display a healthy default. + +### LIFECYCLE-04 — Refresh cannot own panel lifetime + +`async_config_entry_first_refresh()` or any equivalent successful read must not be a +precondition for panel registration. A required config-entry retry may block entity +setup, but it must leave the already-owned panel route intact. + +### LIFECYCLE-05 — Recovery uses the normal backend lifecycle + +After an initial failure, the coordinator or Home Assistant config-entry retry +mechanism continues attempts without requiring a browser reload or manual panel +re-registration. A later successful sample patches the existing mounted content. + +### LIFECYCLE-06 — Generated panels follow manifests + +For generated panels, enabled manifest/configuration state controls existence. +Current entity availability controls rendered state only. Missing bindings fail +closed inside the panel and never silently suppress the route. + +### LIFECYCLE-07 — Ownership and unload remain exact + +Registration must not replace a route owned by another integration. Unload removes a +route only when the unloading owner registered it, and one config entry must not +remove a shared route still used by another entry. + +### LIFECYCLE-08 — Frontend bootstrap tolerates zero telemetry + +The production frontend must mount its Header, navigation and unavailable/no-data +surface when bootstrap contains no device, entity or current sample. Retry and +recovery must not rebuild the application shell. + +## Required regression cases + +| Case | Required evidence | +|---|---| +| `registration_before_refresh` | Static or unit test proves route registration precedes fallible first refresh/device I/O. | +| `initial_failure_preserves_route` | Failed first read leaves the owned route/sidebar entry registered. | +| `offline_bootstrap` | Empty or unavailable bootstrap mounts explicit non-healthy content. | +| `retry_recovery` | A later accepted sample updates the mounted panel through normal retry/coordinator flow. | +| `route_collision` | An existing foreign owner is preserved. | +| `unload_ownership` | Only the registering owner removes the route. | +| `generated_manifest_ownership` | Generated route existence follows enabled configuration, not entity state. | + +Repository CI should cover every applicable case. Hardware evidence is required only +when validating a physical protocol or command; lifecycle ordering itself must be +testable without live hardware. diff --git a/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md b/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md index d030c8e..5689a77 100644 --- a/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md +++ b/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md @@ -35,7 +35,7 @@ The specialized-panel registry is: | Stark SolarPower | `/dashboard-ups` | `/dashboard-infrastructure/overview` | | Keenetic Hero 4G+ | `/dashboard-keenetic` | `/dashboard-infrastructure/overview` | | LIDER | `/dashboard-lider` | `/dashboard-infrastructure/overview` | -| Water Accounting | `/dashboard-water-accounting` | `/dashboard-infrastructure/overview` | +| Water Accounting | `/dashboard-water` | `/dashboard-house-v13/home` | `/dashboard-starline` is invalid. The registered StarLine route is `/starline`. @@ -45,7 +45,7 @@ The specialized-panel registry is: - Every integration repository exposes its registered root, canonical route, safe fallback and contract version in a machine-readable panel contract. - One active route has exactly one owner. - Every installed specialized panel has at least one visible entry link from the NikaS base interface; a sidebar-only or direct-URL-only panel is non-conforming. -- Required owners are: House now → Access, ZONT and StarLine; Actions → S8 OMNI and HO-SC-8W; Infrastructure → Stark SolarPower, Keenetic and Water Accounting; the Infrastructure power section → LIDER. +- Required owners are: House now → Access, ZONT, StarLine and Water Accounting; Actions → S8 OMNI and HO-SC-8W; Infrastructure → Stark SolarPower and Keenetic; the Infrastructure power section → LIDER. - A `more-info` action does not count as the required entry link. Legacy `/dashboard-house/*` pages may remain declared detail routes during migration. They are not base-panel source routes and must not be used as a return fallback. diff --git a/docs/NIKAS_REFRESH_ACTION_CONTRACT.md b/docs/NIKAS_REFRESH_ACTION_CONTRACT.md new file mode 100644 index 0000000..a44b6a5 --- /dev/null +++ b/docs/NIKAS_REFRESH_ACTION_CONTRACT.md @@ -0,0 +1,111 @@ +# NikaS Refresh Action Contract v1.1 + +**Status:** REQUIRED companion to NikaS Specialized Panel UI Standard v2.2 +**Canonical source:** `NikaSir/ha-contract-generated-ui` +**Applies to:** every NikaS specialized panel that exposes a refresh action +**Approved:** 2026-09-07 +**Completion feedback amendment:** 2026-09-08 +**Machine contract:** `.nikas-ui-standard.json` → `refresh_action_feedback` + +This companion extends the Header, stable-rendering, data-truth and acceptance rules of [NikaS UI Standard v2.2](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md). It does not change shell geometry, require a refresh button where none was approved, or certify unchanged panels as compliant. + +## Why this rule exists + +In HO-SC-8W UI 0.7.08, the upper-right refresh button gave no visible progress feedback. A request could be sent while the user could not distinguish execution, inactivity or failure. The fix is tracked in [HO-SC-8W PR #170](https://github.com/NikaSir/ha-ho-sc-8w/pull/170), UI 0.7.10 / integration 1.0.0-b006.31. + +A working service call without observable feedback is a UI defect. An animation without a real refresh request is also a defect. + +The completion check was accepted by the user in Climate UI 1.4.17. S8 OMNI +[PR #129](https://github.com/NikaSir/ha-s8-omni/pull/129) and HO-SC-8W +[PR #175](https://github.com/NikaSir/ha-ho-sc-8w/pull/175) add the same visible result. +These implementations are evidence for the completion presentation, not automatic +certification of every requirement of this companion. + +## Mandatory behavior + +### REFRESH-01 — Immediate, truthful feedback + +On accepted activation, mark the existing button busy before awaiting asynchronous work. Start the real read-only telemetry refresh through the panel's declared Home Assistant/integration API. The button does not reload Home Assistant, restart an integration, change device settings or invoke unrelated commands. + +### REFRESH-02 — Visible duration follows the request + +With ordinary motion settings, rotate the refresh glyph continuously at one full turn per 900 ms. Keep the busy presentation for at least 900 ms and until the actual awaited request settles, whichever takes longer. Start the request immediately; the minimum duration delays only removal of feedback. A timer must not re-enable the button while its request is still pending. + +### REFRESH-03 — No duplicate requests + +Disable repeat activation for the complete busy interval. A handler-level single-flight guard is mandatory in addition to the button's disabled state. Rapid taps, keyboard activation and repeated handler calls must not dispatch a second request or queue a delayed duplicate. + +### REFRESH-04 — Completion and failure are observable + +After the request settles and the minimum busy interval ends, stop rotation and +clear busy/disabled state on every exit path, including exceptions. Show the result +in the same button for 1400 ms, then restore its idle glyph and color: + +| State | Glyph | Glyph color | Accessible name | +|---|---|---|---| +| Idle | `mdi:refresh` | `var(--primary-color)` | `Обновить` | +| Busy | rotating `mdi:refresh` | `var(--primary-color)` | `Обновление данных` | +| Success | static `mdi:check` | green `#43a047` | `Запрос обновления выполнен` | +| Error | static `mdi:alert-circle-outline` | red `#e53935` | `Не удалось обновить данные` | + +Success requires an explicit successful result from the declared refresh API. +A caught exception or returned `false` must not become success merely because the +promise resolved. If several required requests form one refresh, all must settle +successfully before the check appears; partial failure uses the error state. +A rejected request, missing update service or absence of refreshable entities must +also produce a visible message such as `Не удалось обновить данные`. +Technical details must not disclose credentials or tokens. + +The result is informational and does not lock out a deliberate retry. On a new +accepted activation, cancel the previous result timer, clear the old result and +enter busy immediately. An old timer or completion callback must never overwrite +a newer request's state. Dispose of result timers on panel removal; late callbacks +must not update a disconnected or replacement panel. + +The canonical `44px × 44px` plaque, `25px` glyph, border, background and Header +rail remain unchanged in every state. Only the glyph, its color and accessible +status change for success/error. Do not add a separate check button, resize the +control, flash the whole Header or use a panel-wide loading CSS class for the button. + +Successful completion of the request does not by itself prove that every device supplied a new sample. Never mark preserved telemetry current, change its timestamp, or turn a status healthy merely because the animation ended. Freshness changes only when factual new data is accepted. + +### REFRESH-05 — Preserve the user's context + +Patch the mounted UI; do not rebuild the panel or call `location.reload()`. Keep Header, selected tab/peer, work viewport, scroll, zoom and unsubmitted editor draft. New telemetry may update untouched fields according to the product's existing editor contract, but refresh must not submit or silently discard user edits. The button keeps its size and rail so the title does not move. + +### REFRESH-06 — Stable busy state during rendering + +Ordinary Home Assistant updates and tab changes must not replace the Header button, lose its busy or result state, restart its animation/result timer or re-enable it early. Reconciliation restores the current state on the mounted control, including the check/error glyph for the remaining result interval. Unchanged state must not cause unnecessary DOM writes. + +### REFRESH-07 — Accessibility and reduced motion + +Use a semantic button with an accessible name. While busy expose `aria-busy="true"`, disabled semantics and a name such as `Обновление данных`. During success/error clear busy semantics, use the result name from the table, and restore `Обновить` after 1400 ms. Announce the result through an accessible status region when changing the button name alone is not announced. With `prefers-reduced-motion: reduce`, suppress rotation but retain a visibly distinct static busy surface and accessible status; success/error glyphs still appear without animation for 1400 ms. Neither busy nor result may be signalled by color alone. + +## Required production regression cases + +The owning panel's CI must execute its registered production entrypoint and assert behavior, not only search source text for `busy`, CSS animation or a function name. Test the actual button-to-handler wiring and exact allowed service/API call. The following case identifiers are part of the machine contract: + +| Case | Required assertion | +|---|---| +| `activation` | Pointer and keyboard activation start the real permitted request and immediately expose busy state. | +| `fast_success` | An immediately settled request shows busy for at least 900 ms, then a green check for 1400 ms, then the idle arrow. | +| `slow_success` | A request exceeding 900 ms remains busy/disabled until it settles; no timer produces false completion. | +| `duplicate_activation` | Repeat taps, keyboard events and handler calls during busy produce only one request. | +| `failure_cleanup` | Rejection or returned false shows a red error glyph and message, clears busy, and restores the arrow after 1400 ms; retry is possible. | +| `unavailable_targets` | Missing service or no refreshable entities produce visible failure and no false success. | +| `render_stability` | State updates and tab changes preserve the control, active request, result glyph and original result deadline. | +| `context_preservation` | Refresh preserves tab/peer, scroll, zoom and unsubmitted editor changes, without page reload. | +| `reduced_motion` | Browser-computed reduced-motion styling stops rotation but keeps visible busy feedback and ARIA. | +| `truthful_freshness` | Completion without a new accepted sample does not invent a fresh timestamp or healthy state. | +| `result_presentation` | Browser-computed glyph, color, accessible name and 1400 ms result interval match the table; plaque and Header geometry remain unchanged. | +| `retry_during_result` | A new activation clears the old result; its timer cannot clear the newer busy/result state or issue a duplicate request. | +| `partial_failure` | One failed required subrequest prevents a green check even if all other requests succeeded. | +| `disconnect_cleanup` | Panel removal clears result timers and isolates late completions from a replacement panel. | + +A static contract check is additional protection, not a substitute for these behavioral tests. Browser checks are required for actual animation, reduced-motion styling and visual geometry; a fake DOM or screenshot alone does not prove them. + +## Acceptance and adoption + +Include the refresh action in the v2.2 mandatory viewport matrix. Check rapid repeat activation, a slow response and a rejected response on the phone; ensure there is no title shift, viewport reset or lost draft. A still screenshot cannot establish that an icon rotates. + +This registry stores the rule, its digest and its regression-case list. Registry CI protects that agreement from deletion or silent weakening; it does not execute every panel's runtime. Each owning repository records implementation, production-test evidence and phone/browser acceptance separately. Missing evidence is `GAP`, not an assumed pass. HO-SC-8W PR #170 is the initial fix, not certification of every case above or of other NikaS panels. diff --git a/scripts/check_nikas_ui_standard.py b/scripts/check_nikas_ui_standard.py index 5bd6541..03e9f37 100644 --- a/scripts/check_nikas_ui_standard.py +++ b/scripts/check_nikas_ui_standard.py @@ -40,8 +40,43 @@ def main() -> None: baseline = f"Normative baseline:** NikaS Specialized Panel UI Standard v{config['version']}" require(baseline in knowledge_base, "engineering knowledge base baseline does not match the canonical standard") require("### 2.10 Peer status and selection are different facts" in knowledge_base, "knowledge base is missing the v2.2 peer-status lesson") + require("### 2.13 Connection plaque and blue corner must use locked tokens" in knowledge_base, "knowledge base is missing the locked connection/decoration lesson") digest = hashlib.sha256(standard.encode("utf-8")).hexdigest() require(digest == config.get("standard_sha256"), "local NikaS UI standard is not the canonical v2.2 copy") + # Registry integrity only: production layout needs the companion's browser evidence. + geometry = config.get("connection_decoration_contract", {}) + require(geometry.get("version") == "1.1", "connection/decoration contract must be v1.1") + require(geometry.get("status") == "required_when_present", "connection/decoration applicability drift") + require(geometry.get("production_browser_acceptance_required") is True, "production geometry evidence is required") + require(geometry.get("state_layout_delta_px") == 0, "state changes must not move connection/decoration") + require(geometry.get("measurement_noise_px") == 0.1, "state geometry measurement noise drift") + require(geometry.get("geometry_tolerance_px") == 1, "card geometry tolerance must be 1px") + geometry_doc = read_relative(geometry.get("path", "docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md")) + require(hashlib.sha256(geometry_doc.encode("utf-8")).hexdigest() == geometry.get("sha256"), "connection/decoration contract hash drift") + require("NIKAS_CONNECTION_DECORATION_CONTRACT.md" in standard, "UI standard must bind the locked geometry contract") + plaque = config.get("connection_plaque_reference", {}) + expected_plaque = { + "contract_version": "1.1", "width_px": 168, "height_px": 58, + "box_sizing": "border-box", "top_px": 13, "right_px": 13, + "coordinate_origin": "card_inner_border_edge", "padding_px": "11 12", + "radius_px": 18, "lamp_px": 10, "column_gap_px": 9, "text_gap_px": 3, + "font_family": '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif', + "main_font": "16px/700", "freshness_font": "13px/600", + "main_line_height_px": 17, "freshness_line_height_px": 14, + "geometry_overrides_allowed": False, + "shadow": "0 4px 14px rgba(0,0,0,.055)", + } + for key, value in expected_plaque.items(): + require(plaque.get(key) == value, f"connection plaque token drift: {key}") + require("min_height_px" not in plaque, "retire the minimum-only connection height") + expected_corner = { + "width_px": 205, "height_px": 205, "top_px": -92, "right_px": -70, + "base_color": "#03A9D9", "background": "rgba(3,169,217,0.07)", + "opacity": 1, "radius": "50%", "coordinate_origin": "card_inner_border_edge", + "clip": "persistent_card_decoration_layer", "theme_primary_dependent": False, + "geometry_overrides_allowed": False, "pointer_events": "none", "aria_hidden": True, + } + require(config.get("blue_corner_reference") == expected_corner, "blue corner token drift") navigation_contract = read_relative(config["navigation_contract_path"]) navigation_digest = hashlib.sha256(navigation_contract.encode("utf-8")).hexdigest() require( @@ -87,6 +122,8 @@ def main() -> None: "Unchanged lamp state produces no DOM write.", "icon no larger than `26px`", "canonical glyph size is `26px`", + "Panel lifecycle and availability", + "NIKAS_PANEL_LIFECYCLE_CONTRACT.md", ): require(clause in standard, f"canonical Header-return clause missing: {clause}") @@ -147,6 +184,45 @@ def main() -> None: require(lamp.get("update_mode") == "point-patch", "status lamps must use point-only DOM updates") require(lamp.get("accessible_status_required") is True, "status lamps require accessible text") + lifecycle = config.get("panel_lifecycle", {}) + require(lifecycle.get("version") == "1.0", "NikaS panel lifecycle contract version must be 1.0") + require(lifecycle.get("status") == "required", "panel lifecycle contract must be required") + require(lifecycle.get("registration_before_device_io") is True, "panel route must precede fallible device I/O") + require(lifecycle.get("initial_failure") == "panel_remains_registered", "initial failure must preserve the panel route") + require(lifecycle.get("unavailable_rendering") == "fail_closed", "offline panel content must fail closed") + require(lifecycle.get("recovery") == "coordinator_or_config_entry_retry", "panel recovery must use the backend retry lifecycle") + lifecycle_contract = read_relative(lifecycle["path"]) + lifecycle_digest = hashlib.sha256(lifecycle_contract.encode("utf-8")).hexdigest() + require(lifecycle_digest == lifecycle.get("sha256"), "local NikaS panel lifecycle contract hash drift") + required_lifecycle_cases = [ + "registration_before_refresh", + "initial_failure_preserves_route", + "offline_bootstrap", + "retry_recovery", + "route_collision", + "unload_ownership", + "generated_manifest_ownership", + ] + require(lifecycle.get("required_cases") == required_lifecycle_cases, "panel lifecycle regression cases drift") + for clause in ( + "LIFECYCLE-01", + "LIFECYCLE-02", + "LIFECYCLE-03", + "LIFECYCLE-04", + "LIFECYCLE-05", + "LIFECYCLE-06", + "LIFECYCLE-07", + "LIFECYCLE-08", + "async_config_entry_first_refresh()", + "Hardware evidence is required only", + ): + require(clause in lifecycle_contract, f"canonical panel lifecycle clause missing: {clause}") + require( + "NIKAS_PANEL_LIFECYCLE_CONTRACT.md" in standard + and "configured panel route is application infrastructure" in standard, + "UI standard must require the panel lifecycle companion", + ) + role = config.get("role") require(role in {"mirror", "registry", "base", "specialized", "readiness"}, f"unsupported NikaS UI role: {role}") runtime_files = config.get("runtime_files", []) @@ -161,12 +237,6 @@ def main() -> None: "production_entrypoint must be one of the checked runtime_files", ) - if role == "mirror": - require(not runtime_files, "mirror repository must not claim a panel runtime") - require(not production_entrypoint, "mirror repository must not claim a production panel entrypoint") - require(config.get("canonical_repository") == "NikaSir/ha-contract-generated-ui", "mirror must identify the canonical repository") - return - if role == "readiness": require(not runtime_files, "readiness-only repository must not claim a panel runtime") compliance = read_relative(config["compliance_path"]) @@ -175,6 +245,16 @@ def main() -> None: require("autonomous" in compliance.lower(), "readiness record must cover the autonomous-bundle GAP") return + if role == "mirror": + require(not runtime_files, "mirror repository must not claim a panel runtime") + require(not production_entrypoint, "mirror repository must not claim a production panel entrypoint") + require(config.get("canonical_repository") == "NikaSir/ha-contract-generated-ui", "mirror must identify the canonical repository") + require( + re.fullmatch(r"[0-9a-f]{40}", config.get("canonical_revision", "")) is not None, + "mirror must pin the reviewed canonical revision", + ) + return + if role == "registry": require(not runtime_files, "registry repository must not claim a panel runtime") require(not production_entrypoint, "registry repository must not claim a production panel entrypoint") @@ -221,7 +301,7 @@ def main() -> None: "/dashboard-actions/home", "/dashboard-infrastructure/overview", "/dashboard-access-v1/home", - "/dashboard-water-accounting", + "/dashboard-water", ): require(route in route_registry_text, f"canonical route missing from registry: {route}") return