329 files
This commit is contained in:
@@ -66,7 +66,7 @@ export async function mountPanel(
|
||||
objects: unknown[],
|
||||
extraHandlers: Record<string, WsHandler> = {},
|
||||
) {
|
||||
const { hass, sent } = createMockHass({
|
||||
const { hass, sent, subscriptions } = createMockHass({
|
||||
handlers: {
|
||||
"maintenance_supporter/objects": () => ({ objects }),
|
||||
"maintenance_supporter/statistics": () => ({
|
||||
@@ -97,7 +97,7 @@ export async function mountPanel(
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await el.updateComplete;
|
||||
return { el, sent };
|
||||
return { el, sent, subscriptions };
|
||||
}
|
||||
|
||||
export function sr(el: HTMLElement): ShadowRoot {
|
||||
|
||||
@@ -97,7 +97,10 @@ export type WsHandler = (msg: SentMessage) => Promise<unknown> | unknown;
|
||||
export interface CreateMockHassResult {
|
||||
hass: {
|
||||
language: string;
|
||||
connection: { sendMessagePromise: (msg: SentMessage) => Promise<unknown> };
|
||||
connection: {
|
||||
sendMessagePromise: (msg: SentMessage) => Promise<unknown>;
|
||||
subscribeMessage: (cb: (event: unknown) => void, msg: SentMessage) => Promise<() => void>;
|
||||
};
|
||||
callService: (
|
||||
domain: string, service: string,
|
||||
data?: Record<string, unknown>, target?: Record<string, unknown>,
|
||||
@@ -107,6 +110,8 @@ export interface CreateMockHassResult {
|
||||
};
|
||||
sent: SentMessage[];
|
||||
serviceCalls: ServiceCall[];
|
||||
/** Captured subscribeMessage registrations — push events via `.push(ev)`. */
|
||||
subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }>;
|
||||
}
|
||||
|
||||
export interface CreateMockHassOptions {
|
||||
@@ -157,15 +162,31 @@ export function createMockHass(opts: CreateMockHassOptions = {}): CreateMockHass
|
||||
serviceCalls.push({ domain, service, data, target });
|
||||
};
|
||||
|
||||
// Captured subscriptions: tests push events into components via
|
||||
// `subscriptions.find(...).push(event)`.
|
||||
const subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }> = [];
|
||||
const subscribeMessage = async (
|
||||
cb: (event: unknown) => void,
|
||||
msg: SentMessage,
|
||||
): Promise<() => void> => {
|
||||
const entry = { msg, push: (event: unknown) => cb(event) };
|
||||
subscriptions.push(entry);
|
||||
return () => {
|
||||
const i = subscriptions.indexOf(entry);
|
||||
if (i >= 0) subscriptions.splice(i, 1);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
hass: {
|
||||
language: opts.language ?? "en",
|
||||
connection: { sendMessagePromise },
|
||||
connection: { sendMessagePromise, subscribeMessage },
|
||||
callService,
|
||||
services: opts.services,
|
||||
states: opts.states,
|
||||
},
|
||||
sent,
|
||||
serviceCalls,
|
||||
subscriptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,31 @@ async function mount(canWrite: boolean): Promise<MaintenancePartsSection> {
|
||||
}
|
||||
|
||||
describe("parts-section", () => {
|
||||
it("shows the inventory value only when a part has price AND tracked stock (#104)", async () => {
|
||||
// PARTS as-is: p1 has stock but no cost, p2 no stock → chip hidden.
|
||||
const bare = await mount(false);
|
||||
expect(bare.shadowRoot!.querySelector(".inventory-value")).to.equal(null);
|
||||
|
||||
const priced: MaintenancePart[] = [
|
||||
{ id: "p1", name: "Filter", cost: 12.5, stock: 3, is_low: false },
|
||||
{ id: "p2", name: "Brush", cost: 4, stock: null, is_low: false }, // untracked → no contribution
|
||||
{ id: "p3", name: "Seal", cost: null, stock: 5, is_low: false }, // unpriced → no contribution
|
||||
];
|
||||
const el = await fixture<MaintenancePartsSection>(html`
|
||||
<maintenance-parts-section
|
||||
.hass=${{ language: "en", connection: { sendMessagePromise: async () => ({}) } } as never}
|
||||
.entryId=${"e1"}
|
||||
.parts=${priced}
|
||||
.currencySymbol=${"€"}
|
||||
></maintenance-parts-section>
|
||||
`);
|
||||
await el.updateComplete;
|
||||
const chip = el.shadowRoot!.querySelector(".inventory-value")!;
|
||||
expect(chip, "value chip rendered").to.exist;
|
||||
expect(chip.textContent).to.include("37.50");
|
||||
expect(chip.textContent).to.include("€");
|
||||
});
|
||||
|
||||
it("renders a row per part with stock badge, identifiers and location", async () => {
|
||||
const el = await mount(false);
|
||||
const rows = el.shadowRoot!.querySelectorAll(".part-row");
|
||||
|
||||
+54
@@ -57,6 +57,9 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
|
||||
taskId: "t1",
|
||||
objectName: "Pool Pump",
|
||||
objectDocUrl: null,
|
||||
objectManualDocs: [],
|
||||
openManualDoc: () => {},
|
||||
setChecklistItem: () => {},
|
||||
isOperator: false,
|
||||
actionLoading: false,
|
||||
moreMenuOpen: false,
|
||||
@@ -212,6 +215,57 @@ describe("task-detail renderer", () => {
|
||||
expect(host2.querySelector(".kpi-bar")).to.be.null;
|
||||
});
|
||||
|
||||
it("object-manual row falls back to an attached manual when the URL is empty", () => {
|
||||
let opened: unknown = null;
|
||||
const host = mount(task(), ctx({
|
||||
objectDocUrl: null,
|
||||
objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }],
|
||||
openManualDoc: (d) => { opened = d; },
|
||||
}));
|
||||
const link = [...host.querySelectorAll(".task-meta-link a")]
|
||||
.find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined;
|
||||
expect(link, "fallback manual link rendered").to.exist;
|
||||
expect(link!.getAttribute("title")).to.equal("Pump Handbook");
|
||||
link!.click();
|
||||
expect((opened as { id?: string })?.id).to.equal("d1");
|
||||
});
|
||||
|
||||
it("the URL field still wins over attached manuals", () => {
|
||||
const host = mount(task(), ctx({
|
||||
objectDocUrl: "https://vendor.example/manual",
|
||||
objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }],
|
||||
}));
|
||||
const link = [...host.querySelectorAll(".task-meta-link a")]
|
||||
.find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined;
|
||||
expect(link, "manual row rendered").to.exist;
|
||||
expect(link!.getAttribute("href")).to.equal("https://vendor.example/manual");
|
||||
});
|
||||
|
||||
it("checklist ticks render from progress and fire setChecklistItem (#73)", () => {
|
||||
const calls: Array<[string, boolean]> = [];
|
||||
const host = mount(
|
||||
task({ checklist: ["Drain", "Clean", "Refill"], checklist_progress: { Clean: true } }),
|
||||
ctx({
|
||||
features: {
|
||||
adaptive: false, predictions: false, seasonal: false,
|
||||
environmental: false, budget: false, groups: false,
|
||||
checklists: true, schedule_time: false, completion_actions: false,
|
||||
},
|
||||
setChecklistItem: (item, done) => calls.push([item, done]),
|
||||
}),
|
||||
);
|
||||
const header = host.querySelector(".checklist-preview-header")!;
|
||||
expect(header.textContent).to.include("1/3");
|
||||
const boxes = [...host.querySelectorAll<HTMLInputElement>(".checklist-preview-list input")];
|
||||
expect(boxes.length).to.equal(3);
|
||||
expect(boxes[1].checked).to.be.true;
|
||||
expect(boxes[0].checked).to.be.false;
|
||||
expect(host.querySelectorAll(".checklist-preview-list li.checked").length).to.equal(1);
|
||||
|
||||
boxes[0].click();
|
||||
expect(calls).to.deep.equal([["Drain", true]]);
|
||||
});
|
||||
|
||||
it("KPI bar shows warning days and currency symbol", () => {
|
||||
const host = mount(task(), ctx({ currencySymbol: "$" }));
|
||||
const kpi = host.querySelector(".kpi-bar")!;
|
||||
|
||||
+3
@@ -54,6 +54,9 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
|
||||
taskId: "t1",
|
||||
objectName: "Pool Pump",
|
||||
objectDocUrl: null,
|
||||
objectManualDocs: [],
|
||||
openManualDoc: () => {},
|
||||
setChecklistItem: () => {},
|
||||
isOperator: false,
|
||||
actionLoading: false,
|
||||
moreMenuOpen: false,
|
||||
|
||||
+333
-11
@@ -18,10 +18,35 @@ interface BatteryRow {
|
||||
level: number | null;
|
||||
days_until: number | null;
|
||||
available?: boolean;
|
||||
/** Where the ~date comes from: "trend" (discharge regression) or "typical"
|
||||
* (type-lifetime table). */
|
||||
predicted_source?: "trend" | "typical";
|
||||
prediction_confidence?: "medium" | "high" | null;
|
||||
/** Charged, never bought — the row never feeds the shopping groupings and
|
||||
* a ~date only appears when the discharge trend earned one. */
|
||||
rechargeable?: boolean;
|
||||
/** This battery's own low threshold (Battery Notes' configured value or
|
||||
* the fleet floor, whichever is higher) — the level bar colors against
|
||||
* it, not against a fixed 20 %. */
|
||||
low_threshold?: number;
|
||||
}
|
||||
interface RosterRow extends BatteryRow {
|
||||
status: "low" | "soon" | "ok";
|
||||
}
|
||||
/** 30 d downsampled level history per battery, for the roster sparklines.
|
||||
* threshold = the same low threshold the trend forecast regresses toward,
|
||||
* so the dotted projection ends exactly where the ~date comes from.
|
||||
* jump = an upward step that looks like a swap nobody recorded in Battery
|
||||
* Notes (the forecast still anchors on the dead battery's date) — with the
|
||||
* device to call `battery_notes.set_battery_replaced` on. */
|
||||
type HistorySeries = Record<
|
||||
string,
|
||||
{
|
||||
points: [number, number][];
|
||||
threshold: number;
|
||||
jump?: { at: number; from: number; to: number; device_id: string };
|
||||
}
|
||||
>;
|
||||
interface Overview {
|
||||
available: boolean;
|
||||
configured: boolean;
|
||||
@@ -45,6 +70,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
@state() private _loading = false;
|
||||
@state() private _marking = false;
|
||||
@state() private _error = "";
|
||||
@state() private _history: HistorySeries | null = null;
|
||||
// Urgency by default (issue #123: "soon sat in the middle of the list");
|
||||
// the choice is remembered per browser.
|
||||
@state() private _rosterSort: "name" | "urgency" = MaintenanceBatteryFleetSection._storedSort();
|
||||
@state() private _typeFilter: string | null = null;
|
||||
@state() private _recorded: string[] = [];
|
||||
private _historyRequested = false;
|
||||
private _localeReady = false;
|
||||
|
||||
private get _lang(): string {
|
||||
@@ -139,20 +171,165 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazy: the recorder-backed history is fetched once, when the roster is
|
||||
* first expanded — most panel visits never open it. */
|
||||
private _loadHistory = async (e: Event): Promise<void> => {
|
||||
if (!(e.target as HTMLDetailsElement).open || this._historyRequested) return;
|
||||
this._historyRequested = true;
|
||||
try {
|
||||
const res = await this.hass.connection.sendMessagePromise<{ series: HistorySeries }>({
|
||||
type: "maintenance_supporter/battery_fleet/overview_history",
|
||||
});
|
||||
this._history = res.series;
|
||||
} catch {
|
||||
this._history = null; // sparklines are an enhancement — rows render without them
|
||||
}
|
||||
};
|
||||
|
||||
/** Inline-SVG sparkline: 30 d level line, a faint threshold line, and —
|
||||
* where the ~date comes from the discharge trend — a dotted projection
|
||||
* from the last reading down to the threshold, so the date is visible
|
||||
* instead of merely stated. */
|
||||
private _sparkline(b: RosterRow) {
|
||||
const h = this._history?.[b.entity_id];
|
||||
if (!h || h.points.length < 2) return nothing;
|
||||
const W = 110, H = 24, P = 2;
|
||||
const t0 = h.points[0][0];
|
||||
const tLast = h.points[h.points.length - 1][0];
|
||||
const nowSec = Date.now() / 1000;
|
||||
const projEnd =
|
||||
b.status !== "low" && b.predicted_source === "trend" && b.days_until != null
|
||||
? nowSec + b.days_until * 86400
|
||||
: null;
|
||||
const tMax = Math.max(tLast, projEnd ?? tLast);
|
||||
const x = (t: number) => (tMax === t0 ? P : P + ((t - t0) / (tMax - t0)) * (W - 2 * P));
|
||||
const y = (v: number) => P + (1 - Math.min(100, Math.max(0, v)) / 100) * (H - 2 * P);
|
||||
const line = h.points.map(([t, v]) => `${x(t).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||||
const vLast = h.points[h.points.length - 1][1];
|
||||
const yTh = y(h.threshold).toFixed(1);
|
||||
return html`<svg
|
||||
class="bf-spark"
|
||||
viewBox="0 0 ${W} ${H}"
|
||||
role="img"
|
||||
aria-label=${t("battery_fleet_sparkline_hint", this._lang)}
|
||||
>
|
||||
<title>${t("battery_fleet_sparkline_hint", this._lang)}</title>
|
||||
<line class="bf-spark-th" x1="0" y1=${yTh} x2=${W} y2=${yTh}></line>
|
||||
<polyline class="bf-spark-line" points=${line}></polyline>
|
||||
${projEnd !== null
|
||||
? html`<line
|
||||
class="bf-spark-proj"
|
||||
x1=${x(tLast).toFixed(1)}
|
||||
y1=${y(vLast).toFixed(1)}
|
||||
x2=${x(projEnd).toFixed(1)}
|
||||
y2=${yTh}
|
||||
></line>`
|
||||
: nothing}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
private static readonly _SORT_KEY = "ms_bf_roster_sort";
|
||||
|
||||
private static _storedSort(): "name" | "urgency" {
|
||||
try {
|
||||
const v = localStorage.getItem(MaintenanceBatteryFleetSection._SORT_KEY);
|
||||
return v === "name" ? "name" : "urgency";
|
||||
} catch {
|
||||
return "urgency";
|
||||
}
|
||||
}
|
||||
|
||||
private _setSort(mode: "name" | "urgency"): void {
|
||||
this._rosterSort = mode;
|
||||
try {
|
||||
localStorage.setItem(MaintenanceBatteryFleetSection._SORT_KEY, mode);
|
||||
} catch {
|
||||
// storage unavailable — the toggle still works for this visit
|
||||
}
|
||||
}
|
||||
|
||||
/** Urgency (the default, issue #123): low rows first — emptiest first —
|
||||
* then the soonest forecast, dateless rows last. Name mode keeps the
|
||||
* alphabetical lookup list. */
|
||||
private _sortedRoster(rows: RosterRow[]): RosterRow[] {
|
||||
const filtered = this._typeFilter === null ? rows : rows.filter((r) => r.battery_type === this._typeFilter);
|
||||
if (this._rosterSort === "name") return filtered;
|
||||
// Low rows rank far below everything and among themselves by LEVEL
|
||||
// ascending (a 6 % battery before an 18 % one); the rest by days-until.
|
||||
const rank = (r: RosterRow) => (r.status === "low" ? -1000 + (r.level ?? 101) / 101 : (r.days_until ?? Infinity));
|
||||
return [...filtered].sort(
|
||||
(a, b) => rank(a) - rank(b) || a.device_name.localeCompare(b.device_name),
|
||||
);
|
||||
}
|
||||
|
||||
/** The forecast as a date a person can plan with, not a day count.
|
||||
* `days_until` comes from last-replaced + typical lifetime, so it is an
|
||||
* estimate — the tilde in the template says so. Negative values (past the
|
||||
* typical lifetime but not reported low yet) render as past dates, which
|
||||
* is honest: the battery is living on borrowed time. */
|
||||
private _predictedDate(daysUntil: number): string {
|
||||
const when = new Date(Date.now() + daysUntil * 864e5);
|
||||
return new Intl.DateTimeFormat(this._lang, { day: "numeric", month: "numeric", year: "numeric" }).format(when);
|
||||
return this._fmtDate(Date.now() + daysUntil * 864e5);
|
||||
}
|
||||
|
||||
private _shoppingLine(needs: Record<string, number>): string {
|
||||
return Object.entries(needs)
|
||||
.map(([type, qty]) => `${qty}× ${type}`)
|
||||
.join(" · ");
|
||||
private _fmtDate(epochMs: number): string {
|
||||
return new Intl.DateTimeFormat(this._lang, { day: "numeric", month: "numeric", year: "numeric" }).format(new Date(epochMs));
|
||||
}
|
||||
|
||||
/** The grouped shopping quantities as CLICKABLE chips: a type filters the
|
||||
* roster to the devices that need it — "which devices need those 4× AAA?"
|
||||
* without scanning. Clicking the active chip clears the filter. */
|
||||
private _shoppingLine(needs: Record<string, number>) {
|
||||
return Object.entries(needs).map(
|
||||
([type, qty]) => html`<button
|
||||
class="bf-type-chip ${this._typeFilter === type ? "bf-type-chip-active" : ""}"
|
||||
title=${t("battery_fleet_filter_type", this._lang)}
|
||||
@click=${() => this._toggleTypeFilter(type)}
|
||||
>
|
||||
${qty}× ${type}
|
||||
</button>`,
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleTypeFilter(type: string): void {
|
||||
this._typeFilter = this._typeFilter === type ? null : type;
|
||||
if (this._typeFilter !== null) {
|
||||
const details = this.shadowRoot?.querySelector<HTMLDetailsElement>("details.bf-roster");
|
||||
if (details && !details.open) details.open = true; // fires toggle → history loads
|
||||
}
|
||||
}
|
||||
|
||||
/** One-click fix for a detected-but-unrecorded swap: record the DETECTED
|
||||
* jump time in Battery Notes, so the forecast re-anchors on the real
|
||||
* replacement instead of the dead battery's date. */
|
||||
private async _recordJump(entityId: string, jump: { at: number; device_id: string }): Promise<void> {
|
||||
if (this._marking) return;
|
||||
this._marking = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.callService("battery_notes", "set_battery_replaced", {
|
||||
device_id: jump.device_id,
|
||||
datetime_replaced: new Date(jump.at * 1000).toISOString(),
|
||||
});
|
||||
this._recorded = [...this._recorded, entityId];
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._marking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Purely visual level bar next to the number — scannable at a glance.
|
||||
* Colored against the battery's OWN low threshold: red at/below it,
|
||||
* amber inside a 20-point approach band, green above. */
|
||||
private _levelBar(b: BatteryRow) {
|
||||
const level = b.level;
|
||||
if (level == null) return nothing;
|
||||
const t = b.low_threshold ?? 20;
|
||||
const cls = level <= t ? "bad" : level <= t + 20 ? "warn" : "good";
|
||||
return html`<span class="bf-bar" aria-hidden="true"
|
||||
><span class="bf-bar-fill bf-bar-${cls}" style="width: ${Math.min(100, Math.max(0, level))}%"></span
|
||||
></span>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -199,10 +376,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
? html`<span class="bf-offline">${t("battery_fleet_offline", L)}</span>`
|
||||
: nothing}
|
||||
<span class="bf-type">${b.quantity}× ${b.battery_type}</span>
|
||||
${b.rechargeable
|
||||
? html`<span class="bf-recharge" title=${t("battery_fleet_rechargeable", L)}
|
||||
><ha-icon icon="mdi:battery-charging-outline"></ha-icon
|
||||
></span>`
|
||||
: nothing}
|
||||
${this._levelBar(b)}
|
||||
${b.level != null ? html`<span class="bf-level">${b.level}%</span>` : nothing}
|
||||
<button
|
||||
class="bf-mark"
|
||||
title=${t("battery_fleet_mark_one", L)}
|
||||
title=${b.rechargeable ? t("battery_fleet_mark_recharged", L) : t("battery_fleet_mark_one", L)}
|
||||
.disabled=${this._marking}
|
||||
@click=${() => this._mark([b.entity_id])}
|
||||
>
|
||||
@@ -238,20 +421,57 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
: nothing}
|
||||
${ov.all?.length
|
||||
? html`
|
||||
<details class="bf-roster">
|
||||
<details class="bf-roster" @toggle=${this._loadHistory}>
|
||||
<summary>${t("battery_fleet_all", L)} (${ov.all.length})</summary>
|
||||
<div class="bf-roster-tools">
|
||||
<button
|
||||
class="bf-sort ${this._rosterSort === "urgency" ? "bf-sort-active" : ""}"
|
||||
@click=${() => this._setSort("urgency")}
|
||||
>
|
||||
${t("battery_fleet_sort_urgency", L)}
|
||||
</button>
|
||||
<button
|
||||
class="bf-sort ${this._rosterSort === "name" ? "bf-sort-active" : ""}"
|
||||
@click=${() => this._setSort("name")}
|
||||
>
|
||||
${t("battery_fleet_sort_name", L)}
|
||||
</button>
|
||||
</div>
|
||||
<div class="bf-rows">
|
||||
${ov.all.map(
|
||||
${this._sortedRoster(ov.all).map(
|
||||
(b) => html`
|
||||
<div class="bf-row">
|
||||
<span class="bf-dev">${b.device_name}</span>
|
||||
<span class="bf-status bf-${b.status}">${t("battery_fleet_status_" + b.status, L)}</span>
|
||||
<span class="bf-type">${b.quantity}× ${b.battery_type}</span>
|
||||
${b.rechargeable
|
||||
? html`<span class="bf-recharge" title=${t("battery_fleet_rechargeable", L)}
|
||||
><ha-icon icon="mdi:battery-charging-outline"></ha-icon
|
||||
></span>`
|
||||
: nothing}
|
||||
${this._sparkline(b)}
|
||||
${this._levelBar(b)}
|
||||
${b.level != null ? html`<span class="bf-level">${b.level}%</span>` : nothing}
|
||||
${(() => {
|
||||
const jump = this._history?.[b.entity_id]?.jump;
|
||||
if (!jump || this._recorded.includes(b.entity_id)) return nothing;
|
||||
return html`<button
|
||||
class="bf-mark bf-jump"
|
||||
title=${t("battery_fleet_record_replacement", L).replace("{date}", this._fmtDate(jump.at * 1000))}
|
||||
.disabled=${this._marking}
|
||||
@click=${() => this._recordJump(b.entity_id, jump)}
|
||||
>
|
||||
<ha-icon icon="mdi:calendar-sync"></ha-icon>
|
||||
</button>`;
|
||||
})()}
|
||||
${b.days_until != null
|
||||
? html`<span
|
||||
class="bf-predicted"
|
||||
title=${t("battery_fleet_predicted_on", L).replace("{date}", this._predictedDate(b.days_until))}
|
||||
class="bf-predicted ${b.predicted_source === "trend" ? "bf-trend" : ""}"
|
||||
title=${b.predicted_source === "trend"
|
||||
? t("battery_fleet_predicted_trend", L)
|
||||
.replace("{date}", this._predictedDate(b.days_until))
|
||||
.replace("{confidence}", t("cal_confidence_" + (b.prediction_confidence || "medium"), L))
|
||||
: t("battery_fleet_predicted_on", L).replace("{date}", this._predictedDate(b.days_until))}
|
||||
>~${this._predictedDate(b.days_until)}</span
|
||||
>`
|
||||
: nothing}
|
||||
@@ -385,6 +605,102 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.bf-recharge {
|
||||
color: var(--secondary-text-color);
|
||||
display: inline-flex;
|
||||
cursor: help;
|
||||
}
|
||||
.bf-recharge ha-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.bf-spark {
|
||||
width: 110px;
|
||||
height: 24px;
|
||||
flex: 0 0 auto;
|
||||
cursor: help;
|
||||
}
|
||||
/* On phones the row cannot fit name + chips + curve + bar + date: the
|
||||
* decorations yield (the percentage still carries the number). */
|
||||
@media (max-width: 640px) {
|
||||
.bf-spark,
|
||||
.bf-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.bf-spark-line {
|
||||
fill: none;
|
||||
stroke: var(--primary-color);
|
||||
stroke-width: 1.5;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.bf-spark-proj {
|
||||
stroke: var(--primary-color);
|
||||
stroke-width: 1.2;
|
||||
stroke-dasharray: 2 3;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.bf-spark-th {
|
||||
stroke: var(--error-color, #f44336);
|
||||
stroke-width: 1;
|
||||
opacity: 0.35;
|
||||
}
|
||||
.bf-type-chip {
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 10px;
|
||||
padding: 1px 8px;
|
||||
margin: 0 4px 2px 0;
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bf-type-chip-active {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.bf-bar {
|
||||
width: 30px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--divider-color);
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.bf-bar-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.bf-bar-good {
|
||||
background: var(--success-color, #4caf50);
|
||||
}
|
||||
.bf-bar-warn {
|
||||
background: var(--warning-color, #ff9800);
|
||||
}
|
||||
.bf-bar-bad {
|
||||
background: var(--error-color, #f44336);
|
||||
}
|
||||
.bf-jump ha-icon {
|
||||
color: var(--warning-color, #ff9800);
|
||||
}
|
||||
.bf-roster-tools {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin: 8px 0 2px;
|
||||
}
|
||||
.bf-sort {
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 12px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bf-sort-active {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.bf-level {
|
||||
font-size: 12px;
|
||||
color: var(--error-color, #f44336);
|
||||
@@ -448,6 +764,12 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Trend-based dates (discharge regression) get a dotted underline — the
|
||||
tooltip carries source + confidence. */
|
||||
.bf-predicted.bf-trend {
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.bf-total {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -52,6 +52,10 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
* objects can carry the same part id, so part_id alone would merge pools. */
|
||||
@state() private _usedParts: Record<string, TaskPartLink> = {};
|
||||
|
||||
/** #73: in-cycle ticks (keyed by item TEXT) recorded on the task detail —
|
||||
* prefill the dialog so nobody re-ticks what is already done. */
|
||||
@property({ attribute: false }) public checklistPrefill: Record<string, boolean> = {};
|
||||
|
||||
public open(): void {
|
||||
if (this._open) return;
|
||||
this._open = true;
|
||||
@@ -59,7 +63,13 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
this._cost = "";
|
||||
this._duration = "";
|
||||
this._error = "";
|
||||
this._checklistState = {};
|
||||
// The dialog's own state is INDEX-keyed (historical shape, flows into the
|
||||
// history entry as-is) — map the text-keyed in-cycle ticks onto indices.
|
||||
this._checklistState = Object.fromEntries(
|
||||
this.checklist
|
||||
.map((item, i) => [String(i), !!this.checklistPrefill[item]] as const)
|
||||
.filter(([, done]) => done),
|
||||
);
|
||||
this._feedback = "needed";
|
||||
this._photoDocId = "";
|
||||
this._photoPreview = "";
|
||||
|
||||
@@ -59,6 +59,7 @@ export class MaintenancePartsSection extends LitElement {
|
||||
@property({ attribute: false }) public entryId!: string;
|
||||
@property({ attribute: false }) public parts: MaintenancePart[] = [];
|
||||
@property({ type: Boolean }) public canWrite = false;
|
||||
@property({ attribute: false }) public currencySymbol = "€";
|
||||
|
||||
@state() private _editing: PartForm | null = null;
|
||||
@state() private _busy = false;
|
||||
@@ -335,6 +336,19 @@ export class MaintenancePartsSection extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** Inventory value = Σ unit cost × tracked stock (#104). Parts without a
|
||||
* price or without tracked stock contribute nothing; null = no part has
|
||||
* both, so the chip stays hidden rather than showing a misleading 0. */
|
||||
private _inventoryValue(): number | null {
|
||||
let sum = 0, any = false;
|
||||
for (const p of this.parts) {
|
||||
const cost = typeof p.cost === "number" ? p.cost : null;
|
||||
const stock = typeof p.stock === "number" ? p.stock : null;
|
||||
if (cost !== null && stock !== null) { sum += cost * stock; any = true; }
|
||||
}
|
||||
return any ? sum : null;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const L = this._lang;
|
||||
if (!this.parts.length && !this.canWrite) return nothing;
|
||||
@@ -343,6 +357,11 @@ export class MaintenancePartsSection extends LitElement {
|
||||
<h3>
|
||||
<ha-icon icon="mdi:package-variant"></ha-icon>
|
||||
${t("parts_section", L)} (${this.parts.length})
|
||||
${this._inventoryValue() !== null
|
||||
? html`<span class="inventory-value" title=${t("parts_inventory_value", L)}
|
||||
>${t("parts_inventory_value", L)}:
|
||||
${this._inventoryValue()!.toFixed(2)} ${this.currencySymbol}</span>`
|
||||
: nothing}
|
||||
</h3>
|
||||
${this.canWrite && !this._editing
|
||||
? html`<ha-button appearance="plain" @click=${() => this._openAdd()}>
|
||||
@@ -361,6 +380,13 @@ export class MaintenancePartsSection extends LitElement {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.inventory-value {
|
||||
margin-left: 8px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 400;
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Počet opakování",
|
||||
"series_end_until_label": "Datum konce",
|
||||
"parts_section": "Díly a spotřební materiál",
|
||||
"parts_inventory_value": "Hodnota zásob",
|
||||
"part_add": "Přidat díl",
|
||||
"part_name": "Název",
|
||||
"part_vendor": "Výrobce",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Brzy",
|
||||
"battery_fleet_status_ok": "V pořádku",
|
||||
"battery_fleet_predicted_on": "Očekáváno kolem {date}",
|
||||
"battery_fleet_predicted_trend": "Předpověď z trendu vybíjení této baterie: přibližně {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulátor: nabíjí se místo výměny — nikdy na nákupním seznamu",
|
||||
"battery_fleet_sort_name": "Řadit podle názvu",
|
||||
"battery_fleet_sort_urgency": "Řadit podle naléhavosti",
|
||||
"battery_fleet_mark_recharged": "Označit jako nabitou",
|
||||
"battery_fleet_sparkline_hint": "Stav baterie za posledních 30 dní — tečkovaně: projekce k prahu vybití",
|
||||
"battery_fleet_filter_type": "Zobrazit pouze tento typ baterie",
|
||||
"battery_fleet_record_replacement": "Stav poskočil kolem {date} — zaznamenat tuto výměnu do Battery Notes",
|
||||
"battery_fleet_total": "Sledováno baterií: {n}",
|
||||
"battery_fleet_setup_button": "Flotila baterií",
|
||||
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Antal gange",
|
||||
"series_end_until_label": "Slutdato",
|
||||
"parts_section": "Dele & forbrugsvarer",
|
||||
"parts_inventory_value": "Lagerværdi",
|
||||
"part_add": "Tilføj del",
|
||||
"part_name": "Navn",
|
||||
"part_vendor": "Producent",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_predicted_on": "Forventes omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Forudsagt ud fra batteriets afladningstendens: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Genopladeligt: oplad i stedet for at udskifte — aldrig på indkøbslisten",
|
||||
"battery_fleet_sort_name": "Sortér efter navn",
|
||||
"battery_fleet_sort_urgency": "Sortér efter hastende grad",
|
||||
"battery_fleet_mark_recharged": "Markér som genopladet",
|
||||
"battery_fleet_sparkline_hint": "Batteriniveau de seneste 30 dage — stiplet: fremskrivning ned til lav-tærsklen",
|
||||
"battery_fleet_filter_type": "Vis kun denne batteritype",
|
||||
"battery_fleet_record_replacement": "Niveauet sprang omkring {date} — registrér denne udskiftning i Battery Notes",
|
||||
"battery_fleet_total": "{n} batterier overvåges",
|
||||
"battery_fleet_setup_button": "Batteriflåde",
|
||||
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Anzahl",
|
||||
"series_end_until_label": "Enddatum",
|
||||
"parts_section": "Teile & Verbrauchsmaterial",
|
||||
"parts_inventory_value": "Lagerwert",
|
||||
"part_add": "Teil hinzufügen",
|
||||
"part_name": "Name",
|
||||
"part_vendor": "Hersteller",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Bald",
|
||||
"battery_fleet_status_ok": "In Ordnung",
|
||||
"battery_fleet_predicted_on": "Voraussichtlich um {date}",
|
||||
"battery_fleet_predicted_trend": "Aus dem Entladetrend dieser Batterie vorhergesagt: etwa {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Wiederaufladbar: laden statt ersetzen — nie auf der Einkaufsliste",
|
||||
"battery_fleet_sort_name": "Nach Name sortieren",
|
||||
"battery_fleet_sort_urgency": "Nach Dringlichkeit sortieren",
|
||||
"battery_fleet_mark_recharged": "Als aufgeladen markieren",
|
||||
"battery_fleet_sparkline_hint": "Batteriestand der letzten 30 Tage — gepunktet: Prognose bis zur Low-Schwelle",
|
||||
"battery_fleet_filter_type": "Nur diesen Batterietyp anzeigen",
|
||||
"battery_fleet_record_replacement": "Der Batteriestand ist um den {date} gesprungen — diesen Wechsel in Battery Notes nachtragen",
|
||||
"battery_fleet_total": "{n} Batterien überwacht",
|
||||
"battery_fleet_setup_button": "Batterie-Flotte",
|
||||
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
"series_end_count_label": "Number of times",
|
||||
"series_end_until_label": "End date",
|
||||
"parts_section": "Parts & consumables",
|
||||
"parts_inventory_value": "Inventory value",
|
||||
"part_add": "Add part",
|
||||
"part_name": "Name",
|
||||
"part_vendor": "Manufacturer",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Soon",
|
||||
"battery_fleet_status_ok": "Healthy",
|
||||
"battery_fleet_predicted_on": "Expected around {date}",
|
||||
"battery_fleet_predicted_trend": "Predicted from this battery's discharge trend: around {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable: charge instead of replacing — never on the shopping list",
|
||||
"battery_fleet_sort_name": "Sort by name",
|
||||
"battery_fleet_sort_urgency": "Sort by urgency",
|
||||
"battery_fleet_mark_recharged": "Mark as recharged",
|
||||
"battery_fleet_sparkline_hint": "Battery level over the last 30 days — dotted: projected until the low threshold",
|
||||
"battery_fleet_filter_type": "Show only this battery type",
|
||||
"battery_fleet_record_replacement": "The level jumped around {date} — record this replacement in Battery Notes",
|
||||
"battery_fleet_total": "{n} batteries tracked",
|
||||
"battery_fleet_setup_button": "Battery fleet",
|
||||
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Número de veces",
|
||||
"series_end_until_label": "Fecha de fin",
|
||||
"parts_section": "Piezas y consumibles",
|
||||
"parts_inventory_value": "Valor del inventario",
|
||||
"part_add": "Añadir pieza",
|
||||
"part_name": "Nombre",
|
||||
"part_vendor": "Fabricante",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Pronto",
|
||||
"battery_fleet_status_ok": "Correcta",
|
||||
"battery_fleet_predicted_on": "Previsto hacia {date}",
|
||||
"battery_fleet_predicted_trend": "Predicción según la tendencia de descarga de esta pila: hacia {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recargable: se recarga en lugar de sustituirse — nunca en la lista de compra",
|
||||
"battery_fleet_sort_name": "Ordenar por nombre",
|
||||
"battery_fleet_sort_urgency": "Ordenar por urgencia",
|
||||
"battery_fleet_mark_recharged": "Marcar como recargada",
|
||||
"battery_fleet_sparkline_hint": "Nivel de batería de los últimos 30 días — punteado: proyección hasta el umbral bajo",
|
||||
"battery_fleet_filter_type": "Mostrar solo este tipo de pila",
|
||||
"battery_fleet_record_replacement": "El nivel dio un salto hacia el {date} — registrar esta sustitución en Battery Notes",
|
||||
"battery_fleet_total": "{n} baterías monitorizadas",
|
||||
"battery_fleet_setup_button": "Flota de baterías",
|
||||
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Kertojen määrä",
|
||||
"series_end_until_label": "Päättymispäivä",
|
||||
"parts_section": "Osat ja tarvikkeet",
|
||||
"parts_inventory_value": "Varaston arvo",
|
||||
"part_add": "Lisää osa",
|
||||
"part_name": "Nimi",
|
||||
"part_vendor": "Valmistaja",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Pian",
|
||||
"battery_fleet_status_ok": "Kunnossa",
|
||||
"battery_fleet_predicted_on": "Odotettavissa noin {date}",
|
||||
"battery_fleet_predicted_trend": "Ennustettu tämän pariston purkautumistrendistä: noin {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ladattava: lataa vaihtamisen sijaan — ei koskaan ostoslistalle",
|
||||
"battery_fleet_sort_name": "Lajittele nimen mukaan",
|
||||
"battery_fleet_sort_urgency": "Lajittele kiireellisyyden mukaan",
|
||||
"battery_fleet_mark_recharged": "Merkitse ladatuksi",
|
||||
"battery_fleet_sparkline_hint": "Akun varaustaso viimeisten 30 päivän ajalta — pisteviiva: ennuste alarajaan asti",
|
||||
"battery_fleet_filter_type": "Näytä vain tämä paristotyyppi",
|
||||
"battery_fleet_record_replacement": "Varaustaso hyppäsi noin {date} — kirjaa tämä vaihto Battery Notesiin",
|
||||
"battery_fleet_total": "{n} paristoa seurannassa",
|
||||
"battery_fleet_setup_button": "Paristokanta",
|
||||
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Nombre de fois",
|
||||
"series_end_until_label": "Date de fin",
|
||||
"parts_section": "Pièces & consommables",
|
||||
"parts_inventory_value": "Valeur du stock",
|
||||
"part_add": "Ajouter une pièce",
|
||||
"part_name": "Nom",
|
||||
"part_vendor": "Fabricant",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Bientôt",
|
||||
"battery_fleet_status_ok": "Bon état",
|
||||
"battery_fleet_predicted_on": "Prévu vers {date}",
|
||||
"battery_fleet_predicted_trend": "Prédit à partir de la tendance de décharge de cette pile : vers {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable : à recharger plutôt qu'à remplacer — jamais sur la liste d'achats",
|
||||
"battery_fleet_sort_name": "Trier par nom",
|
||||
"battery_fleet_sort_urgency": "Trier par urgence",
|
||||
"battery_fleet_mark_recharged": "Marquer comme rechargée",
|
||||
"battery_fleet_sparkline_hint": "Niveau de batterie des 30 derniers jours — pointillé : projection jusqu'au seuil bas",
|
||||
"battery_fleet_filter_type": "Afficher uniquement ce type de pile",
|
||||
"battery_fleet_record_replacement": "Le niveau a bondi vers le {date} — enregistrer ce remplacement dans Battery Notes",
|
||||
"battery_fleet_total": "{n} piles suivies",
|
||||
"battery_fleet_setup_button": "Parc de piles",
|
||||
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "कितनी बार",
|
||||
"series_end_until_label": "समाप्ति तिथि",
|
||||
"parts_section": "पुर्ज़े और उपभोग्य",
|
||||
"parts_inventory_value": "इन्वेंट्री मूल्य",
|
||||
"part_add": "पुर्ज़ा जोड़ें",
|
||||
"part_name": "नाम",
|
||||
"part_vendor": "निर्माता",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "जल्द",
|
||||
"battery_fleet_status_ok": "ठीक",
|
||||
"battery_fleet_predicted_on": "{date} के आसपास अपेक्षित",
|
||||
"battery_fleet_predicted_trend": "इस बैटरी के डिस्चार्ज रुझान से अनुमानित: लगभग {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "रिचार्जेबल: बदलने के बजाय चार्ज करें — खरीदारी सूची में कभी नहीं",
|
||||
"battery_fleet_sort_name": "नाम के अनुसार क्रमबद्ध करें",
|
||||
"battery_fleet_sort_urgency": "तात्कालिकता के अनुसार क्रमबद्ध करें",
|
||||
"battery_fleet_mark_recharged": "रिचार्ज हो गई के रूप में चिह्नित करें",
|
||||
"battery_fleet_sparkline_hint": "पिछले 30 दिनों का बैटरी स्तर — बिंदीदार: लो थ्रेशोल्ड तक का अनुमान",
|
||||
"battery_fleet_filter_type": "केवल यही बैटरी प्रकार दिखाएँ",
|
||||
"battery_fleet_record_replacement": "स्तर लगभग {date} को उछला — इस बदलाव को Battery Notes में दर्ज करें",
|
||||
"battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं",
|
||||
"battery_fleet_setup_button": "बैटरी फ्लीट",
|
||||
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
"series_end_count_label": "Alkalmak száma",
|
||||
"series_end_until_label": "Befejezés dátuma",
|
||||
"parts_section": "Alkatrészek és fogyóeszközök",
|
||||
"parts_inventory_value": "Készletérték",
|
||||
"part_add": "Alkatrész hozzáadása",
|
||||
"part_name": "Név",
|
||||
"part_vendor": "Gyártó",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Hamarosan",
|
||||
"battery_fleet_status_ok": "Rendben",
|
||||
"battery_fleet_predicted_on": "Várhatóan {date} körül",
|
||||
"battery_fleet_predicted_trend": "Az elem merülési trendjéből előrejelezve: kb. {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Újratölthető: cserélés helyett töltse fel — soha nem kerül a bevásárlólistára",
|
||||
"battery_fleet_sort_name": "Rendezés név szerint",
|
||||
"battery_fleet_sort_urgency": "Rendezés sürgősség szerint",
|
||||
"battery_fleet_mark_recharged": "Megjelölés feltöltöttként",
|
||||
"battery_fleet_sparkline_hint": "Akkumulátorszint az elmúlt 30 napban — pontozott: előrejelzés az alacsony küszöbig",
|
||||
"battery_fleet_filter_type": "Csak ez az elemtípus megjelenítése",
|
||||
"battery_fleet_record_replacement": "A szint {date} körül megugrott — rögzítse ezt a cserét a Battery Notes-ban",
|
||||
"battery_fleet_total": "{n} elem követve",
|
||||
"battery_fleet_setup_button": "Elemflotta",
|
||||
"battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Numero di volte",
|
||||
"series_end_until_label": "Data di fine",
|
||||
"parts_section": "Ricambi e consumabili",
|
||||
"parts_inventory_value": "Valore delle scorte",
|
||||
"part_add": "Aggiungi ricambio",
|
||||
"part_name": "Nome",
|
||||
"part_vendor": "Produttore",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "A breve",
|
||||
"battery_fleet_status_ok": "A posto",
|
||||
"battery_fleet_predicted_on": "Previsto intorno al {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto dalla tendenza di scarica di questa batteria: intorno al {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ricaricabile: da ricaricare anziché sostituire — mai nella lista della spesa",
|
||||
"battery_fleet_sort_name": "Ordina per nome",
|
||||
"battery_fleet_sort_urgency": "Ordina per urgenza",
|
||||
"battery_fleet_mark_recharged": "Segna come ricaricata",
|
||||
"battery_fleet_sparkline_hint": "Livello batteria degli ultimi 30 giorni — tratteggiato: proiezione fino alla soglia di batteria scarica",
|
||||
"battery_fleet_filter_type": "Mostra solo questo tipo di batteria",
|
||||
"battery_fleet_record_replacement": "Il livello è balzato intorno al {date} — registra questa sostituzione in Battery Notes",
|
||||
"battery_fleet_total": "{n} batterie monitorate",
|
||||
"battery_fleet_setup_button": "Parco batterie",
|
||||
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "回数",
|
||||
"series_end_until_label": "終了日",
|
||||
"parts_section": "部品・消耗品",
|
||||
"parts_inventory_value": "在庫金額",
|
||||
"part_add": "部品を追加",
|
||||
"part_name": "名前",
|
||||
"part_vendor": "メーカー",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "まもなく",
|
||||
"battery_fleet_status_ok": "良好",
|
||||
"battery_fleet_predicted_on": "{date} 頃の見込み",
|
||||
"battery_fleet_predicted_trend": "この電池の放電傾向から予測: {date} 頃 ({confidence})",
|
||||
"battery_fleet_rechargeable": "充電式:交換ではなく充電——買い物リストには載りません",
|
||||
"battery_fleet_sort_name": "名前順に並べ替え",
|
||||
"battery_fleet_sort_urgency": "緊急度順に並べ替え",
|
||||
"battery_fleet_mark_recharged": "充電済みにする",
|
||||
"battery_fleet_sparkline_hint": "過去30日間のバッテリー残量——点線:低残量しきい値までの予測",
|
||||
"battery_fleet_filter_type": "この電池タイプのみ表示",
|
||||
"battery_fleet_record_replacement": "{date}頃に残量が急上昇——この交換をBattery Notesに記録する",
|
||||
"battery_fleet_total": "{n} 個の電池を監視",
|
||||
"battery_fleet_setup_button": "電池フリート",
|
||||
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
"series_end_count_label": "횟수",
|
||||
"series_end_until_label": "종료일",
|
||||
"parts_section": "부품 및 소모품",
|
||||
"parts_inventory_value": "재고 가치",
|
||||
"part_add": "부품 추가",
|
||||
"part_name": "이름",
|
||||
"part_vendor": "제조사",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "곧",
|
||||
"battery_fleet_status_ok": "정상",
|
||||
"battery_fleet_predicted_on": "{date}쯤 예상",
|
||||
"battery_fleet_predicted_trend": "이 배터리의 방전 추세로 예측: 약 {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "충전식: 교체 대신 충전 — 쇼핑 목록에 오르지 않습니다",
|
||||
"battery_fleet_sort_name": "이름순 정렬",
|
||||
"battery_fleet_sort_urgency": "긴급도순 정렬",
|
||||
"battery_fleet_mark_recharged": "충전 완료로 표시",
|
||||
"battery_fleet_sparkline_hint": "지난 30일간의 배터리 잔량 — 점선: 낮음 임계값까지의 예측",
|
||||
"battery_fleet_filter_type": "이 배터리 유형만 표시",
|
||||
"battery_fleet_record_replacement": "{date}쯤 잔량이 급상승했습니다 — 이 교체를 Battery Notes에 기록",
|
||||
"battery_fleet_total": "배터리 {n}개 추적 중",
|
||||
"battery_fleet_setup_button": "배터리 플릿",
|
||||
"battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Antall ganger",
|
||||
"series_end_until_label": "Sluttdato",
|
||||
"parts_section": "Deler & forbruksvarer",
|
||||
"parts_inventory_value": "Lagerverdi",
|
||||
"part_add": "Legg til del",
|
||||
"part_name": "Navn",
|
||||
"part_vendor": "Produsent",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_predicted_on": "Forventes rundt {date}",
|
||||
"battery_fleet_predicted_trend": "Forutsagt ut fra batteriets utladingstrend: rundt {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oppladbart: lad i stedet for å bytte — aldri på handlelisten",
|
||||
"battery_fleet_sort_name": "Sorter etter navn",
|
||||
"battery_fleet_sort_urgency": "Sorter etter hastegrad",
|
||||
"battery_fleet_mark_recharged": "Merk som oppladet",
|
||||
"battery_fleet_sparkline_hint": "Batterinivå de siste 30 dagene — stiplet: fremskrevet ned til lavnivåterskelen",
|
||||
"battery_fleet_filter_type": "Vis bare denne batteritypen",
|
||||
"battery_fleet_record_replacement": "Nivået hoppet rundt {date} — registrer dette byttet i Battery Notes",
|
||||
"battery_fleet_total": "{n} batterier spores",
|
||||
"battery_fleet_setup_button": "Batteriflåte",
|
||||
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Aantal keer",
|
||||
"series_end_until_label": "Einddatum",
|
||||
"parts_section": "Onderdelen & verbruiksartikelen",
|
||||
"parts_inventory_value": "Voorraadwaarde",
|
||||
"part_add": "Onderdeel toevoegen",
|
||||
"part_name": "Naam",
|
||||
"part_vendor": "Fabrikant",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Binnenkort",
|
||||
"battery_fleet_status_ok": "In orde",
|
||||
"battery_fleet_predicted_on": "Verwacht rond {date}",
|
||||
"battery_fleet_predicted_trend": "Voorspeld uit de ontladingstrend van deze batterij: rond {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oplaadbaar: opladen in plaats van vervangen — nooit op het boodschappenlijstje",
|
||||
"battery_fleet_sort_name": "Sorteren op naam",
|
||||
"battery_fleet_sort_urgency": "Sorteren op urgentie",
|
||||
"battery_fleet_mark_recharged": "Markeren als opgeladen",
|
||||
"battery_fleet_sparkline_hint": "Batterijniveau van de afgelopen 30 dagen — gestippeld: prognose tot de lage drempel",
|
||||
"battery_fleet_filter_type": "Alleen dit batterijtype tonen",
|
||||
"battery_fleet_record_replacement": "Het niveau maakte rond {date} een sprong — deze vervanging vastleggen in Battery Notes",
|
||||
"battery_fleet_total": "{n} batterijen gevolgd",
|
||||
"battery_fleet_setup_button": "Batterijvloot",
|
||||
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Liczba razy",
|
||||
"series_end_until_label": "Data końcowa",
|
||||
"parts_section": "Części i materiały",
|
||||
"parts_inventory_value": "Wartość zapasów",
|
||||
"part_add": "Dodaj część",
|
||||
"part_name": "Nazwa",
|
||||
"part_vendor": "Producent",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Wkrótce",
|
||||
"battery_fleet_status_ok": "W porządku",
|
||||
"battery_fleet_predicted_on": "Przewidywane około {date}",
|
||||
"battery_fleet_predicted_trend": "Prognoza na podstawie trendu rozładowania tej baterii: około {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulator: ładowanie zamiast wymiany — nigdy na liście zakupów",
|
||||
"battery_fleet_sort_name": "Sortuj według nazwy",
|
||||
"battery_fleet_sort_urgency": "Sortuj według pilności",
|
||||
"battery_fleet_mark_recharged": "Oznacz jako naładowaną",
|
||||
"battery_fleet_sparkline_hint": "Poziom baterii z ostatnich 30 dni — kropkowana linia: prognoza do progu niskiego poziomu",
|
||||
"battery_fleet_filter_type": "Pokaż tylko ten typ baterii",
|
||||
"battery_fleet_record_replacement": "Poziom skoczył około {date} — zapisz tę wymianę w Battery Notes",
|
||||
"battery_fleet_total": "Śledzone baterie: {n}",
|
||||
"battery_fleet_setup_button": "Flota baterii",
|
||||
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
"series_end_count_label": "Número de vezes",
|
||||
"series_end_until_label": "Data final",
|
||||
"parts_section": "Peças e consumíveis",
|
||||
"parts_inventory_value": "Valor do estoque",
|
||||
"part_add": "Adicionar peça",
|
||||
"part_name": "Nome",
|
||||
"part_vendor": "Fabricante",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta bateria: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: recarregue em vez de substituir — nunca na lista de compras",
|
||||
"battery_fleet_sort_name": "Ordenar por nome",
|
||||
"battery_fleet_sort_urgency": "Ordenar por urgência",
|
||||
"battery_fleet_mark_recharged": "Marcar como recarregada",
|
||||
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até o limite baixo",
|
||||
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
|
||||
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registrar esta substituição no Battery Notes",
|
||||
"battery_fleet_total": "{n} baterias acompanhadas",
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Número de vezes",
|
||||
"series_end_until_label": "Data final",
|
||||
"parts_section": "Peças e consumíveis",
|
||||
"parts_inventory_value": "Valor do stock",
|
||||
"part_add": "Adicionar peça",
|
||||
"part_name": "Nome",
|
||||
"part_vendor": "Fabricante",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta pilha: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: carrega-se em vez de se substituir — nunca na lista de compras",
|
||||
"battery_fleet_sort_name": "Ordenar por nome",
|
||||
"battery_fleet_sort_urgency": "Ordenar por urgência",
|
||||
"battery_fleet_mark_recharged": "Marcar como recarregada",
|
||||
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até ao limiar baixo",
|
||||
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
|
||||
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registar esta substituição no Battery Notes",
|
||||
"battery_fleet_total": "{n} baterias monitorizadas",
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Число раз",
|
||||
"series_end_until_label": "Дата окончания",
|
||||
"parts_section": "Детали и расходники",
|
||||
"parts_inventory_value": "Стоимость запасов",
|
||||
"part_add": "Добавить деталь",
|
||||
"part_name": "Название",
|
||||
"part_vendor": "Производитель",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Скоро",
|
||||
"battery_fleet_status_ok": "В норме",
|
||||
"battery_fleet_predicted_on": "Ожидается примерно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз по тренду разряда этой батареи: примерно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Аккумулятор: заряжается, а не заменяется — никогда не попадает в список покупок",
|
||||
"battery_fleet_sort_name": "Сортировать по имени",
|
||||
"battery_fleet_sort_urgency": "Сортировать по срочности",
|
||||
"battery_fleet_mark_recharged": "Отметить как заряженную",
|
||||
"battery_fleet_sparkline_hint": "Уровень заряда за последние 30 дней — пунктир: прогноз до порога разряда",
|
||||
"battery_fleet_filter_type": "Показать только этот тип батарей",
|
||||
"battery_fleet_record_replacement": "Уровень резко вырос примерно {date} — записать эту замену в Battery Notes",
|
||||
"battery_fleet_total": "Отслеживается батарей: {n}",
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Antal gånger",
|
||||
"series_end_until_label": "Slutdatum",
|
||||
"parts_section": "Delar & förbrukning",
|
||||
"parts_inventory_value": "Lagervärde",
|
||||
"part_add": "Lägg till del",
|
||||
"part_name": "Namn",
|
||||
"part_vendor": "Tillverkare",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I ordning",
|
||||
"battery_fleet_predicted_on": "Väntas omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Förutspått utifrån batteriets urladdningstrend: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Uppladdningsbart: ladda i stället för att byta — aldrig på inköpslistan",
|
||||
"battery_fleet_sort_name": "Sortera efter namn",
|
||||
"battery_fleet_sort_urgency": "Sortera efter angelägenhet",
|
||||
"battery_fleet_mark_recharged": "Markera som uppladdad",
|
||||
"battery_fleet_sparkline_hint": "Batterinivå de senaste 30 dagarna — prickad: prognos ner till lågnivåtröskeln",
|
||||
"battery_fleet_filter_type": "Visa endast denna batterityp",
|
||||
"battery_fleet_record_replacement": "Nivån hoppade omkring {date} — registrera detta byte i Battery Notes",
|
||||
"battery_fleet_total": "{n} batterier spåras",
|
||||
"battery_fleet_setup_button": "Batteriflotta",
|
||||
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
"series_end_count_label": "Tekrar sayısı",
|
||||
"series_end_until_label": "Bitiş tarihi",
|
||||
"parts_section": "Parçalar ve sarf malzemeleri",
|
||||
"parts_inventory_value": "Stok değeri",
|
||||
"part_add": "Parça ekle",
|
||||
"part_name": "Ad",
|
||||
"part_vendor": "Üretici",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Yakında",
|
||||
"battery_fleet_status_ok": "İyi durumda",
|
||||
"battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor",
|
||||
"battery_fleet_predicted_trend": "Bu pilin deşarj eğiliminden tahmin edildi: yaklaşık {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Şarj edilebilir: değiştirmek yerine şarj edin — alışveriş listesine asla girmez",
|
||||
"battery_fleet_sort_name": "Ada göre sırala",
|
||||
"battery_fleet_sort_urgency": "Aciliyete göre sırala",
|
||||
"battery_fleet_mark_recharged": "Şarj edildi olarak işaretle",
|
||||
"battery_fleet_sparkline_hint": "Son 30 günün pil seviyesi — noktalı: düşük eşiğe kadar projeksiyon",
|
||||
"battery_fleet_filter_type": "Yalnızca bu pil türünü göster",
|
||||
"battery_fleet_record_replacement": "Seviye {date} civarında sıçradı — bu değişimi Battery Notes'a kaydet",
|
||||
"battery_fleet_total": "{n} pil takip ediliyor",
|
||||
"battery_fleet_setup_button": "Pil filosu",
|
||||
"battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "Кількість разів",
|
||||
"series_end_until_label": "Дата завершення",
|
||||
"parts_section": "Деталі та витратні матеріали",
|
||||
"parts_inventory_value": "Вартість запасів",
|
||||
"part_add": "Додати деталь",
|
||||
"part_name": "Назва",
|
||||
"part_vendor": "Виробник",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "Незабаром",
|
||||
"battery_fleet_status_ok": "У нормі",
|
||||
"battery_fleet_predicted_on": "Очікується приблизно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз за трендом розряду цієї батареї: приблизно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Акумулятор: заряджається, а не замінюється — ніколи не потрапляє до списку покупок",
|
||||
"battery_fleet_sort_name": "Сортувати за назвою",
|
||||
"battery_fleet_sort_urgency": "Сортувати за терміновістю",
|
||||
"battery_fleet_mark_recharged": "Позначити як заряджену",
|
||||
"battery_fleet_sparkline_hint": "Рівень заряду за останні 30 днів — пунктир: прогноз до порогу розряду",
|
||||
"battery_fleet_filter_type": "Показати лише цей тип батарей",
|
||||
"battery_fleet_record_replacement": "Рівень різко зріс приблизно {date} — записати цю заміну в Battery Notes",
|
||||
"battery_fleet_total": "Відстежується батарей: {n}",
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
|
||||
|
||||
@@ -716,6 +716,7 @@
|
||||
"series_end_count_label": "次数",
|
||||
"series_end_until_label": "结束日期",
|
||||
"parts_section": "配件与耗材",
|
||||
"parts_inventory_value": "库存价值",
|
||||
"part_add": "添加配件",
|
||||
"part_name": "名称",
|
||||
"part_vendor": "制造商",
|
||||
@@ -820,6 +821,14 @@
|
||||
"battery_fleet_status_soon": "即将",
|
||||
"battery_fleet_status_ok": "正常",
|
||||
"battery_fleet_predicted_on": "预计在 {date} 前后",
|
||||
"battery_fleet_predicted_trend": "根据此电池的放电趋势预测:约 {date}({confidence})",
|
||||
"battery_fleet_rechargeable": "可充电电池:充电即可,无需更换——不会出现在购物清单中",
|
||||
"battery_fleet_sort_name": "按名称排序",
|
||||
"battery_fleet_sort_urgency": "按紧急程度排序",
|
||||
"battery_fleet_mark_recharged": "标记为已充电",
|
||||
"battery_fleet_sparkline_hint": "过去 30 天的电池电量——虚线:外推至低电量阈值",
|
||||
"battery_fleet_filter_type": "仅显示此电池类型",
|
||||
"battery_fleet_record_replacement": "电量在 {date} 前后跳升——将此次更换记录到 Battery Notes",
|
||||
"battery_fleet_total": "已跟踪 {n} 个电池",
|
||||
"battery_fleet_setup_button": "电池群",
|
||||
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Maintenance Supporter Lovelace Card. */
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { mergeSubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays } from "./styles";
|
||||
import type {
|
||||
@@ -274,10 +275,14 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
try {
|
||||
const unsub = await this.hass.connection.subscribeMessage(
|
||||
(msg: unknown) => {
|
||||
const data = msg as { objects: MaintenanceObjectResponse[] };
|
||||
this._objects = data.objects;
|
||||
const next = mergeSubscriptionEvent(
|
||||
this._objects,
|
||||
msg as SubscriptionEvent<MaintenanceObjectResponse>,
|
||||
);
|
||||
if (next !== null) this._objects = next;
|
||||
},
|
||||
{ type: "maintenance_supporter/subscribe" }
|
||||
// deltas: only changed entries arrive — see helpers/subscription-merge.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true }
|
||||
);
|
||||
// Detached mid-subscribe → drop the orphaned subscription.
|
||||
if (!this.isConnected) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { mergeSubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { isStaleBundle } from "./helpers/bundle-version";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs } from "./styles";
|
||||
@@ -29,6 +30,7 @@ import type {
|
||||
StatisticsPoint,
|
||||
SavedView,
|
||||
SavedViewFilters,
|
||||
ManualDocRef,
|
||||
} from "./types";
|
||||
import { StatisticsService } from "./statistics-service";
|
||||
import { UserService } from "./user-service";
|
||||
@@ -385,10 +387,18 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
]);
|
||||
if (viewsResult) this._savedViews = (viewsResult as { views: SavedView[] }).views || [];
|
||||
if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects;
|
||||
// Battery Fleet availability (Battery Notes present + not yet set up).
|
||||
// A data refresh means the open task's history may have grown (complete,
|
||||
// history edit) — the truncated list payload can't tell, so refetch.
|
||||
if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) {
|
||||
this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
|
||||
}
|
||||
// Battery Fleet availability (batteries present + not yet set up).
|
||||
// The slim status check, NOT the overview: the full overview runs the
|
||||
// trend machinery server-side (one recorder regression per healthy
|
||||
// battery on a cold cache) — far too expensive for hiding a button.
|
||||
this.hass.connection
|
||||
.sendMessagePromise<{ available: boolean; configured: boolean }>({
|
||||
type: "maintenance_supporter/battery_fleet/overview",
|
||||
type: "maintenance_supporter/battery_fleet/status",
|
||||
})
|
||||
.then((ov) => {
|
||||
this._batteryFleetSetupAvailable = !!ov.available && !ov.configured;
|
||||
@@ -591,10 +601,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
try {
|
||||
const unsub = await this.hass.connection.subscribeMessage(
|
||||
(msg: unknown) => {
|
||||
const data = msg as { objects: MaintenanceObjectResponse[] };
|
||||
this._objects = data.objects;
|
||||
const next = mergeSubscriptionEvent(
|
||||
this._objects,
|
||||
msg as SubscriptionEvent<MaintenanceObjectResponse>,
|
||||
);
|
||||
if (next !== null) this._objects = next;
|
||||
},
|
||||
{ type: "maintenance_supporter/subscribe" }
|
||||
// deltas: only entries whose rebuilt response actually changed —
|
||||
// no-op timer waves send nothing, a real change ships one object.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true }
|
||||
);
|
||||
// If the element was detached while the subscribe was in flight, drop the
|
||||
// now-orphaned subscription instead of storing it on a dead component.
|
||||
@@ -880,6 +895,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._activeTab = "overview";
|
||||
this._historyFilter = null;
|
||||
this._scrollContentToTop();
|
||||
// Payload diet: list responses carry only the most recent history
|
||||
// window — the detail's full timeline/charts load here, on demand.
|
||||
this._fetchFullHistory(entryId, taskId);
|
||||
|
||||
// Lazy-load statistics for the task's trigger entity
|
||||
const task = this._getTask(entryId, taskId);
|
||||
@@ -1785,6 +1803,52 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Open a manual-tagged document from the objects table / object header:
|
||||
* web-links directly, stored files through a signed path (the same
|
||||
* Companion-safe recipe as the documents section). */
|
||||
private _openManualDoc(doc: ManualDocRef): void {
|
||||
if (doc.kind !== "file") {
|
||||
if (isSafeHttpUrl(doc.url)) window.open(doc.url!, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
// Open the tab synchronously (inside the click gesture) so it isn't
|
||||
// popup-blocked, then point it at the freshly signed URL.
|
||||
const win = window.open("about:blank", "_blank");
|
||||
void this.hass.connection
|
||||
.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 300,
|
||||
})
|
||||
.then((signed) => {
|
||||
if (win) win.location.href = new URL(signed.path, window.location.origin).href;
|
||||
})
|
||||
.catch(() => win?.close());
|
||||
}
|
||||
|
||||
/** #73: persist one checklist tick. Sends the FULL current state (the
|
||||
* server replaces, not merges — idempotent) and reloads so the progress
|
||||
* header and any other open surface agree. */
|
||||
private async _setChecklistItem(entryId: string, taskId: string, item: string, done: boolean): Promise<void> {
|
||||
const obj = this._getObject(entryId);
|
||||
const task = obj?.tasks.find((x) => x.id === taskId);
|
||||
if (!task) return;
|
||||
const state: Record<string, boolean> = {};
|
||||
for (const step of task.checklist || []) {
|
||||
const current = task.checklist_progress?.[step] ?? false;
|
||||
state[step] = step === item ? done : current;
|
||||
}
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/task/checklist_progress",
|
||||
entry_id: entryId, task_id: taskId, checklist_state: state,
|
||||
});
|
||||
await this._loadData();
|
||||
} catch {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
}
|
||||
}
|
||||
|
||||
private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog");
|
||||
if (!dlg) return;
|
||||
@@ -1801,6 +1865,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
?.tasks.find((tsk) => tsk.id === taskId);
|
||||
dlg.taskType = tk?.type || "";
|
||||
dlg.readingUnit = tk?.reading_unit || "";
|
||||
// #73: ticks recorded during the cycle prefill the dialog's checklist.
|
||||
dlg.checklistPrefill = tk?.checklist_progress || {};
|
||||
dlg.requiredFields = tk?.required_completion_fields || [];
|
||||
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
||||
// task shows what it will decrement (incl. the storage location).
|
||||
@@ -2765,13 +2831,22 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const area = o.area_id ? (this.hass?.areas?.[o.area_id]?.name || o.area_id) : "—";
|
||||
return html`<td class="oc-area_id">${area}</td>`;
|
||||
}
|
||||
case "documentation_url":
|
||||
case "documentation_url": {
|
||||
// Fallback: an UPLOADED manual (category "manual") is the object's
|
||||
// manual just as much as the legacy URL field — an object with its
|
||||
// handbook attached must not render "—" here (prod: Easee vs Epson).
|
||||
const manualDoc = (o.manual_docs || [])[0];
|
||||
return html`<td class="oc-documentation_url">${
|
||||
isSafeHttpUrl(o.documentation_url)
|
||||
? html`<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer"
|
||||
@click=${(e: Event) => e.stopPropagation()}><ha-icon icon="mdi:file-document-outline"></ha-icon></a>`
|
||||
: "—"
|
||||
: manualDoc
|
||||
? html`<a href="#" title=${manualDoc.title}
|
||||
@click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this._openManualDoc(manualDoc); }}
|
||||
><ha-icon icon="mdi:file-document-outline"></ha-icon></a>`
|
||||
: "—"
|
||||
}</td>`;
|
||||
}
|
||||
case "notes":
|
||||
return html`<td class="oc-notes" title=${o.notes || ""}>${o.notes || "—"}</td>`;
|
||||
case "task_count":
|
||||
@@ -3085,7 +3160,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
? html`<p class="meta">${t("documentation_url_label", L)}:
|
||||
<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer">${o.documentation_url}</a>
|
||||
</p>`
|
||||
: nothing}
|
||||
: (o.manual_docs || []).length
|
||||
? html`<p class="meta">${t("documentation_url_label", L)}:
|
||||
${o.manual_docs!.slice(0, 3).map(
|
||||
(m, i) => html`${i > 0 ? " · " : ""}<a href="#"
|
||||
@click=${(e: Event) => { e.preventDefault(); this._openManualDoc(m); }}>${m.title}</a>`,
|
||||
)}${o.manual_docs!.length > 3
|
||||
? html` … +${o.manual_docs!.length - 3}`
|
||||
: nothing}
|
||||
</p>`
|
||||
: nothing}
|
||||
${o.installation_date ? html`<p class="meta">${t("installed", L)}: ${formatDate(o.installation_date, L)}</p>` : nothing}
|
||||
${o.warranty_expiry ? this._renderWarrantyMeta(o.warranty_expiry, L) : nothing}
|
||||
${o.notes
|
||||
@@ -3157,6 +3241,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.entryId=${obj.entry_id}
|
||||
.parts=${obj.parts || []}
|
||||
.canWrite=${!isOperator}
|
||||
.currencySymbol=${this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL}
|
||||
@parts-changed=${() => this._loadData()}
|
||||
></maintenance-parts-section>
|
||||
</div>
|
||||
@@ -3221,7 +3306,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const task = this._selectedEntryId && this._selectedTaskId
|
||||
? this._getObject(this._selectedEntryId)?.tasks.find((tk) => tk.id === this._selectedTaskId)
|
||||
: undefined;
|
||||
const readings = (task?.history || [])
|
||||
// Payload diet: the summary's history is truncated to the recent window —
|
||||
// the reading DELTAS must come from the full record (the oldest visible
|
||||
// reading would otherwise lose or falsify its delta), which
|
||||
// _fetchFullHistory loads for the open task.
|
||||
const fh = this._fullHistory;
|
||||
const fullHistory =
|
||||
fh && fh.entryId === this._selectedEntryId && fh.taskId === this._selectedTaskId && fh.entries.length > (task?.history || []).length
|
||||
? fh.entries
|
||||
: task?.history || [];
|
||||
const readings = fullHistory
|
||||
.filter((h) => h.reading_value != null)
|
||||
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
||||
return {
|
||||
@@ -3256,6 +3350,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
taskId,
|
||||
objectName: obj?.object.name || "",
|
||||
objectDocUrl: obj?.object?.documentation_url ?? null,
|
||||
objectManualDocs: obj?.object?.manual_docs ?? [],
|
||||
openManualDoc: (doc) => this._openManualDoc(doc),
|
||||
setChecklistItem: (item, done) => this._setChecklistItem(entryId, taskId, item, done),
|
||||
isOperator: this._isOperator,
|
||||
actionLoading: this._actionLoading,
|
||||
moreMenuOpen: this._moreMenuOpen,
|
||||
@@ -3293,12 +3390,37 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
/** Full history for the OPEN task (list payloads are truncated to the
|
||||
* most recent window). null until loaded; a failure — e.g. an older
|
||||
* backend without `task/history` — falls back to the truncated list. */
|
||||
@state() private _fullHistory: { entryId: string; taskId: string; entries: HistoryEntry[] } | null = null;
|
||||
|
||||
private async _fetchFullHistory(entryId: string, taskId: string): Promise<void> {
|
||||
try {
|
||||
const res = (await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/task/history",
|
||||
entry_id: entryId,
|
||||
task_id: taskId,
|
||||
})) as { history: HistoryEntry[] };
|
||||
if (this._selectedEntryId === entryId && this._selectedTaskId === taskId) {
|
||||
this._fullHistory = { entryId, taskId, entries: res.history || [] };
|
||||
}
|
||||
} catch {
|
||||
this._fullHistory = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderTaskDetail() {
|
||||
if (!this._selectedEntryId || !this._selectedTaskId) return nothing;
|
||||
const task = this._getTask(this._selectedEntryId, this._selectedTaskId);
|
||||
if (!task) return html`<p>Task not found.</p>`;
|
||||
const fh = this._fullHistory;
|
||||
const detailTask =
|
||||
fh && fh.entryId === this._selectedEntryId && fh.taskId === this._selectedTaskId && fh.entries.length > (task.history || []).length
|
||||
? { ...task, history: fh.entries }
|
||||
: task;
|
||||
return html`<maintenance-task-detail-view
|
||||
.task=${task}
|
||||
.task=${detailTask}
|
||||
.ctx=${this._taskDetailCtx()}
|
||||
></maintenance-task-detail-view>`;
|
||||
}
|
||||
|
||||
@@ -31,14 +31,25 @@
|
||||
* (all of which only matter once a dashboard is actually being generated).
|
||||
*/
|
||||
|
||||
import { BUNDLE_VERSION } from "./helpers/bundle-version";
|
||||
|
||||
const STRATEGY_TYPE = "maintenance-supporter";
|
||||
const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`;
|
||||
const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor";
|
||||
|
||||
// Absolute URL of the full strategy bundle (served at STRATEGY_URL by the
|
||||
// integration). The shim's only dependency, loaded on demand.
|
||||
//
|
||||
// Version-busted (issue #124): the bundle's chunk names are content-hashed
|
||||
// and change every release, but browsers heuristically cache this entry
|
||||
// (static serving sends no Cache-Control). A stale cached entry then imports
|
||||
// chunk names the update deleted → 404 → the strategy dashboard dies until
|
||||
// a hard refresh. The `?v=` makes the URL change with the release, so a
|
||||
// fresh entry always pulls its matching chunks. BUNDLE_VERSION is inlined
|
||||
// by esbuild — the built shim keeps its zero-import guarantee.
|
||||
const BUNDLE_URL =
|
||||
"/maintenance_supporter_strategy/maintenance-dashboard-strategy.js";
|
||||
"/maintenance_supporter_strategy/maintenance-dashboard-strategy.js" +
|
||||
`?v=${BUNDLE_VERSION}`;
|
||||
|
||||
let _bundle: Promise<unknown> | null = null;
|
||||
function loadBundle(): Promise<unknown> {
|
||||
|
||||
+46
-667
@@ -9,10 +9,10 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"playwright": "1.61"
|
||||
"playwright": "1.62"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/testing": "^4.0.0",
|
||||
"@open-wc/testing": "^5.0.0",
|
||||
"@web/dev-server-esbuild": "^2.0.0",
|
||||
"@web/test-runner": "^1.0.0",
|
||||
"@web/test-runner-playwright": "^1.0.0",
|
||||
@@ -864,25 +864,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@open-wc/semantic-dom-diff": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz",
|
||||
"integrity": "sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.21.0.tgz",
|
||||
"integrity": "sha512-2hrNt9MWhz4kfuIWI6M6zyK+UJXd2ehjaP+nJ1shf9HZAperr+braPToTbRODx1oE6EvoVvTJGQlsCqHv7NKQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^4.3.1",
|
||||
"@web/test-runner-commands": "^0.9.0"
|
||||
"@web/test-runner-commands": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-wc/testing": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-4.0.0.tgz",
|
||||
"integrity": "sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-5.0.0.tgz",
|
||||
"integrity": "sha512-2IcM2py1wtz4pyTW5whhltP5L71TVqt0l0IeA2KqHILyxOzI77YnDc9drfN7ezl0+fZbGOXM6qXe4Y4o0R7xpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esm-bundle/chai": "^4.3.4-fix.0",
|
||||
"@open-wc/semantic-dom-diff": "^0.20.0",
|
||||
"@open-wc/semantic-dom-diff": "^0.21.0",
|
||||
"@open-wc/testing-helpers": "^3.0.0",
|
||||
"@types/chai-dom": "^1.11.0",
|
||||
"@types/sinon-chai": "^3.2.3",
|
||||
@@ -2011,16 +2011,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@web/browser-logs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz",
|
||||
"integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/config-loader": {
|
||||
@@ -2064,56 +2064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-core": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz",
|
||||
"integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^2.1.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.13.0",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.2.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-esbuild": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-2.0.0.tgz",
|
||||
"integrity": "sha512-D4BPYj3jO3kTDmytKWYB97xIVpR/Mdpy+zOyY1rEpFfFAcM59LCmO76Y8hp78Ssm4+4c2Gvd27gBVLSqWTPN3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdn/browser-compat-data": "^4.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"parse5": "^6.0.1",
|
||||
"ua-parser-js": "^1.0.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-esbuild/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.1.tgz",
|
||||
"integrity": "sha512-hwps4+UoNDoAP5oM2wLuwWi7AB9drW6k1ybRx0eLSly966zqj3gRSHCJmtGtC0u77HdnlED4PDFSdJfrq2Oo3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2140,15 +2093,18 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-esbuild/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"node_modules/@web/dev-server-esbuild": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-2.0.0.tgz",
|
||||
"integrity": "sha512-D4BPYj3jO3kTDmytKWYB97xIVpR/Mdpy+zOyY1rEpFfFAcM59LCmO76Y8hp78Ssm4+4c2Gvd27gBVLSqWTPN3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
"@mdn/browser-compat-data": "^4.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"parse5": "^6.0.1",
|
||||
"ua-parser-js": "^1.0.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -2172,98 +2128,10 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-rollup/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server-rollup/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/dev-server/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/parse5-utils": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.1.tgz",
|
||||
"integrity": "sha512-7rBVZEMGfrq2iPcAEwJ0KSNSvmA2a6jT2CK8/gyIOHgn4reg7bSSRbzyWIEYWyIkeRoYEukX/aW+nAeCgSSqhQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2271,7 +2139,7 @@
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner": {
|
||||
@@ -2322,64 +2190,21 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-chrome/node_modules/@web/browser-logs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"node_modules/@web/test-runner-commands": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-1.0.1.tgz",
|
||||
"integrity": "sha512-rvAvHJKlNzh5Tv8L2wGjnNX/hSkPmQx5eyKqeyExEvA7A2pVXiNYFstJzUGCD52TlAJSPF3BtW1yOwpf6vG7Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
"@web/test-runner-core": "^1.0.0",
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-chrome/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-chrome/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-chrome/node_modules/@web/test-runner-core": {
|
||||
"node_modules/@web/test-runner-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz",
|
||||
"integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==",
|
||||
@@ -2417,58 +2242,6 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-commands": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz",
|
||||
"integrity": "sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@web/test-runner-core": "^0.13.0",
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-core": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz",
|
||||
"integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@types/babel__code-frame": "^7.0.2",
|
||||
"@types/co-body": "^6.1.0",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/istanbul-lib-coverage": "^2.0.3",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@web/browser-logs": "^0.4.0",
|
||||
"@web/dev-server-core": "^0.7.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"co-body": "^6.1.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debounce": "^1.2.0",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"globby": "^11.0.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.0.2",
|
||||
"log-update": "^4.0.0",
|
||||
"nanocolors": "^0.2.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"open": "^8.0.2",
|
||||
"picomatch": "^2.2.2",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-coverage-v8": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-1.0.0.tgz",
|
||||
@@ -2486,101 +2259,6 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-coverage-v8/node_modules/@web/browser-logs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-coverage-v8/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-coverage-v8/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-coverage-v8/node_modules/@web/test-runner-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz",
|
||||
"integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@types/babel__code-frame": "^7.0.2",
|
||||
"@types/co-body": "^6.1.0",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/istanbul-lib-coverage": "^2.0.3",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@web/browser-logs": "^1.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"co-body": "^6.1.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debounce": "^1.2.0",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"globby": "^11.0.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.0.2",
|
||||
"log-update": "^4.0.0",
|
||||
"nanocolors": "^0.2.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"open": "^8.0.2",
|
||||
"picomatch": "^2.3.2",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-mocha": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-1.0.0.tgz",
|
||||
@@ -2594,101 +2272,6 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-mocha/node_modules/@web/browser-logs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-mocha/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-mocha/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-mocha/node_modules/@web/test-runner-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz",
|
||||
"integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@types/babel__code-frame": "^7.0.2",
|
||||
"@types/co-body": "^6.1.0",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/istanbul-lib-coverage": "^2.0.3",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@web/browser-logs": "^1.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"co-body": "^6.1.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debounce": "^1.2.0",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"globby": "^11.0.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.0.2",
|
||||
"log-update": "^4.0.0",
|
||||
"nanocolors": "^0.2.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"open": "^8.0.2",
|
||||
"picomatch": "^2.3.2",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-playwright": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-1.0.0.tgz",
|
||||
@@ -2704,210 +2287,6 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-playwright/node_modules/@web/browser-logs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-playwright/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-playwright/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner-playwright/node_modules/@web/test-runner-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz",
|
||||
"integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@types/babel__code-frame": "^7.0.2",
|
||||
"@types/co-body": "^6.1.0",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/istanbul-lib-coverage": "^2.0.3",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@web/browser-logs": "^1.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"co-body": "^6.1.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debounce": "^1.2.0",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"globby": "^11.0.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.0.2",
|
||||
"log-update": "^4.0.0",
|
||||
"nanocolors": "^0.2.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"open": "^8.0.2",
|
||||
"picomatch": "^2.3.2",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner/node_modules/@web/browser-logs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz",
|
||||
"integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"errorstacks": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner/node_modules/@web/dev-server-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz",
|
||||
"integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@web/parse5-utils": "^3.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"es-module-lexer": "^1.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-etag": "^4.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lru-cache": "^8.0.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"parse5": "^6.0.1",
|
||||
"picomatch": "^2.3.2",
|
||||
"ws": "^7.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner/node_modules/@web/parse5-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse5": "^6.0.1",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner/node_modules/@web/test-runner-commands": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-1.0.0.tgz",
|
||||
"integrity": "sha512-8wXJkwvWWCc6GTPsdjbEN141kz+4MQPFjsId47hoibkY3FhkgeKJUm+cnwMYCIXHuWA0e7V+L2fmH087UPrXXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@web/test-runner-core": "^1.0.0",
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@web/test-runner/node_modules/@web/test-runner-core": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz",
|
||||
"integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@types/babel__code-frame": "^7.0.2",
|
||||
"@types/co-body": "^6.1.0",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/istanbul-lib-coverage": "^2.0.3",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@web/browser-logs": "^1.0.0",
|
||||
"@web/dev-server-core": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"co-body": "^6.1.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debounce": "^1.2.0",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"globby": "^11.0.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.0.2",
|
||||
"log-update": "^4.0.0",
|
||||
"nanocolors": "^0.2.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"open": "^8.0.2",
|
||||
"picomatch": "^2.3.2",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -5366,33 +4745,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"version": "1.62.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz",
|
||||
"integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
"playwright-core": "1.62.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"version": "1.62.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz",
|
||||
"integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:watch": "web-test-runner --config web-test-runner.config.mjs --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/testing": "^4.0.0",
|
||||
"@open-wc/testing": "^5.0.0",
|
||||
"@web/dev-server-esbuild": "^2.0.0",
|
||||
"@web/test-runner": "^1.0.0",
|
||||
"@web/test-runner-playwright": "^1.0.0",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"playwright": "1.61"
|
||||
"playwright": "1.62"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "$esbuild",
|
||||
|
||||
@@ -81,7 +81,10 @@ export const panelStyles = css`
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 4px 0 8px;
|
||||
justify-content: flex-end;
|
||||
/* Left-aligned on purpose: flex-end mimicked the pre-v2.37 look (buttons
|
||||
trailing the filter row), but on wide desktops it strands the primary
|
||||
"new task" action at the far right of an otherwise empty row. */
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
:host([narrow]) .actions-bar {
|
||||
@@ -1009,6 +1012,21 @@ export const panelStyles = css`
|
||||
.checklist-preview-list li {
|
||||
padding: 1px 0;
|
||||
}
|
||||
/* #73: interactive in-cycle ticks. */
|
||||
.checklist-preview-list label {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checklist-preview-list input[type="checkbox"] {
|
||||
accent-color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.checklist-preview-list li.checked label span {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Recommendation Card */
|
||||
.recommendation-card {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { t, formatDate, formatDateTime, formatRecurrence } from "../styles";
|
||||
import type { AdvancedFeatures, HomeAssistant, MaintenanceTask } from "../types";
|
||||
import type { AdvancedFeatures, HomeAssistant, MaintenanceTask, ManualDocRef } from "../types";
|
||||
import { renderTriggerSection, type SparklineContext } from "./sparkline";
|
||||
import { renderPredictionSection } from "./prediction";
|
||||
import { renderWeibullSection } from "./weibull";
|
||||
@@ -32,6 +32,13 @@ export interface TaskDetailContext {
|
||||
objectName: string;
|
||||
/** Parent object's documentation_url (raw; sanitised here). */
|
||||
objectDocUrl: string | null | undefined;
|
||||
/** Parent object's manual-tagged documents — the fallback for the manual
|
||||
* row when documentation_url is empty (same rule as the objects table). */
|
||||
objectManualDocs: ManualDocRef[];
|
||||
/** Opens a manual document (signed path / weblink) — panel-owned. */
|
||||
openManualDoc: (doc: ManualDocRef) => void;
|
||||
/** #73: persist one checklist tick (panel sends the full state via WS). */
|
||||
setChecklistItem: (item: string, done: boolean) => void;
|
||||
isOperator: boolean;
|
||||
actionLoading: boolean;
|
||||
moreMenuOpen: boolean;
|
||||
@@ -178,22 +185,38 @@ function collapsible(key: string, titleKey: string, body: unknown, ctx: TaskDeta
|
||||
`;
|
||||
}
|
||||
|
||||
/** Read-only preview of the configured checklist steps so users can see
|
||||
* the steps without having to open the Edit or Complete dialog. Only
|
||||
* rendered when the Checklists feature is enabled and steps are set. */
|
||||
/** Interactive checklist (#73): steps can be ticked off DURING the cycle
|
||||
* without completing the task — progress persists server-side (survives
|
||||
* reloads, prefills the complete dialog) and resets when the task is
|
||||
* completed or skipped. Only rendered when the Checklists feature is
|
||||
* enabled and steps are set. */
|
||||
function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
if (!ctx.features.checklists) return nothing;
|
||||
const items = task.checklist || [];
|
||||
if (items.length === 0) return nothing;
|
||||
const L = ctx.lang;
|
||||
const progress = task.checklist_progress || {};
|
||||
const done = items.filter((item) => progress[item]).length;
|
||||
return html`
|
||||
<div class="checklist-preview-card">
|
||||
<div class="checklist-preview-header">
|
||||
<ha-icon icon="mdi:format-list-checks"></ha-icon>
|
||||
<span>${t("checklist", L)} (${items.length})</span>
|
||||
<span>${t("checklist", L)} (${done}/${items.length})</span>
|
||||
</div>
|
||||
<ol class="checklist-preview-list">
|
||||
${items.map((item) => html`<li>${item}</li>`)}
|
||||
${items.map((item) => html`
|
||||
<li class=${progress[item] ? "checked" : ""}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${!!progress[item]}
|
||||
@change=${(e: Event) =>
|
||||
ctx.setChecklistItem(item, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span>${item}</span>
|
||||
</label>
|
||||
</li>
|
||||
`)}
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
@@ -206,7 +229,10 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
const safeTaskUrl = isSafeHttpUrl(task.documentation_url)
|
||||
? task.documentation_url : null;
|
||||
const safeObjUrl = isSafeHttpUrl(ctx.objectDocUrl) ? ctx.objectDocUrl : null;
|
||||
if (!task.notes && !safeTaskUrl && !safeObjUrl) return nothing;
|
||||
// Same fallback rule as the objects table: an UPLOADED manual (category
|
||||
// "manual") stands in when the object's URL field is empty.
|
||||
const manualDoc = safeObjUrl ? null : (ctx.objectManualDocs || [])[0];
|
||||
if (!task.notes && !safeTaskUrl && !safeObjUrl && !manualDoc) return nothing;
|
||||
const L = ctx.lang;
|
||||
return html`
|
||||
<div class="task-meta-card">
|
||||
@@ -227,6 +253,13 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
<ha-icon icon="mdi:book-open-variant"></ha-icon>
|
||||
<a href="${safeObjUrl}" target="_blank" rel="noopener noreferrer">${t("documentation_url_label", L)} (${ctx.objectName})</a>
|
||||
</div>
|
||||
` : manualDoc ? html`
|
||||
<div class="task-meta-row task-meta-link">
|
||||
<ha-icon icon="mdi:book-open-variant"></ha-icon>
|
||||
<a href="#" title=${manualDoc.title}
|
||||
@click=${(e: Event) => { e.preventDefault(); ctx.openManualDoc(manualDoc); }}
|
||||
>${t("documentation_url_label", L)} (${ctx.objectName})</a>
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -31,6 +31,17 @@ export interface MaintenanceObject {
|
||||
/** (roadmap P2) number of attached documents (files + web-links); drives the
|
||||
* objects-table paperclip badge. Computed server-side, not persisted. */
|
||||
document_count?: number;
|
||||
/** Attached documents tagged as manuals — the fallback for the "manual"
|
||||
* column/header when documentation_url is unset. Computed server-side. */
|
||||
manual_docs?: ManualDocRef[];
|
||||
}
|
||||
|
||||
/** Slim reference to a manual-tagged document (subset of MaintenanceDocument). */
|
||||
export interface ManualDocRef {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: string; // "file" | "weblink"
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
export interface TriggerConfig {
|
||||
@@ -161,6 +172,9 @@ export interface MaintenanceTask {
|
||||
notes?: string | null;
|
||||
documentation_url?: string | null;
|
||||
checklist?: string[];
|
||||
/** #73: in-cycle ticks ({item text: bool}); persists server-side, resets on
|
||||
* complete/skip. */
|
||||
checklist_progress?: Record<string, boolean>;
|
||||
labels?: string[];
|
||||
assignee_pool?: string[];
|
||||
rotation_strategy?: string | null;
|
||||
@@ -184,7 +198,11 @@ export interface MaintenanceTask {
|
||||
trigger_entity_infos?: TriggerEntityInfo[] | null;
|
||||
/** Battery Fleet: the single aggregate task renders the battery section. */
|
||||
battery_fleet_task?: boolean;
|
||||
/** LIST payloads carry only the most recent window (payload diet) — the
|
||||
* task detail fetches the full record via `task/history`. */
|
||||
history: HistoryEntry[];
|
||||
/** Total entries that exist, including those beyond the list window. */
|
||||
history_count?: number;
|
||||
// Computed
|
||||
status: string; // "ok" | "due_soon" | "overdue" | "triggered" | "archived"
|
||||
/** True for a one-time task that has been completed (done; never re-arms). */
|
||||
|
||||
Reference in New Issue
Block a user