mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user