Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5278f130bd | ||
|
|
6d87b1e595 | ||
|
|
2180b2fd49 |
@@ -13,14 +13,14 @@ NestQuest helps households coordinate daily life, chore tracking, custody schedu
|
||||
1. Ensure [HACS (Home Assistant Community Store)](https://hacs.xyz/) is installed.
|
||||
2. In Home Assistant, open **HACS** > **Integrations**.
|
||||
3. Select the three-dots menu in the top right and click **Custom repositories**.
|
||||
4. Enter the repository URL: `https://github.com/Maniacs-Mansion/NestQuest`
|
||||
4. Enter the repository URL: `https://github.com/talon2king/NestQuest`
|
||||
5. Select category **Integration** and click **Add**.
|
||||
6. Search for **NestQuest** and click **Download**.
|
||||
7. Restart Home Assistant.
|
||||
|
||||
> **Note:** Development happens in the private Gitea repository
|
||||
> (`https://code.cubecraftlabs.com/Maniacs_Mansion/NestQuest`). The GitHub
|
||||
> repository is a private mirror maintained for HACS distribution.
|
||||
> repository is a public mirror maintained for HACS distribution.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
@@ -155,85 +155,6 @@ Sensors the automations read: `sensor.nestquest_<child>_quests_due_today`
|
||||
`..._quests_completed_today`, and
|
||||
`binary_sensor.nestquest_<child>_present_today`.
|
||||
|
||||
## TouchHub setup
|
||||
|
||||
Run these steps in order to display the NestQuest panel on a TouchHub
|
||||
wall device. The panel is an HA Lovelace dashboard, so the kiosk browser
|
||||
is simply a Home Assistant client.
|
||||
|
||||
1. **Create a dedicated kiosk user.** In *Settings → People → Users*, add
|
||||
a new user for the wall device. Make it **non-admin**, and ensure it is
|
||||
not in the NestQuest admin allowlist. The allowlist blocks the NestQuest
|
||||
**admin** operations — `uncomplete_quest`, `create_quest_definition`,
|
||||
`update_quest_definition`, `set_quest_definition_active`,
|
||||
`set_presence_pattern`, `create_presence_override`,
|
||||
`delete_presence_override`, `export_history_csv`, and `manage_child`
|
||||
(plus `regenerate` flows) — it does not make the account view-only. The
|
||||
kiosk user *can* call `nestquest.complete_quest`, the one open NestQuest
|
||||
service, to complete quests from the panel, and as a non-admin Home
|
||||
Assistant user it may also reach other permitted Home Assistant entities
|
||||
and services.
|
||||
|
||||
2. **Build a dashboard with only the NestQuest panel cards.** Create a
|
||||
Lovelace dashboard whose **URL slug is `nestquest`** (Settings →
|
||||
Dashboards → Edit → URL slug). A functional panel needs **two** views
|
||||
in that dashboard, each with its own card config from
|
||||
`design/ENTITIES-AND-SERVICES.md` §4. Give the views these exact URL
|
||||
paths (the path segment after the dashboard slug):
|
||||
|
||||
- **View 1 path `board`** (URL `/nestquest/board`) — party board
|
||||
(`type: custom:nestquest-party-board-card`): the household quest
|
||||
board. Set `weather_entity` (optional; the dock falls back to
|
||||
date/time), `quest_log_path: /nestquest/log` (the view the crest tap
|
||||
navigates to), and `child_order: [slug1, slug2, slug3]` listing each
|
||||
child's slug in display order.
|
||||
|
||||
- **View 2 path `log`** (URL `/nestquest/log`) — quest log
|
||||
(`type: custom:nestquest-quest-log-card`): the per-child quest screen
|
||||
the party board hops to. Set `board_path: /nestquest/board` (the
|
||||
party-board view to return to), `weather_entity`,
|
||||
`idle_return_seconds` (must be shorter than TouchHub Auto-Return),
|
||||
`confirm_timeout_seconds`, and `complete_screen_seconds`. The quest
|
||||
log reads the selected child's slug from the view's URL path, so the
|
||||
board → log hop is a plain Lovelace navigation.
|
||||
|
||||
Each view's layout MUST be set to **Panel** (Edit Dashboard → pencil on the
|
||||
view → View type: Panel). Home Assistant's default Masonry layout constrains
|
||||
the 1080px-tall NestQuest card to a narrow column; Panel (single-card)
|
||||
layout is what renders the card full-screen at 1920×1080.
|
||||
|
||||
The dashboard contains only the NestQuest panel card for each view —
|
||||
no other cards, no admin controls.
|
||||
|
||||
The `quest_log_path` and `board_path` values must match the dashboard
|
||||
slug and view paths above: `/<slug>/<view>`. If you chose `nestquest`
|
||||
as the dashboard slug with view paths `board` and `log`, the card
|
||||
configs are already correct. If you pick different names, substitute
|
||||
your actual paths in **both** card configs — e.g. a dashboard slug
|
||||
`chores` with view paths `main` and `history` needs
|
||||
`quest_log_path: /chores/history` and `board_path: /chores/main`.
|
||||
Navigation between the two views fails (the crest tap or back button
|
||||
lands on a 404) if the card config paths do not match the dashboard
|
||||
slug and view paths exactly.
|
||||
|
||||
See `design/ENTITIES-AND-SERVICES.md` §4 for the full card YAML.
|
||||
|
||||
3. **Point TouchHub at it.** In the TouchHub launcher, add the **Home
|
||||
Assistant Lovelace** app to the dock and paste the dashboard URL from
|
||||
step 2 — the party-board view URL, e.g. `/nestquest/board` (TouchHub
|
||||
loads that URL directly as the kiosk landing page). Set TouchHub
|
||||
**Auto-Return** to a value GREATER than the quest log's
|
||||
`idle_return_seconds` (default 40s) — recommend **60s or higher**. If
|
||||
Auto-Return is at or below 40s, TouchHub returns to its launcher before
|
||||
the quest log's idle return can navigate back to the party board.
|
||||
|
||||
4. **Log the kiosk session in.** Sign in on the device as the dedicated
|
||||
kiosk user from step 1, and leave that session logged in.
|
||||
|
||||
TouchHub loads the dashboard URL directly. The NestQuest cards are normal
|
||||
Lovelace cards, not an iframe, so no Home Assistant framing setting is
|
||||
needed.
|
||||
|
||||
## Developer Setup and Testing
|
||||
|
||||
This project uses [`uv`](https://github.com/astral-sh/uv) for fast Python package and dependency management.
|
||||
@@ -265,14 +186,6 @@ npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Kids panel layout
|
||||
|
||||
The Lovelace panel cards are designed for the wall display's confirmed
|
||||
resolution, 1920×1080 landscape, with no page scroll. TouchHub's bottom
|
||||
dock overlays the NestQuest weather dock strip until it auto-hides, so
|
||||
no tappable NestQuest control sits in that strip (the dock carries
|
||||
`pointer-events: none`). Keep it that way when editing the cards.
|
||||
|
||||
### Running Tests
|
||||
|
||||
Run the test suite with `pytest`:
|
||||
|
||||
+1
-83
@@ -1,87 +1,5 @@
|
||||
# NestQuest Release Notes
|
||||
|
||||
## Version 0.5.0 — 2026-09-18
|
||||
|
||||
### Scope
|
||||
|
||||
Fifth release of NestQuest, promoting Features 06, 07, and 08 from `dev` onto 0.4.0 (quest model, children registry, presence engine):
|
||||
|
||||
- **Feature 06 — quest definitions and assignment:** business layer `quest_definitions.py` over the multi-assignee / multi-window model (D-008). Create/edit/deactivate definitions; assign/unassign children; query helpers for materialization. `ScheduleRule` ⇄ `schedule_rules` storage mapping in the DAO layer.
|
||||
- **Feature 07 — instance materialization:** `materialize.py` fans out one instance per (definition, child, window, firing date) when the recurrence rule fires and the child is present. Idempotent on `(definition_id, child_id, due_date, window)`; skips completed instances; never generates past dates. HA-local `today` and configured `horizon_days` threaded end-to-end. Regeneration on definition/assignee/presence changes; daily rollover listener; startup backfill; `nestquest.regenerate` service.
|
||||
- **Feature 08 — completion and event log:** business layer `completion.py` wrapping the existing append-only `CompletionEventsDao`. `complete_instance` / `uncomplete_instance` (reversal event, original row untouched); state derived from the latest event; HA-local `was_on_time`; derived `missed` (never stored). Actor shapes `'user'` / `'panel'` with `actor_child_id` (D-008). Permission gating of un-complete is Feature 09.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Minimum Home Assistant version: **2024.6.0**.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- None at the schema layer. Schema remains **version 6** (advanced in 0.4.0). This release adds application code only.
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- No permission decorator yet (Feature 09): `complete_instance` / `uncomplete_instance` are Python APIs, not gated HA services. The only HA service in this release is `nestquest.regenerate`.
|
||||
- No entity or event surface (Feature 10): no sensors, no `nestquest_quest_completed` bus events.
|
||||
- No panel or admin Lovelace cards (Features 12/13).
|
||||
- Un-complete is implemented but not admin-gated until Feature 09.
|
||||
- Single-instance only.
|
||||
|
||||
### Rollback
|
||||
|
||||
Schema version is unchanged from 0.4.0 (still 6). A code downgrade to 0.4.0 does not require a database restore for schema compatibility, but **back up `nestquest.db` (plus `-wal`/`-shm`) before upgrading** — generated instances and completion events written by 0.5.0 remain in the file after downgrade; 0.4.0 will not generate or complete them.
|
||||
|
||||
**HACS installs:**
|
||||
|
||||
1. Downgrade via HACS (HACS > Integrations > NestQuest > Redownload > 0.4.0).
|
||||
2. Restart Home Assistant.
|
||||
|
||||
**Manual installs:**
|
||||
|
||||
1. Replace `custom_components/nestquest/` with the 0.4.0 tree.
|
||||
2. Restart Home Assistant.
|
||||
|
||||
**Repository operators:** git-revert the 0.5.0 release merge on `main`.
|
||||
|
||||
## Version 0.4.0 — 2026-09-15
|
||||
|
||||
### Scope
|
||||
|
||||
Fourth release of NestQuest, shipping three completed features on top of the data layer and recurrence engine:
|
||||
|
||||
- **Feature 14 — quest domain model (BREAKING rename, D-007/D-008):** `task_definitions`/`task_instances` are now `quest_definitions`/`quest_instances` everywhere; definitions are multi-assignee (`quest_definition_assignees`, no single child column) and multi-window (`quest_definition_windows` — morning/afternoon/evening, clock ranges in `const.py`); the instance key widens to `(definition_id, child_id, due_date, window)` so twice-daily and shared quests materialise one instance per child per window per day; `completion_events` gains `actor_child_id` (the tapped panel profile; admin events stay NULL) with a second actor-pair CHECK. Schema version advances to **6**.
|
||||
- **Feature 03 — children registry & admin allowlist:** typed business layer for child profiles (create/edit with trimmed required names and duplicate-name warnings, whole-table reorder, deactivation as the only removal path — history is never touched); the admin allowlist with a fail-closed `is_admin` resolver (empty allowlist denies everyone; the last admin cannot be removed); automatic seeding on first setup (the config-flow user is persisted in entry data, with the options-flow list and HA owner accounts as fallbacks) so the owner is never locked out; the options flow gains a multi-select picker of existing Home Assistant users.
|
||||
- **Feature 05 — presence & custody engine:** a pure `PresenceSchedule`/`PresenceOverride` model with total validation and lossless round-trips; anchor-date cycle arithmetic (D-004 — never ISO week numbers or parity); `PresenceEngine.is_present` with the always-present default for schedule-free children; date overrides beat the repeating pattern; a `next_present_dates` preview for the admin schedule UI; a full-year regression guard walking 2020 (366 days, ISO week 53) and 2015 (365 days, ISO week 53) proving the rotation never inverts at a year boundary.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Minimum Home Assistant version: **2024.6.0**.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The database schema advances from version 1 to version 6. Upgrades are automatic and data-preserving (legacy `task_*` tables are renamed; the definitions table is rebuilt for multi-assignee; legacy instances pin to the morning window; panel events backfill `actor_child_id`). **Back up `nestquest.db` before upgrading** — see Rollback.
|
||||
- Dev installs from before 2026-09-15 migrate automatically on first start.
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Still no entities, services, calendar platforms, or panel cards: nothing is user-visible in Home Assistant yet (Features 06-10 land the business layer, materialisation, permission gate, and entity surface).
|
||||
- No materialisation yet: instances are not generated from definitions + presence yet (Feature 07).
|
||||
- Single-instance only.
|
||||
|
||||
### Rollback
|
||||
|
||||
**Back up `nestquest.db` (plus `-wal`/`-shm` sidecars) BEFORE upgrading.** This release migrates the database to schema version 6 with renamed tables; an older NestQuest build refuses a version-6 database ("newer than this integration understands") and cannot read renamed tables at all — a plain downgrade is NOT possible once the database has been migrated.
|
||||
|
||||
**Rollback procedure (after a migrated database):**
|
||||
|
||||
1. Stop Home Assistant.
|
||||
2. Restore the pre-upgrade `nestquest.db` backup (and sidecars) you made before upgrading.
|
||||
3. Downgrade the integration (HACS > Integrations > NestQuest > Redownload > 0.3.0) or replace `custom_components/nestquest/` manually.
|
||||
4. Restart Home Assistant.
|
||||
|
||||
Without a backup, the only path is staying on 0.4.0 (recommended) or deleting the integration and its database to start fresh (Settings > Devices & Services > NestQuest > Delete, then remove `nestquest.db`).
|
||||
|
||||
**Repository operators:** instead of uninstalling, git-revert the release merge on `main` and restore the pre-upgrade database.
|
||||
|
||||
## Version 0.3.0 — 2026-09-14
|
||||
|
||||
### Scope
|
||||
@@ -208,4 +126,4 @@ v0.1.0 is the first release — there is no previous NestQuest version to downgr
|
||||
|
||||
**Data:** the integration does not yet create its own database file, so there is no application data. However, Home Assistant persists the config entry and its selected options — removing the integration deletes that configuration, and the option values (`horizon_days`, `day_rollover_time`, `panel_idle_timeout`) will be lost on removal.
|
||||
|
||||
**Repository operators:** instead of uninstalling, you may git-revert the release merge on `main`, which removes the integration files entirely. End users should use the uninstall path above.
|
||||
**Repository operators:** instead of uninstalling, you may git-revert the release merge on `main`, which removes the integration files entirely. End users should use the uninstall path above.
|
||||
@@ -9,10 +9,10 @@
|
||||
"frontend",
|
||||
"http"
|
||||
],
|
||||
"documentation": "https://github.com/Maniacs-Mansion/NestQuest",
|
||||
"documentation": "https://github.com/talon2king/NestQuest",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/Maniacs-Mansion/NestQuest/issues",
|
||||
"issue_tracker": "https://github.com/talon2king/NestQuest/issues",
|
||||
"requirements": [],
|
||||
"version": "0.5.0"
|
||||
"version": "0.3.0"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,20 +8,6 @@ type StateObject = {
|
||||
attributes?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type HassConnection = {
|
||||
subscribeEvents: (
|
||||
callback: (event: unknown) => void,
|
||||
eventType: string
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
|
||||
const NESTQUEST_EVENT_TYPES = [
|
||||
"nestquest_quest_completed",
|
||||
"nestquest_quest_uncompleted",
|
||||
"nestquest_quest_missed",
|
||||
"nestquest_child_day_complete",
|
||||
];
|
||||
|
||||
type ChildPlate = {
|
||||
slug: string;
|
||||
name: string;
|
||||
@@ -729,10 +715,6 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
_config?: CardConfig;
|
||||
_now = new Date();
|
||||
private _clockTimer?: number;
|
||||
private _unsubs: Array<() => void> = [];
|
||||
private _subscribed = false;
|
||||
private _subGeneration = 0;
|
||||
private _retryTimer?: number;
|
||||
|
||||
setConfig(config: CardConfig): void {
|
||||
if (!config || typeof config !== "object") {
|
||||
@@ -741,101 +723,19 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _hassConnection(): HassConnection | null {
|
||||
const connection = (this.hass as { connection?: unknown } | undefined)
|
||||
?.connection;
|
||||
if (
|
||||
!connection ||
|
||||
typeof connection !== "object" ||
|
||||
typeof (connection as HassConnection).subscribeEvents !== "function"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return connection as HassConnection;
|
||||
}
|
||||
|
||||
private _subscribeLive(): void {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (this._subscribed) {
|
||||
return;
|
||||
}
|
||||
const connection = this._hassConnection();
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = true;
|
||||
const token = ++this._subGeneration;
|
||||
const track = (unsubPromise: Promise<() => void>): void => {
|
||||
unsubPromise
|
||||
.then((unsub) => {
|
||||
if (this._subscribed && token === this._subGeneration) {
|
||||
this._unsubs.push(unsub);
|
||||
} else {
|
||||
unsub();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (token === this._subGeneration) {
|
||||
this._unsubscribeLive();
|
||||
this._armSubscribeRetry();
|
||||
}
|
||||
});
|
||||
};
|
||||
for (const eventType of NESTQUEST_EVENT_TYPES) {
|
||||
track(
|
||||
connection.subscribeEvents(() => this.requestUpdate(), eventType)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _armSubscribeRetry(): void {
|
||||
if (this._retryTimer !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._retryTimer = window.setTimeout(() => {
|
||||
this._retryTimer = undefined;
|
||||
this._subscribeLive();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private _clearSubscribeRetry(): void {
|
||||
if (this._retryTimer !== undefined) {
|
||||
window.clearTimeout(this._retryTimer);
|
||||
this._retryTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _unsubscribeLive(): void {
|
||||
this._clearSubscribeRetry();
|
||||
this._subGeneration++;
|
||||
for (const unsub of this._unsubs) {
|
||||
unsub();
|
||||
}
|
||||
this._unsubs = [];
|
||||
this._subscribed = false;
|
||||
}
|
||||
|
||||
getCardSize(): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._subscribeLive();
|
||||
this._now = new Date();
|
||||
this._clockTimer = window.setInterval(() => {
|
||||
this._now = new Date();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
this._subscribeLive();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._unsubscribeLive();
|
||||
if (this._clockTimer !== undefined) {
|
||||
window.clearInterval(this._clockTimer);
|
||||
this._clockTimer = undefined;
|
||||
|
||||
@@ -13,20 +13,6 @@ type StateObject = {
|
||||
attributes?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type HassConnection = {
|
||||
subscribeEvents: (
|
||||
callback: (event: unknown) => void,
|
||||
eventType: string
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
|
||||
const NESTQUEST_EVENT_TYPES = [
|
||||
"nestquest_quest_completed",
|
||||
"nestquest_quest_uncompleted",
|
||||
"nestquest_quest_missed",
|
||||
"nestquest_child_day_complete",
|
||||
];
|
||||
|
||||
type PanelInstance = {
|
||||
id: number;
|
||||
child_id: number | null;
|
||||
@@ -1375,10 +1361,6 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
private _confirmTimer?: number;
|
||||
private _toastTimer?: number;
|
||||
private _completeTimer?: number;
|
||||
private _unsubs: Array<() => void> = [];
|
||||
private _subscribed = false;
|
||||
private _subGeneration = 0;
|
||||
private _retryTimer?: number;
|
||||
|
||||
setConfig(config: QuestLogCardConfig): void {
|
||||
if (!config || typeof config !== "object") {
|
||||
@@ -1387,89 +1369,12 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _hassConnection(): HassConnection | null {
|
||||
const connection = (this.hass as { connection?: unknown } | undefined)
|
||||
?.connection;
|
||||
if (
|
||||
!connection ||
|
||||
typeof connection !== "object" ||
|
||||
typeof (connection as HassConnection).subscribeEvents !== "function"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return connection as HassConnection;
|
||||
}
|
||||
|
||||
private _subscribeLive(): void {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (this._subscribed) {
|
||||
return;
|
||||
}
|
||||
const connection = this._hassConnection();
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = true;
|
||||
const token = ++this._subGeneration;
|
||||
const track = (unsubPromise: Promise<() => void>): void => {
|
||||
unsubPromise
|
||||
.then((unsub) => {
|
||||
if (this._subscribed && token === this._subGeneration) {
|
||||
this._unsubs.push(unsub);
|
||||
} else {
|
||||
unsub();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (token === this._subGeneration) {
|
||||
this._unsubscribeLive();
|
||||
this._armSubscribeRetry();
|
||||
}
|
||||
});
|
||||
};
|
||||
for (const eventType of NESTQUEST_EVENT_TYPES) {
|
||||
track(
|
||||
connection.subscribeEvents(() => this.requestUpdate(), eventType)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _armSubscribeRetry(): void {
|
||||
if (this._retryTimer !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._retryTimer = window.setTimeout(() => {
|
||||
this._retryTimer = undefined;
|
||||
this._subscribeLive();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private _clearSubscribeRetry(): void {
|
||||
if (this._retryTimer !== undefined) {
|
||||
window.clearTimeout(this._retryTimer);
|
||||
this._retryTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _unsubscribeLive(): void {
|
||||
this._clearSubscribeRetry();
|
||||
this._subGeneration++;
|
||||
for (const unsub of this._unsubs) {
|
||||
unsub();
|
||||
}
|
||||
this._unsubs = [];
|
||||
this._subscribed = false;
|
||||
}
|
||||
|
||||
getCardSize(): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._subscribeLive();
|
||||
this._now = new Date();
|
||||
this._clockTimer = window.setInterval(() => {
|
||||
this._now = new Date();
|
||||
@@ -1484,7 +1389,6 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
this._subscribeLive();
|
||||
if (this._logView().kind === "complete-day") {
|
||||
this._clearIdle();
|
||||
this._armCompleteTimer();
|
||||
@@ -1498,7 +1402,6 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._unsubscribeLive();
|
||||
this._stopClock();
|
||||
this._clearIdle();
|
||||
this._clearConfirmTimer();
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nestquest"
|
||||
version = "0.5.0"
|
||||
version = "0.3.0"
|
||||
description = "NestQuest Home Assistant Integration"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -93,14 +93,6 @@ def test_readme_documents_npm_build() -> None:
|
||||
assert "nestquest-cards.js" in readme
|
||||
|
||||
|
||||
def test_readme_documents_panel_resolution_and_touchhub_dock() -> None:
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
assert "1920×1080" in readme
|
||||
assert "TouchHub" in readme
|
||||
assert "dock" in readme
|
||||
assert "pointer-events: none" in readme
|
||||
|
||||
|
||||
WOFF2_FILES = (
|
||||
"nunito-latin-400-normal.woff2",
|
||||
"nunito-latin-600-normal.woff2",
|
||||
@@ -163,19 +155,3 @@ def test_frontend_and_www_have_no_cdn_font_references() -> None:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
for marker in CDN_MARKERS:
|
||||
assert marker not in text, f"{path}: {marker}"
|
||||
|
||||
|
||||
def test_readme_documents_the_touchhub_setup_runbook() -> None:
|
||||
readme = " ".join(
|
||||
(REPO_ROOT / "README.md").read_text(encoding="utf-8").split()
|
||||
)
|
||||
for required in ("TouchHub", "kiosk"):
|
||||
assert required in readme, required
|
||||
assert "not in the NestQuest admin allowlist" in readme
|
||||
assert "contains only the NestQuest panel card" in readme
|
||||
assert "nestquest-party-board-card" in readme
|
||||
assert "nestquest-quest-log-card" in readme
|
||||
assert "Panel" in readme
|
||||
assert "Auto-Return" in readme
|
||||
assert "use_x_frame_options" not in readme
|
||||
assert "clickjacking" not in readme
|
||||
|
||||
+2
-2
@@ -67,9 +67,9 @@ def test_readme_exists_and_contains_required_sections() -> None:
|
||||
def test_readme_hacs_points_at_public_github_mirror() -> None:
|
||||
"""Verify README directs HACS users to the public GitHub mirror, not the private Gitea repo."""
|
||||
content = README_PATH.read_text(encoding="utf-8")
|
||||
assert "https://github.com/Maniacs-Mansion/NestQuest" in content
|
||||
assert "https://github.com/talon2king/NestQuest" in content
|
||||
assert "https://code.cubecraftlabs.com/Maniacs_Mansion/NestQuest" in content
|
||||
assert content.index("https://github.com/Maniacs-Mansion/NestQuest") < content.index(
|
||||
assert content.index("https://github.com/talon2king/NestQuest") < content.index(
|
||||
"https://code.cubecraftlabs.com/Maniacs_Mansion/NestQuest"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
"""Source-level tests for the kid panel cards.
|
||||
|
||||
The panel cards are Lit/TypeScript modules bundled by Vite; these tests assert
|
||||
the contract by reading the card sources and the built bundle, in the same
|
||||
spirit as test_frontend.py. They cover the states decided in
|
||||
design/PANEL-EMPTY-STATES.md and PANEL-SPEC.md, and guard the kid-facing
|
||||
bundles against ever gaining a parent-only service call.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
FRONTEND_CARDS_DIR = REPO_ROOT / "frontend" / "src" / "cards"
|
||||
PARTY_BOARD = FRONTEND_CARDS_DIR / "party-board.ts"
|
||||
QUEST_LOG = FRONTEND_CARDS_DIR / "quest-log.ts"
|
||||
BUNDLE_PATH = REPO_ROOT / "custom_components" / "nestquest" / "www" / "nestquest-cards.js"
|
||||
|
||||
PANEL_SOURCES = (PARTY_BOARD, QUEST_LOG)
|
||||
|
||||
# Parent-only NestQuest services. The party board and quest log are the
|
||||
# kid-facing panel: they may complete a quest for the tapped child, and nothing
|
||||
# else. Any of these names appearing in a panel source or bundle means an admin
|
||||
# control has leaked into the kid panel.
|
||||
ADMIN_ONLY_SERVICES = (
|
||||
"uncomplete_quest",
|
||||
"create_quest_definition",
|
||||
"update_quest_definition",
|
||||
"set_quest_definition_active",
|
||||
"set_presence_pattern",
|
||||
"create_presence_override",
|
||||
"delete_presence_override",
|
||||
"export_history_csv",
|
||||
"manage_child",
|
||||
"regenerate",
|
||||
)
|
||||
|
||||
WINDOW_KEYS = ("morning", "afternoon", "evening")
|
||||
WINDOW_NAMES = ("Morning", "Afternoon", "Evening")
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_party_board_renders_present_and_away_plates() -> None:
|
||||
source = _read(PARTY_BOARD)
|
||||
assert "nestquest-party-board-card" in source
|
||||
# Present adventurers get the tappable crest plate.
|
||||
assert "button.plate" in source
|
||||
assert 'class="plate"' in source
|
||||
assert "Home today" in source
|
||||
assert "aria-label=\"Open ${plate.name}'s Quest Log\"" in source
|
||||
# Away adventurers get the dashed On travels plate, not a tappable one.
|
||||
assert ".plate.away" in source
|
||||
assert 'class="plate away"' in source
|
||||
assert "On travels" in source
|
||||
assert "next_present" in source
|
||||
assert "Returns ${formatDay(returnsDate" in source
|
||||
|
||||
|
||||
def test_quest_log_renders_three_window_columns() -> None:
|
||||
source = _read(QUEST_LOG)
|
||||
for key, name in zip(WINDOW_KEYS, WINDOW_NAMES):
|
||||
assert f'key: "{key}"' in source, key
|
||||
assert f'name: "{name}"' in source, name
|
||||
assert "grid-template-columns: repeat(3, 1fr)" in source
|
||||
assert 'class="column-head"' in source
|
||||
assert 'class="column-names"' in source
|
||||
assert 'class="column-range"' in source
|
||||
|
||||
|
||||
def test_empty_window_still_renders_its_column_head() -> None:
|
||||
"""A window with no quests renders the head alone (PANEL-SPEC §3.1)."""
|
||||
source = _read(QUEST_LOG)
|
||||
column = source[source.index("private _renderColumn(windowDef: WindowDef)") :]
|
||||
column = column[: column.index("private _renderQuest(")]
|
||||
# The head is emitted unconditionally, before the stack is built.
|
||||
assert 'class="column-head"' in column
|
||||
assert "sealed.length}/${instances.length}" in column
|
||||
# There is no early return that would drop the head on an empty window.
|
||||
assert "instances.length === 0" not in column
|
||||
assert "return nothing" not in column
|
||||
|
||||
|
||||
def test_complete_quest_is_optimistic_with_rollback() -> None:
|
||||
source = _read(QUEST_LOG)
|
||||
assert "_optimistic" in source
|
||||
assert "new Date().toISOString()" in source
|
||||
assert ".set(instance.id, stampedAt)" in source
|
||||
# The overlay is applied to the rendered instance before the service answers.
|
||||
assert 'instance.state = "completed"' in source
|
||||
# A rejected call reverts the overlay and raises the parchment toast.
|
||||
assert "reverted.delete(instance.id)" in source
|
||||
assert "NestQuest could not set the seal just now. Please try again." in source
|
||||
assert "_showToast(" in source
|
||||
assert "await this._callCompleteQuest(instance)" in source
|
||||
assert '"nestquest", "complete_quest"' in source
|
||||
|
||||
|
||||
def test_idle_return_to_board() -> None:
|
||||
source = _read(QUEST_LOG)
|
||||
assert "idle_return_seconds" in source
|
||||
assert "_armIdle" in source
|
||||
assert "_clearIdle" in source
|
||||
assert "this._idleSeconds() * 1000" in source
|
||||
assert "board_path" in source
|
||||
assert "_returnToBoard" in source
|
||||
assert 'window.dispatchEvent(new Event("location-changed"))' in source
|
||||
|
||||
|
||||
def test_decided_empty_and_error_states_are_present() -> None:
|
||||
log = _read(QUEST_LOG)
|
||||
board = _read(PARTY_BOARD)
|
||||
for kind in (
|
||||
"no-adventurer",
|
||||
"not-set-up",
|
||||
"unreachable",
|
||||
"away",
|
||||
"empty-day",
|
||||
"complete-day",
|
||||
):
|
||||
assert f'kind: "{kind}"' in log, kind
|
||||
assert "PANEL-EMPTY-STATES" in log
|
||||
assert "No adventurer chosen" in log
|
||||
assert "NestQuest is not set up yet" in log
|
||||
assert "The records cannot be reached" in log
|
||||
assert "No quests today" in log
|
||||
assert 'role="status"' in log
|
||||
# The board distinguishes the same setup/unreachable cases at board level.
|
||||
assert "NestQuest is not set up yet" in board
|
||||
assert "The records cannot be reached" in board
|
||||
assert "BOARD_NOTICE_NOT_SET_UP" in board
|
||||
assert "BOARD_NOTICE_UNREACHABLE" in board
|
||||
|
||||
|
||||
def test_quest_complete_trigger() -> None:
|
||||
source = _read(QUEST_LOG)
|
||||
assert "binary_sensor.nestquest_${slug}_all_done" in source
|
||||
assert "nestquest_child_day_complete" in source
|
||||
assert "NESTQUEST_EVENT_TYPES" in source
|
||||
assert 'class="complete-screen"' in source
|
||||
assert "Quest complete" in source
|
||||
assert "Returning to The Party in" in source
|
||||
assert "_armCompleteTimer" in source
|
||||
assert "complete_screen_seconds" in source
|
||||
|
||||
|
||||
def test_panel_cards_reference_no_admin_only_services() -> None:
|
||||
# Only the kid panel sources are scanned: the bundle also contains the
|
||||
# sibling admin card, so its parent-only service names are expected there.
|
||||
for path in PANEL_SOURCES:
|
||||
text = _read(path)
|
||||
for service in ADMIN_ONLY_SERVICES:
|
||||
assert service not in text, f"{path}: {service}"
|
||||
# The bundle is still built and carries both panel card custom elements.
|
||||
assert BUNDLE_PATH.is_file()
|
||||
bundle = _read(BUNDLE_PATH)
|
||||
assert "nestquest-party-board-card" in bundle
|
||||
assert "nestquest-quest-log-card" in bundle
|
||||
Reference in New Issue
Block a user