stand: demo_guard neuters homeassistant.restart/stop for visitor-admins; console reset countdown (www/stand-reset-timer.js)

Visitors hold admin sessions (the editor demo needs is_admin), and some of
them restarted HA from the UI — exit code 100 between hourly resets looked
like stand instability. demo_guard re-registers restart/stop as no-op
services after startup; the hourly docker-level reset is untouched.
stand-reset-timer.js logs a per-minute countdown to the :00 reset in the
browser console (warn for the last 5 minutes); stand-dev-info.js is the
one-shot dev-stand note. Also on the stand host: mem_limit 1536m per
container in compose.yml as an OOM safety net.
This commit is contained in:
Matysh
2026-07-31 15:16:36 +03:00
parent 9c224e7606
commit c9775141c9
5 changed files with 110 additions and 0 deletions
+16
View File
@@ -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.
+46
View File
@@ -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
+10
View File
@@ -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"
}
+7
View File
@@ -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"
);
+31
View File
@@ -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);
})();