393 files
This commit is contained in:
1 parent
b05b7a897e
commit
39d97ac2db
404 files changed
+54006
-11034
No files matched your search
@@ -32,12 +32,17 @@ import {
|
||||
buildCalendarBuckets,
|
||||
buildPastBuckets,
|
||||
isoDateLocal,
|
||||
pastHistoryGaps,
|
||||
pastHistoryKey,
|
||||
type CalendarEvent,
|
||||
type HistoryEntryShape,
|
||||
} from "./helpers/calendar-bucket";
|
||||
import { calendarStyles } from "./calendar-styles";
|
||||
import { syncLocaleFromHass, sharedStyles, currencySymbolOf, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, formatWeekday, formatMonth, langOf, formatCost, syncCurrencyDecimals} from "./styles";
|
||||
import { syncLocaleFromHass, sharedStyles, currencySymbolOf, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, formatInterval, formatWeekday, formatMonth, langOf, formatCost, syncCurrencyDecimals} from "./styles";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
import { loadHistoryEntryDraft } from "./helpers/history-draft";
|
||||
import { canWrite } from "./helpers/permissions";
|
||||
import { fetchSettingsOnce } from "./helpers/settings-cache";
|
||||
import { openHistoryEditDialog, openTaskQuickActions } from "./dialog-mount";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
@@ -77,6 +82,11 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
// 2+ configured object_filter values — restricts the card to this set.
|
||||
private _configuredObjects: string[] = [];
|
||||
@state() private _unsub: (() => void) | null = null;
|
||||
/** Past mode: full histories of the tasks whose list payload (last 20
|
||||
* entries) may not reach back over the whole window. */
|
||||
@state() private _pastHistory: Record<string, HistoryEntryShape[]> = {};
|
||||
private _pastHistorySig = "";
|
||||
private _pastSeq = 0;
|
||||
|
||||
private _dataLoaded = false;
|
||||
private _lastConnection: unknown = null;
|
||||
@@ -128,8 +138,26 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
/** An entry edited or deleted anywhere on the page (the history-edit
|
||||
* dialog's composed event reaches window). An edit of an entry OLDER than
|
||||
* the listed window leaves the list payload — and so the refetch
|
||||
* signature — untouched, so a fetched full history of that task is
|
||||
* reloaded here (bug audit 2026-09-26 #2). */
|
||||
private readonly _onHistorySaved = (ev: Event): void => {
|
||||
const d = (ev as CustomEvent<{ entry_id?: string; task_id?: string }>).detail;
|
||||
if (!d?.entry_id || !d.task_id || !(pastHistoryKey(d.entry_id, d.task_id) in this._pastHistory)) return;
|
||||
this._pastHistorySig = "";
|
||||
void this._loadPastHistories();
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("history-entry-saved", this._onHistorySaved);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("history-entry-saved", this._onHistorySaved);
|
||||
if (this._unsub) {
|
||||
try { this._unsub(); } catch { /* ignore */ }
|
||||
this._unsub = null;
|
||||
@@ -141,6 +169,9 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
super.updated(changedProps);
|
||||
syncLocaleFromHass(this, changedProps);
|
||||
if (this.hass && this._pastDays > 0 && (changedProps.has("_objects") || changedProps.has("_pastDays"))) {
|
||||
void this._loadPastHistories();
|
||||
}
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
if (!this._dataLoaded) {
|
||||
this._dataLoaded = true;
|
||||
@@ -177,6 +208,33 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch the full history of every task `pastHistoryGaps` names (refetch
|
||||
* only when that set or its entry counts change); only the newest round
|
||||
* may land. */
|
||||
private async _loadPastHistories(): Promise<void> {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const gaps = pastHistoryGaps(this._objects, today, this._pastDays || 30);
|
||||
const sig = gaps.map((g) => g.sig).join("|");
|
||||
if (sig === this._pastHistorySig) return;
|
||||
this._pastHistorySig = sig;
|
||||
const seq = ++this._pastSeq;
|
||||
const results = await Promise.all(gaps.map(async (g) => {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{ history?: HistoryEntryShape[] }>({
|
||||
type: "maintenance_supporter/task/history",
|
||||
entry_id: g.entryId,
|
||||
task_id: g.taskId,
|
||||
});
|
||||
return [g.key, r.history ?? []] as const;
|
||||
} catch {
|
||||
return null; // degrade to the listed window for this task
|
||||
}
|
||||
}));
|
||||
if (seq !== this._pastSeq) return;
|
||||
this._pastHistory = Object.fromEntries(results.filter((r): r is readonly [string, HistoryEntryShape[]] => r !== null));
|
||||
}
|
||||
|
||||
private async _subscribe(): Promise<void> {
|
||||
try {
|
||||
const unsub = await this.hass.connection.subscribeMessage(
|
||||
@@ -209,6 +267,10 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
void this._openHistoryEntry(ev);
|
||||
return;
|
||||
}
|
||||
this._openTask(ev);
|
||||
}
|
||||
|
||||
private _openTask(ev: CalendarEvent): void {
|
||||
if (openTaskQuickActions(ev.entry_id, ev.task_id)) return;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ll-custom", {
|
||||
@@ -226,6 +288,18 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
/** Fetch the recorded entry and open the history-edit dialog directly
|
||||
* (mirrors the strategy shim's ll-custom "edit-history" path). */
|
||||
private async _openHistoryEntry(ev: CalendarEvent): Promise<void> {
|
||||
// Editing a history entry is write tier: a household member without
|
||||
// write access gets the task's quick actions (read view) instead of an
|
||||
// editor whose Save the server refuses (bug audit 2026-09-27).
|
||||
try {
|
||||
const settings = await fetchSettingsOnce(this.hass);
|
||||
if (!canWrite(this.hass?.user, settings.access)) {
|
||||
this._openTask(ev);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* settings unavailable — keep the editor; the server still decides */
|
||||
}
|
||||
try {
|
||||
const draft = await loadHistoryEntryDraft(this.hass, ev.entry_id, ev.task_id, ev.history_timestamp!);
|
||||
if (!draft) return;
|
||||
@@ -294,7 +368,7 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const isPast = this._pastDays > 0;
|
||||
const buckets = isPast
|
||||
? buildPastBuckets(objects, today, this._pastDays, userFilter)
|
||||
? buildPastBuckets(objects, today, this._pastDays, userFilter, this._pastHistory)
|
||||
: buildCalendarBuckets(objects, today, this._windowDays, userFilter);
|
||||
|
||||
const todayIso = isoDateLocal(today);
|
||||
@@ -312,11 +386,15 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
const overdueLabel = ev.status === "overdue" && ev.days_until_due != null
|
||||
? ` (${formatDueDays(ev.days_until_due, L)})`
|
||||
: "";
|
||||
// Days read "every N days" (the calendar's own wording, singular for
|
||||
// 1); other units go through the shared interval formatter.
|
||||
const recurEvery = ev.projected && ev.interval_days
|
||||
? html`<span class="cal-event-recur">${
|
||||
ev.interval_unit && ev.interval_unit !== "days"
|
||||
? `${ev.interval_days} ${t("unit_" + ev.interval_unit, L)}`
|
||||
: t("cal_every_n_days", L).replace("{n}", String(ev.interval_days))
|
||||
? formatInterval(ev.interval_days, ev.interval_unit, L)
|
||||
: ev.interval_days === 1
|
||||
? t("cal_every_day", L)
|
||||
: t("cal_every_n_days", L).replace("{n}", String(ev.interval_days))
|
||||
}</span>`
|
||||
: nothing;
|
||||
const isSensor = ev.schedule_type === "sensor_based";
|
||||
|
||||
Reference in new issue
Block a user