Compare commits

..
Author SHA1 Message Date
overseerandClaude Opus 5.5 cd0ff68ea3 Reject an out-of-range planning horizon before saving
A digit string too long for a safe integer became Infinity, which
JSON.stringify sends as null, and the API reads null as "reset to
default" -- so saving could silently replace the household's horizon.
Require a safe integer >= 1 and send nothing otherwise (F19-01).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:03:02 -04:00
dev_agent 50428f04a4 Task cd3e0209: Settings preferences section (horizon, rollover, notifications) (#224) 2026-09-24 15:45:16 -04:00
10 changed files with 11 additions and 145 deletions
-68
View File
@@ -1,73 +1,5 @@
# NestQuest Release Notes
## Version 0.7.1 — 2026-09-24
### Scope
Patch release bringing the admin PWA follow-ups and its hosting deployment from
`dev` onto 0.7.0.
- **Feature 24 — Admin PWA Hosting:** the admin PWA's static hosting is now
reproducible from the repository — `deploy/pwa/Dockerfile` (multi-stage, pinned
`node`/`nginx` base images by digest, `VITE_AUTHENTIK_CLIENT_ID` /
`VITE_AUTHENTIK_ISSUER` / `VITE_API_BASE_URL` build args), `deploy/pwa/nginx.conf`
(SPA fallback plus a source allow-list for the reverse proxy and the host), a
`pwa` service in `deploy/compose.yaml` published on `:8081`, and a
`deploy/RUNBOOK.md` section describing the build/serve and the Traefik
`nestquest-app` router. (PRs #220, #221.)
- **Feature 19 follow-up — Manage children:** the Today header's Settings button
now opens a Settings screen. Its **Children** section lists the household's
children and lets a parent add (POST `/api/v1/admin/children`), edit
(PATCH `.../{id}`), activate/deactivate (PATCH `.../{id}/active`) and reorder
them (POST `.../reorder`). (PRs #222, #223.)
- **Feature 19 follow-up — Settings preferences:** the same screen has a
**Preferences** section for the planning horizon, day rollover time, notify
target, the morning/afternoon/end-of-day notification times and their toggles,
and celebration, read from `GET /api/v1/admin/settings` and saved with a partial
`PATCH` of only the changed fields. (PRs #224, #225, #226.)
### Requirements
- Minimum Home Assistant version: **2024.6.0**.
- A running NestQuest API service as in 0.7.0 (the Feature 17 deployment); the
`admin/` PWA is served from the owner's deployment at
`https://nestquest.cubecraftlabs.com`, not by HACS.
### Breaking changes
- **None.** The schema is unchanged at version 8 and no migration runs.
`custom_components/` is unchanged from 0.7.0 apart from the manifest
release-version metadata — no Home Assistant entity, service, event or
automation surface changes. This release adds admin PWA screens and deployment
artifacts only.
### Known Limitations
- **HACS does not ship the admin PWA or the deployment artifacts.** HACS copies
only `custom_components/nestquest`, so upgrading via HACS changes nothing
functional over 0.7.0; the PWA screens and hosting are deployed from `admin/`
and `deploy/` per the runbook.
- Serving the PWA and API still requires the Feature 17 deployment (host, reverse
proxy, Authentik, backup target) as documented in `deploy/RUNBOOK.md`.
- **Feature 12's TouchHub/DAKOS runbook task remains physically unverified**
(needs the wall panel).
- The Settings UI does not expose resetting a field to its default with `null`.
### Rollback
Schema is unchanged (8), so no migration is involved in either direction.
`custom_components/` is unchanged from 0.7.0 apart from the release-version
metadata; rolling back the integration is a plain swap, and the PWA/hosting can be
redeployed from the 0.7.0 tree.
**HACS installs:** HACS > Integrations > NestQuest > Redownload > 0.7.0, then
restart Home Assistant.
**Manual installs:** replace `custom_components/nestquest/` with the 0.7.0 tree,
then restart Home Assistant.
**Repository operators:** git-revert the 0.7.1 release merge on `main`.
## Version 0.7.0 — 2026-09-24
### Scope
@@ -362,39 +362,6 @@ describe("DefinitionsTab", () => {
expect(screen.getByText("4 definitions · 3 children")).toBeTruthy();
});
it("the weekday picker lists Sunday first and Sunday still stores 6", async () => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date(2026, 8, 23, 9, 0)); // Wed 23 Sep 2026, local
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" }));
const s = sheet();
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
target: { value: "Feed the cat" },
});
fireEvent.click(within(s).getByRole("button", { name: /Assigned children/ }));
fireEvent.click(within(s).getByRole("checkbox", { name: "Declan" }));
fireEvent.change(within(s).getByRole("combobox", { name: "Repeats" }), {
target: { value: "weekly" },
});
const days = within(s).getByRole("group", { name: "Days" });
expect(
within(days)
.getAllByRole("button")
.map((b) => b.getAttribute("aria-label")),
).toEqual(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]);
fireEvent.click(within(days).getByRole("button", { name: "Sunday" }));
fireEvent.click(within(s).getByRole("button", { name: "Evening" }));
await act(async () => {
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
});
const body = api.writes()[0].body as { rule: DefinitionRule };
expect(body.rule.weekday_set).toEqual([2, 6]);
expect(meta(await screen.findByTestId("definition-99"))).toContain("on Sun, Wed");
});
it("tapping a row opens the sheet in editing state and saving PATCHes", async () => {
await renderReady();
const row = screen.getByTestId("definition-11");
+3 -4
View File
@@ -27,7 +27,6 @@ import {
} from "../api/definitions";
import {
MONTH_SHORT,
WEEKDAY_DISPLAY_ORDER,
WEEKDAY_SHORT,
WINDOW_LABELS,
WINDOW_ORDER,
@@ -681,9 +680,9 @@ function EditSheet({
</div>
{draft.repeats === "weekly" || draft.repeats === "custom" ? (
<div className="defs-weekdays" role="group" aria-label="Days">
{WEEKDAY_DISPLAY_ORDER.map((day) => (
{WEEKDAY_SHORT.map((label, day) => (
<button
key={day}
key={label}
type="button"
aria-pressed={draft.weekdays.includes(day)}
aria-label={WEEKDAY_LONG[day]}
@@ -692,7 +691,7 @@ function EditSheet({
}
onClick={() => update({ weekdays: toggle(draft.weekdays, day) })}
>
{WEEKDAY_SHORT[day].charAt(0)}
{label.charAt(0)}
</button>
))}
</div>
+1 -4
View File
@@ -21,9 +21,6 @@ export const WINDOW_LABELS: Record<WindowName, string> = {
/** Index 0 = Monday .. 6 = Sunday, the core's weekday numbering. */
export const WEEKDAY_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
/** Display order for a week: Sunday first. The stored indices stay Monday=0. */
export const WEEKDAY_DISPLAY_ORDER = [6, 0, 1, 2, 3, 4, 5];
export const MONTH_SHORT = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
@@ -54,7 +51,7 @@ export function assigneeSummary(
function weekdayList(days: number[] | null): string {
return [...(days ?? [])]
.sort((a, b) => WEEKDAY_DISPLAY_ORDER.indexOf(a) - WEEKDAY_DISPLAY_ORDER.indexOf(b))
.sort((a, b) => a - b)
.map((day) => WEEKDAY_SHORT[day])
.join(", ");
}
+2 -12
View File
@@ -131,20 +131,10 @@ describe("ScheduleTab", () => {
expect(today.className).toContain("schedule-day--today");
expect(today.dataset.state).toBe("home");
expect(today.getAttribute("aria-label")).toBe("Sep 23 (today): Home");
// Sunday-first header; September 2026 starts on a Tuesday: two leading pads.
// September 2026 starts on a Tuesday: one leading pad after the initials.
const grid = screen.getByRole("group", { name: "Month grid" });
expect(Array.from(grid.children, (c) => c.textContent).slice(0, 7)).toEqual([
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]);
expect(grid.children[7].tagName).toBe("SPAN");
expect(grid.children[8].tagName).toBe("SPAN");
expect(grid.children[9].getAttribute("data-date")).toBe("2026-09-01");
expect(grid.children[8].getAttribute("data-date")).toBe("2026-09-01");
const legend = screen.getByRole("list", { name: "Legend" });
expect(within(legend).getAllByRole("listitem").map((li) => li.textContent)).toEqual([
+2 -3
View File
@@ -46,8 +46,7 @@ export const SCROLL_BOTTOM_PADDING = "92px";
export const DEFAULT_CYCLE_LENGTH_WEEKS = 2;
const CYCLE_LENGTH_CHOICES = [1, 2, 3, 4];
/** Sunday-first header; `weekdayOf` stays Monday=0. */
const WEEKDAY_INITIALS = ["S", "M", "T", "W", "T", "F", "S"];
const WEEKDAY_INITIALS = ["M", "T", "W", "T", "F", "S", "S"];
export const STATE_LABELS: Record<DayState, string> = {
home: "Home",
@@ -131,7 +130,7 @@ interface MonthGridProps {
function MonthGrid({ month, today, schedule, effective, overrides, onMonth, onToggle }: MonthGridProps) {
const dates = monthDates(month);
const leading = (weekdayOf(month) + 1) % 7;
const leading = weekdayOf(month);
return (
<>
<div className="schedule-month-head">
-18
View File
@@ -58,24 +58,6 @@ describe("calendar helpers", () => {
expect(weekdayOf("2026-09-06")).toBe(6);
});
it("weekdayOf keeps the stored Monday=0 .. Sunday=6 numbering (display order is separate)", () => {
// A full known week plus dates across month and year ends.
expect(
[
"2026-09-07",
"2026-09-08",
"2026-09-09",
"2026-09-10",
"2026-09-11",
"2026-09-12",
"2026-09-13",
].map(weekdayOf),
).toEqual([0, 1, 2, 3, 4, 5, 6]);
expect(weekdayOf("2026-11-01")).toBe(6); // Sunday
expect(weekdayOf("2027-01-01")).toBe(4); // Friday
expect(weekdayOf("2024-02-29")).toBe(3); // Thursday
});
it("monthStart shifts across year ends", () => {
expect(monthStart("2026-12-15", 1)).toBe("2027-01-01");
expect(monthStart("2026-01-31", -1)).toBe("2025-12-01");
+1 -1
View File
@@ -15,5 +15,5 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/Maniacs-Mansion/NestQuest/issues",
"requirements": [],
"version": "0.7.1"
"version": "0.7.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "nestquest"
version = "0.7.1"
version = "0.7.0"
description = "NestQuest Home Assistant Integration"
readme = "README.md"
requires-python = ">=3.11"
Generated
+1 -1
View File
@@ -373,7 +373,7 @@ wheels = [
[[package]]
name = "nestquest"
version = "0.7.1"
version = "0.7.0"
source = { virtual = "." }
[package.dev-dependencies]