361 files

This commit is contained in:
Home Assistant Version Control committed 2026-10-06 17:11:43 +00:00
1 parent d2c5af43e3
commit bf886c9347
362 files changed
+22060 -1285

No files matched your search

@@ -131,6 +131,7 @@ import type { PartsCostMode } from "./helpers/parts-cost";
import { canWrite } from "./helpers/permissions";
import { renderStatusBadge } from "./renderers/status";
import { TOAST_MS, ACTION_TOAST_MS } from "./helpers/toast";
import { primaryTriggerEntity } from "./helpers/trigger-entities";
import { buildHistoryEntryDraft } from "./helpers/history-draft";
import { readingSlotDelta } from "./helpers/reading-slots";
import { bulkResultMessage, codedFailures, runWs, runWsEach } from "./helpers/ws-run";
@@ -236,6 +237,8 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _filterStatus = "";
@state() private _filterUser: string | null = null;
@state() private _filterLabel: string | null = null;
// 2026-10 places: "" = all, "home" = objects without a place, else a zone id.
@state() private _filterPlace = "";
/** #134: only tasks of this priority (low|normal|high); "" = no filter. */
@state() private _filterPriority = "";
// v2.24: shared saved filter views + the id of the one currently applied ("" =
@@ -264,7 +267,7 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _groups: Record<string, MaintenanceGroup> = {};
@state() private _detailStatsData: Map<string, StatisticsPoint[]> = new Map();
@state() private _miniStatsData: Map<string, StatisticsPoint[]> = new Map();
@state() private _features: AdvancedFeatures = { adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false };
@state() private _features: AdvancedFeatures = { adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false, places: false };
// HA user IDs (UUIDs) granted full panel access despite not being HA admins.
@state() private _adminPanelUserIds: string[] = [];
// v2.8.4: master switch — the allowlist only grants the full panel when this
@@ -1087,7 +1090,7 @@ export class MaintenanceSupporterPanel extends LitElement {
const task = this._selectedEntryId && this._selectedTaskId
? this._getTask(this._selectedEntryId, this._selectedTaskId)
: null;
const entityId = task?.trigger_config?.entity_id;
const entityId = primaryTriggerEntity(task?.trigger_config);
if (entityId) {
// Drop the stale-range series so the chart shows its loading state.
const updated = new Map(this._detailStatsData);
@@ -1111,7 +1114,8 @@ export class MaintenanceSupporterPanel extends LitElement {
const entities: Array<{ entityId: string; isCounter: boolean }> = [];
for (const obj of this._objects) {
for (const task of obj.tasks) {
const entityId = task.trigger_config?.entity_id;
// The battery fleet's row shows "N to replace", not a sparkline.
const entityId = task.battery_fleet_task ? undefined : primaryTriggerEntity(task.trigger_config);
if (!entityId) continue;
entities.push({ entityId, isCounter: this._isCounterEntity(task.trigger_config) });
}
@@ -1217,6 +1221,9 @@ export class MaintenanceSupporterPanel extends LitElement {
// Label filter (v2.26 — also captured by saved views)
if (this._filterLabel && !(task.labels || []).includes(this._filterLabel)) continue;
// Place filter (2026-10): the object's zone, "home" for none.
if (this._filterPlace && (obj.object.place || "home") !== this._filterPlace) continue;
// Priority filter (#134 — also captured by saved views). Tasks
// without an explicit priority are "normal", the model default.
if (this._filterPriority && (task.priority || "normal") !== this._filterPriority) continue;
@@ -1353,8 +1360,9 @@ export class MaintenanceSupporterPanel extends LitElement {
if (s.msp_view === "task" && s.msp_entry && s.msp_task) {
this._historyFilter = null;
const task = this._getTask(s.msp_entry, s.msp_task);
if (task?.trigger_config?.entity_id) {
this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config));
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
}
}
@@ -1454,6 +1462,16 @@ export class MaintenanceSupporterPanel extends LitElement {
return [...seen].sort((a, b) => a.localeCompare(b));
}
/** 2026-10 places: the zones objects are maintained at, for the filter. */
private get _allPlaces(): { id: string; name: string }[] {
const seen = new Map<string, string>();
for (const obj of this._objects) {
const o = obj.object;
if (o.place && !seen.has(o.place)) seen.set(o.place, o.place_name || o.place);
}
return [...seen].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
}
/** The panel's current task-list filter state, in the shape a view stores. */
private get _currentFilters(): SavedViewFilters {
return {
@@ -1615,8 +1633,9 @@ export class MaintenanceSupporterPanel extends LitElement {
this._resetStickyPane();
this._fetchFullHistory(entryId, taskId);
const task = this._getTask(entryId, taskId);
if (task?.trigger_config?.entity_id) {
this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config));
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
return;
}
@@ -1637,10 +1656,9 @@ export class MaintenanceSupporterPanel extends LitElement {
// Lazy-load statistics for the task's trigger entity
const task = this._getTask(entryId, taskId);
if (task?.trigger_config?.entity_id) {
const entityId = task.trigger_config.entity_id;
const isCounter = this._isCounterEntity(task.trigger_config);
this._fetchDetailStats(entityId, isCounter);
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
}
@@ -3196,6 +3214,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<maintenance-object-dialog
.hass=${this.hass}
.objects=${this._objects}
.placesEnabled=${this._features.places}
@object-saved=${this._onObjectSaved}
@object-replaced=${this._onObjectReplaced}
></maintenance-object-dialog>
@@ -3543,6 +3562,7 @@ export class MaintenanceSupporterPanel extends LitElement {
(this._filterStatus ? 1 : 0) +
(this._filterUser ? 1 : 0) +
(this._filterLabel ? 1 : 0) +
(this._filterPlace ? 1 : 0) +
(this._filterPriority ? 1 : 0) +
(this._activeViewId ? 1 : 0);
@@ -3626,6 +3646,24 @@ export class MaintenanceSupporterPanel extends LitElement {
</select>
</label>
` : nothing}
${this._features.places && this._allPlaces.length > 0 ? html`
<label class="filter-field">
<span class="filter-label">${t("filter_place", L)}</span>
<select
.value=${this._filterPlace}
@change=${(e: Event) => {
this._filterPlace = (e.target as HTMLSelectElement).value;
this._activeViewId = "";
}}
>
<option value="">${t("filter_place_all", L)}</option>
<option value="home" ?selected=${this._filterPlace === "home"}>${t("place_home", L)}</option>
${this._allPlaces.map(
(pl) => html`<option value=${pl.id} ?selected=${this._filterPlace === pl.id}>${pl.name}</option>`
)}
</select>
</label>
` : nothing}
<label class="filter-field">
<span class="filter-label">${t("priority", L)}</span>
<select
@@ -4743,6 +4781,13 @@ export class MaintenanceSupporterPanel extends LitElement {
<a href="#" class="object-area-link" @click=${(e: Event) => { e.preventDefault(); this._showArea(o.area_id!); }}
>${areaDisplayName(o.area_id, this.hass?.areas, t("no_area", L))}</a></p>`
: nothing}
${o.place
? html`<p class="meta place-meta">
<ha-icon icon="mdi:map-marker-outline"></ha-icon>
${t("filter_place", L)}: ${o.place_name || o.place}${o.remind_on_site ? html` · ${t("remind_on_site", L)}` : nothing}
${o.place_missing ? html`<span class="place-missing-badge">${t("place_missing", L)}</span>` : nothing}
</p>`
: nothing}
${isSafeHttpUrl(o.documentation_url)
? html`<p class="meta">${t("documentation_url_label", L)}:
<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer">${o.documentation_url}</a>