113 files

This commit is contained in:
Home Assistant Version Control
2026-08-26 07:17:31 +00:00
parent 0533eaca8d
commit 15c0e35c18
113 changed files with 12448 additions and 1673 deletions
@@ -48,8 +48,13 @@ describe("budget KPI tiles: spent-only display (#104)", () => {
const tiles = sr(el).querySelectorAll(".budget-tile");
expect(tiles.length).to.equal(2);
expect(sr(el).querySelectorAll(".budget-tile-bar").length).to.equal(1);
expect(tiles[0].textContent).to.contain("9.00 / 150 €");
// 2026-08-24: the spent amount carries the full stat-value typography
// (same size as the other KPI chips); the "/ max" ratio is its own
// small line so it can't overflow the grid cell.
expect(tiles[0].querySelector(".stat-value")!.textContent).to.contain("9.00 €");
expect(tiles[0].querySelector(".budget-tile-max")!.textContent).to.contain("/ 150 €");
expect(tiles[1].textContent).to.contain("429.60 €");
expect(tiles[1].querySelector(".budget-tile-max"), "spent-only tile has no ratio line").to.equal(null);
});
it("tiles live INSIDE the stats strip (#125)", async () => {
@@ -99,6 +99,25 @@ describe("trigger-chart", () => {
const marks = el.shadowRoot!.querySelectorAll('rect[fill="var(--success-color, #4caf50)"]');
expect(marks.length).to.equal(1);
});
it("renders the production-shaped projection with real horizontal extent", async () => {
// sparkline.ts builds the projection as [last sample, last sample + 30d].
// Before the domain fix (2026-08-24) the data-only time domain put that
// start on the right plot edge and the x2 clamp collapsed the dashed
// line to zero width — the degradation projection never rendered.
const el = await mount();
const last = POINTS[POINTS.length - 1];
el.projection = [last, { ts: last.ts + 30 * DAY, val: last.val + 15 }];
await el.updateComplete;
const line = el.shadowRoot!.querySelector('line[stroke-dasharray="4,3"]');
expect(line, "projection line rendered").to.exist;
const x1 = Number(line!.getAttribute("x1"));
const x2 = Number(line!.getAttribute("x2"));
expect(x2 - x1, `projection width (${x1} -> ${x2})`).to.be.greaterThan(50);
// still clamped inside the plot
const svgW = Number(el.shadowRoot!.querySelector("svg")!.getAttribute("width"));
expect(x2).to.be.at.most(svgW);
});
});
describe("chart-utils", () => {
@@ -76,8 +76,11 @@ export class MaintenanceBudgetSectionCard extends LitElement {
this._busy = true;
this._error = "";
try {
const m = parseFloat(this._localMonthly);
const y = parseFloat(this._localYearly);
// An emptied field means "remove this budget" and must SEND 0 (the
// backend's off-state) — omitting the key kept the old value, so a
// budget could never be cleared from this card (bug audit 2026-08-22).
const m = this._localMonthly.trim() === "" ? 0 : parseFloat(this._localMonthly);
const y = this._localYearly.trim() === "" ? 0 : parseFloat(this._localYearly);
const settings: Record<string, number> = {};
if (!isNaN(m) && m >= 0) settings.budget_monthly = m;
if (!isNaN(y) && y >= 0) settings.budget_yearly = y;
@@ -71,7 +71,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
private _onAddTask(): void {
if (!this._entryId) return;
import("../dialog-mount").then(({ openCreateTaskDialog }) => {
openCreateTaskDialog();
openCreateTaskDialog(this._entryId!);
this.close();
});
}
@@ -16,6 +16,8 @@ import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { isoDateLocal } from "../helpers/calendar-bucket";
import { partsForCompletion } from "../helpers/shared-parts";
import { renderWeibullSection } from "../renderers/weibull";
import { renderPredictionSection } from "../renderers/prediction";
import { renderRecommendationBars } from "../renderers/recommendation";
@@ -75,7 +77,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
this._showReset = false;
this._showAdaptive = false;
this._skipReason = "";
this._resetDate = new Date().toISOString().slice(0, 10);
// Local calendar date — toISOString() is UTC and prefills YESTERDAY for
// users east of UTC before their morning (bug audit 2026-08-22).
this._resetDate = isoDateLocal(new Date());
this._open = true;
await Promise.all([this._loadTask(), this._loadFeatures()]);
}
@@ -146,14 +150,36 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
private _onComplete(): void {
if (!this._entryId || !this._taskId || !this._task) return;
// Reuse the existing rich complete-dialog by mounting it on body
import("../dialog-mount").then(({ openCompleteDialog }) => {
// Reuse the existing rich complete-dialog by mounting it on body.
// Pass EVERYTHING the card's direct path passes — omitting
// required_completion_fields let a mandatory note be skipped, and a
// reading task without type+unit never rendered its value field
// (bug audit 2026-08-22).
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
const task = this._task!;
const isBuy = !!(task as { part_ref?: string }).part_ref;
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
if (!isBuy) {
try {
const r = await this.hass.connection.sendMessagePromise<{
objects: MaintenanceObjectResponse[];
}>({ type: "maintenance_supporter/objects", compact: true });
parts = partsForCompletion(task, this._entryId!, r.objects || [], this._lang);
} catch {
// Parts stay empty — the dialog still completes without them.
}
}
const ok = openCompleteDialog({
entry_id: this._entryId!,
task_id: this._taskId!,
task_name: this._task!.name,
checklist: this._task!.checklist || [],
adaptive_enabled: !!this._task!.adaptive_config?.enabled,
task_name: task.name,
checklist: task.checklist || [],
adaptive_enabled: !!task.adaptive_config?.enabled,
required_completion_fields: task.required_completion_fields || [],
task_type: task.type || "",
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
parts,
consumes_parts: isBuy ? [] : (task.consumes_parts || []),
});
if (ok) {
this._notifyChanged("complete");
@@ -156,7 +156,14 @@ export class MaintenanceTriggerChart extends LitElement {
}
const tsMin = pts[0].ts;
const tsMax = pts[pts.length - 1].ts;
// The dashed projection extends the TIME domain. sparkline.ts builds it
// as [last sample, last sample + 30 d]; with a data-only domain its
// start sat exactly on the right plot edge and the x2 clamp below
// collapsed the line to zero width — the production degradation
// projection never rendered (found via the design-system previews,
// 2026-08-24).
const projEnd = this.projection && this.projection.length === 2 ? this.projection[1].ts : null;
const tsMax = projEnd != null ? Math.max(pts[pts.length - 1].ts, projEnd) : pts[pts.length - 1].ts;
const tsSpan = tsMax - tsMin || 1;
const withYear = needsYear(tsMin, tsMax);
@@ -167,7 +167,10 @@ export function openEditObjectDialog(
return true;
}
export function openCreateTaskDialog(): boolean {
export function openCreateTaskDialog(
entryId = "",
objects?: Array<{ entry_id: string; object: { name: string } }>,
): boolean {
const dlg = getOrCreate<MaintenanceTaskDialog>(TASK_DIALOG_TAG);
if (!syncHass(dlg)) return false;
const hass = getHass();
@@ -179,13 +182,20 @@ export function openCreateTaskDialog(): boolean {
scheduleTimeEnabled: boolean;
completionActionsEnabled: boolean;
defaultWarningDays: number;
openCreate: (entryId?: string) => void;
openCreate: (
entryId: string,
objects?: Array<{ entry_id: string; object: { name: string } }>,
) => void;
};
dlgFull.checklistsEnabled = settings.features.checklists;
dlgFull.scheduleTimeEnabled = settings.features.schedule_time;
dlgFull.completionActionsEnabled = settings.features.completion_actions;
dlgFull.defaultWarningDays = settings.defaultWarningDays;
dlgFull.openCreate();
// openCreate NEEDS a target: a bare call left _entryId undefined and
// _objectChoices empty — no object picker, and save failed on the
// backend's required entry_id (bug audit 2026-08-22). Callers pass the
// entry (quick-actions) or their object list (card header button).
dlgFull.openCreate(entryId, objects);
})();
return true;
}
@@ -273,6 +283,12 @@ export function openCompleteDialog(args: {
adaptive_enabled?: boolean;
/** Details the task demands before it counts as done (v2.44). */
required_completion_fields?: string[];
/** Reading tasks need type+unit or the value field never renders. */
task_type?: string;
reading_unit?: string;
/** #99/#111: per-completion parts selection incl. shared pools. */
parts?: MaintenanceCompleteDialog["parts"];
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
}): boolean {
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
if (!syncHass(dlg)) return false;
@@ -282,6 +298,12 @@ export function openCompleteDialog(args: {
dlg.checklist = args.checklist ?? [];
dlg.adaptiveEnabled = !!args.adaptive_enabled;
dlg.requiredFields = args.required_completion_fields ?? [];
// Always assign — the dialog is a singleton, so an omitted field must not
// leak the previous task's value.
dlg.taskType = args.task_type ?? "";
dlg.readingUnit = args.reading_unit ?? "";
dlg.parts = args.parts ?? [];
dlg.consumesParts = args.consumes_parts ?? [];
dlg.lang = (getHass()?.language) || "en";
dlg.open();
return true;
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Výchozí filtr uživatele",
"cal_editor_my_tasks": "Moje úkoly (aktuální uživatel)",
"cal_editor_show_object_filter": "Zobrazit filtr objektu",
"cal_editor_object_hint": "Předvyberte objekt přes YAML: object_filter: \"<název>\" — nebo seznam názvů pro omezení karty na více objektů."
"cal_editor_object_hint": "Předvyberte objekt přes YAML: object_filter: \"<název>\" — nebo seznam názvů pro omezení karty na více objektů.",
"object_history_section": "Historie (všechny úkoly)",
"object_history_all_tasks": "Všechny úkoly",
"object_history_empty": "V tomto období nejsou žádné záznamy.",
"object_history_cap_note": "Historie uchovává až 500 záznamů na úkol — velmi staré záznamy mohou chybět.",
"service_record_title": "Servisní knížka",
"service_record_print": "Servisní knížka (PDF)",
"date": "Datum",
"service_record_entries": "záznamů",
"completed_by": "Dokončil",
"date_from": "Od",
"date_to": "Do"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Standard brugerfilter",
"cal_editor_my_tasks": "Mine opgaver (aktuel bruger)",
"cal_editor_show_object_filter": "Vis objektfilter",
"cal_editor_object_hint": "Forvælg et objekt via YAML: object_filter: \"<navn>\" — eller en liste af navne for at begrænse kortet til flere objekter."
"cal_editor_object_hint": "Forvælg et objekt via YAML: object_filter: \"<navn>\" — eller en liste af navne for at begrænse kortet til flere objekter.",
"object_history_section": "Historik (alle opgaver)",
"object_history_all_tasks": "Alle opgaver",
"object_history_empty": "Ingen poster i denne periode.",
"object_history_cap_note": "Historikken gemmer op til 500 poster pr. opgave — meget gamle poster kan mangle.",
"service_record_title": "Servicebog",
"service_record_print": "Servicebog (PDF)",
"date": "Dato",
"service_record_entries": "poster",
"completed_by": "Udført af",
"date_from": "Fra",
"date_to": "Til"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Standard-Benutzerfilter",
"cal_editor_my_tasks": "Meine Aufgaben (aktueller Benutzer)",
"cal_editor_show_object_filter": "Objektfilter-Dropdown anzeigen",
"cal_editor_object_hint": "Ein Objekt per YAML vorauswählen: object_filter: \"<Objektname>\" — oder eine Namensliste, um die Karte auf mehrere Objekte zu beschränken."
"cal_editor_object_hint": "Ein Objekt per YAML vorauswählen: object_filter: \"<Objektname>\" — oder eine Namensliste, um die Karte auf mehrere Objekte zu beschränken.",
"object_history_section": "Verlauf (alle Aufgaben)",
"object_history_all_tasks": "Alle Aufgaben",
"object_history_empty": "Keine Einträge in diesem Zeitraum.",
"object_history_cap_note": "Der Verlauf umfasst bis zu 500 Einträge pro Aufgabe — sehr alte Einträge können fehlen.",
"service_record_title": "Serviceheft",
"service_record_print": "Serviceheft (PDF)",
"date": "Datum",
"service_record_entries": "Einträge",
"completed_by": "Erledigt von",
"date_from": "Von",
"date_to": "Bis"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Default user filter",
"cal_editor_my_tasks": "My tasks (current user)",
"cal_editor_show_object_filter": "Show object filter dropdown",
"cal_editor_object_hint": "Pre-select one object via YAML: object_filter: \"<object name>\" — or a list of names to restrict the card to several objects."
"cal_editor_object_hint": "Pre-select one object via YAML: object_filter: \"<object name>\" — or a list of names to restrict the card to several objects.",
"object_history_section": "History (all tasks)",
"object_history_all_tasks": "All tasks",
"object_history_empty": "No entries in this range.",
"object_history_cap_note": "History keeps up to 500 entries per task — very old entries may be missing.",
"service_record_title": "Service record",
"service_record_print": "Service record (PDF)",
"date": "Date",
"service_record_entries": "entries",
"completed_by": "Completed by",
"date_from": "From",
"date_to": "To"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Filtro de usuario predeterminado",
"cal_editor_my_tasks": "Mis tareas (usuario actual)",
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
"cal_editor_object_hint": "Preselecciona un objeto por YAML: object_filter: \"<nombre>\" — o una lista de nombres para limitar la tarjeta a varios objetos."
"cal_editor_object_hint": "Preselecciona un objeto por YAML: object_filter: \"<nombre>\" — o una lista de nombres para limitar la tarjeta a varios objetos.",
"object_history_section": "Historial (todas las tareas)",
"object_history_all_tasks": "Todas las tareas",
"object_history_empty": "No hay entradas en este periodo.",
"object_history_cap_note": "El historial conserva hasta 500 entradas por tarea; las entradas muy antiguas pueden faltar.",
"service_record_title": "Registro de mantenimiento",
"service_record_print": "Registro de mantenimiento (PDF)",
"date": "Fecha",
"service_record_entries": "entradas",
"completed_by": "Realizado por",
"date_from": "Desde",
"date_to": "Hasta"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Oletuskäyttäjäsuodatin",
"cal_editor_my_tasks": "Omat tehtävät (nykyinen käyttäjä)",
"cal_editor_show_object_filter": "Näytä kohdesuodatin",
"cal_editor_object_hint": "Esivalitse kohde YAML:lla: object_filter: \"<nimi>\" — tai nimilista rajataksesi kortin useisiin kohteisiin."
"cal_editor_object_hint": "Esivalitse kohde YAML:lla: object_filter: \"<nimi>\" — tai nimilista rajataksesi kortin useisiin kohteisiin.",
"object_history_section": "Historia (kaikki tehtävät)",
"object_history_all_tasks": "Kaikki tehtävät",
"object_history_empty": "Ei merkintöjä tällä aikavälillä.",
"object_history_cap_note": "Historia säilyttää enintään 500 merkintää tehtävää kohden — hyvin vanhat merkinnät voivat puuttua.",
"service_record_title": "Huoltokirja",
"service_record_print": "Huoltokirja (PDF)",
"date": "Päivämäärä",
"service_record_entries": "merkintää",
"completed_by": "Suorittanut",
"date_from": "Alkaen",
"date_to": "Asti"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Filtre utilisateur par défaut",
"cal_editor_my_tasks": "Mes tâches (utilisateur actuel)",
"cal_editor_show_object_filter": "Afficher le filtre d'objet",
"cal_editor_object_hint": "Présélectionnez un objet via YAML : object_filter : \"<nom>\" — ou une liste de noms pour limiter la carte à plusieurs objets."
"cal_editor_object_hint": "Présélectionnez un objet via YAML : object_filter : \"<nom>\" — ou une liste de noms pour limiter la carte à plusieurs objets.",
"object_history_section": "Historique (toutes les tâches)",
"object_history_all_tasks": "Toutes les tâches",
"object_history_empty": "Aucune entrée sur cette période.",
"object_history_cap_note": "L'historique conserve jusqu'à 500 entrées par tâche — les entrées très anciennes peuvent manquer.",
"service_record_title": "Carnet d'entretien",
"service_record_print": "Carnet d'entretien (PDF)",
"date": "Date",
"service_record_entries": "entrées",
"completed_by": "Réalisé par",
"date_from": "Du",
"date_to": "Au"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "डिफ़ॉल्ट उपयोगकर्ता फ़िल्टर",
"cal_editor_my_tasks": "मेरे कार्य (वर्तमान उपयोगकर्ता)",
"cal_editor_show_object_filter": "ऑब्जेक्ट फ़िल्टर दिखाएँ",
"cal_editor_object_hint": "YAML से एक ऑब्जेक्ट पहले से चुनें: object_filter: \"<नाम>\" — या कार्ड को कई ऑब्जेक्ट तक सीमित करने हेतु नामों की सूची।"
"cal_editor_object_hint": "YAML से एक ऑब्जेक्ट पहले से चुनें: object_filter: \"<नाम>\" — या कार्ड को कई ऑब्जेक्ट तक सीमित करने हेतु नामों की सूची।",
"object_history_section": "इतिहास (सभी कार्य)",
"object_history_all_tasks": "सभी कार्य",
"object_history_empty": "इस अवधि में कोई प्रविष्टि नहीं है।",
"object_history_cap_note": "इतिहास प्रति कार्य अधिकतम 500 प्रविष्टियाँ रखता है — बहुत पुरानी प्रविष्टियाँ अनुपस्थित हो सकती हैं।",
"service_record_title": "सेवा रिकॉर्ड",
"service_record_print": "सेवा रिकॉर्ड (PDF)",
"date": "दिनांक",
"service_record_entries": "प्रविष्टियाँ",
"completed_by": "द्वारा पूर्ण",
"date_from": "से",
"date_to": "तक"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Alapértelmezett felhasználószűrő",
"cal_editor_my_tasks": "Saját feladatok (aktuális felhasználó)",
"cal_editor_show_object_filter": "Objektumszűrő megjelenítése",
"cal_editor_object_hint": "Előválasztás YAML-lel: object_filter: \"<név>\" — vagy névlista, hogy a kártya több objektumra korlátozódjon."
"cal_editor_object_hint": "Előválasztás YAML-lel: object_filter: \"<név>\" — vagy névlista, hogy a kártya több objektumra korlátozódjon.",
"object_history_section": "Előzmények (összes feladat)",
"object_history_all_tasks": "Összes feladat",
"object_history_empty": "Nincs bejegyzés ebben az időszakban.",
"object_history_cap_note": "Az előzmények feladatonként legfeljebb 500 bejegyzést őriznek meg — a nagyon régiek hiányozhatnak.",
"service_record_title": "Szervizkönyv",
"service_record_print": "Szervizkönyv (PDF)",
"date": "Dátum",
"service_record_entries": "bejegyzés",
"completed_by": "Elvégezte",
"date_from": "Ettől",
"date_to": "Eddig"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Filtro utente predefinito",
"cal_editor_my_tasks": "Le mie attività (utente attuale)",
"cal_editor_show_object_filter": "Mostra il filtro oggetto",
"cal_editor_object_hint": "Preseleziona un oggetto via YAML: object_filter: \"<nome>\" — o un elenco di nomi per limitare la scheda a più oggetti."
"cal_editor_object_hint": "Preseleziona un oggetto via YAML: object_filter: \"<nome>\" — o un elenco di nomi per limitare la scheda a più oggetti.",
"object_history_section": "Cronologia (tutte le attività)",
"object_history_all_tasks": "Tutte le attività",
"object_history_empty": "Nessuna voce in questo periodo.",
"object_history_cap_note": "La cronologia conserva fino a 500 voci per attività — le voci molto vecchie potrebbero mancare.",
"service_record_title": "Libretto di manutenzione",
"service_record_print": "Libretto di manutenzione (PDF)",
"date": "Data",
"service_record_entries": "voci",
"completed_by": "Completato da",
"date_from": "Dal",
"date_to": "Al"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "既定のユーザーフィルター",
"cal_editor_my_tasks": "自分のタスク(現在のユーザー)",
"cal_editor_show_object_filter": "オブジェクトフィルターを表示",
"cal_editor_object_hint": "YAML でオブジェクトを事前選択:object_filter: \"<名前>\" — 複数指定はカードを複数オブジェクトに限定します。"
"cal_editor_object_hint": "YAML でオブジェクトを事前選択:object_filter: \"<名前>\" — 複数指定はカードを複数オブジェクトに限定します。",
"object_history_section": "履歴(全タスク)",
"object_history_all_tasks": "すべてのタスク",
"object_history_empty": "この期間の記録はありません。",
"object_history_cap_note": "履歴はタスクごとに最大500件まで保持されます。非常に古い記録は含まれない場合があります。",
"service_record_title": "整備記録",
"service_record_print": "整備記録(PDF)",
"date": "日付",
"service_record_entries": "件",
"completed_by": "実施者",
"date_from": "開始",
"date_to": "終了"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "기본 사용자 필터",
"cal_editor_my_tasks": "내 작업 (현재 사용자)",
"cal_editor_show_object_filter": "객체 필터 표시",
"cal_editor_object_hint": "YAML로 객체를 미리 선택: object_filter: \"<이름>\" — 이름 목록으로 카드를 여러 객체로 제한할 수 있습니다."
"cal_editor_object_hint": "YAML로 객체를 미리 선택: object_filter: \"<이름>\" — 이름 목록으로 카드를 여러 객체로 제한할 수 있습니다.",
"object_history_section": "기록(전체 작업)",
"object_history_all_tasks": "모든 작업",
"object_history_empty": "이 기간에 기록이 없습니다.",
"object_history_cap_note": "기록은 작업당 최대 500건까지 보관됩니다. 아주 오래된 기록은 없을 수 있습니다.",
"service_record_title": "정비 기록",
"service_record_print": "정비 기록(PDF)",
"date": "날짜",
"service_record_entries": "건",
"completed_by": "수행자",
"date_from": "시작",
"date_to": "종료"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Standard brukerfilter",
"cal_editor_my_tasks": "Mine oppgaver (gjeldende bruker)",
"cal_editor_show_object_filter": "Vis objektfilter",
"cal_editor_object_hint": "Forhåndsvelg et objekt via YAML: object_filter: \"<navn>\" — eller en liste med navn for å begrense kortet til flere objekter."
"cal_editor_object_hint": "Forhåndsvelg et objekt via YAML: object_filter: \"<navn>\" — eller en liste med navn for å begrense kortet til flere objekter.",
"object_history_section": "Historikk (alle oppgaver)",
"object_history_all_tasks": "Alle oppgaver",
"object_history_empty": "Ingen oppføringer i denne perioden.",
"object_history_cap_note": "Historikken beholder opptil 500 oppføringer per oppgave — svært gamle oppføringer kan mangle.",
"service_record_title": "Servicehefte",
"service_record_print": "Servicehefte (PDF)",
"date": "Dato",
"service_record_entries": "oppføringer",
"completed_by": "Utført av",
"date_from": "Fra",
"date_to": "Til"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Standaard gebruikersfilter",
"cal_editor_my_tasks": "Mijn taken (huidige gebruiker)",
"cal_editor_show_object_filter": "Objectfilter tonen",
"cal_editor_object_hint": "Selecteer een object vooraf via YAML: object_filter: \"<naam>\" — of een lijst met namen om de kaart tot meerdere objecten te beperken."
"cal_editor_object_hint": "Selecteer een object vooraf via YAML: object_filter: \"<naam>\" — of een lijst met namen om de kaart tot meerdere objecten te beperken.",
"object_history_section": "Geschiedenis (alle taken)",
"object_history_all_tasks": "Alle taken",
"object_history_empty": "Geen items in deze periode.",
"object_history_cap_note": "De geschiedenis bewaart maximaal 500 items per taak — zeer oude items kunnen ontbreken.",
"service_record_title": "Onderhoudsboekje",
"service_record_print": "Onderhoudsboekje (PDF)",
"date": "Datum",
"service_record_entries": "items",
"completed_by": "Voltooid door",
"date_from": "Van",
"date_to": "Tot"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Domyślny filtr użytkownika",
"cal_editor_my_tasks": "Moje zadania (bieżący użytkownik)",
"cal_editor_show_object_filter": "Pokaż filtr obiektu",
"cal_editor_object_hint": "Wybierz obiekt w YAML: object_filter: \"<nazwa>\" — lub listę nazw, aby ograniczyć kartę do kilku obiektów."
"cal_editor_object_hint": "Wybierz obiekt w YAML: object_filter: \"<nazwa>\" — lub listę nazw, aby ograniczyć kartę do kilku obiektów.",
"object_history_section": "Historia (wszystkie zadania)",
"object_history_all_tasks": "Wszystkie zadania",
"object_history_empty": "Brak wpisów w tym okresie.",
"object_history_cap_note": "Historia przechowuje do 500 wpisów na zadanie — bardzo stare wpisy mogą brakować.",
"service_record_title": "Książka serwisowa",
"service_record_print": "Książka serwisowa (PDF)",
"date": "Data",
"service_record_entries": "wpisów",
"completed_by": "Wykonane przez",
"date_from": "Od",
"date_to": "Do"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Filtro de usuário padrão",
"cal_editor_my_tasks": "Minhas tarefas (usuário atual)",
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos."
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos.",
"object_history_section": "Histórico (todas as tarefas)",
"object_history_all_tasks": "Todas as tarefas",
"object_history_empty": "Sem registros neste período.",
"object_history_cap_note": "O histórico guarda até 500 registros por tarefa — registros muito antigos podem faltar.",
"service_record_title": "Registro de manutenção",
"service_record_print": "Registro de manutenção (PDF)",
"date": "Data",
"service_record_entries": "registros",
"completed_by": "Concluído por",
"date_from": "De",
"date_to": "Até"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Filtro de utilizador predefinido",
"cal_editor_my_tasks": "As minhas tarefas (utilizador atual)",
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos."
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos.",
"object_history_section": "Histórico (todas as tarefas)",
"object_history_all_tasks": "Todas as tarefas",
"object_history_empty": "Sem registos neste período.",
"object_history_cap_note": "O histórico guarda até 500 registos por tarefa — registos muito antigos podem faltar.",
"service_record_title": "Registo de manutenção",
"service_record_print": "Registo de manutenção (PDF)",
"date": "Data",
"service_record_entries": "registos",
"completed_by": "Concluído por",
"date_from": "De",
"date_to": "Até"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Фильтр пользователя по умолчанию",
"cal_editor_my_tasks": "Мои задачи (текущий пользователь)",
"cal_editor_show_object_filter": "Показывать фильтр объекта",
"cal_editor_object_hint": "Предварительный выбор объекта через YAML: object_filter: \"<имя>\" — или список имён, чтобы ограничить карточку несколькими объектами."
"cal_editor_object_hint": "Предварительный выбор объекта через YAML: object_filter: \"<имя>\" — или список имён, чтобы ограничить карточку несколькими объектами.",
"object_history_section": "История (все задачи)",
"object_history_all_tasks": "Все задачи",
"object_history_empty": "Нет записей за этот период.",
"object_history_cap_note": "История хранит до 500 записей на задачу — очень старые записи могут отсутствовать.",
"service_record_title": "Сервисная книжка",
"service_record_print": "Сервисная книжка (PDF)",
"date": "Дата",
"service_record_entries": "записей",
"completed_by": "Выполнил",
"date_from": "С",
"date_to": "По"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Standardanvändarfilter",
"cal_editor_my_tasks": "Mina uppgifter (aktuell användare)",
"cal_editor_show_object_filter": "Visa objektfilter",
"cal_editor_object_hint": "Förvälj ett objekt via YAML: object_filter: \"<namn>\" — eller en lista med namn för att begränsa kortet till flera objekt."
"cal_editor_object_hint": "Förvälj ett objekt via YAML: object_filter: \"<namn>\" — eller en lista med namn för att begränsa kortet till flera objekt.",
"object_history_section": "Historik (alla uppgifter)",
"object_history_all_tasks": "Alla uppgifter",
"object_history_empty": "Inga poster under denna period.",
"object_history_cap_note": "Historiken sparar upp till 500 poster per uppgift — mycket gamla poster kan saknas.",
"service_record_title": "Servicebok",
"service_record_print": "Servicebok (PDF)",
"date": "Datum",
"service_record_entries": "poster",
"completed_by": "Utförd av",
"date_from": "Från",
"date_to": "Till"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Varsayılan kullanıcı filtresi",
"cal_editor_my_tasks": "Görevlerim (geçerli kullanıcı)",
"cal_editor_show_object_filter": "Nesne filtresini göster",
"cal_editor_object_hint": "YAML ile bir nesne önceden seçin: object_filter: \"<ad>\" — veya kartı birden çok nesneyle sınırlamak için ad listesi."
"cal_editor_object_hint": "YAML ile bir nesne önceden seçin: object_filter: \"<ad>\" — veya kartı birden çok nesneyle sınırlamak için ad listesi.",
"object_history_section": "Geçmiş (tüm görevler)",
"object_history_all_tasks": "Tüm görevler",
"object_history_empty": "Bu aralıkta kayıt yok.",
"object_history_cap_note": "Geçmiş, görev başına en fazla 500 kayıt tutar — çok eski kayıtlar eksik olabilir.",
"service_record_title": "Servis kaydı",
"service_record_print": "Servis kaydı (PDF)",
"date": "Tarih",
"service_record_entries": "kayıt",
"completed_by": "Tamamlayan",
"date_from": "Başlangıç",
"date_to": "Bitiş"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "Типовий фільтр користувача",
"cal_editor_my_tasks": "Мої завдання (поточний користувач)",
"cal_editor_show_object_filter": "Показувати фільтр об'єкта",
"cal_editor_object_hint": "Попередній вибір об'єкта через YAML: object_filter: \"<назва>\" — або список назв, щоб обмежити картку кількома об'єктами."
"cal_editor_object_hint": "Попередній вибір об'єкта через YAML: object_filter: \"<назва>\" — або список назв, щоб обмежити картку кількома об'єктами.",
"object_history_section": "Історія (усі завдання)",
"object_history_all_tasks": "Усі завдання",
"object_history_empty": "Немає записів за цей період.",
"object_history_cap_note": "Історія зберігає до 500 записів на завдання — дуже старі записи можуть бути відсутні.",
"service_record_title": "Сервісна книжка",
"service_record_print": "Сервісна книжка (PDF)",
"date": "Дата",
"service_record_entries": "записів",
"completed_by": "Виконав",
"date_from": "Від",
"date_to": "До"
}
@@ -878,5 +878,16 @@
"cal_editor_default_user": "默认用户筛选",
"cal_editor_my_tasks": "我的任务(当前用户)",
"cal_editor_show_object_filter": "显示对象筛选",
"cal_editor_object_hint": "通过 YAML 预选对象:object_filter: \"<对象名>\" — 或名称列表,将卡片限定为多个对象。"
"cal_editor_object_hint": "通过 YAML 预选对象:object_filter: \"<对象名>\" — 或名称列表,将卡片限定为多个对象。",
"object_history_section": "历史(全部任务)",
"object_history_all_tasks": "全部任务",
"object_history_empty": "此时间段内没有记录。",
"object_history_cap_note": "每个任务的历史最多保留 500 条记录,很早的记录可能缺失。",
"service_record_title": "维护记录",
"service_record_print": "维护记录(PDF)",
"date": "日期",
"service_record_entries": "条记录",
"completed_by": "完成人",
"date_from": "从",
"date_to": "至"
}
@@ -429,7 +429,7 @@ export class MaintenanceSupporterCard extends LitElement {
<mwc-icon-button
class="hdr-add"
title="${t("add_task", L)}"
@click=${() => openCreateTaskDialog()}
@click=${() => openCreateTaskDialog("", this._objects)}
>
<ha-icon icon="mdi:playlist-plus"></ha-icon>
</mwc-icon-button>
@@ -44,6 +44,7 @@ import { UserService } from "./user-service";
import type { MaintenanceObjectDialog } from "./components/object-dialog";
import "./components/documents-section";
import "./components/parts-section";
import "./components/object-history-section";
import "./components/task-documents";
import type { MaintenanceTaskDialog } from "./components/task-dialog";
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
@@ -3078,7 +3079,8 @@ export class MaintenanceSupporterPanel extends LitElement {
const color = pct >= 100 ? "var(--error-color, #f44336)" : pct >= b.alert_threshold_pct ? "var(--warning-color, #ff9800)" : "var(--success-color, #4caf50)";
return html`
<div class="stat-item budget-tile" title="${label}: ${spent.toFixed(2)} / ${budget.toFixed(2)} ${cs}">
<span class="stat-value budget-tile-value">${spent.toFixed(2)} / ${budget.toFixed(0)} ${cs}</span>
<span class="stat-value budget-tile-value">${spent.toFixed(2)} ${cs}</span>
<span class="budget-tile-max">/ ${budget.toFixed(0)} ${cs}</span>
<div class="budget-tile-bar"><div style="width:${pct}%; background:${color}"></div></div>
<span class="stat-label">${label}</span>
</div>
@@ -3341,6 +3343,16 @@ export class MaintenanceSupporterPanel extends LitElement {
.currencySymbol=${this._currencySymbol}
@parts-changed=${() => this._loadData()}
></maintenance-parts-section>
<maintenance-object-history-section
.hass=${this.hass}
.entryId=${obj.entry_id}
.object=${o}
.tasks=${obj.tasks}
.currencySymbol=${this._currencySymbol}
.userName=${(id: string) => this._userService?.getUserName(id) ?? null}
@open-task=${(e: CustomEvent<{ taskId: string }>) => this._showTask(obj.entry_id, e.detail.taskId)}
></maintenance-object-history-section>
</div>
`;
}
@@ -151,6 +151,13 @@ function progressSpec(task: MaintenanceTask, unit: string, ctx: SparklineContext
case "counter": {
const target = tc.trigger_target_value;
if (target == null || target <= 0) return null;
if (!tc.trigger_delta_mode) {
// Non-delta counters count from zero since the last reset — the raw
// value IS the progress. Subtracting a baseline here showed a fresh
// cycle as stuck at 0 (progress.ts branches the same way; bug audit
// 2026-08-22).
return { progress: Math.max(0, cur), target, unit, meter: null };
}
const base = counterBaseline(task, rawStatsPoints(task, ctx));
return { progress: Math.max(0, cur - (base?.value ?? cur)), target, unit, meter: cur };
}
@@ -296,13 +303,17 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
let forceZero = false;
if (triggerType === "counter" && tc.trigger_target_value != null && points.length) {
// Progress domain: cumulative since the last service, never negative.
const base = counterBaseline(task, points);
if (base) {
if (base.ts != null) {
const kept = points.filter((p) => p.ts >= base.ts!);
if (kept.length >= 2) points = kept;
// Baseline subtraction is a DELTA-mode concept — a non-delta counter's
// raw value already is the cycle progress (bug audit 2026-08-22).
if (tc.trigger_delta_mode) {
const base = counterBaseline(task, points);
if (base) {
if (base.ts != null) {
const kept = points.filter((p) => p.ts >= base.ts!);
if (kept.length >= 2) points = kept;
}
points = points.map((p) => ({ ...p, val: Math.max(0, p.val - base.value) }));
}
points = points.map((p) => ({ ...p, val: Math.max(0, p.val - base.value) }));
}
targetValue = tc.trigger_target_value;
forceZero = true;
@@ -954,10 +954,20 @@ export const sharedStyles = css`
}
/* Budget KPI tiles in the stats strip (#125) — replaced the full-width
budget-bars row. */
budget-bars row. The spent amount inherits .stat-value's full 24px bold
so the budget tiles read exactly like the other KPI chips (user report
2026-08-24: the old 15px override made them visibly smaller); only the
"/ max" suffix stays secondary. */
.stat-item.budget-tile .budget-tile-value {
font-size: 15px;
padding-top: 5px;
white-space: nowrap;
}
/* The "/ max" ratio is its OWN small line between value and bar — inline
it overflowed the ~150px grid cell into the neighbouring tile once the
value took the full 24px. */
.budget-tile-max {
font-size: 11px;
line-height: 1.2;
color: var(--secondary-text-color);
white-space: nowrap;
}
.budget-tile-bar {
@@ -1440,9 +1450,10 @@ export const sharedStyles = css`
.weibull-info-row { flex-direction: column; gap: 8px; }
/* Budget tiles on narrow screens (#125): slightly smaller value so the
"x / y €" pair fits the wrapped grid cell. */
.stat-item.budget-tile .budget-tile-value { font-size: 13px; }
/* Budget tiles on narrow screens (#125): the spent amount keeps the
full chip size (consistency, user report 2026-08-24); the "/ max"
suffix is hidden instead — the bar and the title carry the ratio. */
.budget-tile-max { display: none; }
.group-card { min-width: 0; max-width: 100%; }
@@ -16,6 +16,7 @@ import { t } from "./styles";
* the raw key as-is so the user can still tell which input was rejected.
*/
const FIELD_LABEL_KEYS: Record<string, string> = {
entry_id: "object",
name: "name",
task_type: "maintenance_type",
schedule_type: "schedule_type",