${Ot.map((e) => this._renderColumn(e))}
@@ -2781,11 +2884,11 @@ class dn extends A {
const n = this._childName(t) ?? Y(t);
if (!this._childPresent(t))
return { kind: "away", name: n, returns: this._awayReturns(t) };
- const s = this._state(`binary_sensor.nestquest_${t}_all_done`);
- if (s && String(s.state ?? "").trim().toLowerCase() === "on")
+ const i = this._state(`binary_sensor.nestquest_${t}_all_done`);
+ if (i && String(i.state ?? "").trim().toLowerCase() === "on")
return { kind: "complete-day", name: n };
const r = this._state(`sensor.nestquest_${t}_quests_due_today`);
- return v(y(r?.state, 0)) === 0 ? { kind: "empty-day", name: n } : this._remaining() === 0 ? { kind: "complete-day", name: n } : { kind: "normal" };
+ return b(y(r?.state, 0)) === 0 ? { kind: "empty-day", name: n } : this._remaining() === 0 ? { kind: "complete-day", name: n } : { kind: "normal" };
}
/** "missing" when the due sensor does not exist (integration not
* configured), "stale" when it answers unavailable/unknown (backend
@@ -2802,22 +2905,22 @@ class dn extends A {
const n = this._state(`sensor.nestquest_${t}_quests_due_today`)?.attributes?.present;
if (typeof n == "boolean")
return n;
- const s = this._state(
+ const i = this._state(
`binary_sensor.nestquest_${t}_present_today`
);
- return s ? String(s.state ?? "").trim().toLowerCase() === "on" : !0;
+ return i ? String(i.state ?? "").trim().toLowerCase() === "on" : !0;
}
_awayReturns(t) {
const e = this._timeZone(), n = this._state(
`binary_sensor.nestquest_${t}_present_today`
- ), s = Tt(
+ ), i = Nt(
n?.attributes?.next_present,
e
);
- return s ? L(s, e) : null;
+ return i ? H(i, e) : null;
}
_renderNotice(t) {
- return c`
+ return d`
${t.icon}
@@ -2828,14 +2931,14 @@ class dn extends A {
`;
}
_timeZone() {
- return De(this.hass);
+ return He(this.hass);
}
_state(t) {
const n = this.hass?.states?.[t];
return n && typeof n == "object" ? n : null;
}
_childSlug() {
- return Be();
+ return Ze();
}
_boardPath() {
return $(this._config?.board_path).replace(/\/+$/, "");
@@ -2861,8 +2964,8 @@ class dn extends A {
_childCounts(t) {
const e = this._state(`sensor.nestquest_${t}_quests_due_today`), n = this._state(
`sensor.nestquest_${t}_quests_completed_today`
- ), s = v(y(e?.state, 0)), r = v(y(n?.state, 0));
- return { due: s, completed: r };
+ ), i = b(y(e?.state, 0)), r = b(y(n?.state, 0));
+ return { due: i, completed: r };
}
_instances() {
const t = this._childSlug();
@@ -2871,21 +2974,21 @@ class dn extends A {
const e = this._state(`sensor.nestquest_${t}_quests_due_today`)?.attributes?.instances;
if (!Array.isArray(e))
return [];
- const n = e.map(Fe).filter((r) => r !== null);
+ const n = e.map(Ve).filter((r) => r !== null);
if (this._optimistic.size === 0)
return n;
- let s = !1;
+ let i = !1;
for (const r of n) {
const o = this._optimistic.get(r.id);
if (o !== void 0) {
if (r.state === "completed") {
- this._optimistic.delete(r.id), s = !0;
+ this._optimistic.delete(r.id), i = !0;
continue;
}
r.state = "completed", r.overdue = !1, r.completed_at = o;
}
}
- return s && (this._optimistic = new Map(this._optimistic)), n;
+ return i && (this._optimistic = new Map(this._optimistic)), n;
}
_remaining() {
const t = this._childSlug();
@@ -2895,9 +2998,9 @@ class dn extends A {
`sensor.nestquest_${t}_quests_remaining_today`
);
if (e)
- return v(y(e.state, 0));
- const { due: n, completed: s } = this._childCounts(t);
- return v(n - s);
+ return b(y(e.state, 0));
+ const { due: n, completed: i } = this._childCounts(t);
+ return b(n - i);
}
_partyCounts() {
const t = this._state(
@@ -2907,32 +3010,32 @@ class dn extends A {
);
if (t || e)
return {
- completed: v(y(e?.state, 0)),
- due: v(y(t?.state, 0))
+ completed: b(y(e?.state, 0)),
+ due: b(y(t?.state, 0))
};
- const n = this._childSlug(), { due: s, completed: r } = this._childCounts(n);
- return { completed: r, due: s };
+ const n = this._childSlug(), { due: i, completed: r } = this._childCounts(n);
+ return { completed: r, due: i };
}
_completeStats() {
const t = this._childCounts(this._childSlug()).completed, e = this._instances().filter(
(r) => r.on_time === !0
- ).length, n = v(t - e), s = this._partyCounts();
+ ).length, n = b(t - e), i = this._partyCounts();
return {
claimed: t,
onTime: e,
late: n,
- party: `${s.completed}/${s.due}`
+ party: `${i.completed}/${i.due}`
};
}
_completeSub(t) {
- const e = L(this._now, this._timeZone()), { claimed: n, onTime: s, late: r } = this._completeStats();
- return n === 0 ? `${t} sealed the day on ${e}.` : r === 0 ? `${t} claimed every quest on ${e} — all on time.` : s === 0 ? `${t} claimed every quest on ${e} — all late.` : `${t} claimed every quest on ${e} — ${s} on time, ${r} late.`;
+ const e = H(this._now, this._timeZone()), { claimed: n, onTime: i, late: r } = this._completeStats();
+ return n === 0 ? `${t} sealed the day on ${e}.` : r === 0 ? `${t} claimed every quest on ${e} — all on time.` : i === 0 ? `${t} claimed every quest on ${e} — all late.` : `${t} claimed every quest on ${e} — ${i} on time, ${r} late.`;
}
_otherChildren() {
const t = this.hass?.states;
if (!t)
return [];
- const e = this._childSlug(), n = this._timeZone(), s = [];
+ const e = this._childSlug(), n = this._timeZone(), i = [];
for (const r of Object.keys(t)) {
const o = /^sensor\.nestquest_(.+)_quests_due_today$/.exec(r);
if (!o || o[1] === "household" || o[1] === e)
@@ -2949,20 +3052,20 @@ class dn extends A {
);
u = _ === null || String(_.state ?? "").trim().toLowerCase() !== "off";
}
- const d = this._state(
+ const c = this._state(
`sensor.nestquest_${l}_quests_completed_today`
- ), f = v(y(t[r]?.state, 0)), h = v(y(d?.state, 0));
+ ), f = b(y(t[r]?.state, 0)), h = b(y(c?.state, 0));
let m = null;
if (!u) {
const _ = this._state(
`binary_sensor.nestquest_${l}_present_today`
- ), x = Tt(
+ ), x = Nt(
_?.attributes?.next_present,
n
);
- m = x ? Ue(x, n) : null;
+ m = x ? Re(x, n) : null;
}
- s.push({
+ i.push({
slug: l,
name: p,
present: u,
@@ -2971,11 +3074,11 @@ class dn extends A {
returnsWeekday: m
});
}
- return s.sort((r, o) => r.name.localeCompare(o.name));
+ return i.sort((r, o) => r.name.localeCompare(o.name));
}
_renderHeader(t) {
- const e = this._childSlug(), n = t.kind === "away", s = n ? t.name : this._childName(e) ?? (e ? Y(e) : null), { due: r, completed: o } = this._childCounts(e), l = this._remaining(), a = this._partyCounts(), p = s ? `${s}'s Quest Log` : "Quest Log", u = (s?.charAt(0) || "?").toUpperCase();
- return c`
+ const e = this._childSlug(), n = t.kind === "away", i = n ? t.name : this._childName(e) ?? (e ? Y(e) : null), { due: r, completed: o } = this._childCounts(e), l = this._remaining(), a = this._partyCounts(), p = i ? `${i}'s Quest Log` : "Quest Log", u = (i?.charAt(0) || "?").toUpperCase();
+ return d`
${St(
@@ -3052,7 +3155,7 @@ class dn extends A {
(l) => this._renderQuest(l, !1, 0)
)}
${St(
- s,
+ i,
(l) => l.id,
(l, a) => this._renderQuest(l, !0, a)
)}
@@ -3063,8 +3166,8 @@ class dn extends A {
}
_renderQuest(t, e, n) {
if (e) {
- const o = Mt[n % Mt.length], l = t.completed_at ? je(t.completed_at, this._timeZone()) : "";
- return c`
+ const o = Mt[n % Mt.length], l = t.completed_at ? Be(t.completed_at, this._timeZone()) : "";
+ return d`
${tt}
@@ -3083,14 +3186,14 @@ class dn extends A {
`;
}
- const s = Le(t.due_time), r = t.overdue ? {
- text: s ? `Overdue · due ${s}` : "Overdue",
+ const i = Qe(t.due_time), r = t.overdue ? {
+ text: i ? `Overdue · due ${i}` : "Overdue",
late: !0
} : {
- text: s ? `Due by ${s}` : "Due today",
+ text: i ? `Due by ${i}` : "Due today",
late: !1
};
- return c`
+ return d`
n.present ? n.due > 0 && n.completed >= n.due ? `${n.name} has finished their log.` : n.due > n.completed ? `${n.name} has ${n.due - n.completed} quests left.` : `${n.name} has no quests today.` : n.returnsWeekday ? `${n.name} is on travels until ${n.returnsWeekday}.` : `${n.name} is on travels.`);
- return c`
+ return d`
Party roll-up
${e.join(" ")}
@@ -3139,7 +3242,7 @@ class dn extends A {
if (!t)
return g;
const e = Ot.find((n) => n.key === t.window);
- return c`
+ return d`
this._closeConfirm()}>
this._closeConfirm()}
>
- ${Je}
+ ${en}
Not yet
0) {
const a = l[0];
if (a && typeof a == "object") {
@@ -3306,7 +3409,7 @@ class dn extends A {
}
return {
condition: n,
- temperature: this._optionalNumber(s.temperature),
+ temperature: this._optionalNumber(i.temperature),
high: r,
low: o
};
@@ -3314,18 +3417,18 @@ class dn extends A {
_renderDock() {
const t = this._dockWeather();
if (!t)
- return c`
+ return d`
- ${L(this._now, this._timeZone())}
+ ${H(this._now, this._timeZone())}
- ${He(this._now, this._timeZone())}
+ ${Fe(this._now, this._timeZone())}
`;
- const e = Ze(t.condition), n = Ve(t.condition, e), s = t.temperature === null ? g : c`${Math.round(t.temperature)}° `, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`;
- return c`
+ const e = Je(t.condition), n = Xe(t.condition, e), i = t.temperature === null ? g : d`${Math.round(t.temperature)}° `, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`;
+ return d`
- ${rn}
- ${s}
+ ${cn}
+ ${i}
${r === null ? e : `${e} · ${r}`}
@@ -3346,19 +3449,19 @@ class dn extends A {
this._logView().kind !== "complete-day" && this._armIdle(), this._confirm !== null && this._armConfirmTimer();
};
}
-customElements.define("nestquest-quest-log-card", dn);
+customElements.define("nestquest-quest-log-card", un);
at({
type: "nestquest-quest-log-card",
name: "NestQuest Quest Log",
description: "One child's daily quests, grouped by window."
});
-const cn = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}";
-class pn extends A {
+const fn = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}";
+class mn extends A {
static properties = {
hass: { attribute: !1 },
_config: { state: !0 }
};
- static styles = [b(cn)];
+ static styles = [v(fn)];
hass;
_config;
setConfig(t) {
@@ -3370,17 +3473,17 @@ class pn extends A {
return 8;
}
render() {
- return c`NestQuest Admin
`;
+ return d`NestQuest Admin
`;
}
}
-customElements.define("nestquest-admin-card", pn);
+customElements.define("nestquest-admin-card", mn);
at({
type: "nestquest-admin-card",
name: "NestQuest Admin",
description: "Parent phone view for today, tasks, schedule, and history."
});
-const hn = "/nestquest-static/nestquest-fonts.css";
+const gn = "/nestquest-static/nestquest-fonts.css";
if (!document.querySelector('link[data-nq-fonts=""]')) {
- const i = document.createElement("link");
- i.rel = "stylesheet", i.href = hn, i.dataset.nqFonts = "", document.head.appendChild(i);
+ const s = document.createElement("link");
+ s.rel = "stylesheet", s.href = gn, s.dataset.nqFonts = "", document.head.appendChild(s);
}
diff --git a/frontend/src/cards/party-board.ts b/frontend/src/cards/party-board.ts
index 13a022d..d8ce085 100644
--- a/frontend/src/cards/party-board.ts
+++ b/frontend/src/cards/party-board.ts
@@ -8,6 +8,32 @@ type StateObject = {
attributes?: Record | 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",
+];
+
+function stateChangedEntity(event: unknown): string {
+ if (!event || typeof event !== "object") {
+ return "";
+ }
+ const data = (event as Record).data;
+ if (!data || typeof data !== "object") {
+ return "";
+ }
+ const entityId = (data as Record).entity_id;
+ return typeof entityId === "string" ? entityId : "";
+}
+
type ChildPlate = {
slug: string;
name: string;
@@ -715,6 +741,8 @@ export class NestQuestPartyBoardCard extends LitElement {
_config?: CardConfig;
_now = new Date();
private _clockTimer?: number;
+ private _unsubs: Array<() => void> = [];
+ private _subscribed = false;
setConfig(config: CardConfig): void {
if (!config || typeof config !== "object") {
@@ -723,19 +751,84 @@ 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._subscribed) {
+ return;
+ }
+ const connection = this._hassConnection();
+ if (!connection) {
+ return;
+ }
+ this._subscribed = true;
+ const track = (unsubPromise: Promise<() => void>): void => {
+ unsubPromise
+ .then((unsub) => {
+ if (this._subscribed) {
+ this._unsubs.push(unsub);
+ } else {
+ unsub();
+ }
+ })
+ .catch(() => {});
+ };
+ for (const eventType of NESTQUEST_EVENT_TYPES) {
+ track(
+ connection.subscribeEvents(() => this.requestUpdate(), eventType)
+ );
+ }
+ track(
+ connection.subscribeEvents((event: unknown) => {
+ const entityId = stateChangedEntity(event);
+ if (
+ entityId.startsWith("sensor.nestquest_") ||
+ entityId.startsWith("binary_sensor.nestquest_")
+ ) {
+ this.requestUpdate();
+ }
+ }, "state_changed")
+ );
+ }
+
+ private _unsubscribeLive(): void {
+ 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;
diff --git a/frontend/src/cards/quest-log.ts b/frontend/src/cards/quest-log.ts
index 3271526..ed74734 100644
--- a/frontend/src/cards/quest-log.ts
+++ b/frontend/src/cards/quest-log.ts
@@ -13,6 +13,32 @@ type StateObject = {
attributes?: Record | 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",
+];
+
+function stateChangedEntity(event: unknown): string {
+ if (!event || typeof event !== "object") {
+ return "";
+ }
+ const data = (event as Record).data;
+ if (!data || typeof data !== "object") {
+ return "";
+ }
+ const entityId = (data as Record).entity_id;
+ return typeof entityId === "string" ? entityId : "";
+}
+
type PanelInstance = {
id: number;
child_id: number | null;
@@ -1361,6 +1387,8 @@ export class NestQuestQuestLogCard extends LitElement {
private _confirmTimer?: number;
private _toastTimer?: number;
private _completeTimer?: number;
+ private _unsubs: Array<() => void> = [];
+ private _subscribed = false;
setConfig(config: QuestLogCardConfig): void {
if (!config || typeof config !== "object") {
@@ -1369,12 +1397,72 @@ 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._subscribed) {
+ return;
+ }
+ const connection = this._hassConnection();
+ if (!connection) {
+ return;
+ }
+ this._subscribed = true;
+ const track = (unsubPromise: Promise<() => void>): void => {
+ unsubPromise
+ .then((unsub) => {
+ if (this._subscribed) {
+ this._unsubs.push(unsub);
+ } else {
+ unsub();
+ }
+ })
+ .catch(() => {});
+ };
+ for (const eventType of NESTQUEST_EVENT_TYPES) {
+ track(
+ connection.subscribeEvents(() => this.requestUpdate(), eventType)
+ );
+ }
+ track(
+ connection.subscribeEvents((event: unknown) => {
+ const entityId = stateChangedEntity(event);
+ if (
+ entityId.startsWith("sensor.nestquest_") ||
+ entityId.startsWith("binary_sensor.nestquest_")
+ ) {
+ this.requestUpdate();
+ }
+ }, "state_changed")
+ );
+ }
+
+ private _unsubscribeLive(): void {
+ 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();
@@ -1389,6 +1477,7 @@ export class NestQuestQuestLogCard extends LitElement {
}
protected updated(): void {
+ this._subscribeLive();
if (this._logView().kind === "complete-day") {
this._clearIdle();
this._armCompleteTimer();
@@ -1402,6 +1491,7 @@ export class NestQuestQuestLogCard extends LitElement {
}
disconnectedCallback(): void {
+ this._unsubscribeLive();
this._stopClock();
this._clearIdle();
this._clearConfirmTimer();