diff --git a/demo/stand/README.md b/demo/stand/README.md index 3251687b..8824594f 100644 --- a/demo/stand/README.md +++ b/demo/stand/README.md @@ -11,3 +11,19 @@ of the shipped integration. (template LQI sensors, alarm helpers, the smoke automation) lives in the seeds on the stand host — see `docs/TESTING-DEMO.md` and the memory note `houseplan-demo-stand`. + +- `demo_guard/` — stand-only guard (2026-07-31). Visitors log in as an + administrator (the card editor is gated on `is_admin`) and kept restarting + HA from the UI, which looked like the stand crashing between hourly resets. + The component re-registers `homeassistant.restart`/`homeassistant.stop` as + no-ops after startup. Deployed to `custom_components/demo_guard` in + seed-demo only (the dev stand sits behind basic auth) + `demo_guard:` in + configuration.yaml. + +- `www/stand-reset-timer.js` — console-only countdown to the next hourly + reset (`hp-reset.timer`, every hour at :00). Served as + `/local/stand-reset-timer.js` from `seed-demo/www/`, wired through + `frontend: extra_module_url`. Logs a styled `console.info` every minute, + switching to `console.warn` for the last 5 minutes. `www/stand-dev-info.js` + is the dev-stand counterpart: a single `console.info` saying the dev stand + only resets on deploy. diff --git a/demo/stand/demo_guard/__init__.py b/demo/stand/demo_guard/__init__.py new file mode 100644 index 00000000..5a7b426b --- /dev/null +++ b/demo/stand/demo_guard/__init__.py @@ -0,0 +1,46 @@ +"""Stand-only guard: keep visitor-admins from killing the public demo. + +Visitors log in as `demo`, and that user HAS to be an administrator — the +House Plan card gates its editor on `user.is_admin`, and demonstrating the +editor is the whole point of the stand. But an administrator can also call +`homeassistant.restart` / `homeassistant.stop`, and visitors do exactly that +(seen live 2026-07-31: HA exited with the restart code 100 minutes after a +scheduled reset, right as a visitor session from an external IP was active — +that is what looked like «стенд перезагружается чаще, чем раз в час»). + +Once startup finishes this component re-registers both services as no-ops: +the UI buttons stay, pressing them only leaves a WARNING in the log. The +hourly stand reset is NOT affected — hp-reset.timer restarts the docker +container from the outside. +""" +from __future__ import annotations + +import logging + +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "demo_guard" +BLOCKED = ("restart", "stop") + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + async def _blocked(call: ServiceCall) -> None: + _LOGGER.warning( + "демо-стенд: homeassistant.%s отключён для посетителей " + "(стенд и так сбрасывается сам, ежечасно в :00)", + call.service, + ) + + @callback + def _neuter(_event) -> None: + # After EVENT_HOMEASSISTANT_STARTED every core service is in place; + # registering the same domain/name again replaces the handler. + for service in BLOCKED: + hass.services.async_register("homeassistant", service, _blocked) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _neuter) + return True diff --git a/demo/stand/demo_guard/manifest.json b/demo/stand/demo_guard/manifest.json new file mode 100644 index 00000000..a3f75f08 --- /dev/null +++ b/demo/stand/demo_guard/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "demo_guard", + "name": "Demo Stand Guard", + "codeowners": [], + "dependencies": [], + "documentation": "https://github.com/Matysh/houseplan-card/tree/dev/demo/stand/demo_guard", + "iot_class": "calculated", + "requirements": [], + "version": "1.0.0" +} diff --git a/demo/stand/www/stand-dev-info.js b/demo/stand/www/stand-dev-info.js new file mode 100644 index 00000000..838f2245 --- /dev/null +++ b/demo/stand/www/stand-dev-info.js @@ -0,0 +1,7 @@ +// Dev-стенд House Plan: ежечасного сброса НЕТ. Контейнер перезапускается +// только при новом коммите в ветку dev (hp-update-dev.timer, проверка раз +// в 10 минут). Одно информационное сообщение при загрузке, без таймера. +console.info( + "%c[dev-стенд] сброс только при деплое: перезапуск при новом коммите в dev (проверка раз в 10 мин)", + "background:#5d4037;color:#fff;padding:2px 6px;border-radius:3px" +); diff --git a/demo/stand/www/stand-reset-timer.js b/demo/stand/www/stand-reset-timer.js new file mode 100644 index 00000000..5efd8de3 --- /dev/null +++ b/demo/stand/www/stand-reset-timer.js @@ -0,0 +1,31 @@ +// Демо-стенд House Plan: таймер до ближайшего сброса — пишет ТОЛЬКО в консоль +// DevTools, никакого UI. Стенд сбрасывается systemd-таймером hp-reset.timer +// ежечасно в :00 (RandomizedDelaySec=0; сам перезапуск занимает ~15 секунд, +// отсюда «~» в сообщении). +(() => { + "use strict"; + const INFO = "background:#03589c;color:#fff;padding:2px 6px;border-radius:3px"; + const WARN = "background:#b71c1c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:bold"; + + const tick = () => { + const now = new Date(); + const next = new Date(now); + next.setMinutes(60, 0, 0); // ближайший :00 + const left = Math.max(1, Math.ceil((next - now) / 60000)); + if (left <= 5) { + console.warn( + `%c[демо-стенд] до сброса ~${left} мин — несохранённые эксперименты пропадут (сброс ежечасно в :00)`, + WARN + ); + } else { + console.info(`%c[демо-стенд] до сброса ~${left} мин (сброс ежечасно в :00)`, INFO); + } + }; + + tick(); + // выравниваемся по границе минуты, дальше — раз в минуту + setTimeout(() => { + tick(); + setInterval(tick, 60000); + }, (60 - new Date().getSeconds()) * 1000); +})();