224 files
This commit is contained in:
+73
@@ -29,6 +29,7 @@ interface Overview {
|
||||
needs_now: Record<string, number>;
|
||||
needs_soon: Record<string, number>;
|
||||
types: string[];
|
||||
excluded?: { entity_id: string; device_name: string }[];
|
||||
}
|
||||
|
||||
export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
@@ -111,6 +112,27 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Manual exclude/include (#107): a rechargeable device the heuristics
|
||||
// missed (or any battery the user never wants tracked) leaves the fleet;
|
||||
// the restore list below the section brings it back.
|
||||
private async _setExcluded(entityId: string, excluded: boolean): Promise<void> {
|
||||
if (this._marking) return;
|
||||
this._marking = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/battery_fleet/set_excluded",
|
||||
entity_id: entityId,
|
||||
excluded,
|
||||
});
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._marking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _shoppingLine(needs: Record<string, number>): string {
|
||||
return Object.entries(needs)
|
||||
.map(([type, qty]) => `${qty}× ${type}`)
|
||||
@@ -170,6 +192,14 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
>
|
||||
<ha-icon icon="mdi:battery-sync"></ha-icon>
|
||||
</button>
|
||||
<button
|
||||
class="bf-mark bf-exclude"
|
||||
title=${t("battery_fleet_exclude", L)}
|
||||
.disabled=${this._marking}
|
||||
@click=${() => this._setExcluded(b.entity_id, true)}
|
||||
>
|
||||
<ha-icon icon="mdi:eye-off-outline"></ha-icon>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
@@ -190,6 +220,28 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${ov.excluded?.length
|
||||
? html`
|
||||
<div class="bf-excluded">
|
||||
<span class="bf-label">${t("battery_fleet_excluded", L)}</span>
|
||||
${ov.excluded.map(
|
||||
(x) => html`
|
||||
<span class="bf-excluded-chip">
|
||||
${x.device_name}
|
||||
<button
|
||||
class="bf-mark"
|
||||
title=${t("battery_fleet_include", L)}
|
||||
.disabled=${this._marking}
|
||||
@click=${() => this._setExcluded(x.entity_id, false)}
|
||||
>
|
||||
<ha-icon icon="mdi:eye-outline"></ha-icon>
|
||||
</button>
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="bf-total">${t("battery_fleet_total", L).replace("{n}", String(ov.total))}</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -315,6 +367,27 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.bf-exclude {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.bf-excluded {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.bf-excluded-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
background: var(--secondary-background-color);
|
||||
border-radius: 10px;
|
||||
padding: 1px 4px 1px 10px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@ import { cpSync, mkdirSync, rmSync } from "fs";
|
||||
// this — every frontend change left orphans behind).
|
||||
rmSync("../frontend/strategy/chunks", { recursive: true, force: true });
|
||||
|
||||
// Stamp the integration version into every bundle (helpers/bundle-version.ts
|
||||
// reads it) — the panel compares it against the backend at runtime and shows
|
||||
// a "reload" banner when a stale cached bundle is running (roadmap guard 2;
|
||||
// the #106 follow-up class: HA's service worker serves stale-while-revalidate,
|
||||
// so "I updated and restarted" routinely still runs old frontend code).
|
||||
import { readFileSync } from "node:fs";
|
||||
const manifestVersion = JSON.parse(readFileSync("../manifest.json", "utf-8")).version;
|
||||
|
||||
const common = {
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
@@ -14,6 +22,7 @@ const common = {
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
external: [],
|
||||
define: { __MS_BUNDLE_VERSION__: JSON.stringify(manifestVersion) },
|
||||
};
|
||||
|
||||
// Panel
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Senzorový spouštěč této úlohy byl ztracen — nespustí se ani se automaticky nedokončí.",
|
||||
"battery_fleet_repair": "Opravit",
|
||||
"battery_fleet_exclude": "Vyloučit z flotily",
|
||||
"battery_fleet_excluded": "Vyloučené",
|
||||
"battery_fleet_include": "Znovu sledovat",
|
||||
"battery_fleet_total": "Sledováno baterií: {n}",
|
||||
"battery_fleet_setup_button": "Flotila baterií",
|
||||
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny."
|
||||
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
|
||||
"update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.",
|
||||
"update_reload": "Načíst znovu"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Opgavens sensorudløser er gået tabt — den udløses ikke og fuldføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparér",
|
||||
"battery_fleet_exclude": "Udeluk fra flåden",
|
||||
"battery_fleet_excluded": "Udelukket",
|
||||
"battery_fleet_include": "Følg igen",
|
||||
"battery_fleet_total": "{n} batterier overvåges",
|
||||
"battery_fleet_setup_button": "Batteriflåde",
|
||||
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier."
|
||||
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
|
||||
"update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.",
|
||||
"update_reload": "Genindlæs"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Der Sensor-Trigger dieser Aufgabe ist verloren gegangen — sie wird nicht ausgelöst oder automatisch abgeschlossen.",
|
||||
"battery_fleet_repair": "Reparieren",
|
||||
"battery_fleet_exclude": "Aus der Flotte ausschließen",
|
||||
"battery_fleet_excluded": "Ausgeschlossen",
|
||||
"battery_fleet_include": "Wieder aufnehmen",
|
||||
"battery_fleet_total": "{n} Batterien überwacht",
|
||||
"battery_fleet_setup_button": "Batterie-Flotte",
|
||||
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien."
|
||||
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
|
||||
"update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.",
|
||||
"update_reload": "Neu laden"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "This task's sensor trigger was lost — it will not fire or auto-complete.",
|
||||
"battery_fleet_repair": "Repair",
|
||||
"battery_fleet_exclude": "Exclude from the fleet",
|
||||
"battery_fleet_excluded": "Excluded",
|
||||
"battery_fleet_include": "Track again",
|
||||
"battery_fleet_total": "{n} batteries tracked",
|
||||
"battery_fleet_setup_button": "Battery fleet",
|
||||
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries."
|
||||
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
|
||||
"update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.",
|
||||
"update_reload": "Reload"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "sin conexión",
|
||||
"battery_fleet_trigger_lost": "El disparador de sensor de esta tarea se perdió — no se activará ni se completará automáticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir de la flota",
|
||||
"battery_fleet_excluded": "Excluidas",
|
||||
"battery_fleet_include": "Seguir de nuevo",
|
||||
"battery_fleet_total": "{n} baterías monitorizadas",
|
||||
"battery_fleet_setup_button": "Flota de baterías",
|
||||
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas."
|
||||
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
|
||||
"update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.",
|
||||
"update_reload": "Recargar"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Tehtävän anturiliipaisin on kadonnut — se ei laukea eikä valmistu automaattisesti.",
|
||||
"battery_fleet_repair": "Korjaa",
|
||||
"battery_fleet_exclude": "Sulje pois kannasta",
|
||||
"battery_fleet_excluded": "Pois suljetut",
|
||||
"battery_fleet_include": "Seuraa uudelleen",
|
||||
"battery_fleet_total": "{n} paristoa seurannassa",
|
||||
"battery_fleet_setup_button": "Paristokanta",
|
||||
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia."
|
||||
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
|
||||
"update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.",
|
||||
"update_reload": "Lataa uudelleen"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "hors ligne",
|
||||
"battery_fleet_trigger_lost": "Le déclencheur de capteur de cette tâche a été perdu — elle ne se déclenchera pas et ne se terminera pas automatiquement.",
|
||||
"battery_fleet_repair": "Réparer",
|
||||
"battery_fleet_exclude": "Exclure du parc",
|
||||
"battery_fleet_excluded": "Exclues",
|
||||
"battery_fleet_include": "Suivre à nouveau",
|
||||
"battery_fleet_total": "{n} piles suivies",
|
||||
"battery_fleet_setup_button": "Parc de piles",
|
||||
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles."
|
||||
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
|
||||
"update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.",
|
||||
"update_reload": "Recharger"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "ऑफ़लाइन",
|
||||
"battery_fleet_trigger_lost": "इस कार्य का सेंसर ट्रिगर खो गया है — यह सक्रिय नहीं होगा और स्वतः पूर्ण नहीं होगा।",
|
||||
"battery_fleet_repair": "मरम्मत करें",
|
||||
"battery_fleet_exclude": "फ्लीट से बाहर करें",
|
||||
"battery_fleet_excluded": "बाहर किए गए",
|
||||
"battery_fleet_include": "फिर से ट्रैक करें",
|
||||
"battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं",
|
||||
"battery_fleet_setup_button": "बैटरी फ्लीट",
|
||||
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।"
|
||||
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
|
||||
"update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।",
|
||||
"update_reload": "पुनः लोड करें"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Il trigger del sensore di questa attività è andato perso — non si attiverà né si completerà automaticamente.",
|
||||
"battery_fleet_repair": "Ripara",
|
||||
"battery_fleet_exclude": "Escludi dal parco",
|
||||
"battery_fleet_excluded": "Escluse",
|
||||
"battery_fleet_include": "Traccia di nuovo",
|
||||
"battery_fleet_total": "{n} batterie monitorate",
|
||||
"battery_fleet_setup_button": "Parco batterie",
|
||||
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte."
|
||||
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
|
||||
"update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.",
|
||||
"update_reload": "Ricarica"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "オフライン",
|
||||
"battery_fleet_trigger_lost": "このタスクのセンサートリガーが失われました — 発動も自動完了もしません。",
|
||||
"battery_fleet_repair": "修復",
|
||||
"battery_fleet_exclude": "フリートから除外",
|
||||
"battery_fleet_excluded": "除外済み",
|
||||
"battery_fleet_include": "再び追跡",
|
||||
"battery_fleet_total": "{n} 個の電池を監視",
|
||||
"battery_fleet_setup_button": "電池フリート",
|
||||
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。"
|
||||
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
|
||||
"update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。",
|
||||
"update_reload": "再読み込み"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "frakoblet",
|
||||
"battery_fleet_trigger_lost": "Oppgavens sensorutløser er tapt — den utløses ikke og fullføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparer",
|
||||
"battery_fleet_exclude": "Utelukk fra flåten",
|
||||
"battery_fleet_excluded": "Utelukket",
|
||||
"battery_fleet_include": "Følg igjen",
|
||||
"battery_fleet_total": "{n} batterier spores",
|
||||
"battery_fleet_setup_button": "Batteriflåte",
|
||||
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene."
|
||||
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
|
||||
"update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.",
|
||||
"update_reload": "Last inn på nytt"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "De sensortrigger van deze taak is verloren gegaan — deze wordt niet geactiveerd of automatisch voltooid.",
|
||||
"battery_fleet_repair": "Repareren",
|
||||
"battery_fleet_exclude": "Uitsluiten van de vloot",
|
||||
"battery_fleet_excluded": "Uitgesloten",
|
||||
"battery_fleet_include": "Opnieuw volgen",
|
||||
"battery_fleet_total": "{n} batterijen gevolgd",
|
||||
"battery_fleet_setup_button": "Batterijvloot",
|
||||
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen."
|
||||
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
|
||||
"update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.",
|
||||
"update_reload": "Herladen"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Wyzwalacz czujnika tego zadania został utracony — nie uruchomi się ani nie zakończy automatycznie.",
|
||||
"battery_fleet_repair": "Napraw",
|
||||
"battery_fleet_exclude": "Wyklucz z floty",
|
||||
"battery_fleet_excluded": "Wykluczone",
|
||||
"battery_fleet_include": "Śledź ponownie",
|
||||
"battery_fleet_total": "Śledzone baterie: {n}",
|
||||
"battery_fleet_setup_button": "Flota baterii",
|
||||
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie."
|
||||
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
|
||||
"update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.",
|
||||
"update_reload": "Załaduj ponownie"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "O gatilho de sensor desta tarefa foi perdido — ela não será acionada nem concluída automaticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir da frota",
|
||||
"battery_fleet_excluded": "Excluídas",
|
||||
"battery_fleet_include": "Acompanhar novamente",
|
||||
"battery_fleet_total": "{n} baterias monitorizadas",
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas."
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
|
||||
"update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
|
||||
"update_reload": "Recarregar"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "не в сети",
|
||||
"battery_fleet_trigger_lost": "Сенсорный триггер этой задачи утерян — она не сработает и не завершится автоматически.",
|
||||
"battery_fleet_repair": "Восстановить",
|
||||
"battery_fleet_exclude": "Исключить из парка",
|
||||
"battery_fleet_excluded": "Исключены",
|
||||
"battery_fleet_include": "Снова отслеживать",
|
||||
"battery_fleet_total": "Отслеживается батарей: {n}",
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми."
|
||||
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
|
||||
"update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.",
|
||||
"update_reload": "Перезагрузить"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_trigger_lost": "Uppgiftens sensorutlösare har gått förlorad — den utlöses inte och slutförs inte automatiskt.",
|
||||
"battery_fleet_repair": "Reparera",
|
||||
"battery_fleet_exclude": "Uteslut från flottan",
|
||||
"battery_fleet_excluded": "Uteslutna",
|
||||
"battery_fleet_include": "Spåra igen",
|
||||
"battery_fleet_total": "{n} batterier spåras",
|
||||
"battery_fleet_setup_button": "Batteriflotta",
|
||||
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier."
|
||||
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
|
||||
"update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.",
|
||||
"update_reload": "Ladda om"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "не в мережі",
|
||||
"battery_fleet_trigger_lost": "Сенсорний тригер цього завдання втрачено — воно не спрацює і не завершиться автоматично.",
|
||||
"battery_fleet_repair": "Відновити",
|
||||
"battery_fleet_exclude": "Виключити з парку",
|
||||
"battery_fleet_excluded": "Виключені",
|
||||
"battery_fleet_include": "Знову відстежувати",
|
||||
"battery_fleet_total": "Відстежується батарей: {n}",
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма."
|
||||
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
|
||||
"update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.",
|
||||
"update_reload": "Перезавантажити"
|
||||
}
|
||||
|
||||
@@ -800,7 +800,12 @@
|
||||
"battery_fleet_offline": "离线",
|
||||
"battery_fleet_trigger_lost": "此任务的传感器触发器已丢失——它不会触发,也不会自动完成。",
|
||||
"battery_fleet_repair": "修复",
|
||||
"battery_fleet_exclude": "从电池群中排除",
|
||||
"battery_fleet_excluded": "已排除",
|
||||
"battery_fleet_include": "重新跟踪",
|
||||
"battery_fleet_total": "已跟踪 {n} 个电池",
|
||||
"battery_fleet_setup_button": "电池群",
|
||||
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。"
|
||||
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
|
||||
"update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。",
|
||||
"update_reload": "重新加载"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
* show_window_chips: true # default true; hide for embedded use
|
||||
* show_user_filter: true # default true
|
||||
* user_filter: "" # "" | "current_user" | "<uuid>"
|
||||
* show_object_filter: true # default true; dropdown appears with 2+ objects
|
||||
* object_filter: "" # "" | "<entry_id>" | "<object name>" (Discussion #83)
|
||||
*/
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
@@ -47,6 +49,9 @@ interface CalendarCardConfig {
|
||||
show_window_chips?: boolean;
|
||||
show_user_filter?: boolean;
|
||||
user_filter?: string;
|
||||
/** Discussion #83 — in-card object dropdown + optional preselect. */
|
||||
show_object_filter?: boolean;
|
||||
object_filter?: string;
|
||||
/** v2.2.0 — when set, the card renders past N days from history instead
|
||||
* of forward N days from next_due. Mutually exclusive with window_days. */
|
||||
past_days?: PastDays;
|
||||
@@ -62,6 +67,7 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
@state() private _windowDays: WindowDays = 30;
|
||||
@state() private _pastDays: PastDays | 0 = 0; // 0 = forward mode
|
||||
@state() private _userFilter = "";
|
||||
@state() private _objectFilter = "";
|
||||
@state() private _unsub: (() => void) | null = null;
|
||||
|
||||
private _dataLoaded = false;
|
||||
@@ -92,6 +98,9 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
if (typeof config.user_filter === "string") {
|
||||
this._userFilter = config.user_filter;
|
||||
}
|
||||
if (typeof config.object_filter === "string") {
|
||||
this._objectFilter = config.object_filter;
|
||||
}
|
||||
}
|
||||
|
||||
getCardSize(): number {
|
||||
@@ -224,12 +233,26 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
: this._userFilter;
|
||||
}
|
||||
|
||||
// Object filter (#83): the config value may be an entry_id or an object
|
||||
// NAME (friendlier in hand-written YAML) — resolve against the loaded
|
||||
// objects; an unknown value falls back to "all" instead of a blank card.
|
||||
const showObjectFilter = this._config.show_object_filter !== false && this._objects.length > 1;
|
||||
let filterEntryId: string | null = null;
|
||||
if (this._objectFilter) {
|
||||
const needle = this._objectFilter.toLowerCase();
|
||||
const match = this._objects.find(
|
||||
(o) => o.entry_id === this._objectFilter || o.object.name.toLowerCase() === needle,
|
||||
);
|
||||
filterEntryId = match?.entry_id ?? null;
|
||||
}
|
||||
const objects = filterEntryId ? this._objects.filter((o) => o.entry_id === filterEntryId) : this._objects;
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const isPast = this._pastDays > 0;
|
||||
const buckets = isPast
|
||||
? buildPastBuckets(this._objects, today, this._pastDays, userFilter)
|
||||
: buildCalendarBuckets(this._objects, today, this._windowDays, userFilter);
|
||||
? buildPastBuckets(objects, today, this._pastDays, userFilter)
|
||||
: buildCalendarBuckets(objects, today, this._windowDays, userFilter);
|
||||
|
||||
const todayIso = isoDateLocal(today);
|
||||
// Year view AND past views collapse empty days — past windows can be
|
||||
@@ -363,6 +386,22 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
</select>
|
||||
`
|
||||
: nothing}
|
||||
${showObjectFilter
|
||||
? html`
|
||||
<select class="cal-user-filter"
|
||||
.value=${filterEntryId ?? ""}
|
||||
@change=${(e: Event) => {
|
||||
this._objectFilter = (e.target as HTMLSelectElement).value;
|
||||
}}>
|
||||
<option value="">${t("all_objects", L)}</option>
|
||||
${[...this._objects]
|
||||
.sort((a, b) => a.object.name.localeCompare(b.object.name))
|
||||
.map(
|
||||
(o) => html`<option value=${o.entry_id} ?selected=${o.entry_id === filterEntryId}>${o.object.name}</option>`,
|
||||
)}
|
||||
</select>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -417,6 +456,9 @@ class MaintenanceCalendarCardEditor extends LitElement {
|
||||
if (key === "show_user_filter" && value === true) {
|
||||
delete (newConfig as unknown as Record<string, unknown>).show_user_filter;
|
||||
}
|
||||
if (key === "show_object_filter" && value === true) {
|
||||
delete (newConfig as unknown as Record<string, unknown>).show_object_filter;
|
||||
}
|
||||
if (key === "title" && (!value || (typeof value === "string" && value.trim() === ""))) {
|
||||
delete (newConfig as unknown as Record<string, unknown>).title;
|
||||
}
|
||||
@@ -513,6 +555,22 @@ class MaintenanceCalendarCardEditor extends LitElement {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row toggle">
|
||||
<label for="objf">Show object filter dropdown</label>
|
||||
<input
|
||||
id="objf"
|
||||
type="checkbox"
|
||||
.checked=${this._config.show_object_filter !== false}
|
||||
@change=${(e: Event) =>
|
||||
this._valueChanged(
|
||||
"show_object_filter",
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Pre-select one object via YAML: object_filter: "<object name>".
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { isStaleBundle } from "./helpers/bundle-version";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs } from "./styles";
|
||||
import { LS_KEYS } from "./helpers/storage-keys";
|
||||
@@ -142,6 +143,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// Battery Fleet: offer one-click setup only when Battery Notes is present
|
||||
// and the fleet isn't set up yet.
|
||||
@state() private _batteryFleetSetupAvailable = false;
|
||||
@state() private _staleBundle = false;
|
||||
private _staleChecked = false;
|
||||
private _toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private _dismissedSuggestions = new Set<string>();
|
||||
|
||||
@@ -392,6 +395,21 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.catch(() => {
|
||||
this._batteryFleetSetupAvailable = false;
|
||||
});
|
||||
// Stale-bundle handshake (roadmap guard 2): compare the version esbuild
|
||||
// stamped into this bundle with the backend's — a mismatch means the
|
||||
// browser/service worker is still serving an OLD cached frontend, and no
|
||||
// amount of HA restarts fixes that. Checked once per panel lifetime.
|
||||
if (!this._staleChecked) {
|
||||
this._staleChecked = true;
|
||||
this.hass.connection
|
||||
.sendMessagePromise<{ version: string }>({ type: "maintenance_supporter/version" })
|
||||
.then((v) => {
|
||||
this._staleBundle = isStaleBundle(v?.version);
|
||||
})
|
||||
.catch(() => {
|
||||
/* backend too old to answer — no banner */
|
||||
});
|
||||
}
|
||||
if (statsResult) this._stats = statsResult as StatisticsResponse;
|
||||
if (budgetResult) this._budget = budgetResult as BudgetStatus;
|
||||
if (groupsResult) this._groups = (groupsResult as { groups: Record<string, MaintenanceGroup> }).groups || {};
|
||||
@@ -1827,6 +1845,17 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<div class="panel">
|
||||
${this._staleBundle
|
||||
? html`
|
||||
<div class="update-banner" role="status">
|
||||
<ha-icon icon="mdi:update"></ha-icon>
|
||||
<span>${t("update_banner", this._lang)}</span>
|
||||
<ha-button appearance="plain" @click=${() => location.reload()}>
|
||||
${t("update_reload", this._lang)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this.narrow || this._view !== "overview" ? this._renderHeader() : nothing}
|
||||
<div class="content">
|
||||
${this._view === "overview"
|
||||
|
||||
@@ -735,6 +735,20 @@ export const panelStyles = css`
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Stale-bundle handshake banner (roadmap guard 2) */
|
||||
.update-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
background: color-mix(in srgb, var(--primary-color) 14%, var(--card-background-color, #fff));
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.update-banner span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.popup-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
@@ -1223,8 +1237,10 @@ export const panelStyles = css`
|
||||
}
|
||||
|
||||
:host([narrow]) .tab {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
/* Tight enough that the four tabs fit 412px in the longest languages —
|
||||
Ukrainian ("Налаштування") overflowed at 12px 16px (overflow sweep). */
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:host([narrow]) .task-header {
|
||||
@@ -1435,7 +1451,7 @@ export const panelStyles = css`
|
||||
.kpi-value { font-size: 14px; }
|
||||
.kpi-value-large { font-size: 18px; }
|
||||
.two-column-layout { grid-template-columns: 1fr; }
|
||||
.tab { padding: 12px 16px; font-size: 14px; }
|
||||
.tab { padding: 12px 8px; font-size: 13px; }
|
||||
.task-header { flex-direction: column; align-items: flex-start; }
|
||||
.task-header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
|
||||
.filter-bar { flex-wrap: wrap; }
|
||||
|
||||
@@ -52,8 +52,8 @@ if (!STORE.en) STORE.en = EN as Translations;
|
||||
|
||||
/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */
|
||||
const SUPPORTED_LANGS = new Set<string>([
|
||||
"de", "nl", "fr", "it", "es", "pt", "ru", "uk", "pl", "cs", "sv", "zh",
|
||||
"da", "fi", "nb", "ja", "hi",
|
||||
"de", "nl", "fr", "it", "es", "pt", "pt-br", "ru", "uk", "pl", "cs", "sv", "zh",
|
||||
"da", "fi", "nb", "ja", "hi", "hu", "ko", "tr",
|
||||
]);
|
||||
|
||||
/** Served base for the runtime locale files (mirrors LOCALES_URL in const.py). */
|
||||
@@ -61,9 +61,16 @@ const LOCALES_BASE = "/maintenance_supporter_locales";
|
||||
|
||||
const _localeInflight = _localeGlobals.inflight;
|
||||
|
||||
/** Normalize an HA language code to our 2-letter table key. */
|
||||
/** Normalize an HA language code to our table key.
|
||||
*
|
||||
* Brazilian Portuguese is the one regional variant with its OWN table
|
||||
* ("pt-br") — it must not collapse into European "pt". Mirrors
|
||||
* normalize_language_code in helpers/i18n.py.
|
||||
*/
|
||||
function normLang(lang?: string): string {
|
||||
return (lang || DEFAULT_LANG).substring(0, 2).toLowerCase();
|
||||
const l = (lang || DEFAULT_LANG).toLowerCase();
|
||||
if (l.startsWith("pt") && l.endsWith("br")) return "pt-br";
|
||||
return l.substring(0, 2);
|
||||
}
|
||||
|
||||
/** Get a localized string. Falls back to English, then to the key itself. */
|
||||
@@ -117,15 +124,16 @@ export function setLocale(lang: string, table: Translations): void {
|
||||
STORE[normLang(lang)] = table;
|
||||
}
|
||||
|
||||
/** Map language prefix to BCP-47 locale for date formatting. */
|
||||
/** Map language table key to BCP-47 locale for date formatting. */
|
||||
function langToLocale(lang?: string): string {
|
||||
const l = (lang || "en").substring(0, 2).toLowerCase();
|
||||
const l = normLang(lang);
|
||||
const map: Record<string, string> = {
|
||||
de: "de-DE", en: "en-US", nl: "nl-NL", fr: "fr-FR", it: "it-IT", es: "es-ES", pt: "pt-PT", ru: "ru-RU", uk: "uk-UA", zh: "zh-CN",
|
||||
da: "da-DK", fi: "fi-FI", nb: "nb-NO", ja: "ja-JP", hi: "hi-IN",
|
||||
// pl/cs/sv were missing and silently fell back to en-US — Polish users
|
||||
// saw MM/DD dates (caught by the live multi-language check, 2026-07-19).
|
||||
pl: "pl-PL", cs: "cs-CZ", sv: "sv-SE",
|
||||
"pt-br": "pt-BR", hu: "hu-HU", ko: "ko-KR", tr: "tr-TR",
|
||||
};
|
||||
return map[l] ?? "en-US";
|
||||
}
|
||||
@@ -224,8 +232,7 @@ export function formatInterval(
|
||||
/** Localized weekday name (0=Mon … 6=Sun) via Intl — 2024-01-01 is a Monday.
|
||||
* Single source for the dialog selectors AND formatRecurrence (DRY). */
|
||||
export function weekdayName(i: number, lang?: string, style: "long" | "short" = "long"): string {
|
||||
const locale = (lang || "en").substring(0, 2);
|
||||
return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(locale, { weekday: style, timeZone: "UTC" });
|
||||
return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(langToLocale(lang), { weekday: style, timeZone: "UTC" });
|
||||
}
|
||||
|
||||
/** Recurrence shape carried on the WS payload (see types.TaskSchedule). */
|
||||
|
||||
Reference in New Issue
Block a user