117 files

This commit is contained in:
Home Assistant Version Control
2026-08-08 10:38:07 +00:00
parent c31830deeb
commit ca54a47de7
119 changed files with 9737 additions and 623 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid": 71, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786185306.3297718}
{"pid": 71, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786185437.2270892}
+26 -26
View File
@@ -81,117 +81,117 @@
},
{
"id": "88b90f2b1d6b45f0b44bee786ee1a525",
"url": "/taskmate/taskmate-attr-resolver.js?v=5.0.3",
"url": "/taskmate/taskmate-attr-resolver.js?v=5.0.4",
"type": "module"
},
{
"id": "0796c92931dd458e81dc9c2677248274",
"url": "/taskmate/taskmate-localize.js?v=5.0.3",
"url": "/taskmate/taskmate-localize.js?v=5.0.4",
"type": "module"
},
{
"id": "d2e281920ac7409583eafff1f1db69bd",
"url": "/taskmate/taskmate-design.js?v=5.0.3",
"url": "/taskmate/taskmate-design.js?v=5.0.4",
"type": "module"
},
{
"id": "9eae9632f0734480be5edd784b030673",
"url": "/taskmate/taskmate-badges-card.js?v=5.0.3",
"url": "/taskmate/taskmate-badges-card.js?v=5.0.4",
"type": "module"
},
{
"id": "5df49cd8a4eb420497b5df22cb31362c",
"url": "/taskmate/taskmate-child-card.js?v=5.0.3",
"url": "/taskmate/taskmate-child-card.js?v=5.0.4",
"type": "module"
},
{
"id": "b44c87d0d74f468cb96c5eabc340f9ee",
"url": "/taskmate/taskmate-rewards-card.js?v=5.0.3",
"url": "/taskmate/taskmate-rewards-card.js?v=5.0.4",
"type": "module"
},
{
"id": "787a96f9c30548708165a48457c99feb",
"url": "/taskmate/taskmate-approvals-card.js?v=5.0.3",
"url": "/taskmate/taskmate-approvals-card.js?v=5.0.4",
"type": "module"
},
{
"id": "84031597de424ad68e7c2bc66d06b72e",
"url": "/taskmate/taskmate-points-card.js?v=5.0.3",
"url": "/taskmate/taskmate-points-card.js?v=5.0.4",
"type": "module"
},
{
"id": "02613225844746b699bfbfac9642eca2",
"url": "/taskmate/taskmate-reorder-card.js?v=5.0.3",
"url": "/taskmate/taskmate-reorder-card.js?v=5.0.4",
"type": "module"
},
{
"id": "7a06a07e17ba4b31b712bc91b77360b4",
"url": "/taskmate/taskmate-overview-card.js?v=5.0.3",
"url": "/taskmate/taskmate-overview-card.js?v=5.0.4",
"type": "module"
},
{
"id": "08b42b53b33b4fa88db932455cf8d5aa",
"url": "/taskmate/taskmate-activity-card.js?v=5.0.3",
"url": "/taskmate/taskmate-activity-card.js?v=5.0.4",
"type": "module"
},
{
"id": "15907eff51784acdba88ff694b574509",
"url": "/taskmate/taskmate-streak-card.js?v=5.0.3",
"url": "/taskmate/taskmate-streak-card.js?v=5.0.4",
"type": "module"
},
{
"id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39",
"url": "/taskmate/taskmate-weekly-card.js?v=5.0.3",
"url": "/taskmate/taskmate-weekly-card.js?v=5.0.4",
"type": "module"
},
{
"id": "289ce5943f5a4972a593697db4c96ece",
"url": "/taskmate/taskmate-graph-card.js?v=5.0.3",
"url": "/taskmate/taskmate-graph-card.js?v=5.0.4",
"type": "module"
},
{
"id": "d2904acee4c74a398dcc7b996c23e7e7",
"url": "/taskmate/taskmate-reward-progress-card.js?v=5.0.3",
"url": "/taskmate/taskmate-reward-progress-card.js?v=5.0.4",
"type": "module"
},
{
"id": "46d378736fd54a26be253f2dfb27202e",
"url": "/taskmate/taskmate-leaderboard-card.js?v=5.0.3",
"url": "/taskmate/taskmate-leaderboard-card.js?v=5.0.4",
"type": "module"
},
{
"id": "db11ae36457842fa826c7991e1d9c6f4",
"url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.0.3",
"url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.0.4",
"type": "module"
},
{
"id": "ce85f301b59a412e9abfe5ffd51cb9a2",
"url": "/taskmate/taskmate-penalties-card.js?v=5.0.3",
"url": "/taskmate/taskmate-penalties-card.js?v=5.0.4",
"type": "module"
},
{
"id": "1959e1629de2422daa8adaf87069c288",
"url": "/taskmate/taskmate-bonuses-card.js?v=5.0.3",
"url": "/taskmate/taskmate-bonuses-card.js?v=5.0.4",
"type": "module"
},
{
"id": "2548524b269c4959bc7e0e967df943af",
"url": "/taskmate/taskmate-points-display-card.js?v=5.0.3",
"url": "/taskmate/taskmate-points-display-card.js?v=5.0.4",
"type": "module"
},
{
"id": "36ada49368db4187a283fbbbef45454f",
"url": "/taskmate/taskmate-calendar-card.js?v=5.0.3",
"url": "/taskmate/taskmate-calendar-card.js?v=5.0.4",
"type": "module"
},
{
"id": "67c32687acd2497b90030ef141f4b6db",
"url": "/taskmate/taskmate-photo-gallery-card.js?v=5.0.3",
"url": "/taskmate/taskmate-photo-gallery-card.js?v=5.0.4",
"type": "module"
},
{
"id": "6f0c8348a91f4b74a9b846ab3f62621f",
"url": "/taskmate/taskmate-family-goal-card.js?v=5.0.3",
"url": "/taskmate/taskmate-family-goal-card.js?v=5.0.4",
"type": "module"
},
{
@@ -256,7 +256,7 @@
},
{
"id": "8713ea9c008e4270bb28085eed5be959",
"url": "/hacsfiles/Statistics-Graph-Chart-Card/statistics-graph-chart-card.js?hacstag=1034932856333",
"url": "/hacsfiles/Statistics-Graph-Chart-Card/statistics-graph-chart-card.js?hacstag=1034932856400",
"type": "module"
},
{
@@ -281,7 +281,7 @@
},
{
"id": "73dbd2de03de4530b451a55bfe6c46cf",
"url": "/taskmate/taskmate-routine-card.js?v=5.0.3",
"url": "/taskmate/taskmate-routine-card.js?v=5.0.4",
"type": "module"
},
{
@@ -305,7 +305,7 @@
"type": "module"
},
{
"id": "ac6b7985c72348d3bd9a3a495514e65a",
"id": "350bafe29e8f43749bf07fed623b0a8c",
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
"type": "module"
}
@@ -1,10 +1,12 @@
/**
* Dashboard budget display: spent totals without a maximum (#104).
* Budget KPI tiles: spent totals without a maximum (#104, layout #125).
*
* With budget tracking enabled but no monthly/yearly maximum configured,
* the spent totals used to be invisible (a bar needs a denominator).
* Pins: spent-only lines render without a bar, a configured maximum still
* renders the classic bar, and the mixed case shows one of each.
* Since #125 the budgets render as KPI tiles in the stats strip; the #104
* semantics survive the move: a tile without a maximum shows the plain
* spent total and no bar, a configured maximum keeps its mini bar, and the
* mixed case shows one of each.
*/
import { expect } from "@open-wc/testing";
@@ -25,27 +27,37 @@ function handlers(budget: Record<string, unknown>) {
};
}
describe("dashboard budget: spent-only display (#104)", () => {
describe("budget KPI tiles: spent-only display (#104)", () => {
beforeEach(() => resetTaskSeq());
it("no maximum set: renders spent lines without bars", async () => {
it("no maximum set: tiles show spent totals without bars", async () => {
const { el } = await mountPanel([obj("e1", [task()])], handlers({
monthly_budget: 0, yearly_budget: 0, monthly_spent: 9, yearly_spent: 429.6,
}));
const items = sr(el).querySelectorAll(".budget-spent-only");
expect(items.length).to.equal(2);
expect(items[0].textContent).to.contain("9.00 €");
expect(items[1].textContent).to.contain("429.60 €");
expect(sr(el).querySelectorAll(".budget-bar").length).to.equal(0);
const tiles = sr(el).querySelectorAll(".budget-tile");
expect(tiles.length).to.equal(2);
expect(tiles[0].textContent).to.contain("9.00 €");
expect(tiles[1].textContent).to.contain("429.60 €");
expect(sr(el).querySelectorAll(".budget-tile-bar").length).to.equal(0);
});
it("mixed: monthly maximum renders a bar, yearly stays spent-only", async () => {
it("mixed: monthly maximum renders a mini bar, yearly stays spent-only", async () => {
const { el } = await mountPanel([obj("e1", [task()])], handlers({
monthly_budget: 150, yearly_budget: 0, monthly_spent: 9, yearly_spent: 429.6,
}));
expect(sr(el).querySelectorAll(".budget-bar").length).to.equal(1);
const spentOnly = sr(el).querySelectorAll(".budget-spent-only");
expect(spentOnly.length).to.equal(1);
expect(spentOnly[0].textContent).to.contain("429.60 €");
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 €");
expect(tiles[1].textContent).to.contain("429.60 €");
});
it("tiles live INSIDE the stats strip (#125)", async () => {
const { el } = await mountPanel([obj("e1", [task()])], handlers({
monthly_budget: 150, yearly_budget: 1500, monthly_spent: 9, yearly_spent: 429.6,
}));
const strip = sr(el).querySelector(".stats-bar");
expect(strip, "stats strip rendered").to.exist;
expect(strip!.querySelectorAll(".budget-tile").length).to.equal(2);
});
});
@@ -17,4 +17,5 @@ export const LS_KEYS = {
groupBy: "maintenance_supporter_groupby",
objectView: "maintenance_supporter_object_view",
objectsCache: "msp-objects-cache",
gettingStartedDismissed: "msp-gs-dismissed",
} as const;
@@ -835,5 +835,11 @@
"update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.",
"update_reload": "Načíst znovu",
"battery_fleet_forecast_overdue": "Předpovězené datum uplynulo — baterie stále hlásí dobrý stav. Pokud jste ji vyměnili, zaznamenejte výměnu; jinak byla předpověď mylná.",
"cost_from_parts": "Použít ≈ {amount} z dílů"
"cost_from_parts": "Použít ≈ {amount} z dílů",
"new_menu": "Nový",
"dismiss": "Skrýt",
"gs_label": "Začínáme — tyto tipy zmizí, jak vaše nastavení poroste",
"gs_setups_chip": "Navrhovaná nastavení: nalezeno {n} zařízení s předpřipravenými spouštěči",
"gs_adopt_chip": "{n} problémových senzorů se může stát údržbovými úkoly",
"gs_fleet_chip": "Jedno kliknutí nastaví flotilu baterií"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.",
"update_reload": "Genindlæs",
"battery_fleet_forecast_overdue": "Forudsagt dato er overskredet — batteriet melder stadig god tilstand. Hvis du har skiftet det, registrér udskiftningen; ellers ramte prognosen forbi.",
"cost_from_parts": "Brug ≈ {amount} fra dele"
"cost_from_parts": "Brug ≈ {amount} fra dele",
"new_menu": "Ny",
"dismiss": "Afvis",
"gs_label": "Kom godt i gang — disse tips forsvinder, efterhånden som opsætningen vokser",
"gs_setups_chip": "Foreslåede opsætninger fandt {n} enheder med forudindstillede udløsere",
"gs_adopt_chip": "{n} problemsensorer kan blive vedligeholdelsesopgaver",
"gs_fleet_chip": "Ét klik opsætter batteriflåden"
}
@@ -835,5 +835,11 @@
"update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.",
"update_reload": "Neu laden",
"battery_fleet_forecast_overdue": "Prognosedatum überschritten — die Batterie meldet sich weiterhin gesund. Falls du sie gewechselt hast, trage den Wechsel nach; andernfalls lag die Prognose daneben.",
"cost_from_parts": "≈ {amount} aus Teilen übernehmen"
"cost_from_parts": "≈ {amount} aus Teilen übernehmen",
"new_menu": "Neu",
"dismiss": "Ausblenden",
"gs_label": "Erste Schritte — diese Hinweise verschwinden, wenn dein Setup wächst",
"gs_setups_chip": "Vorgeschlagene Setups: {n} Geräte mit vorverdrahteten Auslösern gefunden",
"gs_adopt_chip": "{n} Problem-Sensoren können zu Wartungsaufgaben werden",
"gs_fleet_chip": "Ein Klick richtet die Batterieflotte ein"
}
@@ -835,5 +835,11 @@
"update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.",
"update_reload": "Reload",
"battery_fleet_forecast_overdue": "Predicted date passed — the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",
"cost_from_parts": "Use ≈ {amount} from parts"
"cost_from_parts": "Use ≈ {amount} from parts",
"new_menu": "New",
"dismiss": "Dismiss",
"gs_label": "Getting started — these hints retire as your setup grows",
"gs_setups_chip": "Suggested setups found {n} devices with pre-wired triggers",
"gs_adopt_chip": "{n} problem sensors can become maintenance tasks",
"gs_fleet_chip": "One click sets up the battery fleet"
}
@@ -835,5 +835,11 @@
"update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.",
"update_reload": "Recargar",
"battery_fleet_forecast_overdue": "Fecha prevista superada: la batería sigue informando buen estado. Si la cambiaste, registra el reemplazo; si no, la previsión falló.",
"cost_from_parts": "Usar ≈ {amount} de las piezas"
"cost_from_parts": "Usar ≈ {amount} de las piezas",
"new_menu": "Nuevo",
"dismiss": "Descartar",
"gs_label": "Primeros pasos: estas sugerencias desaparecen a medida que crece tu configuración",
"gs_setups_chip": "Configuraciones sugeridas: {n} dispositivos con disparadores preconfigurados",
"gs_adopt_chip": "{n} sensores de problemas pueden convertirse en tareas de mantenimiento",
"gs_fleet_chip": "Un clic configura la flota de baterías"
}
@@ -835,5 +835,11 @@
"update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.",
"update_reload": "Lataa uudelleen",
"battery_fleet_forecast_overdue": "Ennustettu päivä on ohitettu — akku ilmoittaa yhä hyvästä kunnosta. Jos vaihdoit sen, kirjaa vaihto; muuten ennuste oli pielessä.",
"cost_from_parts": "Käytä ≈ {amount} osista"
"cost_from_parts": "Käytä ≈ {amount} osista",
"new_menu": "Uusi",
"dismiss": "Hylkää",
"gs_label": "Aloitus — nämä vihjeet poistuvat asennuksen kasvaessa",
"gs_setups_chip": "Ehdotetut asetukset löysivät {n} laitetta valmiilla laukaisimilla",
"gs_adopt_chip": "{n} ongelma-anturia voi muuttua huoltotehtäviksi",
"gs_fleet_chip": "Yksi napsautus määrittää akkukannan"
}
@@ -835,5 +835,11 @@
"update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.",
"update_reload": "Recharger",
"battery_fleet_forecast_overdue": "Date prévue dépassée — la batterie se signale toujours en bon état. Si vous l'avez remplacée, enregistrez le remplacement ; sinon la prévision était erronée.",
"cost_from_parts": "Reprendre ≈ {amount} des pièces"
"cost_from_parts": "Reprendre ≈ {amount} des pièces",
"new_menu": "Nouveau",
"dismiss": "Ignorer",
"gs_label": "Premiers pas — ces conseils disparaissent à mesure que votre configuration grandit",
"gs_setups_chip": "Configurations suggérées : {n} appareils avec déclencheurs pré-câblés trouvés",
"gs_adopt_chip": "{n} capteurs de problème peuvent devenir des tâches de maintenance",
"gs_fleet_chip": "Un clic configure le parc de piles"
}
@@ -835,5 +835,11 @@
"update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।",
"update_reload": "पुनः लोड करें",
"battery_fleet_forecast_overdue": "अनुमानित तिथि बीत गई — बैटरी अब भी अच्छी स्थिति बता रही है। यदि आपने इसे बदला है, तो बदलाव दर्ज करें; अन्यथा पूर्वानुमान गलत था।",
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें"
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें",
"new_menu": "नया",
"dismiss": "हटाएँ",
"gs_label": "शुरुआत — सेटअप बढ़ने पर ये संकेत हट जाते हैं",
"gs_setups_chip": "सुझाए गए सेटअप: पूर्व-निर्धारित ट्रिगर वाले {n} उपकरण मिले",
"gs_adopt_chip": "{n} समस्या सेंसर रखरखाव कार्य बन सकते हैं",
"gs_fleet_chip": "एक क्लिक में बैटरी बेड़ा सेट करें"
}
@@ -835,5 +835,11 @@
"update_banner": "A Maintenance Supporter újabb verziója érhető el a szerveren — töltse újra az oldalt a panel frissítéséhez.",
"update_reload": "Újratöltés",
"battery_fleet_forecast_overdue": "Az előrejelzett dátum elmúlt — az elem továbbra is jó állapotot jelez. Ha kicserélted, rögzítsd a cserét; különben az előrejelzés tévedett.",
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből"
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből",
"new_menu": "Új",
"dismiss": "Elrejtés",
"gs_label": "Első lépések — a tippek eltűnnek, ahogy a beállítás bővül",
"gs_setups_chip": "Javasolt beállítások: {n} eszköz előre bekötött triggerekkel",
"gs_adopt_chip": "{n} problémaérzékelő karbantartási feladattá válhat",
"gs_fleet_chip": "Egy kattintás beállítja az elemflottát"
}
@@ -835,5 +835,11 @@
"update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.",
"update_reload": "Ricarica",
"battery_fleet_forecast_overdue": "Data prevista superata — la batteria risulta ancora in buono stato. Se l'hai sostituita, registra la sostituzione; altrimenti la previsione era errata.",
"cost_from_parts": "Usa ≈ {amount} dai ricambi"
"cost_from_parts": "Usa ≈ {amount} dai ricambi",
"new_menu": "Nuovo",
"dismiss": "Ignora",
"gs_label": "Primi passi — questi suggerimenti scompaiono man mano che la configurazione cresce",
"gs_setups_chip": "Configurazioni suggerite: trovati {n} dispositivi con trigger preconfigurati",
"gs_adopt_chip": "{n} sensori di problemi possono diventare attività di manutenzione",
"gs_fleet_chip": "Un clic configura la flotta di batterie"
}
@@ -835,5 +835,11 @@
"update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。",
"update_reload": "再読み込み",
"battery_fleet_forecast_overdue": "予測日を過ぎましたが、電池はまだ正常と報告しています。交換済みなら交換を記録してください。そうでなければ予測が外れています。",
"cost_from_parts": "部品から ≈ {amount} を適用"
"cost_from_parts": "部品から ≈ {amount} を適用",
"new_menu": "新規",
"dismiss": "閉じる",
"gs_label": "はじめに — セットアップが進むとこれらのヒントは消えます",
"gs_setups_chip": "推奨セットアップ:トリガー設定済みのデバイスを{n}台検出",
"gs_adopt_chip": "{n}個の問題センサーをメンテナンスタスクにできます",
"gs_fleet_chip": "ワンクリックで電池フリートを設定"
}
@@ -835,5 +835,11 @@
"update_banner": "서버에 Maintenance Supporter의 새 버전이 있습니다 — 패널을 업데이트하려면 새로 고침하세요.",
"update_reload": "새로 고침",
"battery_fleet_forecast_overdue": "예측 날짜가 지났지만 배터리는 여전히 정상으로 보고됩니다. 교체했다면 교체를 기록하세요. 아니라면 예측이 빗나간 것입니다.",
"cost_from_parts": "부품에서 ≈ {amount} 적용"
"cost_from_parts": "부품에서 ≈ {amount} 적용",
"new_menu": "새로 만들기",
"dismiss": "닫기",
"gs_label": "시작하기 — 설정이 늘어나면 이 힌트는 사라집니다",
"gs_setups_chip": "추천 설정: 트리거가 준비된 기기 {n}대 발견",
"gs_adopt_chip": "문제 센서 {n}개를 유지보수 작업으로 만들 수 있습니다",
"gs_fleet_chip": "클릭 한 번으로 배터리 플릿 설정"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.",
"update_reload": "Last inn på nytt",
"battery_fleet_forecast_overdue": "Forutsagt dato er passert — batteriet melder fortsatt god tilstand. Hvis du byttet det, registrer byttet; ellers bommet prognosen.",
"cost_from_parts": "Bruk ≈ {amount} fra deler"
"cost_from_parts": "Bruk ≈ {amount} fra deler",
"new_menu": "Ny",
"dismiss": "Avvis",
"gs_label": "Kom i gang — tipsene forsvinner etter hvert som oppsettet vokser",
"gs_setups_chip": "Foreslåtte oppsett fant {n} enheter med ferdigkoblede utløsere",
"gs_adopt_chip": "{n} problemsensorer kan bli vedlikeholdsoppgaver",
"gs_fleet_chip": "Ett klikk setter opp batteriflåten"
}
@@ -835,5 +835,11 @@
"update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.",
"update_reload": "Herladen",
"battery_fleet_forecast_overdue": "Voorspelde datum verstreken — de batterij meldt zich nog steeds gezond. Heb je hem vervangen, registreer dan de vervanging; anders zat de voorspelling ernaast.",
"cost_from_parts": "≈ {amount} uit onderdelen overnemen"
"cost_from_parts": "≈ {amount} uit onderdelen overnemen",
"new_menu": "Nieuw",
"dismiss": "Verbergen",
"gs_label": "Aan de slag — deze tips verdwijnen naarmate je installatie groeit",
"gs_setups_chip": "Voorgestelde setups: {n} apparaten met vooraf ingestelde triggers gevonden",
"gs_adopt_chip": "{n} probleemsensoren kunnen onderhoudstaken worden",
"gs_fleet_chip": "Eén klik stelt het batterijpark in"
}
@@ -835,5 +835,11 @@
"update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.",
"update_reload": "Załaduj ponownie",
"battery_fleet_forecast_overdue": "Przewidywana data minęła — bateria nadal zgłasza dobry stan. Jeśli ją wymieniono, zapisz wymianę; w przeciwnym razie prognoza była błędna.",
"cost_from_parts": "Użyj ≈ {amount} z części"
"cost_from_parts": "Użyj ≈ {amount} z części",
"new_menu": "Nowy",
"dismiss": "Odrzuć",
"gs_label": "Pierwsze kroki — te wskazówki znikają wraz z rozwojem konfiguracji",
"gs_setups_chip": "Sugerowane konfiguracje: znaleziono {n} urządzeń z gotowymi wyzwalaczami",
"gs_adopt_chip": "{n} czujników problemów może stać się zadaniami konserwacji",
"gs_fleet_chip": "Jedno kliknięcie konfiguruje flotę baterii"
}
@@ -835,5 +835,11 @@
"update_banner": "Há uma versão mais nova do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
"update_reload": "Recarregar",
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria ainda reporta bom estado. Se você a trocou, registre a troca; caso contrário, a previsão errou.",
"cost_from_parts": "Usar ≈ {amount} das peças"
"cost_from_parts": "Usar ≈ {amount} das peças",
"new_menu": "Novo",
"dismiss": "Dispensar",
"gs_label": "Primeiros passos — estas dicas somem conforme a configuração cresce",
"gs_setups_chip": "Configurações sugeridas: {n} dispositivos com gatilhos pré-configurados",
"gs_adopt_chip": "{n} sensores de problema podem virar tarefas de manutenção",
"gs_fleet_chip": "Um clique configura a frota de pilhas"
}
@@ -835,5 +835,11 @@
"update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
"update_reload": "Recarregar",
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria continua a reportar bom estado. Se a substituiu, registe a substituição; caso contrário, a previsão falhou.",
"cost_from_parts": "Usar ≈ {amount} das peças"
"cost_from_parts": "Usar ≈ {amount} das peças",
"new_menu": "Novo",
"dismiss": "Dispensar",
"gs_label": "Primeiros passos — estas dicas desaparecem à medida que a configuração cresce",
"gs_setups_chip": "Configurações sugeridas: {n} dispositivos com gatilhos pré-configurados",
"gs_adopt_chip": "{n} sensores de problema podem tornar-se tarefas de manutenção",
"gs_fleet_chip": "Um clique configura a frota de pilhas"
}
@@ -835,5 +835,11 @@
"update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.",
"update_reload": "Перезагрузить",
"battery_fleet_forecast_overdue": "Прогнозируемая дата прошла — батарея по-прежнему сообщает о хорошем состоянии. Если вы её заменили, зафиксируйте замену; иначе прогноз оказался неверным.",
"cost_from_parts": "Взять ≈ {amount} из запчастей"
"cost_from_parts": "Взять ≈ {amount} из запчастей",
"new_menu": "Создать",
"dismiss": "Скрыть",
"gs_label": "Первые шаги — эти подсказки исчезнут по мере роста настройки",
"gs_setups_chip": "Рекомендуемые настройки: найдено {n} устройств с готовыми триггерами",
"gs_adopt_chip": "{n} датчиков проблем могут стать задачами обслуживания",
"gs_fleet_chip": "Один клик настроит парк батарей"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.",
"update_reload": "Ladda om",
"battery_fleet_forecast_overdue": "Förutsagt datum har passerat — batteriet rapporterar fortfarande god status. Om du bytte det, registrera bytet; annars slog prognosen fel.",
"cost_from_parts": "Använd ≈ {amount} från delar"
"cost_from_parts": "Använd ≈ {amount} från delar",
"new_menu": "Ny",
"dismiss": "Avfärda",
"gs_label": "Kom igång — tipsen försvinner när din installation växer",
"gs_setups_chip": "Föreslagna uppsättningar hittade {n} enheter med förkopplade utlösare",
"gs_adopt_chip": "{n} problemsensorer kan bli underhållsuppgifter",
"gs_fleet_chip": "Ett klick konfigurerar batteriflottan"
}
@@ -835,5 +835,11 @@
"update_banner": "Sunucuda Maintenance Supporter'ın daha yeni bir sürümü var — paneli güncellemek için sayfayı yeniden yükleyin.",
"update_reload": "Yeniden yükle",
"battery_fleet_forecast_overdue": "Öngörülen tarih geçti — pil hâlâ sağlıklı görünüyor. Değiştirdiyseniz değişimi kaydedin; aksi halde tahmin yanılmış demektir.",
"cost_from_parts": "Parçalardan ≈ {amount} kullan"
"cost_from_parts": "Parçalardan ≈ {amount} kullan",
"new_menu": "Yeni",
"dismiss": "Kapat",
"gs_label": "Başlarken — kurulumunuz büyüdükçe bu ipuçları kaybolur",
"gs_setups_chip": "Önerilen kurulumlar {n} cihaz buldu (hazır tetikleyicilerle)",
"gs_adopt_chip": "{n} sorun sensörü bakım görevine dönüşebilir",
"gs_fleet_chip": "Tek tıkla pil filosunu kurun"
}
@@ -835,5 +835,11 @@
"update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.",
"update_reload": "Перезавантажити",
"battery_fleet_forecast_overdue": "Прогнозована дата минула — батарея й далі повідомляє про добрий стан. Якщо ви її замінили, зафіксуйте заміну; інакше прогноз не справдився.",
"cost_from_parts": "Узяти ≈ {amount} із запчастин"
"cost_from_parts": "Узяти ≈ {amount} із запчастин",
"new_menu": "Створити",
"dismiss": "Приховати",
"gs_label": "Перші кроки — ці підказки зникнуть у міру зростання налаштування",
"gs_setups_chip": "Рекомендовані налаштування: знайдено {n} пристроїв із готовими тригерами",
"gs_adopt_chip": "{n} датчиків проблем можуть стати завданнями обслуговування",
"gs_fleet_chip": "Один клік налаштує парк батарей"
}
@@ -835,5 +835,11 @@
"update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。",
"update_reload": "重新加载",
"battery_fleet_forecast_overdue": "预测日期已过——电池仍报告状态良好。如果您已更换电池,请记录更换;否则说明预测有误。",
"cost_from_parts": "采用配件合计 ≈ {amount}"
"cost_from_parts": "采用配件合计 ≈ {amount}",
"new_menu": "新建",
"dismiss": "忽略",
"gs_label": "入门提示——随着配置的完善,这些提示会自动消失",
"gs_setups_chip": "推荐配置发现 {n} 台设备(含预设触发器)",
"gs_adopt_chip": "{n} 个问题传感器可转换为维护任务",
"gs_fleet_chip": "一键设置电池车队"
}
@@ -141,7 +141,12 @@ export class MaintenanceSupporterPanel extends LitElement {
// occasional-use — collapsed behind two toggle buttons so the task list
// starts above the fold on phones. Desktop renders them inline as before.
@state() private _filtersOpen = false;
@state() private _actionsMenuOpen = false;
// #125: the one "New" menu replacing the six action buttons, plus the
// getting-started chip discovery counts (fetched once, young installs only).
@state() private _newMenuOpen = false;
@state() private _gsSetupsCount = 0;
@state() private _gsAdoptCount = 0;
private _gsLoaded = false;
// Battery Fleet: offer one-click setup only when Battery Notes is present
// and the fleet isn't set up yet.
@state() private _batteryFleetSetupAvailable = false;
@@ -434,6 +439,10 @@ export class MaintenanceSupporterPanel extends LitElement {
if (objResult) {
this._objects = hydrateObjects((objResult as { objects: MaintenanceObjectResponse[] }).objects);
writeObjectsCache(this._objects, (statsResult as StatisticsResponse | null) ?? this._stats ?? null);
// #125: getting-started discovery runs AFTER the objects are known —
// judging youth on the pre-load empty list would bill every mature
// install for two discovery calls.
this._maybeLoadGettingStarted();
}
// A data refresh means the open task's history may have grown (complete,
// history edit) — the truncated list payload can't tell, so refetch.
@@ -2137,6 +2146,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<span class="stat-value" style="color: #ff5722">${s.triggered}</span>
<span class="stat-label">${t("triggered", L)}</span>
</div>
${this._features.budget ? this._renderBudgetTiles() : nothing}
</div>
`
: nothing}
@@ -2296,7 +2306,6 @@ export class MaintenanceSupporterPanel extends LitElement {
(this._activeViewId ? 1 : 0);
return html`
${this._features.budget ? this._renderBudgetBar() : nothing}
${this.narrow ? html`
<div class="mobile-controls">
@@ -2307,15 +2316,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<ha-icon icon="mdi:filter-variant"></ha-icon>
${t("filter_label", L)}${activeFilterCount > 0 ? ` (${activeFilterCount})` : ""}
</ha-button>
${!isOperator ? html`
<ha-button
class="mobile-toggle ${this._actionsMenuOpen ? "active" : ""}"
@click=${() => { this._actionsMenuOpen = !this._actionsMenuOpen; }}
>
<ha-icon icon="mdi:plus"></ha-icon>
${t("add", L)}
</ha-button>
` : nothing}
${!isOperator ? this._renderNewMenu(L) : nothing}
</div>
` : nothing}
@@ -2439,39 +2440,10 @@ export class MaintenanceSupporterPanel extends LitElement {
${this._bulkMode ? t("cancel", L) : t("bulk_select", L)}
</ha-button>
` : nothing}
${!isOperator && !this.narrow ? this._renderNewMenu(L) : nothing}
</div>
${!isOperator ? html`
<div
class="actions-bar ${this.narrow && !this._actionsMenuOpen ? "collapsed" : ""}"
@click=${() => { if (this.narrow) this._actionsMenuOpen = false; }}
>
<ha-button
@click=${() => this._ui<MaintenanceTaskDialog>("maintenance-task-dialog").then((d) => d?.openCreate("", this._objects))}
>
${t("new_task", L)}
</ha-button>
<ha-button
@click=${() => this._ui<MaintenanceObjectDialog>("maintenance-object-dialog").then((d) => d?.openCreate())}
>
${t("new_object", L)}
</ha-button>
<ha-button @click=${() => this._openTemplateGallery()}>
<ha-icon icon="mdi:view-grid-plus-outline"></ha-icon> ${t("templates_from", L)}
</ha-button>
<ha-button @click=${() => this._openAdoptProblemSensors()}>
<ha-icon icon="mdi:alert-circle-check-outline"></ha-icon> ${t("adopt_problem_button", L)}
</ha-button>
<ha-button @click=${() => this._openSuggestedSetups()}>
<ha-icon icon="mdi:auto-fix"></ha-icon> ${t("setups_button", L)}
</ha-button>
${this._batteryFleetSetupAvailable ? html`
<ha-button @click=${() => this._setupBatteryFleet()}>
<ha-icon icon="mdi:battery-sync"></ha-icon> ${t("battery_fleet_setup_button", L)}
</ha-button>
` : nothing}
</div>
` : nothing}
${!isOperator ? this._renderGettingStartedChips(L) : nothing}
${rows.length === 0
? html`
@@ -3016,49 +2988,39 @@ export class MaintenanceSupporterPanel extends LitElement {
}
}
private _renderBudgetBar() {
/** Budget as KPI tiles in the stats strip (#125) — replaces the old
* full-width budget row. Same semantics as before, incl. #104: a budget
* WITHOUT a maximum still shows its plain spent total (no denominator,
* no bar), so "what did I spend" always has an answer. Being part of the
* strip, the tiles now show on every tab — budget is a global figure
* like the overdue count. */
private _renderBudgetTiles() {
const b = this._budget;
if (!b) return nothing;
const L = this._lang;
const cs = b.currency_symbol || DEFAULT_CURRENCY_SYMBOL;
const bars: { label: string; spent: number; budget: number }[] = [];
if (b.monthly_budget > 0) bars.push({ label: t("budget_monthly", L), spent: b.monthly_spent, budget: b.monthly_budget });
if (b.yearly_budget > 0) bars.push({ label: t("budget_yearly", L), spent: b.yearly_spent, budget: b.yearly_budget });
// #104: budget tracking enabled WITHOUT a maximum — the spent totals were
// invisible (a bar needs a denominator). Show plain spent lines instead,
// so "what did I spend" always has a dashboard answer.
const spentOnly: { label: string; spent: number }[] = [];
if (!(b.monthly_budget > 0)) spentOnly.push({ label: t("budget_monthly", L), spent: b.monthly_spent || 0 });
if (!(b.yearly_budget > 0)) spentOnly.push({ label: t("budget_yearly", L), spent: b.yearly_spent || 0 });
const tile = (label: string, spent: number, budget: number | null) => {
if (budget !== null) {
const pct = Math.min(100, Math.max(0, (spent / budget) * 100));
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>
<div class="budget-tile-bar"><div style="width:${pct}%; background:${color}"></div></div>
<span class="stat-label">${label}</span>
</div>
`;
}
return html`
<div class="stat-item budget-tile" title="${label}: ${spent.toFixed(2)} ${cs}">
<span class="stat-value budget-tile-value">${spent.toFixed(2)} ${cs}</span>
<span class="stat-label">${label}</span>
</div>
`;
};
return html`
<div class="budget-bars">
${bars.map((bar) => {
const pct = Math.min(100, Math.max(0, (bar.spent / bar.budget) * 100));
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="budget-item">
<div class="budget-label">
<span>${bar.label}</span>
<span>${bar.spent.toFixed(2)} / ${bar.budget.toFixed(2)} ${cs}</span>
</div>
<div class="budget-bar">
<div class="budget-bar-fill" style="width:${pct}%; background:${color}"></div>
</div>
</div>
`;
})}
${spentOnly.map(
(s) => html`
<div class="budget-item budget-spent-only">
<div class="budget-label">
<span>${s.label}</span>
<span>${s.spent.toFixed(2)} ${cs}</span>
</div>
</div>
`,
)}
</div>
${tile(t("budget_monthly", L), b.monthly_spent || 0, b.monthly_budget > 0 ? b.monthly_budget : null)}
${tile(t("budget_yearly", L), b.yearly_spent || 0, b.yearly_budget > 0 ? b.yearly_budget : null)}
`;
}
@@ -3314,6 +3276,150 @@ export class MaintenanceSupporterPanel extends LitElement {
* Render compact task header with status chip and action buttons.
*/
// Object-detail ⋮ menu — same click-away pattern as the task-detail menu.
/** #125: the one "New ▾" menu replacing the six action buttons. Same
* popup pattern as the object/task menus; on narrow it lives in the
* mobile-controls row and replaced the old collapsed actions bar. */
private _renderNewMenu(L: string) {
return html`
<div class="new-menu-wrapper">
<ha-button appearance="filled" class="new-menu-button"
@click=${(e: Event) => { e.stopPropagation(); this._toggleNewMenu(); }}>
<ha-icon icon="mdi:plus"></ha-icon> ${t("new_menu", L)}
<ha-icon icon="mdi:menu-down"></ha-icon>
</ha-button>
${this._newMenuOpen ? html`
<div class="popup-menu new-menu-popup" @click=${(e: Event) => e.stopPropagation()}>
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); void this._ui<MaintenanceTaskDialog>("maintenance-task-dialog").then((d) => d?.openCreate("", this._objects)); }}>
<ha-icon icon="mdi:clipboard-plus-outline"></ha-icon> ${t("new_task", L)}
</div>
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); void this._ui<MaintenanceObjectDialog>("maintenance-object-dialog").then((d) => d?.openCreate()); }}>
<ha-icon icon="mdi:package-variant-closed-plus"></ha-icon> ${t("new_object", L).replace(/^\+\s*/, "")}
</div>
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); this._openTemplateGallery(); }}>
<ha-icon icon="mdi:view-grid-plus-outline"></ha-icon> ${t("templates_from", L)}
</div>
<div class="popup-menu-divider"></div>
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); this._openAdoptProblemSensors(); }}>
<ha-icon icon="mdi:alert-circle-check-outline"></ha-icon> ${t("adopt_problem_button", L)}
</div>
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); this._openSuggestedSetups(); }}>
<ha-icon icon="mdi:auto-fix"></ha-icon> ${t("setups_button", L)}
</div>
${this._batteryFleetSetupAvailable ? html`
<div class="popup-menu-item" @click=${() => { this._closeNewMenu(); this._setupBatteryFleet(); }}>
<ha-icon icon="mdi:battery-sync"></ha-icon> ${t("battery_fleet_setup_button", L)}
</div>
` : nothing}
</div>
` : nothing}
</div>
`;
}
private _toggleNewMenu(): void {
this._newMenuOpen = !this._newMenuOpen;
if (this._newMenuOpen) {
setTimeout(() => {
const handler = () => { this._newMenuOpen = false; document.removeEventListener("click", handler); };
document.addEventListener("click", handler);
}, 0);
}
}
private _closeNewMenu(): void {
this._newMenuOpen = false;
}
// ── #125: getting-started chips ──────────────────────────────────────────
// Dismissible onboarding hints while the instance is YOUNG (few objects/
// tasks — deliberately a maturity signal, not install age). The fleet chip
// is content-driven instead (available && not configured), mirroring the
// old inline button's own retirement rule. Nothing ever moves: the New
// menu is the stable home from day one; these are visibly temporary.
/** Young = the discovery hints still earn their place. Fleet objects don't
* count — a one-click fleet setup shouldn't age the install by itself. */
private _isYoungInstall(): boolean {
const objs = this._objects.filter((o) => !o.object?.battery_fleet);
const tasks = objs.reduce((n, o) => n + o.tasks.length, 0);
return objs.length < 3 && tasks < 8;
}
private _gsDismissed(): Set<string> {
try {
return new Set(JSON.parse(localStorage.getItem(LS_KEYS.gettingStartedDismissed) || "[]"));
} catch {
return new Set();
}
}
private _dismissGettingStarted(id: string): void {
const next = this._gsDismissed();
next.add(id);
try { localStorage.setItem(LS_KEYS.gettingStartedDismissed, JSON.stringify([...next])); } catch { /* storage blocked */ }
this.requestUpdate();
}
/** Discovery counts for the chips — fetched ONCE per panel life and ONLY
* while the install is young, so mature installs pay nothing. */
private _maybeLoadGettingStarted(): void {
if (this._gsLoaded || !this._isYoungInstall()) return;
this._gsLoaded = true;
this.hass.connection
.sendMessagePromise<{ setups: unknown[] }>({ type: "maintenance_supporter/integration_setups/discover" })
.then((r) => { this._gsSetupsCount = (r.setups || []).length; })
.catch(() => { this._gsSetupsCount = 0; });
this.hass.connection
.sendMessagePromise<{ sensors: unknown[] }>({ type: "maintenance_supporter/problem_sensors/discover" })
.then((r) => { this._gsAdoptCount = (r.sensors || []).length; })
.catch(() => { this._gsAdoptCount = 0; });
}
private _renderGettingStartedChips(L: string) {
const dismissed = this._gsDismissed();
const young = this._isYoungInstall();
const chips: { id: string; icon: string; text: string; run: () => void }[] = [];
if (young && this._gsSetupsCount > 0 && !dismissed.has("setups")) {
chips.push({
id: "setups", icon: "mdi:auto-fix",
text: t("gs_setups_chip", L).replace("{n}", String(this._gsSetupsCount)),
run: () => this._openSuggestedSetups(),
});
}
if (young && this._gsAdoptCount > 0 && !dismissed.has("adopt")) {
chips.push({
id: "adopt", icon: "mdi:alert-circle-check-outline",
text: t("gs_adopt_chip", L).replace("{n}", String(this._gsAdoptCount)),
run: () => this._openAdoptProblemSensors(),
});
}
if (this._batteryFleetSetupAvailable && !dismissed.has("fleet")) {
chips.push({
id: "fleet", icon: "mdi:battery-sync",
text: t("gs_fleet_chip", L),
run: () => this._setupBatteryFleet(),
});
}
if (chips.length === 0) return nothing;
return html`
<div class="gs-chips-wrap">
<div class="gs-chips-label">${t("gs_label", L)}</div>
<div class="gs-chips">
${chips.map((c) => html`
<div class="gs-chip" @click=${() => c.run()}>
<ha-icon icon="${c.icon}"></ha-icon>
<span>${c.text}</span>
<span class="gs-chip-x" title="${t("dismiss", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._dismissGettingStarted(c.id); }}>
<ha-icon icon="mdi:close"></ha-icon>
</span>
</div>
`)}
</div>
</div>
`;
}
private _toggleObjMenu(): void {
this._objMenuOpen = !this._objMenuOpen;
if (this._objMenuOpen) {
@@ -3838,9 +3838,9 @@
}
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -55,10 +55,9 @@ export const panelStyles = css`
gap: 8px;
}
/* Narrow-viewport disclosure (UX 2026-07): the collapsed classes are only
/* Narrow-viewport disclosure (UX 2026-07): the collapsed class is only
ever set when the host is narrow desktop always renders inline. */
.filter-bar.collapsed,
.actions-bar.collapsed {
.filter-bar.collapsed {
display: none;
}
@@ -68,29 +67,64 @@ export const panelStyles = css`
padding: 8px 0 0;
}
.mobile-controls .mobile-toggle {
.mobile-controls .mobile-toggle,
.mobile-controls .new-menu-wrapper {
flex: 1;
}
.mobile-controls .new-menu-wrapper .new-menu-button {
width: 100%;
}
.mobile-controls .mobile-toggle.active {
--ha-button-background: var(--primary-color);
}
.actions-bar {
/* #125: the one "New" menu that replaced the six-button actions bar. */
.new-menu-wrapper {
position: relative;
margin-left: auto;
}
.new-menu-popup {
right: 0;
left: auto;
min-width: 256px;
}
.new-menu-popup .popup-menu-item {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 4px 0 8px;
/* 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;
align-items: center;
gap: 10px;
}
.new-menu-popup .popup-menu-item ha-icon {
--mdc-icon-size: 18px;
color: var(--secondary-text-color);
}
:host([narrow]) .new-menu-popup { right: 0; left: auto; }
:host([narrow]) .actions-bar {
flex-direction: column;
align-items: stretch;
/* #125: getting-started chips — visibly temporary onboarding hints. */
.gs-chips-wrap { margin: 2px 0 12px; }
.gs-chips-label {
font-size: 11px;
color: var(--secondary-text-color);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 7px;
}
.gs-chips { display: flex; gap: 10px; flex-wrap: wrap; }
.gs-chip {
display: flex;
align-items: center;
gap: 9px;
background: var(--secondary-background-color);
border: 1px dashed var(--divider-color);
border-radius: 16px;
padding: 7px 8px 7px 13px;
font-size: 12.5px;
cursor: pointer;
}
.gs-chip ha-icon { --mdc-icon-size: 16px; color: var(--primary-color); }
.gs-chip-x { display: inline-flex; padding: 0 3px; }
.gs-chip-x ha-icon { --mdc-icon-size: 14px; color: var(--secondary-text-color); }
.filter-field {
display: flex;
@@ -366,7 +366,10 @@ export const sharedStyles = css`
label length. */
.stats-bar {
display: grid;
grid-template-columns: repeat(5, 1fr);
/* auto-fit instead of a fixed 5: with the budget feature on, two KPI
tiles join the strip (#125) and on narrow screens the tiles wrap
instead of crushing. */
grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
gap: 16px;
padding: 16px;
}
@@ -905,38 +908,25 @@ export const sharedStyles = css`
50% { opacity: 0.7; }
}
/* Budget bars */
.budget-bars {
display: flex;
gap: 16px;
padding: 8px 16px;
flex-wrap: wrap;
/* Budget KPI tiles in the stats strip (#125) replaced the full-width
budget-bars row. */
.stat-item.budget-tile .budget-tile-value {
font-size: 15px;
padding-top: 5px;
white-space: nowrap;
}
.budget-item {
flex: 1;
min-width: 200px;
}
.budget-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--secondary-text-color);
margin-bottom: 4px;
}
.budget-bar {
.budget-tile-bar {
width: 100%;
height: 6px;
max-width: 130px;
height: 4px;
border-radius: 2px;
background: var(--divider-color);
border-radius: 3px;
overflow: hidden;
margin-top: 5px;
}
.budget-bar-fill {
.budget-tile-bar > div {
height: 100%;
border-radius: 3px;
border-radius: 2px;
transition: width 0.3s;
}
@@ -1444,21 +1434,9 @@ export const sharedStyles = css`
.weibull-info-row { flex-direction: column; gap: 8px; }
/* One compact line instead of two stacked rows the bars are glanceable
info, not the page's main content (UX 2026-07). The label stacks above
the value: side-by-side, the yearly pair wrapped raggedly on 360px
screens; a uniform two-line stack reads as intended at every width. */
.budget-bars { gap: 12px; padding: 6px 8px; }
.budget-item { min-width: 0; }
.budget-label {
font-size: 11px;
margin-bottom: 2px;
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.budget-label span { white-space: nowrap; }
.budget-bar { height: 4px; }
/* 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; }
.group-card { min-width: 0; max-width: 100%; }
@@ -835,5 +835,11 @@
"update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.",
"update_reload": "Načíst znovu",
"battery_fleet_forecast_overdue": "Předpovězené datum uplynulo — baterie stále hlásí dobrý stav. Pokud jste ji vyměnili, zaznamenejte výměnu; jinak byla předpověď mylná.",
"cost_from_parts": "Použít ≈ {amount} z dílů"
"cost_from_parts": "Použít ≈ {amount} z dílů",
"new_menu": "Nový",
"dismiss": "Skrýt",
"gs_label": "Začínáme — tyto tipy zmizí, jak vaše nastavení poroste",
"gs_setups_chip": "Navrhovaná nastavení: nalezeno {n} zařízení s předpřipravenými spouštěči",
"gs_adopt_chip": "{n} problémových senzorů se může stát údržbovými úkoly",
"gs_fleet_chip": "Jedno kliknutí nastaví flotilu baterií"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.",
"update_reload": "Genindlæs",
"battery_fleet_forecast_overdue": "Forudsagt dato er overskredet — batteriet melder stadig god tilstand. Hvis du har skiftet det, registrér udskiftningen; ellers ramte prognosen forbi.",
"cost_from_parts": "Brug ≈ {amount} fra dele"
"cost_from_parts": "Brug ≈ {amount} fra dele",
"new_menu": "Ny",
"dismiss": "Afvis",
"gs_label": "Kom godt i gang — disse tips forsvinder, efterhånden som opsætningen vokser",
"gs_setups_chip": "Foreslåede opsætninger fandt {n} enheder med forudindstillede udløsere",
"gs_adopt_chip": "{n} problemsensorer kan blive vedligeholdelsesopgaver",
"gs_fleet_chip": "Ét klik opsætter batteriflåden"
}
@@ -835,5 +835,11 @@
"update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.",
"update_reload": "Neu laden",
"battery_fleet_forecast_overdue": "Prognosedatum überschritten — die Batterie meldet sich weiterhin gesund. Falls du sie gewechselt hast, trage den Wechsel nach; andernfalls lag die Prognose daneben.",
"cost_from_parts": "≈ {amount} aus Teilen übernehmen"
"cost_from_parts": "≈ {amount} aus Teilen übernehmen",
"new_menu": "Neu",
"dismiss": "Ausblenden",
"gs_label": "Erste Schritte — diese Hinweise verschwinden, wenn dein Setup wächst",
"gs_setups_chip": "Vorgeschlagene Setups: {n} Geräte mit vorverdrahteten Auslösern gefunden",
"gs_adopt_chip": "{n} Problem-Sensoren können zu Wartungsaufgaben werden",
"gs_fleet_chip": "Ein Klick richtet die Batterieflotte ein"
}
@@ -835,5 +835,11 @@
"update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.",
"update_reload": "Reload",
"battery_fleet_forecast_overdue": "Predicted date passed — the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",
"cost_from_parts": "Use ≈ {amount} from parts"
"cost_from_parts": "Use ≈ {amount} from parts",
"new_menu": "New",
"dismiss": "Dismiss",
"gs_label": "Getting started — these hints retire as your setup grows",
"gs_setups_chip": "Suggested setups found {n} devices with pre-wired triggers",
"gs_adopt_chip": "{n} problem sensors can become maintenance tasks",
"gs_fleet_chip": "One click sets up the battery fleet"
}
@@ -835,5 +835,11 @@
"update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.",
"update_reload": "Recargar",
"battery_fleet_forecast_overdue": "Fecha prevista superada: la batería sigue informando buen estado. Si la cambiaste, registra el reemplazo; si no, la previsión falló.",
"cost_from_parts": "Usar ≈ {amount} de las piezas"
"cost_from_parts": "Usar ≈ {amount} de las piezas",
"new_menu": "Nuevo",
"dismiss": "Descartar",
"gs_label": "Primeros pasos: estas sugerencias desaparecen a medida que crece tu configuración",
"gs_setups_chip": "Configuraciones sugeridas: {n} dispositivos con disparadores preconfigurados",
"gs_adopt_chip": "{n} sensores de problemas pueden convertirse en tareas de mantenimiento",
"gs_fleet_chip": "Un clic configura la flota de baterías"
}
@@ -835,5 +835,11 @@
"update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.",
"update_reload": "Lataa uudelleen",
"battery_fleet_forecast_overdue": "Ennustettu päivä on ohitettu — akku ilmoittaa yhä hyvästä kunnosta. Jos vaihdoit sen, kirjaa vaihto; muuten ennuste oli pielessä.",
"cost_from_parts": "Käytä ≈ {amount} osista"
"cost_from_parts": "Käytä ≈ {amount} osista",
"new_menu": "Uusi",
"dismiss": "Hylkää",
"gs_label": "Aloitus — nämä vihjeet poistuvat asennuksen kasvaessa",
"gs_setups_chip": "Ehdotetut asetukset löysivät {n} laitetta valmiilla laukaisimilla",
"gs_adopt_chip": "{n} ongelma-anturia voi muuttua huoltotehtäviksi",
"gs_fleet_chip": "Yksi napsautus määrittää akkukannan"
}
@@ -835,5 +835,11 @@
"update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.",
"update_reload": "Recharger",
"battery_fleet_forecast_overdue": "Date prévue dépassée — la batterie se signale toujours en bon état. Si vous l'avez remplacée, enregistrez le remplacement ; sinon la prévision était erronée.",
"cost_from_parts": "Reprendre ≈ {amount} des pièces"
"cost_from_parts": "Reprendre ≈ {amount} des pièces",
"new_menu": "Nouveau",
"dismiss": "Ignorer",
"gs_label": "Premiers pas — ces conseils disparaissent à mesure que votre configuration grandit",
"gs_setups_chip": "Configurations suggérées : {n} appareils avec déclencheurs pré-câblés trouvés",
"gs_adopt_chip": "{n} capteurs de problème peuvent devenir des tâches de maintenance",
"gs_fleet_chip": "Un clic configure le parc de piles"
}
@@ -835,5 +835,11 @@
"update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।",
"update_reload": "पुनः लोड करें",
"battery_fleet_forecast_overdue": "अनुमानित तिथि बीत गई — बैटरी अब भी अच्छी स्थिति बता रही है। यदि आपने इसे बदला है, तो बदलाव दर्ज करें; अन्यथा पूर्वानुमान गलत था।",
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें"
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें",
"new_menu": "नया",
"dismiss": "हटाएँ",
"gs_label": "शुरुआत — सेटअप बढ़ने पर ये संकेत हट जाते हैं",
"gs_setups_chip": "सुझाए गए सेटअप: पूर्व-निर्धारित ट्रिगर वाले {n} उपकरण मिले",
"gs_adopt_chip": "{n} समस्या सेंसर रखरखाव कार्य बन सकते हैं",
"gs_fleet_chip": "एक क्लिक में बैटरी बेड़ा सेट करें"
}
@@ -835,5 +835,11 @@
"update_banner": "A Maintenance Supporter újabb verziója érhető el a szerveren — töltse újra az oldalt a panel frissítéséhez.",
"update_reload": "Újratöltés",
"battery_fleet_forecast_overdue": "Az előrejelzett dátum elmúlt — az elem továbbra is jó állapotot jelez. Ha kicserélted, rögzítsd a cserét; különben az előrejelzés tévedett.",
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből"
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből",
"new_menu": "Új",
"dismiss": "Elrejtés",
"gs_label": "Első lépések — a tippek eltűnnek, ahogy a beállítás bővül",
"gs_setups_chip": "Javasolt beállítások: {n} eszköz előre bekötött triggerekkel",
"gs_adopt_chip": "{n} problémaérzékelő karbantartási feladattá válhat",
"gs_fleet_chip": "Egy kattintás beállítja az elemflottát"
}
@@ -835,5 +835,11 @@
"update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.",
"update_reload": "Ricarica",
"battery_fleet_forecast_overdue": "Data prevista superata — la batteria risulta ancora in buono stato. Se l'hai sostituita, registra la sostituzione; altrimenti la previsione era errata.",
"cost_from_parts": "Usa ≈ {amount} dai ricambi"
"cost_from_parts": "Usa ≈ {amount} dai ricambi",
"new_menu": "Nuovo",
"dismiss": "Ignora",
"gs_label": "Primi passi — questi suggerimenti scompaiono man mano che la configurazione cresce",
"gs_setups_chip": "Configurazioni suggerite: trovati {n} dispositivi con trigger preconfigurati",
"gs_adopt_chip": "{n} sensori di problemi possono diventare attività di manutenzione",
"gs_fleet_chip": "Un clic configura la flotta di batterie"
}
@@ -835,5 +835,11 @@
"update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。",
"update_reload": "再読み込み",
"battery_fleet_forecast_overdue": "予測日を過ぎましたが、電池はまだ正常と報告しています。交換済みなら交換を記録してください。そうでなければ予測が外れています。",
"cost_from_parts": "部品から ≈ {amount} を適用"
"cost_from_parts": "部品から ≈ {amount} を適用",
"new_menu": "新規",
"dismiss": "閉じる",
"gs_label": "はじめに — セットアップが進むとこれらのヒントは消えます",
"gs_setups_chip": "推奨セットアップ:トリガー設定済みのデバイスを{n}台検出",
"gs_adopt_chip": "{n}個の問題センサーをメンテナンスタスクにできます",
"gs_fleet_chip": "ワンクリックで電池フリートを設定"
}
@@ -835,5 +835,11 @@
"update_banner": "서버에 Maintenance Supporter의 새 버전이 있습니다 — 패널을 업데이트하려면 새로 고침하세요.",
"update_reload": "새로 고침",
"battery_fleet_forecast_overdue": "예측 날짜가 지났지만 배터리는 여전히 정상으로 보고됩니다. 교체했다면 교체를 기록하세요. 아니라면 예측이 빗나간 것입니다.",
"cost_from_parts": "부품에서 ≈ {amount} 적용"
"cost_from_parts": "부품에서 ≈ {amount} 적용",
"new_menu": "새로 만들기",
"dismiss": "닫기",
"gs_label": "시작하기 — 설정이 늘어나면 이 힌트는 사라집니다",
"gs_setups_chip": "추천 설정: 트리거가 준비된 기기 {n}대 발견",
"gs_adopt_chip": "문제 센서 {n}개를 유지보수 작업으로 만들 수 있습니다",
"gs_fleet_chip": "클릭 한 번으로 배터리 플릿 설정"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.",
"update_reload": "Last inn på nytt",
"battery_fleet_forecast_overdue": "Forutsagt dato er passert — batteriet melder fortsatt god tilstand. Hvis du byttet det, registrer byttet; ellers bommet prognosen.",
"cost_from_parts": "Bruk ≈ {amount} fra deler"
"cost_from_parts": "Bruk ≈ {amount} fra deler",
"new_menu": "Ny",
"dismiss": "Avvis",
"gs_label": "Kom i gang — tipsene forsvinner etter hvert som oppsettet vokser",
"gs_setups_chip": "Foreslåtte oppsett fant {n} enheter med ferdigkoblede utløsere",
"gs_adopt_chip": "{n} problemsensorer kan bli vedlikeholdsoppgaver",
"gs_fleet_chip": "Ett klikk setter opp batteriflåten"
}
@@ -835,5 +835,11 @@
"update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.",
"update_reload": "Herladen",
"battery_fleet_forecast_overdue": "Voorspelde datum verstreken — de batterij meldt zich nog steeds gezond. Heb je hem vervangen, registreer dan de vervanging; anders zat de voorspelling ernaast.",
"cost_from_parts": "≈ {amount} uit onderdelen overnemen"
"cost_from_parts": "≈ {amount} uit onderdelen overnemen",
"new_menu": "Nieuw",
"dismiss": "Verbergen",
"gs_label": "Aan de slag — deze tips verdwijnen naarmate je installatie groeit",
"gs_setups_chip": "Voorgestelde setups: {n} apparaten met vooraf ingestelde triggers gevonden",
"gs_adopt_chip": "{n} probleemsensoren kunnen onderhoudstaken worden",
"gs_fleet_chip": "Eén klik stelt het batterijpark in"
}
@@ -835,5 +835,11 @@
"update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.",
"update_reload": "Załaduj ponownie",
"battery_fleet_forecast_overdue": "Przewidywana data minęła — bateria nadal zgłasza dobry stan. Jeśli ją wymieniono, zapisz wymianę; w przeciwnym razie prognoza była błędna.",
"cost_from_parts": "Użyj ≈ {amount} z części"
"cost_from_parts": "Użyj ≈ {amount} z części",
"new_menu": "Nowy",
"dismiss": "Odrzuć",
"gs_label": "Pierwsze kroki — te wskazówki znikają wraz z rozwojem konfiguracji",
"gs_setups_chip": "Sugerowane konfiguracje: znaleziono {n} urządzeń z gotowymi wyzwalaczami",
"gs_adopt_chip": "{n} czujników problemów może stać się zadaniami konserwacji",
"gs_fleet_chip": "Jedno kliknięcie konfiguruje flotę baterii"
}
@@ -835,5 +835,11 @@
"update_banner": "Há uma versão mais nova do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
"update_reload": "Recarregar",
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria ainda reporta bom estado. Se você a trocou, registre a troca; caso contrário, a previsão errou.",
"cost_from_parts": "Usar ≈ {amount} das peças"
"cost_from_parts": "Usar ≈ {amount} das peças",
"new_menu": "Novo",
"dismiss": "Dispensar",
"gs_label": "Primeiros passos — estas dicas somem conforme a configuração cresce",
"gs_setups_chip": "Configurações sugeridas: {n} dispositivos com gatilhos pré-configurados",
"gs_adopt_chip": "{n} sensores de problema podem virar tarefas de manutenção",
"gs_fleet_chip": "Um clique configura a frota de pilhas"
}
@@ -835,5 +835,11 @@
"update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
"update_reload": "Recarregar",
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria continua a reportar bom estado. Se a substituiu, registe a substituição; caso contrário, a previsão falhou.",
"cost_from_parts": "Usar ≈ {amount} das peças"
"cost_from_parts": "Usar ≈ {amount} das peças",
"new_menu": "Novo",
"dismiss": "Dispensar",
"gs_label": "Primeiros passos — estas dicas desaparecem à medida que a configuração cresce",
"gs_setups_chip": "Configurações sugeridas: {n} dispositivos com gatilhos pré-configurados",
"gs_adopt_chip": "{n} sensores de problema podem tornar-se tarefas de manutenção",
"gs_fleet_chip": "Um clique configura a frota de pilhas"
}
@@ -835,5 +835,11 @@
"update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.",
"update_reload": "Перезагрузить",
"battery_fleet_forecast_overdue": "Прогнозируемая дата прошла — батарея по-прежнему сообщает о хорошем состоянии. Если вы её заменили, зафиксируйте замену; иначе прогноз оказался неверным.",
"cost_from_parts": "Взять ≈ {amount} из запчастей"
"cost_from_parts": "Взять ≈ {amount} из запчастей",
"new_menu": "Создать",
"dismiss": "Скрыть",
"gs_label": "Первые шаги — эти подсказки исчезнут по мере роста настройки",
"gs_setups_chip": "Рекомендуемые настройки: найдено {n} устройств с готовыми триггерами",
"gs_adopt_chip": "{n} датчиков проблем могут стать задачами обслуживания",
"gs_fleet_chip": "Один клик настроит парк батарей"
}
@@ -835,5 +835,11 @@
"update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.",
"update_reload": "Ladda om",
"battery_fleet_forecast_overdue": "Förutsagt datum har passerat — batteriet rapporterar fortfarande god status. Om du bytte det, registrera bytet; annars slog prognosen fel.",
"cost_from_parts": "Använd ≈ {amount} från delar"
"cost_from_parts": "Använd ≈ {amount} från delar",
"new_menu": "Ny",
"dismiss": "Avfärda",
"gs_label": "Kom igång — tipsen försvinner när din installation växer",
"gs_setups_chip": "Föreslagna uppsättningar hittade {n} enheter med förkopplade utlösare",
"gs_adopt_chip": "{n} problemsensorer kan bli underhållsuppgifter",
"gs_fleet_chip": "Ett klick konfigurerar batteriflottan"
}
@@ -835,5 +835,11 @@
"update_banner": "Sunucuda Maintenance Supporter'ın daha yeni bir sürümü var — paneli güncellemek için sayfayı yeniden yükleyin.",
"update_reload": "Yeniden yükle",
"battery_fleet_forecast_overdue": "Öngörülen tarih geçti — pil hâlâ sağlıklı görünüyor. Değiştirdiyseniz değişimi kaydedin; aksi halde tahmin yanılmış demektir.",
"cost_from_parts": "Parçalardan ≈ {amount} kullan"
"cost_from_parts": "Parçalardan ≈ {amount} kullan",
"new_menu": "Yeni",
"dismiss": "Kapat",
"gs_label": "Başlarken — kurulumunuz büyüdükçe bu ipuçları kaybolur",
"gs_setups_chip": "Önerilen kurulumlar {n} cihaz buldu (hazır tetikleyicilerle)",
"gs_adopt_chip": "{n} sorun sensörü bakım görevine dönüşebilir",
"gs_fleet_chip": "Tek tıkla pil filosunu kurun"
}
@@ -835,5 +835,11 @@
"update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.",
"update_reload": "Перезавантажити",
"battery_fleet_forecast_overdue": "Прогнозована дата минула — батарея й далі повідомляє про добрий стан. Якщо ви її замінили, зафіксуйте заміну; інакше прогноз не справдився.",
"cost_from_parts": "Узяти ≈ {amount} із запчастин"
"cost_from_parts": "Узяти ≈ {amount} із запчастин",
"new_menu": "Створити",
"dismiss": "Приховати",
"gs_label": "Перші кроки — ці підказки зникнуть у міру зростання налаштування",
"gs_setups_chip": "Рекомендовані налаштування: знайдено {n} пристроїв із готовими тригерами",
"gs_adopt_chip": "{n} датчиків проблем можуть стати завданнями обслуговування",
"gs_fleet_chip": "Один клік налаштує парк батарей"
}
@@ -835,5 +835,11 @@
"update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。",
"update_reload": "重新加载",
"battery_fleet_forecast_overdue": "预测日期已过——电池仍报告状态良好。如果您已更换电池,请记录更换;否则说明预测有误。",
"cost_from_parts": "采用配件合计 ≈ {amount}"
"cost_from_parts": "采用配件合计 ≈ {amount}",
"new_menu": "新建",
"dismiss": "忽略",
"gs_label": "入门提示——随着配置的完善,这些提示会自动消失",
"gs_setups_chip": "推荐配置发现 {n} 台设备(含预设触发器)",
"gs_adopt_chip": "{n} 个问题传感器可转换为维护任务",
"gs_fleet_chip": "一键设置电池车队"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.53.0 */
var S="2.53.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
/*! maintenance_supporter frontend 2.54.0 */
var S="2.54.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
@@ -0,0 +1,222 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as d}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WZ6RLKNK.js";import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-A7WM72WE.js";import{a,b as _,c as t,f as l,g as h,i as g,j as n,n as i,p as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var r=class extends h{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._users=[];this._responsible="";this._localeReady=!1;this._userService=null;this._toggle=s=>{let o=new Set(this._selected);o.has(s)?o.delete(s):o.add(s),this._selected=o};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(s=>s.entity_id))};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let s=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:e.suggested_entry_id??void 0,object_name:e.suggested_object_name,device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0})),o=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/adopt",selections:s});this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:o})),this._open=!1}catch(s){this._error=d(s,this._lang)}finally{this._adopting=!1}}}}get _lang(){return this.hass?.language||"en"}updated(s){s.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,v(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._responsible="";try{this._userService?this._userService.updateHass(this.hass):this._userService=new u(this.hass);let[s,o]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/discover"}),this._userService.getUsers().catch(()=>[])]);this._sensors=s.sensors||[],this._selected=new Set(this._sensors.map(e=>e.entity_id)),this._users=o}catch(s){this._error=d(s,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return t``;let s=this._lang,o=this._sensors.length>0&&this._selected.size===this._sensors.length;return t`
<div class="overlay" @click=${this._close}>
<div class="card" @click=${e=>e.stopPropagation()}>
<div class="title">${i("adopt_problem_title",s)}</div>
<div class="hint">${i("adopt_problem_hint",s)}</div>
${this._error?t`<div class="error">${this._error}</div>`:l}
${this._loading?t`<div class="loading">…</div>`:this._sensors.length===0?t`<div class="empty">${i("adopt_problem_none",s)}</div>`:t`
<label class="select-all">
<input
type="checkbox"
.checked=${o}
@change=${this._toggleAll}
/>
<span>${i("selected",s)}: ${this._selected.size} / ${this._sensors.length}</span>
</label>
<div class="list">
${this._sensors.map(e=>{let m=this._selected.has(e.entity_id),p=e.state==="on",c=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return t`
<label class="row">
<input
type="checkbox"
.checked=${m}
@change=${()=>this._toggle(e.entity_id)}
/>
<div class="row-main">
<div class="row-top">
<span class="row-name">${e.name}</span>
<span class="chip ${p?"chip-active":"chip-ok"}">
${p?i("adopt_problem_active",s):i("adopt_problem_ok",s)}
</span>
</div>
${c?t`<div class="row-sub">${c}</div>`:l}
<div class="row-target">
${e.suggested_object_name}${e.suggested_entry_id?l:t` <span class="new-tag">${i("adopt_problem_new_object",s)}</span>`}
</div>
${e.suggested_part_name?t`<div class="row-part">
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
${i("adopt_problem_part",s).replace("{name}",e.suggested_part_name)}
</div>`:l}
</div>
</label>
`})}
</div>
`}
${!this._loading&&this._sensors.length>0&&this._users.length>0?t`
<label class="responsible">
<span>${i("adopt_problem_responsible",s)}</span>
<select
.value=${this._responsible}
@change=${e=>{this._responsible=e.target.value}}
>
<option value="" ?selected=${!this._responsible}>${i("no_user_assigned",s)}</option>
${this._users.map(e=>t`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
</select>
</label>
`:l}
<div class="actions">
<ha-button appearance="plain" @click=${this._close}>
${i("cancel",s)}
</ha-button>
<ha-button
@click=${this._adopt}
.disabled=${this._selected.size===0||this._adopting}
>
${i("adopt_problem_adopt",s)}
</ha-button>
</div>
</div>
</div>
`}};r.styles=_`
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.card {
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-width: min(360px, calc(100vw - 24px));
max-width: 560px;
width: 90vw;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.title {
font-size: 18px;
font-weight: 500;
}
.hint {
color: var(--secondary-text-color);
font-size: 13px;
}
.error {
color: var(--error-color, #f44336);
font-size: 13px;
}
.loading,
.empty {
color: var(--secondary-text-color);
font-size: 14px;
padding: 12px 0;
}
.select-all {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--secondary-text-color);
cursor: pointer;
}
.select-all input {
cursor: pointer;
}
.list {
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
max-height: 50vh;
}
.row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px;
border: 1px solid var(--divider-color);
border-radius: 6px;
cursor: pointer;
}
.row input {
margin-top: 2px;
cursor: pointer;
}
.row-main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.row-top {
display: flex;
align-items: center;
gap: 8px;
}
.row-name {
font-weight: 500;
font-size: 13px;
}
.row-sub {
color: var(--secondary-text-color);
font-size: 12px;
}
.row-target {
color: var(--secondary-text-color);
font-size: 12px;
}
.row-part {
color: var(--secondary-text-color);
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.row-part ha-icon {
--mdc-icon-size: 14px;
}
.new-tag {
font-style: italic;
}
.chip {
font-size: 11px;
padding: 1px 8px;
border-radius: 10px;
white-space: nowrap;
}
.chip-active {
background: var(--error-color, #f44336);
color: #fff;
}
.chip-ok {
background: var(--divider-color);
color: var(--secondary-text-color);
}
.responsible {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--secondary-text-color);
flex-wrap: wrap;
}
.responsible select {
flex: 1;
min-width: 140px;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--divider-color);
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
font-size: 13px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 8px;
}
`,a([g({attribute:!1})],r.prototype,"hass",2),a([n()],r.prototype,"_open",2),a([n()],r.prototype,"_loading",2),a([n()],r.prototype,"_adopting",2),a([n()],r.prototype,"_error",2),a([n()],r.prototype,"_sensors",2),a([n()],r.prototype,"_selected",2),a([n()],r.prototype,"_users",2),a([n()],r.prototype,"_responsible",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",r);export{r as MaintenanceAdoptProblemSensorsDialog};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
var r=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{r as a};
@@ -0,0 +1,54 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as t,b as a,c as i,f as l,g as p,i as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var e=class extends p{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(n){let o=n.target.value;this.value=o,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:o}}))}render(){return i`
<label class="field">
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:l}</span>`:l}
<input
.value=${this.value??""}
.type=${this.type}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
step=${this.step??l}
min=${this.min??l}
max=${this.max??l}
pattern=${this.pattern??l}
@input=${this._onInput}
@change=${this._onInput}
/>
${this.helper?i`<span class="helper">${this.helper}</span>`:l}
</label>
`}};e.styles=a`
:host { display: block; }
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
font-size: 12px;
color: var(--secondary-text-color, #888);
font-weight: 500;
}
.req { color: var(--error-color, #f44336); margin-left: 2px; }
input {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
}
input:focus {
border-color: var(--primary-color);
}
input:disabled { opacity: 0.5; cursor: not-allowed; }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
font-style: italic;
}
`,t([r()],e.prototype,"label",2),t([r()],e.prototype,"value",2),t([r()],e.prototype,"placeholder",2),t([r()],e.prototype,"type",2),t([r({type:Boolean})],e.prototype,"required",2),t([r({type:Boolean})],e.prototype,"disabled",2),t([r()],e.prototype,"step",2),t([r()],e.prototype,"min",2),t([r()],e.prototype,"max",2),t([r()],e.prototype,"pattern",2),t([r()],e.prototype,"helper",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
import{n as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";function _(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let t=!!e.entry_id&&e.entry_id!==r,a=t?e.entry_id:r,o=s.find(p=>p.entry_id===a),n=(o?.parts||[]).find(p=>p.id===e.part_id)||null,d=t&&o?.object?.name||"",i=n?.name||u("shared_part_unknown",c);return{part:n,foreign:t,ownerName:d,label:d?`${i} (${d})`:i}}function f(e,r,s,c){let{part:t,label:a}=l(e,r,s,c),o=t&&t.stock!==null&&t.stock!==void 0?` (${t.stock}${t.unit?" "+t.unit:""})`:"",n=t?.storage_location?` \u2014 ${t.storage_location}`:"";return`${e.quantity}\xD7 ${a}${o}${n}`}function g(e,r,s,c){let a=(s.find(n=>n.entry_id===r)?.parts||[]).map(n=>({...n})),o=new Set(a.map(n=>_({part_id:n.id})));for(let n of e?.consumes_parts||[]){if(!n.entry_id||n.entry_id===r)continue;let d=_(n);if(o.has(d))continue;o.add(d);let{part:i,ownerName:p}=l(n,r,s,c);a.push({id:n.part_id,name:i?.name||u("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:n.entry_id,owner_name:p})}return a}export{_ as a,f as b,g as c};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
var i=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],s=i.map(n=>n.key),l=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function c(n){if(!Array.isArray(n))return[...l];let o=new Set,e=[];for(let a of n)typeof a=="string"&&s.includes(a)&&!o.has(a)&&(o.add(a),e.push(a));return e.length?(e.includes("name")||e.unshift("name"),e):[...l]}function u(n,o,e){let a=new Blob([n],{type:e}),r=URL.createObjectURL(a),t=document.createElement("a");t.href=r,t.download=o,t.target="_blank",t.rel="noopener",t.style.display="none",document.body.appendChild(t),t.dispatchEvent(new MouseEvent("click")),document.body.removeChild(t),setTimeout(()=>URL.revokeObjectURL(r),6e4)}function y(n,o){let e=document.createElement("a");e.href=n,e.download=o,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}export{i as a,l as b,c,u as d,y as e};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
var o=["notes","cost","duration","photo","user"],t={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{o as a,t as b};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
import{n as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var s={name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_for_minutes:"trigger_for_minutes"};function c(r,o){let e=s[r];if(!e)return r;let t=a(e,o);return t&&t!==e?t:r}function d(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function g(r,o,e){if(e=e??a("action_error",o),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,_=t.message||t.error?.message||"";if(!_)return e;let i=d(_),l=i.field?c(i.field,o):"",n=u=>a(u,o).replace("{field}",l).replace("{n}",i.param??"");switch(i.rule){case"too_long":return n("err_too_long");case"too_short":return n("err_too_short");case"value_too_high":return n("err_value_too_high");case"value_too_low":return n("err_value_too_low");case"required":return n("err_required");case"wrong_type":return n("err_wrong_type").replace("{type}",i.param??"");case"invalid_choice":return n("err_invalid_choice");case"invalid_value":return n("err_invalid_value");default:return _||e}}export{g as a};
@@ -0,0 +1,292 @@
/*! maintenance_supporter frontend 2.54.0 */
import{b as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TLH3CQAL.js";import{a as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KH7UXWPC.js";import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WZ6RLKNK.js";import{a as s,b,c as a,f as d,g as v,i as n,j as l,n as r,x as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var i=class extends v{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[_(e),{...e}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,o=t.files?.[0];if(t.value="",!!o){this._photoUploading=!0,this._error="";try{let c=new FormData;c.append("entry_id",this.entryId),c.append("tags","photo"),c.append("file",o,o.name);let p=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:c});if(!p.ok){this._error=p.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let u=await p.json();u.id&&(this._photoDocId=u.id,this._photoPreview=URL.createObjectURL(o))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=g(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?a`<span class="req-mark" aria-hidden="true">*</span>`:d}_partsCostSuggestion(){if(this.restockDefault!==null){let o=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(o)||o<=0?null:Math.round(this.restockUnitCost*o*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let o of Object.values(this._usedParts)){let c=this.parts.find(p=>_({part_id:p.id,entry_id:p.entry_id})===_(o));c?.cost!=null&&(e+=c.cost*(o.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return d;let t=this._partsCostSuggestion();if(t==null||t<=0)return d;let o=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return a`<button
type="button"
class="cost-suggestion"
@click=${()=>this._cost=t.toFixed(2)}
>${r("cost_from_parts",e).replace("{amount}",o)}</button>`}_close(){this._open=!1}render(){if(!this._open)return a``;let e=this.lang||this.hass?.language||"en";return a`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${r("complete_title",e)}${this.taskName}</div>
<div class="content">
${this._error?a`<div class="error">${this._error}</div>`:d}
${this.checklist.length>0?a`
<div class="checklist-section">
<label class="checklist-label">${r("checklist",e)}</label>
${this.checklist.map((t,o)=>a`
<label class="checklist-item" @click=${()=>this._toggleCheck(o)}>
<input type="checkbox" .checked=${!!this._checklistState[String(o)]} />
<span>${t}</span>
</label>
`)}
</div>
`:d}
${this.taskType==="reading"?a`
<label class="field">
<span class="field-label">${r("reading_value_label",e)}${this.readingUnit?` (${this.readingUnit})`:""}</span>
<input type="number" step="any" class="field-input"
.value=${this._readingValue}
@input=${t=>this._readingValue=t.target.value} />
</label>`:d}
${this.parts.length?a`<div class="used-parts">
<span class="field-label">${r("complete_parts_used",e)}</span>
${this.parts.map(t=>{let o=_({part_id:t.id,entry_id:t.entry_id}),c=this._usedParts[o],p=c!==void 0,u=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return a`<div class="used-part-row">
<label class="used-part-check">
<input type="checkbox" .checked=${p}
@change=${f=>{let h={...this._usedParts};f.target.checked?h[o]=h[o]||u:delete h[o],this._usedParts=h}} />
<span
>${t.name}${t.owner_name?a`<span class="used-part-owner"> (${t.owner_name})</span>`:d}${t.stock!==null&&t.stock!==void 0?` (${t.stock}${t.unit?" "+t.unit:""})`:""}</span
>
</label>
${p?a`<input class="used-part-qty" type="number" min="0.01" max="999" step="0.01"
.value=${String(c.quantity)}
@input=${f=>{let h=parseFloat(f.target.value);this._usedParts={...this._usedParts,[o]:{...u,quantity:Number.isFinite(h)&&h>=.01?h:1}}}} />`:d}
</div>`})}
</div>`:this.consumesInfo.length?a`<div class="consumes-hint">
${this.consumesInfo.map(t=>a`<div>${t}</div>`)}
</div>`:d}
${this.restockDefault!==null?a`
<label class="field">
<span class="field-label">${r("restock_quantity_label",e)}</span>
<input type="number" step="0.01" min="0.01" class="field-input"
.value=${this._restockQty}
@input=${t=>this._restockQty=t.target.value} />
</label>`:d}
<!-- Native <input>s rather than <ha-textfield>: when this dialog
is opened from a Lovelace card via dialog-mount, ha-textfield
isn't yet registered (HA loads it lazily when its own panels
need it) so the elements render with zero height and the user
only sees the title + Cancel/Complete buttons the original
bug report. Native inputs always render. -->
<label class="field">
<span class="field-label">${r("notes_optional",e)}${this._req("notes")}</span>
<input type="text" class="field-input"
.value=${this._notes}
@input=${t=>this._notes=t.target.value} />
</label>
<label class="field">
<span class="field-label">${r("cost_optional",e)}${this._req("cost")}</span>
<input type="number" step="0.01" min="0" class="field-input"
.value=${this._cost}
@input=${t=>this._cost=t.target.value} />
${this._renderCostSuggestion(e)}
</label>
<label class="field">
<span class="field-label">${r("duration_minutes",e)}${this._req("duration")}</span>
<input type="number" step="0.01" min="0" class="field-input"
.value=${this._duration}
@input=${t=>this._duration=t.target.value} />
</label>
<div class="field">
<span class="field-label">${r("completion_photo_optional",e)}${this._req("photo")}</span>
${this._photoPreview?a`
<div class="photo-preview">
<img src=${this._photoPreview} alt="" />
<button type="button" class="photo-remove" @click=${this._removePhoto}
title="${r("remove",e)}"></button>
</div>`:a`
<label class="photo-pick">
<ha-icon icon="mdi:camera"></ha-icon>
<span>${this._photoUploading?r("uploading",e):r("add_photo",e)}</span>
<input type="file" accept="image/*" capture="environment"
?disabled=${this._photoUploading}
@change=${this._onPhotoInput} />
</label>`}
</div>
${this.adaptiveEnabled?a`
<div class="feedback-section">
<label class="feedback-label">${r("was_maintenance_needed",e)}</label>
<div class="feedback-buttons">
<button
class="feedback-btn ${this._feedback==="needed"?"selected":""}"
@click=${()=>this._setFeedback("needed")}
>${r("feedback_needed",e)}</button>
<button
class="feedback-btn ${this._feedback==="not_needed"?"selected":""}"
@click=${()=>this._setFeedback("not_needed")}
>${r("feedback_not_needed",e)}</button>
<button
class="feedback-btn ${this._feedback==="not_sure"?"selected":""}"
@click=${()=>this._setFeedback("not_sure")}
>${r("feedback_not_sure",e)}</button>
</div>
</div>
`:d}
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${r("cancel",e)}
</ha-button>
<ha-button
@click=${this._complete}
.disabled=${this._loading||this._missingRequired.length>0}
title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(m[t]??t,e))).join(" \xB7 "):""}
>
${this._loading?r("completing",e):r("complete",e)}
</ha-button>
</div>
</ha-dialog>
`}};i.styles=[k,b`
.req-mark {
color: var(--error-color, #f44336);
margin-left: 2px;
font-weight: 600;
}
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
.cost-suggestion {
align-self: flex-start;
margin-top: 4px;
padding: 0;
border: none;
background: none;
color: var(--primary-color);
font-size: 12.5px;
cursor: pointer;
text-decoration: underline dotted;
text-underline-offset: 2px;
}
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 300px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
.consumes-hint {
font-size: 13px;
color: var(--secondary-text-color);
border-left: 3px solid var(--primary-color);
padding: 4px 8px;
margin: 4px 0 8px;
}
/* #99: editable per-completion parts selection */
.used-parts { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 4px; }
.used-part-row { display: flex; align-items: center; gap: 8px; }
.used-part-check {
display: flex; align-items: center; gap: 6px; flex: 1;
font-size: 13px; cursor: pointer;
}
.used-part-check input { cursor: pointer; }
/* #111: whose stock this row draws on. Muted but never omitted an
unlabelled foreign pool is indistinguishable from an own part. */
.used-part-owner { color: var(--secondary-text-color); }
.used-part-qty {
width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px;
border: 1px solid var(--divider-color);
background: var(--card-background-color);
color: var(--primary-text-color);
}
.error {
color: var(--error-color, #f44336);
font-size: 13px;
}
/* .field/.field-label/.field-input come from nativeFieldStyles */
.photo-pick {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px dashed var(--divider-color);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--secondary-text-color);
width: fit-content;
}
.photo-pick:hover { border-color: var(--primary-color); }
.photo-pick input[type="file"] { display: none; }
.photo-preview {
position: relative;
width: fit-content;
}
.photo-preview img {
max-width: 160px;
max-height: 160px;
border-radius: 8px;
display: block;
}
.photo-remove {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
border-radius: 50%;
border: none;
background: var(--error-color, #db4437);
color: #fff;
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.checklist-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid var(--divider-color);
margin-bottom: 4px;
}
.checklist-label {
font-weight: 500;
font-size: 13px;
color: var(--secondary-text-color);
}
.checklist-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 0;
font-size: 14px;
}
.checklist-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.feedback-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
border-top: 1px solid var(--divider-color);
}
.feedback-label {
font-weight: 500;
font-size: 13px;
color: var(--secondary-text-color);
}
.feedback-buttons {
display: flex;
gap: 8px;
}
.feedback-btn {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--divider-color);
border-radius: 8px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
font-size: 13px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.feedback-btn:hover {
background: var(--secondary-background-color, #f5f5f5);
}
.feedback-btn.selected {
background: var(--primary-color);
color: var(--text-primary-color, #fff);
border-color: var(--primary-color);
}
`],s([n({attribute:!1})],i.prototype,"hass",2),s([n()],i.prototype,"entryId",2),s([n()],i.prototype,"taskId",2),s([n()],i.prototype,"taskName",2),s([n()],i.prototype,"lang",2),s([n({type:Array})],i.prototype,"checklist",2),s([n({type:Boolean})],i.prototype,"adaptiveEnabled",2),s([n()],i.prototype,"taskType",2),s([n()],i.prototype,"readingUnit",2),s([n({attribute:!1})],i.prototype,"restockDefault",2),s([n({attribute:!1})],i.prototype,"restockUnitCost",2),s([n()],i.prototype,"currencySymbol",2),s([n({attribute:!1})],i.prototype,"parts",2),s([n({attribute:!1})],i.prototype,"consumesParts",2),s([n({type:Array})],i.prototype,"consumesInfo",2),s([n({type:Array})],i.prototype,"requiredFields",2),s([l()],i.prototype,"_open",2),s([l()],i.prototype,"_notes",2),s([l()],i.prototype,"_cost",2),s([l()],i.prototype,"_duration",2),s([l()],i.prototype,"_loading",2),s([l()],i.prototype,"_error",2),s([l()],i.prototype,"_checklistState",2),s([l()],i.prototype,"_feedback",2),s([l()],i.prototype,"_photoDocId",2),s([l()],i.prototype,"_photoPreview",2),s([l()],i.prototype,"_photoUploading",2),s([l()],i.prototype,"_readingValue",2),s([l()],i.prototype,"_restockQty",2),s([l()],i.prototype,"_usedParts",2),s([n({attribute:!1})],i.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",i);export{i as MaintenanceCompleteDialog};
@@ -0,0 +1,143 @@
/*! maintenance_supporter frontend 2.54.0 */
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-AEF5ZY4E.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WZ6RLKNK.js";import{a as r,b as _,c as l,f as o,g as p,i as d,j as s,n as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var e=class extends p{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return this.hass?.language??navigator.language.split("-")[0]??"en"}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(a,n){this._entryId=a,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(a){this._error=h(a,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(a=>a.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let a=this._lang,n=this._entryId?i("edit_object",a):i("new_object",a);return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${n}</div>
<div class="content">
${this._error?l`<div class="error">${this._error}</div>`:o}
<ms-textfield
label="${i("name",a)}"
required
.value=${this._name}
@input=${t=>this._name=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("manufacturer_optional",a)}"
.value=${this._manufacturer}
@input=${t=>this._manufacturer=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("model_optional",a)}"
.value=${this._model}
@input=${t=>this._model=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("serial_number_optional",a)}"
.value=${this._serialNumber}
@input=${t=>this._serialNumber=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("documentation_url_optional",a)}"
type="url"
.value=${this._documentationUrl}
@input=${t=>this._documentationUrl=t.target.value}
></ms-textfield>
<ha-area-picker
.hass=${this.hass}
label="${i("area_id_optional",a)}"
.value=${this._areaId}
@value-changed=${t=>this._areaId=t.detail.value||""}
></ha-area-picker>
<ms-textfield
label="${i("installation_date_optional",a)}"
type="date"
.value=${this._installationDate}
@input=${t=>this._installationDate=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("warranty_expiry_optional",a)}"
type="date"
.value=${this._warrantyExpiry}
@input=${t=>this._warrantyExpiry=t.target.value}
></ms-textfield>
<ha-form
.hass=${this.hass}
.data=${{device:this._haDeviceId||void 0}}
.schema=${[{name:"device",selector:{device:{}}}]}
.computeLabel=${()=>i("link_device_optional",a)}
@value-changed=${t=>this._haDeviceId=t.detail.value?.device||""}
></ha-form>
${this._parentChoices().length?l`<label class="textarea-field">
<span class="textarea-label">${i("parent_object_optional",a)}</span>
<select
class="parent-select"
.value=${this._parentEntryId}
@change=${t=>this._parentEntryId=t.target.value}
>
<option value="" ?selected=${!this._parentEntryId}>
${i("parent_none",a)}
</option>
${this._parentChoices().map(t=>l`<option
value=${t.entry_id}
?selected=${this._parentEntryId===t.entry_id}
>${t.object.name}</option>`)}
</select>
</label>`:o}
<label class="textarea-field">
<span class="textarea-label">${i("object_notes_optional",a)}</span>
<textarea
rows="3"
.value=${this._notes}
@input=${t=>this._notes=t.target.value}
></textarea>
</label>
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${i("cancel",this._lang)}
</ha-button>
<ha-button
@click=${this._save}
.disabled=${this._loading||!this._name.trim()}
>
${this._loading?i("saving",this._lang):i("save",this._lang)}
</ha-button>
</div>
</ha-dialog>
`}};e.styles=_`
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 300px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
ms-textfield {
display: block;
}
.textarea-field {
display: flex; flex-direction: column; gap: 4px;
}
.textarea-label {
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
}
.textarea-field textarea {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
resize: vertical;
}
.textarea-field textarea:focus {
outline: none; border-color: var(--primary-color);
}
.parent-select {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
}
.error {
color: var(--error-color, #f44336);
font-size: 13px;
}
`,r([d({attribute:!1})],e.prototype,"hass",2),r([d({attribute:!1})],e.prototype,"objects",2),r([s()],e.prototype,"_open",2),r([s()],e.prototype,"_loading",2),r([s()],e.prototype,"_error",2),r([s()],e.prototype,"_name",2),r([s()],e.prototype,"_manufacturer",2),r([s()],e.prototype,"_model",2),r([s()],e.prototype,"_serialNumber",2),r([s()],e.prototype,"_areaId",2),r([s()],e.prototype,"_installationDate",2),r([s()],e.prototype,"_warrantyExpiry",2),r([s()],e.prototype,"_documentationUrl",2),r([s()],e.prototype,"_notes",2),r([s()],e.prototype,"_haDeviceId",2),r([s()],e.prototype,"_parentEntryId",2),r([s()],e.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",e);export{e as MaintenanceObjectDialog};
@@ -0,0 +1,213 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a,b as v,c as n,f as g,g as b,i as m,j as c,n as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";function p(l){return l.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function x(l){return!l.startsWith("data:image/svg+xml,")&&!l.startsWith("data:image/png;base64,")?"":p(l)}function $(l){return l.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var r=class extends b{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,i){this._entryId=e,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,i,o,s){this._entryId=e,this._taskId=i,this._objectName=o,this._taskName=s,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let o=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&o.push(this.hass.connection.sendMessagePromise({...i,action:"complete"}));let s=await Promise.all(o);if(e!==this._generateSeq)return;this._viewResult=s[0],s.length>1&&(this._completeResult=s[1])}catch(i){if(e!==this._generateSeq)return;let o=i?.code,s=i?.message;this._error=o==="no_url"||typeof s=="string"&&s.includes("No Home Assistant URL")?t("qr_error_no_url",this.lang):t("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,i=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,o=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),s=window.open("","_blank","width=600,height=500");if(!s)return;let h=this.lang||"en",d=p(i),u=p(o),_=!!this._completeResult,f=p(t("qr_action_view",h)),w=p(t("qr_action_complete",h));s.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${d}</title>
<style>
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
carry their own white quiet zone and stay scannable either way, but the
labels below are explicit dark greys and would vanish on a WebView's dark
canvas. Same reasoning as helpers/report.ts. */
:root{color-scheme:light}
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
h2{margin:0 0 4px}
.sub{color:#666;font-size:14px;margin-bottom:16px}
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
.qr-col img{width:${_?"200px":"280px"}}
.qr-label{font-size:13px;font-weight:500;color:#333}
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
</style></head><body>
<h2>${d}</h2>
${u?`<div class="sub">${u}</div>`:""}
<div class="qr-row">
<div class="qr-col">
<img src="${x(this._viewResult.svg_data_uri)}" alt="QR Info" />
<div class="qr-label">${f}</div>
</div>
${_?`<div class="qr-col">
<img src="${x(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<div class="qr-label">${w}</div>
</div>`:""}
</div>
<div class="url">${p(this._viewResult.url)}</div>
<script>setTimeout(()=>window.print(),300)<\/script>
</body></html>`),s.document.close()}_downloadSvg(e,i){let o=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),s=new Blob([o],{type:"image/svg+xml"}),h=URL.createObjectURL(s),d=document.createElement("a");d.href=h;let u=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;d.download=`qr-${$(u)}-${i}.svg`,d.click(),URL.revokeObjectURL(h)}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en",i=this._taskName?`${t("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${t("qr_code",e)}: ${this._objectName}`,o=!!this._viewResult;return n`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${i}</div>
<div class="content">
${this._loading?n`<div class="loading">${t("qr_generating",e)}</div>`:this._error?n`<div class="error">${this._error}</div>`:o?n`
<div class="qr-pair">
<div class="qr-item">
<img
class="qr-image ${this._completeResult?"small":""}"
src="${this._viewResult.svg_data_uri}"
alt="QR Info"
/>
<div class="qr-item-label">${t("qr_action_view",e)}</div>
<button class="dl-btn"
@click=${()=>this._downloadSvg(this._viewResult,"info")}>
<ha-icon icon="mdi:download"></ha-icon>
${t("qr_download",e)}
</button>
</div>
${this._completeResult?n`
<div class="qr-item">
<img
class="qr-image small"
src="${this._completeResult.svg_data_uri}"
alt="QR Complete"
/>
<div class="qr-item-label">${t("qr_action_complete",e)}</div>
<button class="dl-btn"
@click=${()=>this._downloadSvg(this._completeResult,"complete")}>
<ha-icon icon="mdi:download"></ha-icon>
${t("qr_download",e)}
</button>
</div>
`:g}
</div>
<div class="url-display">${this._viewResult.url}</div>
`:g}
<div class="action-row">
<label>${t("qr_url_mode",e)}</label>
<div class="action-toggle">
<button class="toggle-btn ${this._urlMode==="companion"?"active":""}"
@click=${()=>this._setUrlMode("companion")}>${t("qr_mode_companion",e)}</button>
<button class="toggle-btn ${this._urlMode==="local"?"active":""}"
@click=${()=>this._setUrlMode("local")}>${t("qr_mode_local",e)}</button>
<button class="toggle-btn ${this._urlMode==="server"?"active":""}"
@click=${()=>this._setUrlMode("server")}>${t("qr_mode_server",e)}</button>
</div>
</div>
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${t("cancel",e)}
</ha-button>
<ha-button
@click=${this._print}
.disabled=${!o}
>
${t("qr_print",e)}
</ha-button>
</div>
</ha-dialog>
`}};r.styles=v`
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
min-width: 300px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
.qr-pair {
display: flex;
gap: 20px;
justify-content: center;
width: 100%;
}
.qr-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.qr-image {
width: 240px;
height: 240px;
image-rendering: pixelated;
}
.qr-image.small {
width: 180px;
height: 180px;
}
.qr-item-label {
font-size: 12px;
font-weight: 500;
color: var(--secondary-text-color);
text-align: center;
}
.dl-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: none;
border: 1px solid var(--divider-color, #e0e0e0);
cursor: pointer;
font-size: 13px;
color: var(--primary-text-color);
padding: 6px 14px;
border-radius: 18px;
transition: background 0.2s, border-color 0.2s;
}
.dl-btn:hover {
background: var(--secondary-background-color, #f5f5f5);
border-color: var(--primary-color);
}
.dl-btn ha-icon {
--mdc-icon-size: 18px;
}
.url-display {
font-size: 11px;
color: var(--secondary-text-color);
word-break: break-all;
text-align: center;
max-width: 400px;
}
.loading {
padding: 40px 0;
color: var(--secondary-text-color);
}
.error {
padding: 20px 0;
color: var(--error-color, #f44336);
}
.action-row {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.action-row label {
font-size: 13px;
color: var(--secondary-text-color);
}
.action-toggle {
display: flex;
gap: 4px;
background: var(--divider-color, #e0e0e0);
border-radius: 6px;
padding: 3px;
}
.toggle-btn {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--primary-text-color);
cursor: pointer;
border-radius: 4px;
font-size: 13px;
transition: all 0.2s;
line-height: 1.3;
}
.toggle-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.toggle-btn.active {
background: var(--primary-color);
color: var(--text-primary-color, #fff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
`,a([m({attribute:!1})],r.prototype,"hass",2),a([m()],r.prototype,"lang",2),a([c()],r.prototype,"_open",2),a([c()],r.prototype,"_loading",2),a([c()],r.prototype,"_error",2),a([c()],r.prototype,"_viewResult",2),a([c()],r.prototype,"_completeResult",2),a([c()],r.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",r);export{r as MaintenanceQrDialog};
@@ -0,0 +1,146 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WZ6RLKNK.js";import{a as l,b as m,c as i,f as h,g as v,i as u,j as d,n as p,p as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C2ERU424.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._setups=[];this._selected=new Set;this._baselines=new Map;this._targets=new Map;this._objects=[];this._localeReady=!1;this._toggle=t=>{let e=new Set(this._selected);e.has(t)?e.delete(t):e.add(t),this._selected=e};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/adopt",selections:[...this._selected].map(e=>{let r={device_id:e},c=this._targets.get(e);c&&(r.entry_id=c);let s=this._setups.find(n=>n.device_id===e);for(let n of s?.tasks??[]){let o=this._baselines.get(`${e} ${n.task_name}`),_=o?parseFloat(o):NaN;!isNaN(_)&&_>=0&&((r.baselines??={})[n.task_name]=_)}return r})});this.dispatchEvent(new CustomEvent("integration-setups-adopted",{bubbles:!0,composed:!0,detail:t})),this._open=!1}catch(t){this._error=g(t,this._lang)}finally{this._adopting=!1}}}}get _lang(){return this.hass?.language||"en"}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._setups=[],this._selected=new Set;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/discover"});this._setups=t.setups||[],this._selected=new Set(this._setups.map(e=>e.device_id)),this._baselines=new Map,this._targets=new Map;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._objects=(e.objects||[]).map(r=>({entry_id:r.entry_id,name:r.object?.name||r.entry_id})).sort((r,c)=>r.name.localeCompare(c.name))}catch{this._objects=[]}}catch(t){this._error=g(t,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return i``;let t=this._lang;return i`
<div class="overlay" @click=${this._close}>
<div class="card" @click=${e=>e.stopPropagation()}>
<div class="title">${p("setups_title",t)}</div>
<div class="hint">${p("setups_hint",t)}</div>
${this._error?i`<div class="error">${this._error}</div>`:h}
${this._loading?i`<div class="loading">…</div>`:this._setups.length===0?i`<div class="empty">${p("setups_none",t)}</div>`:i`
<div class="list">
${this._setups.map(e=>{let r=this._selected.has(e.device_id),c=[e.integration_name,e.area_name].filter(Boolean).join(" \xB7 ");return i`
<label class="row">
<input
type="checkbox"
.checked=${r}
@change=${()=>this._toggle(e.device_id)}
/>
<div class="row-main">
<div class="row-top">
<span class="row-name">${e.device_name}</span>
</div>
<div class="row-sub">${c}</div>
<div class="row-target" @click=${s=>s.preventDefault()}>
${r&&this._objects.length>0?i`
<select
class="target-select"
@change=${s=>{let n=new Map(this._targets),o=s.target.value;o?n.set(e.device_id,o):n.delete(e.device_id),this._targets=n}}
>
<option value="" ?selected=${!this._targets.get(e.device_id)}>
${e.suggested_entry_id?e.suggested_object_name:p("setups_target_new",t).replace("{name}",e.suggested_object_name)}
</option>
${this._objects.filter(s=>s.entry_id!==e.suggested_entry_id).map(s=>i`<option
value=${s.entry_id}
?selected=${this._targets.get(e.device_id)===s.entry_id}
>
${s.name}
</option>`)}
</select>
`:i`${e.suggested_object_name}${e.suggested_entry_id?h:i` <span class="new-tag">${p("adopt_problem_new_object",t)}</span>`}`}
</div>
<div class="row-tasks">
${e.tasks.map(s=>i`<span class="chip" title=${s.entity_ids.join(", ")}>
<ha-icon icon="mdi:link-variant"></ha-icon>${s.task_name_localized||s.task_name}
</span>`)}
</div>
${r?e.tasks.filter(s=>s.direction==="usage_delta").map(s=>{let n=`${e.device_id} ${s.task_name}`;return i`
<div class="baseline-field" @click=${o=>o.preventDefault()}>
<span class="baseline-label"
>${s.task_name_localized||s.task_name}
${p("setups_baseline_hint",t)}</span
>
<input
type="number"
step="any"
min="0"
.value=${this._baselines.get(n)??""}
@click=${o=>o.preventDefault()}
@input=${o=>{let _=new Map(this._baselines);_.set(n,o.target.value),this._baselines=_}}
/>
</div>
`}):h}
</div>
</label>
`})}
</div>
`}
<div class="actions">
<ha-button appearance="plain" @click=${this._close}>
${p("cancel",t)}
</ha-button>
<ha-button
@click=${this._adopt}
.disabled=${this._selected.size===0||this._adopting}
>
${p("setups_adopt",t)}
</ha-button>
</div>
</div>
</div>
`}};a.styles=m`
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.card {
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-width: min(360px, calc(100vw - 24px));
max-width: 560px;
width: 90vw;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.title { font-size: 18px; font-weight: 500; }
.hint { color: var(--secondary-text-color); font-size: 13px; }
.error { color: var(--error-color, #f44336); font-size: 13px; }
.loading, .empty { color: var(--secondary-text-color); font-size: 14px; padding: 12px 0; }
.list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 50vh; }
.row {
display: flex; align-items: flex-start; gap: 10px; padding: 8px;
border: 1px solid var(--divider-color); border-radius: 6px; cursor: pointer;
}
.row input { margin-top: 2px; cursor: pointer; }
.row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.row-name { font-weight: 500; font-size: 13px; }
.row-sub, .row-target { color: var(--secondary-text-color); font-size: 12px; }
.new-tag { font-style: italic; }
.row-tasks { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.chip {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; padding: 2px 8px; border-radius: 10px;
background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
color: var(--primary-text-color); white-space: nowrap;
}
.chip ha-icon { --mdc-icon-size: 12px; color: var(--primary-color); }
.target-select {
font-size: 12px; padding: 2px 4px; max-width: 100%;
border: 1px solid var(--divider-color); border-radius: 4px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
}
.baseline-field {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
margin-top: 4px; font-size: 12px; color: var(--secondary-text-color);
}
.baseline-field input {
width: 110px; padding: 3px 6px; font-size: 12px;
border: 1px solid var(--divider-color); border-radius: 4px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
}
.actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
`,l([u({attribute:!1})],a.prototype,"hass",2),l([d()],a.prototype,"_open",2),l([d()],a.prototype,"_loading",2),l([d()],a.prototype,"_adopting",2),l([d()],a.prototype,"_error",2),l([d()],a.prototype,"_setups",2),l([d()],a.prototype,"_selected",2),l([d()],a.prototype,"_baselines",2),l([d()],a.prototype,"_targets",2),l([d()],a.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",a);export{a as MaintenanceSuggestedSetupsDialog};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as v}from"./chunk-HQRBSDQ5.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-2TCUV3FU.js";import{a as n}from"./chunk-7GA7XVGX.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
<ha-card>
<div class="card-content">
<div class="header">
<div class="title">
<span class="emoji">💰</span>
<span>${this._config.title||r("settings_budget",t)||"Budget"}</span>
</div>
<span class="currency">${i}</span>
</div>
${this._error?o`<div class="error">${this._error}</div>`:h}
${f.map(e=>{if(!(e.budget>0))return o`
<div class="track spent-only">
<div class="track-label-row">
<label>${e.label}</label>
<span class="track-numbers ok">${e.spent.toFixed(0)} ${i}</span>
</div>
</div>
`;let d=Math.min(100,Math.max(0,e.spent/e.budget*100)),c=d>=100?"danger":d>=_?"warning":"ok";return o`
<div class="track">
<div class="track-label-row">
<label>${e.label}</label>
<span class="track-numbers ${c}">
${e.spent.toFixed(0)} / ${e.budget.toFixed(0)} ${i}
</span>
</div>
<div class="bar"><div class="bar-fill ${c}" style="width:${d}%"></div></div>
</div>
`})}
${this._isAdmin?o`
<div class="inputs-row">
<div class="input-field">
<label>${r("budget_monthly_set",t)||"Set monthly"}</label>
<div class="input-wrap">
<input type="number" min="0" step="1"
.value=${this._localMonthly}
?disabled=${this._busy}
@input=${e=>{this._localMonthly=e.target.value,this._dirty=!0}} />
<span class="input-suffix">${i}</span>
</div>
</div>
<div class="input-field">
<label>${r("budget_yearly_set",t)||"Set yearly"}</label>
<div class="input-wrap">
<input type="number" min="0" step="1"
.value=${this._localYearly}
?disabled=${this._busy}
@input=${e=>{this._localYearly=e.target.value,this._dirty=!0}} />
<span class="input-suffix">${i}</span>
</div>
</div>
</div>
<div class="actions">
<button class="btn ${this._dirty?"primary":"muted"}"
@click=${this._save}
?disabled=${this._busy||!this._dirty}>
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
${this._dirty?r("save",t)||"Save":r("saved",t)||"Saved"}
</button>
<button class="btn link" @click=${this._onDeepLink}>
${r("budget_advanced",t)||"Currency, alerts\u2026"}
</button>
</div>
`:o`
<button class="btn link" @click=${this._onDeepLink}>
${r("budget_open_panel",t)||"Open in panel"}
</button>
`}
</div>
</ha-card>
`}};s.styles=[v,u`
.currency {
font-size: 14px; font-weight: 600;
color: var(--secondary-text-color);
background: var(--secondary-background-color);
padding: 2px 10px; border-radius: 999px;
}
.track { display: flex; flex-direction: column; gap: 4px; }
.track-label-row {
display: flex; align-items: center; justify-content: space-between;
}
.track-label-row label {
font-size: 12px; color: var(--secondary-text-color);
text-transform: uppercase; letter-spacing: 0.5px;
}
.track-numbers { font-size: 13px; font-weight: 600; }
.track-numbers.ok { color: var(--primary-text-color); }
.track-numbers.warning { color: #ff9800; }
.track-numbers.danger { color: var(--error-color, #f44336); }
.bar {
height: 6px; background: var(--secondary-background-color);
border-radius: 3px; overflow: hidden;
}
.bar-fill { height: 100%; transition: width 0.3s; border-radius: 3px; }
.bar-fill.ok { background: var(--primary-color); }
.bar-fill.warning { background: #ff9800; }
.bar-fill.danger { background: var(--error-color, #f44336); }
.inputs-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
padding-top: 4px; border-top: 1px solid var(--divider-color);
}
.input-field { display: flex; flex-direction: column; gap: 4px; }
.input-field label {
font-size: 11px; color: var(--secondary-text-color);
text-transform: uppercase; letter-spacing: 0.3px;
}
.input-wrap { position: relative; display: flex; align-items: center; }
.input-wrap input {
flex: 1; padding: 6px 32px 6px 8px; font-size: 13px;
background: var(--secondary-background-color, #2c2c2c);
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
font-family: inherit;
}
.input-suffix {
position: absolute; right: 8px;
color: var(--secondary-text-color); font-size: 13px;
pointer-events: none;
}
.actions { display: flex; gap: 8px; align-items: center; }
`],n([y({attribute:!1})],s.prototype,"hass",2),n([l()],s.prototype,"_config",2),n([l()],s.prototype,"_status",2),n([l()],s.prototype,"_busy",2),n([l()],s.prototype,"_error",2),n([l()],s.prototype,"_localMonthly",2),n([l()],s.prototype,"_localYearly",2),n([l()],s.prototype,"_dirty",2);customElements.get("maintenance-budget-section-card")||customElements.define("maintenance-budget-section-card",s);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-budget-section-card",name:"Maintenance Supporter \u2014 Budget",description:"Inline monthly + yearly budget editor",preview:!1});export{s as MaintenanceBudgetSectionCard};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.54.0 */
var s=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var t=(a,r,c,o)=>{for(var e=o>1?void 0:o?l(r,c):r,i=a.length-1,d;i>=0;i--)(d=a[i])&&(e=(o?d(r,c,e):d(e))||e);return o&&e&&s(r,c,e),e};var m={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},v={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};export{t as a,m as b,v as c};
@@ -0,0 +1,60 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as r}from"./chunk-2TCUV3FU.js";var a=r`
ha-card { overflow: hidden; }
.card-content {
padding: 16px;
display: flex; flex-direction: column;
gap: 12px;
}
.header {
display: flex; align-items: center; justify-content: space-between;
gap: 12px;
}
.title {
display: flex; align-items: center; gap: 8px;
font-size: 16px; font-weight: 500;
}
.emoji { font-size: 20px; }
/* Button family — primary action / muted-saved-state / link / icon-with-text */
.btn {
padding: 6px 12px; font-size: 13px;
border-radius: 6px; cursor: pointer;
border: 1px solid var(--divider-color);
background: var(--secondary-background-color, transparent);
color: var(--primary-text-color);
font-weight: 500;
display: inline-flex; align-items: center; gap: 4px;
}
.btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.btn.primary {
background: var(--primary-color);
color: var(--text-primary-color, white);
border-color: var(--primary-color);
}
.btn.primary[disabled] { opacity: 0.6; }
.btn.muted {
background: transparent;
color: var(--secondary-text-color);
border-style: dashed;
}
.btn.muted[disabled] { opacity: 1; cursor: default; }
.btn.muted ha-icon, .btn.primary ha-icon { --mdc-icon-size: 14px; }
.btn.link {
background: transparent; border: none; padding: 6px 4px;
color: var(--primary-color); margin-left: auto;
}
.btn.link:hover { background: transparent; text-decoration: underline; }
/* Error + loading states */
.error {
padding: 8px; border-radius: 6px;
background: rgba(211, 47, 47, 0.1);
color: var(--error-color, #d32f2f); font-size: 13px;
}
.loading {
padding: 24px; text-align: center;
color: var(--secondary-text-color);
}
`;export{a};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as m}from"./chunk-HQRBSDQ5.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-2TCUV3FU.js";import{a}from"./chunk-7GA7XVGX.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
<ha-card>
<div class="card-content">
<div class="header">
<div class="title">
<span class="emoji">🏷</span>
<span>${this._config.title||i("groups",t)||"Groups"}</span>
<span class="count">${r.length}</span>
</div>
</div>
${this._error?s`<div class="error">${this._error}</div>`:l}
${r.length===0?s`<div class="empty">${i("groups_empty",t)||"No groups yet."}</div>`:s`
<div class="group-list">
${r.map(n=>{let d=this._groups[n],v=d.task_refs?.length??0,b=this._editingId===n;return s`
<div class="group-row">
${b?s`
<input class="edit-input" type="text"
.value=${this._editingName}
?disabled=${this._busy}
@input=${c=>{this._editingName=c.target.value}}
@keydown=${c=>this._onKeyDown(c,this._saveEdit.bind(this))} />
<button class="btn small primary"
@click=${this._saveEdit}
?disabled=${this._busy||!this._editingName.trim()}>
${i("save",t)||"Save"}
</button>
<button class="btn small"
@click=${()=>{this._editingId=null}}>
${i("cancel",t)||"Cancel"}
</button>
`:s`
<span class="group-name">${d.name||"Unnamed"}</span>
<span class="task-count">${v}</span>
${this._isAdmin?s`
<button class="icon-btn"
title="${i("edit",t)||"Edit"}"
@click=${()=>this._startEdit(n)}
?disabled=${this._busy}>
<ha-icon icon="mdi:pencil"></ha-icon>
</button>
<button class="icon-btn danger"
title="${i("delete",t)||"Delete"}"
@click=${()=>this._deleteGroup(n,d.name||"Unnamed")}
?disabled=${this._busy}>
<ha-icon icon="mdi:delete"></ha-icon>
</button>
`:l}
`}
</div>
`})}
</div>
`}
${this._isAdmin?s`
<div class="add-row">
<input type="text"
placeholder="${i("group_new_placeholder",t)||"Add group\u2026"}"
.value=${this._newName}
?disabled=${this._busy}
@input=${n=>{this._newName=n.target.value}}
@keydown=${n=>this._onKeyDown(n,this._addGroup.bind(this))} />
<button class="btn primary"
@click=${this._addGroup}
?disabled=${this._busy||!this._newName.trim()}>
<ha-icon icon="mdi:plus"></ha-icon>
${i("add",t)||"Add"}
</button>
</div>
<button class="btn link" @click=${this._onDeepLink}>
${i("groups_manage_tasks",t)||"Manage task assignments\u2026"}
</button>
`:s`
<button class="btn link" @click=${this._onDeepLink}>
${i("groups_open_panel",t)||"Open in panel"}
</button>
`}
</div>
</ha-card>
`}};e.styles=[m,u`
.count {
font-size: 12px; color: var(--secondary-text-color);
background: var(--secondary-background-color);
padding: 2px 8px; border-radius: 999px;
}
.empty {
padding: 16px; text-align: center;
color: var(--secondary-text-color); font-style: italic;
}
.group-list { display: flex; flex-direction: column; gap: 4px; }
.group-row {
display: flex; align-items: center; gap: 8px;
padding: 6px 8px; border-radius: 6px;
background: var(--secondary-background-color, rgba(255,255,255,0.03));
}
.group-name { flex: 1; font-size: 14px; }
.task-count {
font-size: 11px; color: var(--secondary-text-color);
background: var(--card-background-color, rgba(0,0,0,0.2));
padding: 1px 8px; border-radius: 999px;
font-weight: 500;
}
.edit-input {
flex: 1; padding: 4px 8px; font-size: 14px;
background: var(--card-background-color, #1c1c1c);
color: var(--primary-text-color);
border: 1px solid var(--primary-color); border-radius: 4px;
font-family: inherit;
}
.icon-btn {
background: transparent; border: none; cursor: pointer;
color: var(--secondary-text-color); padding: 4px;
border-radius: 4px;
}
.icon-btn:hover {
background: var(--state-icon-color, rgba(255,255,255,0.06));
color: var(--primary-text-color);
}
.icon-btn.danger:hover { color: var(--error-color); }
.icon-btn ha-icon { --mdc-icon-size: 18px; }
.add-row {
display: flex; gap: 6px;
padding-top: 8px; border-top: 1px solid var(--divider-color);
}
.add-row input {
flex: 1; padding: 6px 8px; font-size: 13px;
background: var(--secondary-background-color, #2c2c2c);
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
font-family: inherit;
}
/* Card-specific overrides on the shared .btn */
.btn.small { padding: 4px 8px; font-size: 12px; }
.btn ha-icon { --mdc-icon-size: 16px; }
`],a([g({attribute:!1})],e.prototype,"hass",2),a([o()],e.prototype,"_config",2),a([o()],e.prototype,"_groups",2),a([o()],e.prototype,"_loaded",2),a([o()],e.prototype,"_busy",2),a([o()],e.prototype,"_error",2),a([o()],e.prototype,"_newName",2),a([o()],e.prototype,"_editingId",2),a([o()],e.prototype,"_editingName",2);customElements.get("maintenance-groups-section-card")||customElements.define("maintenance-groups-section-card",e);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-groups-section-card",name:"Maintenance Supporter \u2014 Groups",description:"Inline group CRUD",preview:!1});export{e as MaintenanceGroupsSectionCard};
@@ -0,0 +1,122 @@
/*! maintenance_supporter frontend 2.54.0 */
import{a as m}from"./chunk-HQRBSDQ5.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-2TCUV3FU.js";import{a as i}from"./chunk-7GA7XVGX.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
<ha-card>
<div class="card-content">
<div class="header">
<div class="title">
<span class="emoji">🏖</span>
<span>${this._config.title||e("vacation_mode",t)||"Vacation mode"}</span>
</div>
<span class="status-pill ${g}">${b}</span>
</div>
${this._error?n`<div class="error">${this._error}</div>`:c}
${this._isAdmin?n`
<div class="row toggle-row">
<label>${e("enable",t)||"Enable"}</label>
<ha-switch
.checked=${d}
.disabled=${this._busy}
@change=${o=>this._toggleEnabled(o.target.checked)}
></ha-switch>
</div>
<div class="dates-row">
<div class="date-field">
<label>${e("vacation_start",t)||"Start"}</label>
<input type="date" .value=${this._localStart}
?disabled=${this._busy}
@input=${o=>{this._localStart=o.target.value,this._dirty=!0}} />
</div>
<div class="date-field">
<label>${e("vacation_end",t)||"End"}</label>
<input type="date" .value=${this._localEnd}
?disabled=${this._busy}
@input=${o=>{this._localEnd=o.target.value,this._dirty=!0}} />
</div>
<div class="date-field buffer">
<label>${e("vacation_buffer",t)||"Buffer days"}</label>
<input type="number" min="0" max="14"
.value=${String(this._localBuffer)}
?disabled=${this._busy}
@input=${o=>{this._localBuffer=parseInt(o.target.value,10)||0,this._dirty=!0}} />
</div>
</div>
<div class="actions">
<button class="btn ${this._dirty?"primary":"muted"}"
@click=${this._save}
?disabled=${this._busy||!this._dirty}>
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
${this._dirty?e("save",t)||"Save":e("saved",t)||"Saved"}
</button>
${p?n`<button class="btn"
@click=${this._endNow}
?disabled=${this._busy}>
${e("vacation_end_now",t)||"End now"}
</button>`:c}
${u>0?n`<button class="btn link"
@click=${this._onDeepLink}>
${u} ${e("vacation_exempt_count",t)||"exempt"}
</button>`:n`<button class="btn link"
@click=${this._onDeepLink}>
${e("vacation_advanced",t)||"Advanced\u2026"}
</button>`}
</div>
`:n`
<div class="readonly">
${d&&s.start&&s.end?n`<div>${s.start}${s.end}</div>`:c}
<button class="btn link" @click=${this._onDeepLink}>
${e("vacation_open_panel",t)||"Open in panel"}
</button>
</div>
`}
</div>
</ha-card>
`}};a.styles=[m,h`
.status-pill {
font-size: 11px; font-weight: 600;
padding: 3px 8px; border-radius: 999px;
text-transform: uppercase; letter-spacing: 0.5px;
}
.status-pill.active {
background: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.status-pill.scheduled {
background: rgba(255, 152, 0, 0.15);
color: #ff9800;
}
.status-pill.inactive {
background: rgba(158, 158, 158, 0.15);
color: var(--secondary-text-color);
}
.row.toggle-row {
display: flex; align-items: center; justify-content: space-between;
}
.row.toggle-row label {
font-size: 14px; color: var(--primary-text-color);
}
.dates-row {
display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px;
}
.date-field.buffer label { white-space: nowrap; }
.date-field { display: flex; flex-direction: column; gap: 4px; }
.date-field label {
font-size: 11px; color: var(--secondary-text-color);
text-transform: uppercase; letter-spacing: 0.3px;
}
.date-field input {
padding: 6px 8px; font-size: 13px;
background: var(--secondary-background-color, #2c2c2c);
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
font-family: inherit;
}
.date-field input:disabled { opacity: 0.5; cursor: not-allowed; }
.actions {
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
}
.readonly { display: flex; flex-direction: column; gap: 8px; }
`],i([v({attribute:!1})],a.prototype,"hass",2),i([r()],a.prototype,"_config",2),i([r()],a.prototype,"_state",2),i([r()],a.prototype,"_busy",2),i([r()],a.prototype,"_error",2),i([r()],a.prototype,"_localStart",2),i([r()],a.prototype,"_localEnd",2),i([r()],a.prototype,"_localBuffer",2),i([r()],a.prototype,"_dirty",2);customElements.get("maintenance-vacation-section-card")||customElements.define("maintenance-vacation-section-card",a);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-vacation-section-card",name:"Maintenance Supporter \u2014 Vacation",description:"Inline vacation mode toggle + dates",preview:!1});export{a as MaintenanceVacationSectionCard};
File diff suppressed because one or more lines are too long
@@ -22,5 +22,5 @@
"requirements": [
"pypdf>=4.3.0"
],
"version": "2.53.0"
"version": "2.54.0"
}
+10 -8
View File
@@ -306,11 +306,13 @@ async def _async_require_admin(hass: HomeAssistant, call: ServiceCall) -> None:
async def _async_require_parent(hass: HomeAssistant, call: ServiceCall) -> None:
"""Reject user-initiated calls that aren't from an admin or a TaskMate parent.
Day-to-day parent actions (approve/reject, gift/adjust points, confirm
rewards/allowance, award badges, complete-as-parent) accept non-admin HA
users listed in ``parent_user_ids`` (issue #661). Context-less calls
(automations, scripts) pass. Structural config stays on
``_async_require_admin``.
Day-to-day parent actions (approve/reject, gift/adjust points, apply a
bonus or penalty, confirm rewards/allowance, award badges,
complete-as-parent) accept non-admin HA users listed in ``parent_user_ids``
(issue #661). Context-less calls (automations, scripts) pass. Structural
config including *defining* bonuses and penalties stays on
``_async_require_admin``; only applying an existing one is a parent action
(issue #749).
"""
if not call.context.user_id:
return
@@ -1324,7 +1326,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
hass.services.async_register(
DOMAIN,
SERVICE_REMOVE_POINTS,
_admin(handle_remove_points),
_parent(handle_remove_points),
schema=vol.Schema(
{
vol.Required(ATTR_CHILD_ID): cv.string,
@@ -1399,7 +1401,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
hass.services.async_register(
DOMAIN,
SERVICE_APPLY_PENALTY,
_admin(handle_apply_penalty),
_parent(handle_apply_penalty),
schema=vol.Schema({
vol.Required(ATTR_PENALTY_ID): cv.string,
vol.Required(ATTR_CHILD_ID): cv.string,
@@ -1443,7 +1445,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
hass.services.async_register(
DOMAIN,
SERVICE_APPLY_BONUS,
_admin(handle_apply_bonus),
_parent(handle_apply_bonus),
schema=vol.Schema({
vol.Required(ATTR_BONUS_ID): cv.string,
vol.Required(ATTR_CHILD_ID): cv.string,

Some files were not shown because too many files have changed in this diff Show More