197 files
This commit is contained in:
+32
@@ -62,3 +62,35 @@ describe("adopt-problem-sensors dialog: suggested part", () => {
|
||||
expect(byId["binary_sensor.pump_problem"].part_id).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adopt-problem-sensors dialog: flicker filter (#136)", () => {
|
||||
it("defaults to 0 and then omits for_minutes from the payload", async () => {
|
||||
const { el, sent } = await mountOpen();
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
|
||||
expect(input, "the for-minutes field renders").to.exist;
|
||||
expect(input.value).to.equal("0");
|
||||
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
|
||||
selections: Array<{ for_minutes?: number }>;
|
||||
};
|
||||
expect(adopt.selections.every((s) => s.for_minutes === undefined)).to.equal(true);
|
||||
});
|
||||
|
||||
it("forwards the entered minutes on every selection", async () => {
|
||||
const { el, sent } = await mountOpen();
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
|
||||
input.value = "10";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
|
||||
selections: Array<{ for_minutes?: number }>;
|
||||
};
|
||||
expect(adopt.selections.length).to.be.greaterThan(0);
|
||||
expect(adopt.selections.every((s) => s.for_minutes === 10)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* the no-fetch fast paths of ensureLocale/isLocaleLoaded.
|
||||
*/
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { t, isLocaleLoaded, ensureLocale } from "../styles";
|
||||
import { t, isLocaleLoaded, ensureLocale, seedEnglish } from "../styles";
|
||||
|
||||
describe("i18n runtime loader", () => {
|
||||
it("serves bundled English synchronously", () => {
|
||||
@@ -60,4 +60,25 @@ describe("i18n runtime loader", () => {
|
||||
delete g!.store.pt;
|
||||
}
|
||||
});
|
||||
|
||||
it("seedEnglish MERGES so a stale first-loader cannot shadow newer keys (#135 regression)", () => {
|
||||
// After an update, a cached app shell can load an OLD card bundle first;
|
||||
// its EN table lacks every key the fresh panel introduced. First-wins
|
||||
// seeding froze that stale table and the panel rendered raw keys
|
||||
// ("BATTERY_FLEET_ADD"). The merge keeps existing entries (steady state
|
||||
// unchanged) but lets every bundle contribute the keys it knows.
|
||||
const g = (window as unknown as {
|
||||
__msLocales: { store: Record<string, Record<string, string>> };
|
||||
}).__msLocales;
|
||||
const original = g.store.en;
|
||||
try {
|
||||
// Simulate the stale bundle having seeded first with a tiny old table.
|
||||
g.store.en = { loading: "Old loading text" };
|
||||
seedEnglish({ loading: "Fresh loading text", brand_new_key: "New feature label" });
|
||||
expect(t("loading", "en"), "existing entries win conflicts").to.equal("Old loading text");
|
||||
expect(t("brand_new_key", "en"), "newer keys are contributed, never raw").to.equal("New feature label");
|
||||
} finally {
|
||||
g.store.en = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+17
@@ -162,6 +162,23 @@ describe("task-dialog trigger_config roundtrip closure (#103 class)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("state_change: the hold filter (for_minutes, #136) survives the roundtrip", async () => {
|
||||
const tc = await saveRoundtrip({
|
||||
type: "state_change",
|
||||
entity_id: "binary_sensor.vacuum_problem",
|
||||
entity_ids: ["binary_sensor.vacuum_problem"],
|
||||
trigger_to_state: "on",
|
||||
trigger_target_changes: 1,
|
||||
trigger_for_minutes: 10,
|
||||
auto_complete_on_recovery: true,
|
||||
});
|
||||
expect(tc).to.deep.include({
|
||||
type: "state_change",
|
||||
trigger_to_state: "on",
|
||||
trigger_for_minutes: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("compound: per-condition attribute/baseline/entity_logic survive via carry", async () => {
|
||||
const tc = await saveRoundtrip({
|
||||
type: "compound",
|
||||
|
||||
@@ -122,6 +122,22 @@ describe("task-documents", () => {
|
||||
window.open = orig;
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses to window.open a non-http(s) weblink (defense-in-depth, XSS)", async () => {
|
||||
const EVIL = { id: "d4", kind: "weblink", title: "Evil", url: "javascript:alert(document.cookie)", tags: [], task_ids: ["t1"] };
|
||||
const orig = window.open;
|
||||
let opened = "NOT_CALLED";
|
||||
window.open = ((u: string) => { opened = u; return null; }) as typeof window.open;
|
||||
try {
|
||||
const { el } = await mount(true, [EVIL]);
|
||||
const info = el.shadowRoot!.querySelector<HTMLElement>(".tdoc-row .tdoc-info")!;
|
||||
info.click();
|
||||
await el.updateComplete;
|
||||
expect(opened, "a javascript: weblink must never reach window.open").to.equal("NOT_CALLED");
|
||||
} finally {
|
||||
window.open = orig;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── part mode (v2.26): same component, linking via part_ids ─────────────────
|
||||
|
||||
+35
@@ -50,6 +50,9 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
@state() private _selected: Set<string> = new Set();
|
||||
@state() private _users: HAUser[] = [];
|
||||
@state() private _responsible = "";
|
||||
// #136: minutes the problem must persist before the created task triggers
|
||||
// (one field applied to every selection). "0" = react to the first flicker.
|
||||
@state() private _forMinutes = "0";
|
||||
|
||||
private _localeReady = false;
|
||||
private _userService: UserService | null = null;
|
||||
@@ -72,6 +75,7 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
this._sensors = [];
|
||||
this._selected = new Set();
|
||||
this._responsible = "";
|
||||
this._forMinutes = "0";
|
||||
try {
|
||||
if (!this._userService) this._userService = new UserService(this.hass);
|
||||
else this._userService.updateHass(this.hass);
|
||||
@@ -126,6 +130,7 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
device_id: s.device_id ?? undefined,
|
||||
part_id: s.suggested_part_id ?? undefined,
|
||||
responsible_user_id: this._responsible || undefined,
|
||||
for_minutes: parseInt(this._forMinutes, 10) > 0 ? parseInt(this._forMinutes, 10) : undefined,
|
||||
}));
|
||||
const result = await this.hass.connection.sendMessagePromise<AdoptResponse>({
|
||||
type: "maintenance_supporter/problem_sensors/adopt",
|
||||
@@ -214,6 +219,23 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
|
||||
${!this._loading && this._sensors.length > 0
|
||||
? html`
|
||||
<label class="responsible">
|
||||
<span>${t("for_at_least_minutes", L)}</span>
|
||||
<input
|
||||
class="for-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1440"
|
||||
.value=${this._forMinutes}
|
||||
@input=${(e: Event) => (this._forMinutes = (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
<div class="for-hint">${t("adopt_for_minutes_hint", L)}</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
${!this._loading && this._sensors.length > 0 && this._users.length > 0
|
||||
? html`
|
||||
<label class="responsible">
|
||||
@@ -382,6 +404,19 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.for-input {
|
||||
width: 70px;
|
||||
background: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.for-hint {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
margin: -4px 0 2px;
|
||||
}
|
||||
.responsible select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
|
||||
+145
-8
@@ -66,11 +66,17 @@ interface Overview {
|
||||
needs_soon: Record<string, number>;
|
||||
types: string[];
|
||||
excluded?: { entity_id: string; device_name: string }[];
|
||||
// #135 follow-up: fleet-wide "track self-charging devices" opt-in.
|
||||
track_self_charging?: boolean;
|
||||
}
|
||||
|
||||
export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
/** Flat mode drops the section's own card chrome (border/background) —
|
||||
* set by the Lovelace card wrapper, whose ha-card provides the chrome. */
|
||||
@property({ type: Boolean }) public flat = false;
|
||||
|
||||
@state() private _ov: Overview | null = null;
|
||||
@state() private _loading = false;
|
||||
@state() private _marking = false;
|
||||
@@ -198,6 +204,26 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// #135 follow-up: fleet-wide opt-in that keeps self-charging devices
|
||||
// (phones, vacuums, smart rings) in the roster as rechargeables.
|
||||
private async _setTrackSelf(e: Event): Promise<void> {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
if (this._marking) return;
|
||||
this._marking = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/battery_fleet/set_track_self_charging",
|
||||
enabled,
|
||||
});
|
||||
await this._load();
|
||||
} catch (e2) {
|
||||
this._error = describeWsError(e2, this._lang);
|
||||
} finally {
|
||||
this._marking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazy: the recorder-backed history is fetched once, when the roster is
|
||||
* first expanded — most panel visits never open it. */
|
||||
private _loadHistory = async (e: Event): Promise<void> => {
|
||||
@@ -517,6 +543,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
></ha-selector>
|
||||
<div class="bf-roster-hint">${t("battery_fleet_add_hint", L)}</div>
|
||||
</div>
|
||||
<label class="bf-track-self">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${!!ov.track_self_charging}
|
||||
.disabled=${this._marking}
|
||||
@change=${this._setTrackSelf}
|
||||
/>
|
||||
${t("battery_fleet_track_self", L)}
|
||||
</label>
|
||||
<div class="bf-roster-hint">${t("battery_fleet_track_self_hint", L)}</div>
|
||||
</details>
|
||||
`
|
||||
: nothing}
|
||||
@@ -558,6 +594,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
:host([flat]) .bf-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.bf-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -614,22 +657,69 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
.bf-list {
|
||||
font-weight: 500;
|
||||
}
|
||||
/* Cross-row column alignment (same subgrid pattern as the task table,
|
||||
* issue 66): the LIST owns one column template, every row spans it via
|
||||
* subgrid, and each element is PINNED to its column below - so an
|
||||
* optional element (sparkline, percentage, forecast date) leaves its
|
||||
* column empty instead of letting the rest of the row drift. max-content
|
||||
* columns collapse to 0 when a whole list never fills them (the low list
|
||||
* has no status chip, no sparkline, no date). */
|
||||
.bf-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) repeat(9, max-content);
|
||||
column-gap: 8px;
|
||||
}
|
||||
.bf-row {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: 1 / -1;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
/* Column pinning: 1 name, 2 status/offline chip, 3 type, 4 charging
|
||||
* icon, 5 sparkline, 6 level bar, 7 percentage, 8 row action
|
||||
* (mark-one / record-swap), 9 forecast date, 10 exclude eye. */
|
||||
.bf-dev {
|
||||
flex: 1;
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.bf-offline,
|
||||
.bf-status {
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
}
|
||||
.bf-type {
|
||||
grid-column: 3;
|
||||
}
|
||||
.bf-recharge {
|
||||
grid-column: 4;
|
||||
}
|
||||
.bf-spark {
|
||||
grid-column: 5;
|
||||
}
|
||||
.bf-bar {
|
||||
grid-column: 6;
|
||||
}
|
||||
.bf-level {
|
||||
grid-column: 7;
|
||||
justify-self: end;
|
||||
}
|
||||
.bf-row .bf-mark {
|
||||
grid-column: 8;
|
||||
}
|
||||
.bf-predicted {
|
||||
grid-column: 9;
|
||||
justify-self: end;
|
||||
}
|
||||
.bf-row .bf-mark.bf-exclude {
|
||||
grid-column: 10;
|
||||
}
|
||||
.bf-offline {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
.bf-type {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
@@ -648,13 +738,48 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
flex: 0 0 auto;
|
||||
cursor: help;
|
||||
}
|
||||
/* On phones the row cannot fit name + chips + curve + bar + date: the
|
||||
* decorations yield (the percentage still carries the number). */
|
||||
/* On phones the row cannot fit name + chips + curve + bar + date in ONE
|
||||
* line: the decorations yield (the percentage still carries the number)
|
||||
* and the row wraps to two lines - the name spans the full width, the
|
||||
* status chip moves under it (left, into the name column) and the rest
|
||||
* keeps its pinned subgrid column, so type / percentage / date / eye
|
||||
* stay aligned across rows. Without this the fixed max-content columns
|
||||
* overflowed 400 px and the chips overlapped the wrapped names. */
|
||||
@media (max-width: 640px) {
|
||||
.bf-spark,
|
||||
.bf-bar {
|
||||
display: none;
|
||||
}
|
||||
.bf-row {
|
||||
row-gap: 2px;
|
||||
}
|
||||
.bf-dev {
|
||||
grid-column: 1 / 9;
|
||||
grid-row: 1;
|
||||
}
|
||||
.bf-offline,
|
||||
.bf-status {
|
||||
/* Line 1, RIGHT - over the date+eye columns, which are wide enough
|
||||
* for any chip. Pinning the chip into the 1fr rest column instead
|
||||
* overlapped the type text (the rest column shrinks below chip
|
||||
* width, and a span onto an fr track never grows fixed tracks). */
|
||||
grid-column: 9 / 11;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
.bf-type {
|
||||
grid-row: 2;
|
||||
max-width: 44vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bf-recharge,
|
||||
.bf-level,
|
||||
.bf-predicted,
|
||||
.bf-row .bf-mark {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
.bf-spark-line {
|
||||
fill: none;
|
||||
@@ -834,6 +959,18 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
border-radius: 10px;
|
||||
padding: 1px 4px 1px 10px;
|
||||
}
|
||||
.bf-track-self {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bf-track-self input {
|
||||
accent-color: var(--primary-color);
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { t, nativeFieldStyles } from "../styles";
|
||||
import { langOf, t, nativeFieldStyles } from "../styles";
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string;
|
||||
@@ -90,7 +90,7 @@ export class MaintenanceConfirmDialog extends LitElement {
|
||||
|
||||
render() {
|
||||
if (!this._open) return nothing;
|
||||
const lang = this.hass?.language || "en";
|
||||
const lang = langOf(this.hass);
|
||||
return html`
|
||||
<ha-dialog open @closed=${this._cancel}>
|
||||
<div class="dialog-title">${this._title}</div>
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import { describeWsError } from "../ws-errors";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { downloadSignedDocument, openSignedDocument, signDocumentPath } from "../helpers/document-url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface MaintenanceDocument {
|
||||
@@ -263,7 +263,7 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
}
|
||||
|
||||
private async _delete(doc: MaintenanceDocument): Promise<void> {
|
||||
const name = doc.title || doc.filename || doc.url || "";
|
||||
const name = docDisplayName(doc);
|
||||
if (!window.confirm(t("doc_delete_confirm", this._lang).replace("{name}", name))) return;
|
||||
this._busy = true;
|
||||
this._error = "";
|
||||
@@ -447,7 +447,7 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="doc-title">${doc.title || doc.filename || doc.url}</div>
|
||||
<div class="doc-title">${docDisplayName(doc)}</div>
|
||||
<div class="doc-meta">${meta}</div>
|
||||
</div>
|
||||
<div class="doc-row-actions">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { t } from "../styles";
|
||||
import { downloadTextFile } from "../helpers/download";
|
||||
|
||||
interface QrResult {
|
||||
svg_data_uri: string;
|
||||
@@ -194,16 +195,13 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
const svgContent = decodeURIComponent(
|
||||
result.svg_data_uri.replace("data:image/svg+xml,", ""),
|
||||
);
|
||||
const blob = new Blob([svgContent], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
const name = this._taskName
|
||||
? `${this._objectName}-${this._taskName}`
|
||||
: this._objectName;
|
||||
a.download = `qr-${sanitizeFilename(name)}-${suffix}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
// helpers/download.ts, not a hand-rolled anchor: the detached-anchor +
|
||||
// immediate-revoke pattern this used silently fails in the Companion
|
||||
// WebView (drift audit 2026-08 caught the bypass).
|
||||
downloadTextFile(svgContent, `qr-${sanitizeFilename(name)}-${suffix}.svg`, "image/svg+xml");
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
|
||||
+7
-2
@@ -7,10 +7,12 @@
|
||||
*/
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { docDisplayName } from "../helpers/document-categories";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { openSignedDocument } from "../helpers/document-url";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -135,7 +137,10 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
|
||||
private async _openResult(doc: SearchResult): Promise<void> {
|
||||
if (doc.kind === "weblink") {
|
||||
window.open(doc.url, "_blank", "noopener");
|
||||
// Defense-in-depth (mirrors task-documents/documents-section): never
|
||||
// hand a non-http(s) weblink to window.open, even though the WS add
|
||||
// path already rejects such schemes.
|
||||
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -150,7 +155,7 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
<div class="obj-row result-row" title=${t("doc_open", L)} @click=${() => this._openResult(doc)}>
|
||||
<ha-icon icon=${doc.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline"}></ha-icon>
|
||||
<div class="result-info">
|
||||
<div class="result-title">${doc.title || doc.filename || doc.url}</div>
|
||||
<div class="result-title">${docDisplayName(doc)}</div>
|
||||
<div class="result-obj">${doc.object_name}</div>
|
||||
</div>
|
||||
<ha-icon class="result-open" icon=${doc.kind === "weblink" ? "mdi:open-in-new" : "mdi:eye-outline"}></ha-icon>
|
||||
|
||||
@@ -1149,6 +1149,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
if (this._triggerFromState) triggerConfig.trigger_from_state = this._triggerFromState;
|
||||
if (this._triggerToState) triggerConfig.trigger_to_state = this._triggerToState;
|
||||
if (this._triggerTargetChanges) { const v = parseInt(this._triggerTargetChanges, 10); if (!isNaN(v)) triggerConfig.trigger_target_changes = v; }
|
||||
// #136: the new state must hold this long before a change counts.
|
||||
if (this._triggerForMinutes) { const v = parseInt(this._triggerForMinutes, 10); if (!isNaN(v)) triggerConfig.trigger_for_minutes = v; }
|
||||
} else if (this._triggerType === "runtime") {
|
||||
if (this._triggerRuntimeHours) { const v = parseFloat(this._triggerRuntimeHours); if (!isNaN(v)) triggerConfig.trigger_runtime_hours = v; }
|
||||
const onStates = this._triggerOnStates.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
@@ -2246,6 +2248,14 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => (this._triggerTargetChanges = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("target_changes_help", L)}</div>
|
||||
<ms-textfield
|
||||
label="${t("for_at_least_minutes", L)}"
|
||||
type="number"
|
||||
min="0"
|
||||
.value=${this._triggerForMinutes}
|
||||
@input=${(e: Event) => (this._triggerForMinutes = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("for_minutes_state_help", L)}</div>
|
||||
`;
|
||||
}
|
||||
if (this._triggerType === "runtime") {
|
||||
|
||||
@@ -15,8 +15,9 @@ import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { downloadSignedDocument, openSignedDocument } from "../helpers/document-url";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface Doc {
|
||||
@@ -144,7 +145,11 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
|
||||
private async _open(doc: Doc): Promise<void> {
|
||||
if (doc.kind === "weblink") {
|
||||
window.open(doc.url, "_blank", "noopener");
|
||||
// Defense-in-depth: the add-link WS path already refuses non-http(s)
|
||||
// schemes, but a weblink stored before that check (or via a future
|
||||
// path) must never reach window.open as javascript:/data: — guard the
|
||||
// sink too, like documents-section does.
|
||||
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
// A per-task page hint jumps straight to the relevant page via the PDF
|
||||
@@ -206,7 +211,7 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
>
|
||||
<option value="" ?selected=${!this._attachId}>${t("doc_link_existing", L)}</option>
|
||||
${available.map(
|
||||
(d) => html`<option value=${d.id} ?selected=${d.id === this._attachId}>${d.title || d.filename || d.url}</option>`,
|
||||
(d) => html`<option value=${d.id} ?selected=${d.id === this._attachId}>${docDisplayName(d)}</option>`,
|
||||
)}
|
||||
</select>
|
||||
<button class="tdoc-btn" ?disabled=${this._busy || !this._attachId} @click=${this._link}>
|
||||
@@ -240,7 +245,7 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="tdoc-title">${doc.title || doc.filename || doc.url}</div>
|
||||
<div class="tdoc-title">${docDisplayName(doc)}</div>
|
||||
<div class="tdoc-meta">
|
||||
${meta}${page ? html` · <span class="tdoc-pagetag">${t("doc_page", L)} ${page}</span>` : nothing}
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ import type { MaintenanceQrDialog } from "./components/qr-dialog";
|
||||
import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-actions-dialog";
|
||||
import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog";
|
||||
import type { HomeAssistant, MaintenanceObject } from "./types";
|
||||
import { ensureLocale, isLocaleLoaded } from "./styles";
|
||||
import { ensureLocale, isLocaleLoaded, langOf } from "./styles";
|
||||
|
||||
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
|
||||
const TASK_DIALOG_TAG = "maintenance-task-dialog";
|
||||
@@ -91,7 +91,7 @@ function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean {
|
||||
// in English even when the rest of HA is localized. Fetch the user's locale
|
||||
// (idempotent + cached) and force a re-render once it's in, so the first paint
|
||||
// (which may be English) is corrected to the user's language.
|
||||
const lang = hass.language || "en";
|
||||
const lang = langOf(hass);
|
||||
if (!isLocaleLoaded(lang)) {
|
||||
void ensureLocale(lang).then(() => {
|
||||
(el as unknown as { requestUpdate?: () => void }).requestUpdate?.();
|
||||
|
||||
@@ -279,6 +279,10 @@ const HISTORY_TYPE_TO_STATUS: Record<string, string> = {
|
||||
completed: "ok",
|
||||
reset: "ok",
|
||||
skipped: "due_soon",
|
||||
// A cycle that was NOT done in time (skip-while-overdue writes it, and the
|
||||
// coordinator stamps it automatically) — must never paint green. Found by
|
||||
// the 2026-08 drift audit: the missing entry fell through to "ok".
|
||||
missed: "overdue",
|
||||
triggered: "triggered",
|
||||
trigger_replaced: "triggered",
|
||||
trigger_removed: "ok", // config change, not a due/triggered event
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Seskupit podle",
|
||||
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
|
||||
"target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).",
|
||||
"for_minutes_state_help": "0 počítá každou změnu ihned. S minutami musí nový stav nejprve vydržet — krátké bliknutí pak nespustí ani se nepočítá.",
|
||||
"qr_print_title": "Tisk QR kódů",
|
||||
"qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.",
|
||||
"qr_print_load": "Načíst objekty",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Nebylo potřeba",
|
||||
"quick_complete_success": "Rychle označeno jako hotové",
|
||||
"trigger_replaced": "Spouštěč nahrazen",
|
||||
"trigger_removed": "Spouštěč odebrán",
|
||||
"add": "Přidat",
|
||||
"show_stats": "Zobrazit statistiky + grafy",
|
||||
"hide_stats": "Skrýt statistiky",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Za předpokladu včasného dokončení.",
|
||||
"schedule_preview_ends": "(série končí)",
|
||||
"adopt_problem_responsible": "Odpovědná osoba pro všechny převzaté úkoly (volitelné)",
|
||||
"adopt_for_minutes_hint": "Spustit až když problém trvá takto dlouho — 0 reaguje na první bliknutí.",
|
||||
"adopt_problem_configure": "Konfigurovat",
|
||||
"history_auto": "Automaticky",
|
||||
"battery_fleet_title": "Flotila baterií",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Vylučte zařízení už dřív, než vůbec ohlásí nízkou úroveň — vysavač, který se sám nabíjí, nebo telefon, který varuje sám.",
|
||||
"battery_fleet_add": "Přidat baterii",
|
||||
"battery_fleet_add_hint": "Vyberte senzor baterie, který automatická detekce přehlédla — okamžitě se objeví v seznamu.",
|
||||
"battery_fleet_track_self": "Sledovat samodobíjecí baterie",
|
||||
"battery_fleet_track_self_hint": "Telefony, vysavače a další samodobíjecí zařízení se zobrazují jako akumulátory — při nízkém stavu žádají o nabití, nikdy o nové články.",
|
||||
"battery_fleet_status_low": "Nízká",
|
||||
"battery_fleet_status_soon": "Brzy",
|
||||
"battery_fleet_status_ok": "V pořádku",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Snart forfalden",
|
||||
"triggered": "Udløst",
|
||||
"trigger_replaced": "Udløser erstattet",
|
||||
"trigger_removed": "Udløser fjernet",
|
||||
"ok": "OK",
|
||||
"all": "Alle",
|
||||
"new_object": "+ Nyt objekt",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "Grupper efter",
|
||||
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).",
|
||||
"for_minutes_state_help": "0 tæller hver ændring straks. Med minutter skal den nye tilstand først holde — korte blink hverken udløser eller tælles så.",
|
||||
"qr_print_title": "Udskriv QR-koder",
|
||||
"qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.",
|
||||
"qr_print_load": "Indlæs objekter",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Forudsat rettidig fuldførelse.",
|
||||
"schedule_preview_ends": "(serien slutter)",
|
||||
"adopt_problem_responsible": "Ansvarlig bruger for alle overtagne opgaver (valgfrit)",
|
||||
"adopt_for_minutes_hint": "Udløs først når problemet har varet så længe — 0 reagerer på første blink.",
|
||||
"adopt_problem_configure": "Konfigurer",
|
||||
"history_auto": "Automatisk",
|
||||
"battery_fleet_title": "Batteriflåde",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Udeluk en enhed her, før den overhovedet melder lavt niveau — en støvsuger, der lader sig selv, eller en telefon, der advarer af sig selv.",
|
||||
"battery_fleet_add": "Tilføj et batteri",
|
||||
"battery_fleet_add_hint": "Vælg en batterisensor, som den automatiske registrering overså — den kommer straks på listen.",
|
||||
"battery_fleet_track_self": "Spor selvopladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvopladende enheder vises som genopladelige — ved lavt niveau bedes om opladning, aldrig om nye batterier.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Bald fällig",
|
||||
"triggered": "Ausgelöst",
|
||||
"trigger_replaced": "Auslöser ersetzt",
|
||||
"trigger_removed": "Trigger entfernt",
|
||||
"ok": "OK",
|
||||
"all": "Alle",
|
||||
"new_object": "+ Neues Objekt",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "Gruppieren nach",
|
||||
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
|
||||
"target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).",
|
||||
"for_minutes_state_help": "0 zählt jede Änderung sofort. Mit Minuten muss der neue Zustand erst so lange bestehen — kurzes Flackern löst dann weder aus noch zählt es.",
|
||||
"qr_print_title": "QR-Codes drucken",
|
||||
"qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.",
|
||||
"qr_print_load": "Objekte laden",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Bei pünktlichem Abschluss.",
|
||||
"schedule_preview_ends": "(Serie endet)",
|
||||
"adopt_problem_responsible": "Verantwortliche Person für alle übernommenen Aufgaben (optional)",
|
||||
"adopt_for_minutes_hint": "Erst auslösen, wenn das Problem so lange besteht — 0 reagiert auf das erste Flackern.",
|
||||
"adopt_problem_configure": "Konfigurieren",
|
||||
"history_auto": "Automatisch",
|
||||
"battery_fleet_title": "Batterie-Flotte",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Ein Gerät hier ausschließen, bevor es überhaupt schwach meldet — etwa ein Sauger, der sich selbst lädt, oder ein Telefon, das von sich aus warnt.",
|
||||
"battery_fleet_add": "Batterie hinzufügen",
|
||||
"battery_fleet_add_hint": "Einen Batteriesensor wählen, den die automatische Erkennung übersehen hat — er erscheint sofort in der Liste.",
|
||||
"battery_fleet_track_self": "Selbstladende Batterien mitverfolgen",
|
||||
"battery_fleet_track_self_hint": "Telefone, Staubsauger und andere selbstladende Geräte erscheinen als Akkus — bei niedrigem Stand wird ans Laden erinnert, nie an neue Zellen.",
|
||||
"battery_fleet_status_low": "Schwach",
|
||||
"battery_fleet_status_soon": "Bald",
|
||||
"battery_fleet_status_ok": "In Ordnung",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Due Soon",
|
||||
"triggered": "Triggered",
|
||||
"trigger_replaced": "Trigger replaced",
|
||||
"trigger_removed": "Trigger removed",
|
||||
"ok": "OK",
|
||||
"all": "All",
|
||||
"new_object": "+ New Object",
|
||||
@@ -524,6 +525,7 @@
|
||||
"group_by_label": "Group by",
|
||||
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
|
||||
"target_changes_help": "Number of matching transitions before the trigger fires (default: 1).",
|
||||
"for_minutes_state_help": "0 counts every change immediately. Set minutes and the new state must hold that long first — brief flickers then neither trigger nor count.",
|
||||
"qr_print_title": "Print QR codes",
|
||||
"qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.",
|
||||
"qr_print_load": "Load objects",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Assuming on-time completion.",
|
||||
"schedule_preview_ends": "(series ends)",
|
||||
"adopt_problem_responsible": "Responsible user for all adopted tasks (optional)",
|
||||
"adopt_for_minutes_hint": "Only trigger once the problem has persisted this long — 0 reacts to the first flicker.",
|
||||
"adopt_problem_configure": "Configure",
|
||||
"history_auto": "Automatic",
|
||||
"battery_fleet_title": "Battery fleet",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Exclude a device here to drop it from the fleet before it ever reports low — a vacuum that recharges itself, or a phone that warns you on its own.",
|
||||
"battery_fleet_add": "Add a battery",
|
||||
"battery_fleet_add_hint": "Pick a battery sensor the automatic discovery missed — it joins the roster immediately.",
|
||||
"battery_fleet_track_self": "Track self-charging batteries",
|
||||
"battery_fleet_track_self_hint": "Phones, vacuums and other devices that recharge themselves appear as rechargeables — a low one asks for a charge, never for new cells.",
|
||||
"battery_fleet_status_low": "Low",
|
||||
"battery_fleet_status_soon": "Soon",
|
||||
"battery_fleet_status_ok": "Healthy",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
|
||||
"target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).",
|
||||
"for_minutes_state_help": "0 cuenta cada cambio de inmediato. Con minutos, el nuevo estado debe mantenerse primero — un parpadeo breve entonces ni dispara ni cuenta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.",
|
||||
"qr_print_load": "Cargar objetos",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "No era necesario",
|
||||
"quick_complete_success": "Completado rápido",
|
||||
"trigger_replaced": "Disparador reemplazado",
|
||||
"trigger_removed": "Disparador eliminado",
|
||||
"add": "Añadir",
|
||||
"show_stats": "Mostrar estadísticas + gráficos",
|
||||
"hide_stats": "Ocultar estadísticas",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Suponiendo finalización puntual.",
|
||||
"schedule_preview_ends": "(la serie termina)",
|
||||
"adopt_problem_responsible": "Usuario responsable para todas las tareas adoptadas (opcional)",
|
||||
"adopt_for_minutes_hint": "Disparar solo si el problema persiste este tiempo — 0 reacciona al primer parpadeo.",
|
||||
"adopt_problem_configure": "Configurar",
|
||||
"history_auto": "Automático",
|
||||
"battery_fleet_title": "Flota de baterías",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Excluye aquí un dispositivo antes de que llegue a avisar de carga baja — un aspirador que se recarga solo o un teléfono que ya avisa por su cuenta.",
|
||||
"battery_fleet_add": "Añadir una batería",
|
||||
"battery_fleet_add_hint": "Elige un sensor de batería que la detección automática pasó por alto — se une a la lista de inmediato.",
|
||||
"battery_fleet_track_self": "Seguir baterías de carga automática",
|
||||
"battery_fleet_track_self_hint": "Teléfonos, aspiradoras y otros dispositivos que se recargan solos aparecen como recargables: si están bajos piden una carga, nunca pilas nuevas.",
|
||||
"battery_fleet_status_low": "Baja",
|
||||
"battery_fleet_status_soon": "Pronto",
|
||||
"battery_fleet_status_ok": "Correcta",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Pian erääntyy",
|
||||
"triggered": "Laukaistu",
|
||||
"trigger_replaced": "Laukaisin korvattu",
|
||||
"trigger_removed": "Laukaisin poistettu",
|
||||
"ok": "OK",
|
||||
"all": "Kaikki",
|
||||
"new_object": "+ Uusi kohde",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "Ryhmittele",
|
||||
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
|
||||
"target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).",
|
||||
"for_minutes_state_help": "0 laskee jokaisen muutoksen heti. Minuuteilla uuden tilan on ensin pysyttävä — lyhyet välähdykset eivät silloin laukaise eivätkä kirjaudu.",
|
||||
"qr_print_title": "Tulosta QR-koodit",
|
||||
"qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.",
|
||||
"qr_print_load": "Lataa kohteet",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Olettaen, että valmistuminen on ajallaan.",
|
||||
"schedule_preview_ends": "(sarja päättyy)",
|
||||
"adopt_problem_responsible": "Vastuuhenkilö kaikille omaksutuille tehtäville (valinnainen)",
|
||||
"adopt_for_minutes_hint": "Laukaise vasta kun ongelma on kestänyt näin kauan — 0 reagoi ensimmäiseen välähdykseen.",
|
||||
"adopt_problem_configure": "Määritä",
|
||||
"history_auto": "Automaattinen",
|
||||
"battery_fleet_title": "Paristokanta",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Sulje laite pois jo ennen kuin se ilmoittaa alhaisesta varauksesta — imuri, joka lataa itsensä, tai puhelin, joka varoittaa itse.",
|
||||
"battery_fleet_add": "Lisää paristo",
|
||||
"battery_fleet_add_hint": "Valitse paristoanturi, jonka automaattinen tunnistus ohitti — se liittyy luetteloon heti.",
|
||||
"battery_fleet_track_self": "Seuraa itselatautuvia akkuja",
|
||||
"battery_fleet_track_self_hint": "Puhelimet, imurit ja muut itselatautuvat laitteet näkyvät ladattavina — vähissä oleva pyytää latausta, ei koskaan uusia paristoja.",
|
||||
"battery_fleet_status_low": "Vähissä",
|
||||
"battery_fleet_status_soon": "Pian",
|
||||
"battery_fleet_status_ok": "Kunnossa",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Grouper par",
|
||||
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
|
||||
"target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).",
|
||||
"for_minutes_state_help": "0 compte chaque changement immédiatement. Avec des minutes, le nouvel état doit d'abord se maintenir — un bref clignotement ne déclenche ni ne compte alors.",
|
||||
"qr_print_title": "Imprimer les QR codes",
|
||||
"qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.",
|
||||
"qr_print_load": "Charger les objets",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Non nécessaire",
|
||||
"quick_complete_success": "Terminé rapidement",
|
||||
"trigger_replaced": "Déclencheur remplacé",
|
||||
"trigger_removed": "Déclencheur supprimé",
|
||||
"add": "Ajouter",
|
||||
"show_stats": "Afficher les statistiques + graphiques",
|
||||
"hide_stats": "Masquer les statistiques",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "En supposant un achèvement ponctuel.",
|
||||
"schedule_preview_ends": "(fin de la série)",
|
||||
"adopt_problem_responsible": "Personne responsable pour toutes les tâches adoptées (optionnel)",
|
||||
"adopt_for_minutes_hint": "Ne déclencher que si le problème persiste aussi longtemps — 0 réagit au premier clignotement.",
|
||||
"adopt_problem_configure": "Configurer",
|
||||
"history_auto": "Automatique",
|
||||
"battery_fleet_title": "Parc de piles",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Excluez un appareil ici avant même qu'il ne signale une charge faible — un aspirateur qui se recharge seul, ou un téléphone qui vous prévient lui-même.",
|
||||
"battery_fleet_add": "Ajouter une batterie",
|
||||
"battery_fleet_add_hint": "Choisissez un capteur de batterie que la détection automatique a manqué — il rejoint la liste immédiatement.",
|
||||
"battery_fleet_track_self": "Suivre les batteries à recharge automatique",
|
||||
"battery_fleet_track_self_hint": "Les téléphones, aspirateurs et autres appareils qui se rechargent seuls apparaissent comme rechargeables — un niveau bas demande une recharge, jamais de nouvelles piles.",
|
||||
"battery_fleet_status_low": "Faible",
|
||||
"battery_fleet_status_soon": "Bientôt",
|
||||
"battery_fleet_status_ok": "Bon état",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "जल्द देय",
|
||||
"triggered": "ट्रिगर हुआ",
|
||||
"trigger_replaced": "ट्रिगर बदला गया",
|
||||
"trigger_removed": "ट्रिगर हटाया गया",
|
||||
"ok": "ठीक",
|
||||
"all": "सभी",
|
||||
"new_object": "+ नई वस्तु",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "इसके अनुसार समूहित करें",
|
||||
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
|
||||
"target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।",
|
||||
"for_minutes_state_help": "0 हर परिवर्तन को तुरंत गिनता है। मिनट सेट करने पर नई स्थिति को पहले उतनी देर बनी रहनी होगी — क्षणिक झिलमिलाहट तब न ट्रिगर करती है न गिनी जाती है।",
|
||||
"qr_print_title": "QR कोड प्रिंट करें",
|
||||
"qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।",
|
||||
"qr_print_load": "वस्तुएँ लोड करें",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "समय पर पूर्णता मानते हुए।",
|
||||
"schedule_preview_ends": "(श्रृंखला समाप्त)",
|
||||
"adopt_problem_responsible": "सभी अपनाए गए कार्यों के लिए ज़िम्मेदार उपयोगकर्ता (वैकल्पिक)",
|
||||
"adopt_for_minutes_hint": "समस्या इतनी देर बनी रहने पर ही ट्रिगर करें — 0 पहली झिलमिलाहट पर प्रतिक्रिया करता है।",
|
||||
"adopt_problem_configure": "कॉन्फ़िगर करें",
|
||||
"history_auto": "स्वचालित",
|
||||
"battery_fleet_title": "बैटरी फ्लीट",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "किसी डिवाइस को यहाँ कम बैटरी बताने से पहले ही बाहर रखें — जैसे खुद चार्ज होने वाला वैक्यूम, या खुद ही चेतावनी देने वाला फ़ोन।",
|
||||
"battery_fleet_add": "बैटरी जोड़ें",
|
||||
"battery_fleet_add_hint": "स्वचालित खोज से छूटा बैटरी सेंसर चुनें — यह तुरंत सूची में जुड़ जाएगा।",
|
||||
"battery_fleet_track_self": "स्वयं चार्ज होने वाली बैटरियों को ट्रैक करें",
|
||||
"battery_fleet_track_self_hint": "फ़ोन, वैक्यूम और अन्य स्वयं चार्ज होने वाले उपकरण रिचार्जेबल के रूप में दिखते हैं — कम होने पर चार्ज करने को कहा जाता है, नई बैटरी कभी नहीं।",
|
||||
"battery_fleet_status_low": "कम",
|
||||
"battery_fleet_status_soon": "जल्द",
|
||||
"battery_fleet_status_ok": "ठीक",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Hamarosan esedékes",
|
||||
"triggered": "Aktiválva",
|
||||
"trigger_replaced": "Kiváltó lecserélve",
|
||||
"trigger_removed": "Eseményindító eltávolítva",
|
||||
"ok": "OK",
|
||||
"all": "Összes",
|
||||
"new_object": "+ Új objektum",
|
||||
@@ -524,6 +525,7 @@
|
||||
"group_by_label": "Csoportosítás",
|
||||
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
|
||||
"target_changes_help": "Az egyező átmenetek száma a kiváltás előtt (alapértelmezés: 1).",
|
||||
"for_minutes_state_help": "A 0 minden változást azonnal számol. Percekkel az új állapotnak előbb fenn kell maradnia — a rövid villanás ekkor se nem indít, se nem számít.",
|
||||
"qr_print_title": "QR-kódok nyomtatása",
|
||||
"qr_print_desc": "Nyomtatható oldal QR-kódokkal — vágja ki és ragassza fel a berendezésekre.",
|
||||
"qr_print_load": "Objektumok betöltése",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Időben történő elvégzést feltételezve.",
|
||||
"schedule_preview_ends": "(a sorozat véget ér)",
|
||||
"adopt_problem_responsible": "Felelős felhasználó az összes átvett feladathoz (opcionális)",
|
||||
"adopt_for_minutes_hint": "Csak akkor induljon, ha a probléma ennyi ideig fennáll — a 0 az első villanásra reagál.",
|
||||
"adopt_problem_configure": "Beállítás",
|
||||
"history_auto": "Automatikus",
|
||||
"battery_fleet_title": "Elemflotta",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Zárjon ki egy eszközt már azelőtt, hogy alacsony töltöttséget jelezne — például egy magát töltő porszívót vagy egy telefont, amely magától figyelmeztet.",
|
||||
"battery_fleet_add": "Elem hozzáadása",
|
||||
"battery_fleet_add_hint": "Válassz egy elem-érzékelőt, amelyet az automatikus felismerés kihagyott — azonnal felkerül a listára.",
|
||||
"battery_fleet_track_self": "Öntöltő akkumulátorok követése",
|
||||
"battery_fleet_track_self_hint": "A telefonok, porszívók és más öntöltő eszközök akkumulátorként jelennek meg — alacsony töltöttségnél töltést kérnek, sosem új elemet.",
|
||||
"battery_fleet_status_low": "Alacsony",
|
||||
"battery_fleet_status_soon": "Hamarosan",
|
||||
"battery_fleet_status_ok": "Rendben",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Raggruppa per",
|
||||
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
|
||||
"target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).",
|
||||
"for_minutes_state_help": "0 conta ogni cambiamento subito. Con i minuti, il nuovo stato deve prima mantenersi — un breve sfarfallio allora non attiva né conta.",
|
||||
"qr_print_title": "Stampa codici QR",
|
||||
"qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.",
|
||||
"qr_print_load": "Carica oggetti",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Non necessario",
|
||||
"quick_complete_success": "Completato rapidamente",
|
||||
"trigger_replaced": "Trigger sostituito",
|
||||
"trigger_removed": "Trigger rimosso",
|
||||
"add": "Aggiungi",
|
||||
"show_stats": "Mostra statistiche + grafici",
|
||||
"hide_stats": "Nascondi statistiche",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Presumendo un completamento puntuale.",
|
||||
"schedule_preview_ends": "(la serie termina)",
|
||||
"adopt_problem_responsible": "Utente responsabile per tutte le attività adottate (opzionale)",
|
||||
"adopt_for_minutes_hint": "Attiva solo se il problema persiste per questo tempo — 0 reagisce al primo sfarfallio.",
|
||||
"adopt_problem_configure": "Configura",
|
||||
"history_auto": "Automatico",
|
||||
"battery_fleet_title": "Parco batterie",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Escludi qui un dispositivo prima ancora che segnali la batteria scarica — un aspirapolvere che si ricarica da solo o un telefono che avvisa da sé.",
|
||||
"battery_fleet_add": "Aggiungi una batteria",
|
||||
"battery_fleet_add_hint": "Scegli un sensore batteria sfuggito al rilevamento automatico — entra subito nell'elenco.",
|
||||
"battery_fleet_track_self": "Traccia le batterie autoricaricabili",
|
||||
"battery_fleet_track_self_hint": "Telefoni, aspirapolvere e altri dispositivi che si ricaricano da soli appaiono come ricaricabili — se scarichi chiedono una ricarica, mai batterie nuove.",
|
||||
"battery_fleet_status_low": "Scarica",
|
||||
"battery_fleet_status_soon": "A breve",
|
||||
"battery_fleet_status_ok": "A posto",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "期限間近",
|
||||
"triggered": "トリガー発生",
|
||||
"trigger_replaced": "トリガー解除済み",
|
||||
"trigger_removed": "トリガーを削除しました",
|
||||
"ok": "OK",
|
||||
"all": "すべて",
|
||||
"new_object": "+ 新規対象",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "グループ化",
|
||||
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
|
||||
"target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。",
|
||||
"for_minutes_state_help": "0 はすべての変化を即座にカウントします。分を設定すると新しい状態がその時間続く必要があり、一瞬の変動はトリガーもカウントもされません。",
|
||||
"qr_print_title": "QRコードを印刷",
|
||||
"qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。",
|
||||
"qr_print_load": "対象を読み込む",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "期日どおりに完了した場合。",
|
||||
"schedule_preview_ends": "(シリーズ終了)",
|
||||
"adopt_problem_responsible": "採用したすべてのタスクの担当ユーザー(任意)",
|
||||
"adopt_for_minutes_hint": "問題がこの時間続いた場合にのみトリガーします。0 は最初の変動に即反応します。",
|
||||
"adopt_problem_configure": "設定",
|
||||
"history_auto": "自動",
|
||||
"battery_fleet_title": "電池フリート",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "電池残量が少ないと報告される前に、ここで機器を除外できます — 自動充電するロボット掃除機や、自分で警告するスマートフォンなど。",
|
||||
"battery_fleet_add": "電池を追加",
|
||||
"battery_fleet_add_hint": "自動検出が見逃した電池センサーを選択すると、すぐに一覧に追加されます。",
|
||||
"battery_fleet_track_self": "自己充電式バッテリーを追跡",
|
||||
"battery_fleet_track_self_hint": "スマートフォンや掃除機など自己充電するデバイスは充電式として表示されます。残量が少ないときは充電を促し、電池交換は求めません。",
|
||||
"battery_fleet_status_low": "残量低下",
|
||||
"battery_fleet_status_soon": "まもなく",
|
||||
"battery_fleet_status_ok": "良好",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "기한 임박",
|
||||
"triggered": "트리거됨",
|
||||
"trigger_replaced": "트리거 교체됨",
|
||||
"trigger_removed": "트리거 제거됨",
|
||||
"ok": "정상",
|
||||
"all": "전체",
|
||||
"new_object": "+ 새 객체",
|
||||
@@ -524,6 +525,7 @@
|
||||
"group_by_label": "그룹화",
|
||||
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
|
||||
"target_changes_help": "트리거가 발동하기까지 필요한 일치 전환 횟수입니다 (기본값: 1).",
|
||||
"for_minutes_state_help": "0은 모든 변화를 즉시 집계합니다. 분을 설정하면 새 상태가 먼저 그 시간 동안 유지되어야 하며, 짧은 깜빡임은 트리거되지도 집계되지도 않습니다.",
|
||||
"qr_print_title": "QR 코드 인쇄",
|
||||
"qr_print_desc": "잘라서 장비에 붙일 수 있는 QR 코드 인쇄 페이지를 생성합니다.",
|
||||
"qr_print_load": "객체 불러오기",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "제때 완료한다고 가정한 일정입니다.",
|
||||
"schedule_preview_ends": "(시리즈 종료)",
|
||||
"adopt_problem_responsible": "가져온 모든 작업의 담당자 (선택)",
|
||||
"adopt_for_minutes_hint": "문제가 이 시간 동안 지속된 경우에만 트리거합니다. 0은 첫 깜빡임에 즉시 반응합니다.",
|
||||
"adopt_problem_configure": "구성",
|
||||
"history_auto": "자동",
|
||||
"battery_fleet_title": "배터리 플릿",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "배터리 부족을 알리기 전에 여기에서 기기를 제외하세요 — 스스로 충전하는 로봇청소기나 자체적으로 경고하는 휴대폰처럼.",
|
||||
"battery_fleet_add": "배터리 추가",
|
||||
"battery_fleet_add_hint": "자동 검색이 놓친 배터리 센서를 선택하면 즉시 목록에 추가됩니다.",
|
||||
"battery_fleet_track_self": "자체 충전 배터리 추적",
|
||||
"battery_fleet_track_self_hint": "휴대폰, 청소기 등 스스로 충전되는 기기는 충전식으로 표시됩니다. 잔량이 낮으면 충전을 요청하며 새 전지를 요구하지 않습니다.",
|
||||
"battery_fleet_status_low": "부족",
|
||||
"battery_fleet_status_soon": "곧",
|
||||
"battery_fleet_status_ok": "정상",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Forfaller snart",
|
||||
"triggered": "Utløst",
|
||||
"trigger_replaced": "Utløser erstattet",
|
||||
"trigger_removed": "Utløser fjernet",
|
||||
"ok": "OK",
|
||||
"all": "Alle",
|
||||
"new_object": "+ Nytt objekt",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "Grupper etter",
|
||||
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).",
|
||||
"for_minutes_state_help": "0 teller hver endring umiddelbart. Med minutter må den nye tilstanden først holde seg — korte blink verken utløser eller telles da.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.",
|
||||
"qr_print_load": "Last objekter",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Forutsatt fullføring i tide.",
|
||||
"schedule_preview_ends": "(serien avsluttes)",
|
||||
"adopt_problem_responsible": "Ansvarlig bruker for alle overtatte oppgaver (valgfritt)",
|
||||
"adopt_for_minutes_hint": "Utløs først når problemet har vart så lenge — 0 reagerer på første blink.",
|
||||
"adopt_problem_configure": "Konfigurer",
|
||||
"history_auto": "Automatisk",
|
||||
"battery_fleet_title": "Batteriflåte",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Utelukk en enhet her før den i det hele tatt melder lavt nivå — en støvsuger som lader seg selv, eller en telefon som varsler av seg selv.",
|
||||
"battery_fleet_add": "Legg til et batteri",
|
||||
"battery_fleet_add_hint": "Velg en batterisensor som den automatiske gjenkjenningen overså — den blir med i listen umiddelbart.",
|
||||
"battery_fleet_track_self": "Spor selvladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvladende enheter vises som oppladbare — ved lavt nivå bes det om lading, aldri om nye batterier.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Groeperen op",
|
||||
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
|
||||
"target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).",
|
||||
"for_minutes_state_help": "0 telt elke wijziging direct. Met minuten moet de nieuwe status eerst zo lang aanhouden — kort flikkeren triggert dan niet en telt niet mee.",
|
||||
"qr_print_title": "QR-codes afdrukken",
|
||||
"qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.",
|
||||
"qr_print_load": "Objecten laden",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Niet nodig",
|
||||
"quick_complete_success": "Snel als voltooid gemarkeerd",
|
||||
"trigger_replaced": "Trigger vervangen",
|
||||
"trigger_removed": "Trigger verwijderd",
|
||||
"add": "Toevoegen",
|
||||
"show_stats": "Statistieken + grafieken tonen",
|
||||
"hide_stats": "Statistieken verbergen",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Uitgaande van tijdige afronding.",
|
||||
"schedule_preview_ends": "(reeks eindigt)",
|
||||
"adopt_problem_responsible": "Verantwoordelijke gebruiker voor alle overgenomen taken (optioneel)",
|
||||
"adopt_for_minutes_hint": "Pas triggeren als het probleem zo lang aanhoudt — 0 reageert op de eerste flikkering.",
|
||||
"adopt_problem_configure": "Configureren",
|
||||
"history_auto": "Automatisch",
|
||||
"battery_fleet_title": "Batterijvloot",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Sluit een apparaat hier uit voordat het ooit laag meldt — een stofzuiger die zichzelf oplaadt, of een telefoon die zelf waarschuwt.",
|
||||
"battery_fleet_add": "Batterij toevoegen",
|
||||
"battery_fleet_add_hint": "Kies een batterijsensor die de automatische detectie heeft gemist — hij verschijnt direct in de lijst.",
|
||||
"battery_fleet_track_self": "Zelfopladende batterijen volgen",
|
||||
"battery_fleet_track_self_hint": "Telefoons, stofzuigers en andere zelfopladende apparaten verschijnen als oplaadbaar — bij een lage stand wordt om opladen gevraagd, nooit om nieuwe batterijen.",
|
||||
"battery_fleet_status_low": "Laag",
|
||||
"battery_fleet_status_soon": "Binnenkort",
|
||||
"battery_fleet_status_ok": "In orde",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Grupuj wg",
|
||||
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
|
||||
"target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).",
|
||||
"for_minutes_state_help": "0 liczy każdą zmianę od razu. Z minutami nowy stan musi się najpierw utrzymać — krótkie mignięcia wtedy nie wyzwalają i nie liczą się.",
|
||||
"qr_print_title": "Drukuj kody QR",
|
||||
"qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.",
|
||||
"qr_print_load": "Załaduj obiekty",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Nie było potrzebne",
|
||||
"quick_complete_success": "Szybko oznaczono jako wykonane",
|
||||
"trigger_replaced": "Wyzwalacz zastąpiony",
|
||||
"trigger_removed": "Wyzwalacz usunięty",
|
||||
"add": "Dodaj",
|
||||
"show_stats": "Pokaż statystyki + wykresy",
|
||||
"hide_stats": "Ukryj statystyki",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Przy założeniu terminowego ukończenia.",
|
||||
"schedule_preview_ends": "(seria się kończy)",
|
||||
"adopt_problem_responsible": "Osoba odpowiedzialna za wszystkie przejęte zadania (opcjonalnie)",
|
||||
"adopt_for_minutes_hint": "Wyzwalaj dopiero, gdy problem utrzymuje się tak długo — 0 reaguje na pierwsze mignięcie.",
|
||||
"adopt_problem_configure": "Konfiguruj",
|
||||
"history_auto": "Automatycznie",
|
||||
"battery_fleet_title": "Flota baterii",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Wyklucz tutaj urządzenie, zanim w ogóle zgłosi niski poziom — odkurzacz, który sam się ładuje, albo telefon, który ostrzega samodzielnie.",
|
||||
"battery_fleet_add": "Dodaj baterię",
|
||||
"battery_fleet_add_hint": "Wybierz czujnik baterii pominięty przez automatyczne wykrywanie — od razu trafi na listę.",
|
||||
"battery_fleet_track_self": "Śledź baterie samoładujące",
|
||||
"battery_fleet_track_self_hint": "Telefony, odkurzacze i inne samoładujące się urządzenia pojawiają się jako akumulatory — przy niskim poziomie proszą o ładowanie, nigdy o nowe baterie.",
|
||||
"battery_fleet_status_low": "Niski",
|
||||
"battery_fleet_status_soon": "Wkrótce",
|
||||
"battery_fleet_status_ok": "W porządku",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Vence em breve",
|
||||
"triggered": "Acionada",
|
||||
"trigger_replaced": "Gatilho substituído",
|
||||
"trigger_removed": "Gatilho removido",
|
||||
"ok": "OK",
|
||||
"all": "Todas",
|
||||
"new_object": "+ Novo objeto",
|
||||
@@ -524,6 +525,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes de o gatilho disparar (padrão: 1).",
|
||||
"for_minutes_state_help": "0 conta cada mudança imediatamente. Com minutos, o novo estado deve se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gere uma página imprimível de códigos QR para recortar e colar nos seus equipamentos.",
|
||||
"qr_print_load": "Carregar objetos",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Supondo conclusão em dia.",
|
||||
"schedule_preview_ends": "(a série termina)",
|
||||
"adopt_problem_responsible": "Usuário responsável por todas as tarefas adotadas (opcional)",
|
||||
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir por esse tempo — 0 reage à primeira oscilação.",
|
||||
"adopt_problem_configure": "Configurar",
|
||||
"history_auto": "Automática",
|
||||
"battery_fleet_title": "Frota de baterias",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes que ele sequer indique carga baixa — um aspirador que se recarrega sozinho ou um celular que já avisa sozinho.",
|
||||
"battery_fleet_add": "Adicionar uma bateria",
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a detecção automática não encontrou — ele entra na lista imediatamente.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem uma recarga, nunca pilhas novas.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
|
||||
@@ -513,6 +513,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).",
|
||||
"for_minutes_state_help": "0 conta cada mudança de imediato. Com minutos, o novo estado tem de se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.",
|
||||
"qr_print_load": "Carregar objetos",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Não era necessário",
|
||||
"quick_complete_success": "Concluído rapidamente",
|
||||
"trigger_replaced": "Acionador substituído",
|
||||
"trigger_removed": "Gatilho removido",
|
||||
"add": "Adicionar",
|
||||
"show_stats": "Mostrar estatísticas + gráficos",
|
||||
"hide_stats": "Ocultar estatísticas",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Assumindo conclusão pontual.",
|
||||
"schedule_preview_ends": "(a série termina)",
|
||||
"adopt_problem_responsible": "Utilizador responsável por todas as tarefas adotadas (opcional)",
|
||||
"adopt_for_minutes_hint": "Disparar apenas se o problema persistir este tempo — 0 reage à primeira oscilação.",
|
||||
"adopt_problem_configure": "Configurar",
|
||||
"history_auto": "Automático",
|
||||
"battery_fleet_title": "Frota de baterias",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Exclua aqui um dispositivo antes de ele sequer indicar carga fraca — um aspirador que se recarrega sozinho ou um telemóvel que já avisa por si.",
|
||||
"battery_fleet_add": "Adicionar uma bateria",
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a deteção automática não encontrou — entra de imediato na lista.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem um carregamento, nunca pilhas novas.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Группировать по",
|
||||
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
|
||||
"target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).",
|
||||
"for_minutes_state_help": "0 — каждое изменение засчитывается сразу. С минутами новое состояние сначала должно удержаться — короткие сбои тогда не срабатывают и не считаются.",
|
||||
"qr_print_title": "Печать QR-кодов",
|
||||
"qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.",
|
||||
"qr_print_load": "Загрузить объекты",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Не было необходимо",
|
||||
"quick_complete_success": "Быстро отмечено как выполнено",
|
||||
"trigger_replaced": "Триггер заменён",
|
||||
"trigger_removed": "Триггер удалён",
|
||||
"add": "Добавить",
|
||||
"show_stats": "Показать статистику + графики",
|
||||
"hide_stats": "Скрыть статистику",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "При условии своевременного выполнения.",
|
||||
"schedule_preview_ends": "(серия завершается)",
|
||||
"adopt_problem_responsible": "Ответственный пользователь для всех принятых задач (необязательно)",
|
||||
"adopt_for_minutes_hint": "Срабатывать только если проблема сохраняется это время — 0 реагирует на первый сбой.",
|
||||
"adopt_problem_configure": "Настроить",
|
||||
"history_auto": "Автоматически",
|
||||
"battery_fleet_title": "Парк батарей",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Исключите устройство здесь ещё до того, как оно сообщит о низком заряде — робот-пылесос, который заряжается сам, или телефон, который предупреждает вас сам.",
|
||||
"battery_fleet_add": "Добавить батарею",
|
||||
"battery_fleet_add_hint": "Выберите датчик батареи, который автоматическое обнаружение пропустило — он сразу появится в списке.",
|
||||
"battery_fleet_track_self": "Отслеживать самозаряжающиеся батареи",
|
||||
"battery_fleet_track_self_hint": "Телефоны, пылесосы и другие самозаряжающиеся устройства отображаются как аккумуляторы — при низком заряде нужна зарядка, а не новые батарейки.",
|
||||
"battery_fleet_status_low": "Низкий",
|
||||
"battery_fleet_status_soon": "Скоро",
|
||||
"battery_fleet_status_ok": "В норме",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Gruppera efter",
|
||||
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
|
||||
"target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).",
|
||||
"for_minutes_state_help": "0 räknar varje ändring direkt. Med minuter måste det nya tillståndet först hålla i sig — korta blinkningar varken utlöser eller räknas då.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.",
|
||||
"qr_print_load": "Ladda objekt",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Inte nödvändigt",
|
||||
"quick_complete_success": "Snabbt markerad som klar",
|
||||
"trigger_replaced": "Utlösare ersatt",
|
||||
"trigger_removed": "Utlösare borttagen",
|
||||
"add": "Lägg till",
|
||||
"show_stats": "Visa statistik + grafer",
|
||||
"hide_stats": "Dölj statistik",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Förutsatt slutförande i tid.",
|
||||
"schedule_preview_ends": "(serien avslutas)",
|
||||
"adopt_problem_responsible": "Ansvarig användare för alla övertagna uppgifter (valfritt)",
|
||||
"adopt_for_minutes_hint": "Utlös först när problemet kvarstått så här länge — 0 reagerar på första blinkningen.",
|
||||
"adopt_problem_configure": "Konfigurera",
|
||||
"history_auto": "Automatiskt",
|
||||
"battery_fleet_title": "Batteriflotta",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Uteslut en enhet här redan innan den rapporterar låg nivå — en dammsugare som laddar sig själv, eller en telefon som varnar på egen hand.",
|
||||
"battery_fleet_add": "Lägg till ett batteri",
|
||||
"battery_fleet_add_hint": "Välj en batterisensor som den automatiska identifieringen missade — den läggs till i listan direkt.",
|
||||
"battery_fleet_track_self": "Spåra självladdande batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, dammsugare och andra självladdande enheter visas som laddningsbara — vid låg nivå begärs laddning, aldrig nya batterier.",
|
||||
"battery_fleet_status_low": "Låg",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I ordning",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "Yaklaşan",
|
||||
"triggered": "Tetiklendi",
|
||||
"trigger_replaced": "Tetikleyici değiştirildi",
|
||||
"trigger_removed": "Tetikleyici kaldırıldı",
|
||||
"ok": "Tamam",
|
||||
"all": "Tümü",
|
||||
"new_object": "+ Yeni Nesne",
|
||||
@@ -524,6 +525,7 @@
|
||||
"group_by_label": "Grupla",
|
||||
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
|
||||
"target_changes_help": "Tetikleyici çalışmadan önceki eşleşen geçiş sayısı (varsayılan: 1).",
|
||||
"for_minutes_state_help": "0 her değişikliği hemen sayar. Dakika ayarlanırsa yeni durumun önce o kadar sürmesi gerekir — kısa titremeler o zaman ne tetikler ne de sayılır.",
|
||||
"qr_print_title": "QR kodlarını yazdır",
|
||||
"qr_print_desc": "Kesip ekipmanınıza yapıştırmak için yazdırılabilir bir QR kodu sayfası oluşturun.",
|
||||
"qr_print_load": "Nesneleri yükle",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "Zamanında tamamlama varsayılarak.",
|
||||
"schedule_preview_ends": "(seri sona erer)",
|
||||
"adopt_problem_responsible": "Devralınan tüm görevler için sorumlu kullanıcı (isteğe bağlı)",
|
||||
"adopt_for_minutes_hint": "Yalnızca sorun bu süre boyunca devam ederse tetikle — 0 ilk titremeye tepki verir.",
|
||||
"adopt_problem_configure": "Yapılandır",
|
||||
"history_auto": "Otomatik",
|
||||
"battery_fleet_title": "Pil filosu",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Bir cihazı, daha düşük şarj bildirmeden burada hariç tutun — kendi kendine şarj olan bir süpürge ya da zaten kendisi uyaran bir telefon gibi.",
|
||||
"battery_fleet_add": "Pil ekle",
|
||||
"battery_fleet_add_hint": "Otomatik keşfin atladığı bir pil sensörü seçin — hemen listeye katılır.",
|
||||
"battery_fleet_track_self": "Kendi kendine şarj olan pilleri izle",
|
||||
"battery_fleet_track_self_hint": "Telefonlar, süpürgeler ve kendi kendine şarj olan diğer cihazlar şarj edilebilir olarak görünür — seviye düşükken şarj istenir, asla yeni pil istenmez.",
|
||||
"battery_fleet_status_low": "Düşük",
|
||||
"battery_fleet_status_soon": "Yakında",
|
||||
"battery_fleet_status_ok": "İyi durumda",
|
||||
|
||||
@@ -517,6 +517,7 @@
|
||||
"group_by_label": "Групувати за",
|
||||
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
|
||||
"target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).",
|
||||
"for_minutes_state_help": "0 — кожна зміна зараховується одразу. З хвилинами новий стан спершу має утриматися — короткі збої тоді не спрацьовують і не рахуються.",
|
||||
"qr_print_title": "Друкувати QR-коди",
|
||||
"qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.",
|
||||
"qr_print_load": "Завантажити об'єкти",
|
||||
@@ -584,6 +585,7 @@
|
||||
"quick_complete_defaults_feedback_not_needed": "Не було необхідно",
|
||||
"quick_complete_success": "Швидко позначено виконаним",
|
||||
"trigger_replaced": "Тригер замінено",
|
||||
"trigger_removed": "Тригер видалено",
|
||||
"add": "Додати",
|
||||
"show_stats": "Показати статистику + графіки",
|
||||
"hide_stats": "Сховати статистику",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "За умови вчасного виконання.",
|
||||
"schedule_preview_ends": "(серія завершується)",
|
||||
"adopt_problem_responsible": "Відповідальний користувач для всіх прийнятих завдань (необов'язково)",
|
||||
"adopt_for_minutes_hint": "Спрацьовувати лише якщо проблема триває цей час — 0 реагує на перший збій.",
|
||||
"adopt_problem_configure": "Налаштувати",
|
||||
"history_auto": "Автоматично",
|
||||
"battery_fleet_title": "Парк батарей",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "Вилучіть пристрій тут ще до того, як він повідомить про низький заряд — робот-пилосос, що заряджається сам, або телефон, який попереджає сам.",
|
||||
"battery_fleet_add": "Додати батарею",
|
||||
"battery_fleet_add_hint": "Виберіть датчик батареї, який автоматичне виявлення пропустило — він одразу з'явиться у списку.",
|
||||
"battery_fleet_track_self": "Відстежувати самозарядні батареї",
|
||||
"battery_fleet_track_self_hint": "Телефони, пилососи та інші самозарядні пристрої відображаються як акумулятори — за низького заряду потрібна зарядка, а не нові батарейки.",
|
||||
"battery_fleet_status_low": "Низький",
|
||||
"battery_fleet_status_soon": "Незабаром",
|
||||
"battery_fleet_status_ok": "У нормі",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"due_soon": "即将到期",
|
||||
"triggered": "已触发",
|
||||
"trigger_replaced": "触发器已更换",
|
||||
"trigger_removed": "触发器已移除",
|
||||
"ok": "正常",
|
||||
"all": "全部",
|
||||
"new_object": "+ 新建维护项",
|
||||
@@ -518,6 +519,7 @@
|
||||
"group_by_label": "分组依据",
|
||||
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
|
||||
"target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。",
|
||||
"for_minutes_state_help": "0 表示每次变化立即计数。设置分钟后,新状态需先保持该时长——短暂闪变既不触发也不计数。",
|
||||
"qr_print_title": "打印二维码",
|
||||
"qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。",
|
||||
"qr_print_load": "加载设备",
|
||||
@@ -819,6 +821,7 @@
|
||||
"schedule_preview_ontime": "假设按时完成。",
|
||||
"schedule_preview_ends": "(系列结束)",
|
||||
"adopt_problem_responsible": "所有采用任务的负责用户(可选)",
|
||||
"adopt_for_minutes_hint": "问题持续此时长后才触发——0 表示对首次闪变立即响应。",
|
||||
"adopt_problem_configure": "配置",
|
||||
"history_auto": "自动",
|
||||
"battery_fleet_title": "电池群",
|
||||
@@ -838,6 +841,8 @@
|
||||
"battery_fleet_all_hint": "在设备报告电量不足之前就可以在这里将其排除——例如会自行充电的扫地机器人,或本身就会提醒你的手机。",
|
||||
"battery_fleet_add": "添加电池",
|
||||
"battery_fleet_add_hint": "选择自动发现遗漏的电池传感器——它会立即加入列表。",
|
||||
"battery_fleet_track_self": "跟踪自充电电池",
|
||||
"battery_fleet_track_self_hint": "手机、吸尘器等自充电设备显示为可充电电池——电量低时提示充电,而不是更换电池。",
|
||||
"battery_fleet_status_low": "电量低",
|
||||
"battery_fleet_status_soon": "即将",
|
||||
"battery_fleet_status_ok": "正常",
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type CalendarEvent,
|
||||
} from "./helpers/calendar-bucket";
|
||||
import { calendarStyles } from "./calendar-styles";
|
||||
import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
|
||||
import { syncLocaleFromHass, sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
import { openHistoryEditDialog, openTaskQuickActions } from "./dialog-mount";
|
||||
import type {
|
||||
@@ -139,12 +139,7 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
super.updated(changedProps);
|
||||
// Dates/times follow the HA profile format, not just the language (#97).
|
||||
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
|
||||
const lang = this.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => this.requestUpdate());
|
||||
}
|
||||
syncLocaleFromHass(this, changedProps);
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
if (!this._dataLoaded) {
|
||||
this._dataLoaded = true;
|
||||
|
||||
@@ -4,8 +4,9 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { hydrateObjects } from "./helpers/hydrate-objects";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
|
||||
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays, langOf } from "./styles";
|
||||
import { openSignedDocument } from "./helpers/document-url";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
@@ -20,6 +21,9 @@ import { UserService } from "./user-service";
|
||||
import { partsForCompletion } from "./helpers/shared-parts";
|
||||
import "./maintenance-card-editor";
|
||||
import "./components/complete-dialog";
|
||||
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
||||
// available on every dashboard without another extra_module_url entry.
|
||||
import "./components/battery-fleet-card";
|
||||
import {
|
||||
openCreateObjectDialog,
|
||||
openCreateTaskDialog,
|
||||
@@ -110,12 +114,7 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
super.updated(changedProps);
|
||||
// Dates/times follow the HA profile format, not just the language (#97).
|
||||
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
|
||||
const lang = this.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => this.requestUpdate());
|
||||
}
|
||||
syncLocaleFromHass(this, changedProps);
|
||||
// Assignee names: resolved once, only when the config allows it AND some
|
||||
// task actually carries a responsible user. The condition is checked
|
||||
// synchronously here — an unconditional call whose promise triggered a
|
||||
@@ -239,7 +238,10 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
* (the same route the panel uses, so it works in the Companion app). */
|
||||
private async _openDoc(doc: CardDoc): Promise<void> {
|
||||
if (doc.kind === "weblink" && doc.url) {
|
||||
window.open(doc.url, "_blank", "noopener");
|
||||
// Same guarded sink as every other weblink opener: never hand a
|
||||
// non-http(s) URL to window.open (defense-in-depth against a link
|
||||
// stored before the server-side scheme check existed).
|
||||
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { isStaleBundle } from "./helpers/bundle-version";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs, langOf } from "./styles";
|
||||
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs, langOf } from "./styles";
|
||||
import { LS_KEYS, lsGet, lsSet } from "./helpers/storage-keys";
|
||||
import { openSignedDocument, signApiPath } from "./helpers/document-url";
|
||||
import { openHtmlInNewTab, openSignedDocument, signApiPath } from "./helpers/document-url";
|
||||
import { readObjectsCache, writeObjectsCache } from "./helpers/objects-cache";
|
||||
import { hydrateObjects } from "./helpers/hydrate-objects";
|
||||
import { daysProgress } from "./helpers/interval";
|
||||
@@ -373,15 +373,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
super.updated(changedProps);
|
||||
// Dates/times follow the HA profile format, not just the language (#97).
|
||||
if (changedProps.has("hass")) setDateTimePrefs(this.hass?.locale);
|
||||
// Lazy-load the user's UI language (non-EN tables aren't bundled) and
|
||||
// re-render once it arrives. EN is bundled, so strings read in English
|
||||
// until then rather than as raw keys.
|
||||
const lang = this.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => this.requestUpdate());
|
||||
}
|
||||
syncLocaleFromHass(this, changedProps);
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
if (!this._dataLoaded) {
|
||||
this._dataLoaded = true;
|
||||
@@ -1477,9 +1469,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._currencySymbol,
|
||||
new Date().toISOString(),
|
||||
);
|
||||
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
openHtmlInNewTab(html);
|
||||
}
|
||||
|
||||
private async _duplicateObject(entryId: string): Promise<void> {
|
||||
@@ -1843,9 +1833,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
new Date().toISOString(),
|
||||
partsLines,
|
||||
);
|
||||
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
openHtmlInNewTab(html);
|
||||
} finally {
|
||||
this._actionLoading = false;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ export interface HistoryContext {
|
||||
readingDelta?: (entry: HistoryEntry) => number | null;
|
||||
}
|
||||
|
||||
const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered"] as const;
|
||||
// Every HistoryEntryType is filterable (drift audit 2026-08: the repair-flow
|
||||
// types were missing, so their entries could not be isolated). Chips only
|
||||
// render for types present in the task's history, so the two rare ones cost
|
||||
// nothing on normal tasks.
|
||||
const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered", "trigger_replaced", "trigger_removed"] as const;
|
||||
|
||||
export function renderHistoryFilters(task: MaintenanceTask, ctx: HistoryContext) {
|
||||
const L = ctx.lang;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { css } from "lit";
|
||||
import EN from "./locales/en.json";
|
||||
import { BUNDLE_VERSION } from "./helpers/bundle-version";
|
||||
|
||||
// Display fallback when the backend hasn't sent a currency symbol. The backend
|
||||
// derives the real symbol from const.BUDGET_CURRENCIES[DEFAULT_BUDGET_CURRENCY].
|
||||
@@ -47,8 +48,21 @@ const _localeGlobals: LocaleGlobals = (() => {
|
||||
})();
|
||||
|
||||
const STORE = _localeGlobals.store;
|
||||
// Each copy guarantees the bundled English fallback is present.
|
||||
if (!STORE.en) STORE.en = EN as Translations;
|
||||
|
||||
/** Seed/extend the shared English table with this bundle's copy.
|
||||
*
|
||||
* MERGE, not first-wins (#135 regression): after an update, a cached app
|
||||
* shell can still list an OLD sibling bundle (the card) next to the fresh
|
||||
* panel. First-wins let the stale bundle's EN — missing every new key —
|
||||
* claim the store, and the fresh panel rendered raw keys
|
||||
* ("BATTERY_FLEET_ADD"). Existing entries win conflicts (steady state is
|
||||
* unchanged); every bundle contributes the keys it knows, so a newer
|
||||
* bundle's keys can never be shadowed by an older first-loader.
|
||||
*/
|
||||
export function seedEnglish(en: Record<string, string>): void {
|
||||
STORE.en = Object.assign({}, en, STORE.en ?? {});
|
||||
}
|
||||
seedEnglish(EN as Translations);
|
||||
|
||||
/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */
|
||||
const SUPPORTED_LANGS = new Set<string>([
|
||||
@@ -79,6 +93,22 @@ export function t(key: string, lang?: string): string {
|
||||
return STORE[l]?.[key] ?? STORE.en[key] ?? key;
|
||||
}
|
||||
|
||||
/** The per-`updated()` locale boot every top-level surface needs: date/time
|
||||
* prefs follow the HA profile (#97), and the user's UI language is lazily
|
||||
* fetched with a re-render once it arrives (EN is bundled, so strings read
|
||||
* in English until then rather than as raw keys). Was copy-pasted across the
|
||||
* panel and both cards (drift audit 2026-08). */
|
||||
export function syncLocaleFromHass(
|
||||
host: { hass?: { language?: string; locale?: unknown }; requestUpdate: () => void },
|
||||
changedProps: Map<string, unknown>,
|
||||
): void {
|
||||
if (changedProps.has("hass")) setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0]);
|
||||
const lang = host.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => host.requestUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI language for a hass object — the ONE rule every component's `_lang`
|
||||
* getter delegates to. Before this helper the 23 hand-copied getters had
|
||||
@@ -108,7 +138,11 @@ export function ensureLocale(lang?: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!(l in _localeInflight)) {
|
||||
_localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json`)
|
||||
// Version-busted (#135, same family as #124): the locale files are served
|
||||
// without Cache-Control, so browsers cache them heuristically — after an
|
||||
// update a stale table would miss every new key and silently fall back to
|
||||
// English for them. "dev" builds keep a stable URL.
|
||||
_localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json?v=${BUNDLE_VERSION}`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
@@ -1067,48 +1101,9 @@ export const sharedStyles = css`
|
||||
.confidence-dot.medium { background: var(--warning-color, #ff9800); }
|
||||
.confidence-dot.high { background: var(--success-color, #4caf50); }
|
||||
|
||||
/* Feedback toggle buttons in complete dialog */
|
||||
.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);
|
||||
}
|
||||
/* (The complete-dialog's .feedback-* rules live in complete-dialog.ts —
|
||||
this sheet carried a dead byte-identical copy until the 2026-08 drift
|
||||
audit; complete-dialog never imports sharedStyles.) */
|
||||
|
||||
/* Seasonal chart */
|
||||
.seasonal-chart {
|
||||
|
||||
Reference in New Issue
Block a user