257 files

This commit is contained in:
Home Assistant Version Control
2026-10-02 16:06:24 +00:00
parent 7c55603942
commit c29f5cc088
258 changed files with 24621 additions and 3136 deletions
@@ -113,6 +113,8 @@ interface SettingsResponse {
/** #173 follow-up: completion notifications — off | automatic | all. */
completed?: string;
extra_data?: string;
/** #44: default | object_name | task_name — was only in the Configure dialog. */
title_style?: string;
};
actions: {
complete_enabled: boolean;
@@ -132,6 +134,8 @@ interface SettingsResponse {
currency_symbol: string;
/** Decimal places for every displayed amount (0 = whole numbers). */
currency_decimals?: number;
/** #104: spare parts count when bought ("purchase") or used ("use"). */
parts_cost_mode?: string;
};
// v2.10.0 archive automation (panel-managed). Optional for forward-compat
// with a backend that predates the feature.
@@ -183,6 +187,9 @@ export class MaintenanceSettingsView extends LitElement {
@state() private _settings: SettingsResponse | null = null;
@state() private _loading = true;
@state() private _importCsv = "";
/** People an import's tasks pointed at that match nobody on this instance
* (by name) — their assignments were cleared; shown until the next import. */
@state() private _importUnmatched: string[] = [];
@state() private _importLoading = false;
@state() private _includeHistory = true;
@state() private _toast = "";
@@ -689,7 +696,6 @@ export class MaintenanceSettingsView extends LitElement {
// "groups" target also lands here as the closest match.
const items: { key: keyof AdvancedFeatures; settingKey: string; label: string; desc: string }[] = [
{ key: "adaptive", settingKey: "advanced_adaptive_visible", label: t("feat_adaptive", L), desc: t("feat_adaptive_desc", L) },
{ key: "predictions", settingKey: "advanced_predictions_visible", label: t("feat_predictions", L), desc: t("feat_predictions_desc", L) },
{ key: "seasonal", settingKey: "advanced_seasonal_visible", label: t("feat_seasonal", L), desc: t("feat_seasonal_desc", L) },
{ key: "environmental", settingKey: "advanced_environmental_visible", label: t("feat_environmental", L), desc: t("feat_environmental_desc", L) },
{ key: "budget", settingKey: "advanced_budget_visible", label: t("feat_budget", L), desc: t("feat_budget_desc", L) },
@@ -1056,6 +1062,15 @@ export class MaintenanceSettingsView extends LitElement {
</select>
</label>
<div class="setting-hint">${t("settings_currency_decimals_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_parts_cost_mode", L)}</span>
<select class="parts-cost-mode" .value=${live(b.parts_cost_mode === "use" ? "use" : "purchase")}
@change=${(e: Event) => this._updateSetting("parts_cost_mode", (e.target as HTMLSelectElement).value)}>
<option value="purchase" ?selected=${b.parts_cost_mode !== "use"}>${t("settings_parts_cost_purchase", L)}</option>
<option value="use" ?selected=${b.parts_cost_mode === "use"}>${t("settings_parts_cost_use", L)}</option>
</select>
</label>
<div class="setting-hint">${t("settings_parts_cost_mode_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_panel_enabled", L)}</span>
<input type="checkbox" .checked=${g.panel_enabled}
@@ -1093,7 +1108,7 @@ export class MaintenanceSettingsView extends LitElement {
<div class="setting-row">
<span class="setting-label">${t("test_notification", L)}</span>
<button class="ha-button secondary"
?disabled=${!g.notify_service || this._testingNotification}
?disabled=${(!g.notify_service && !this._settings?.notifications?.event_only) || this._testingNotification}
@click=${() => this._sendTestNotification()}>
${this._testingNotification ? t("testing", L) : t("send_test", L)}
</button>
@@ -1273,6 +1288,16 @@ export class MaintenanceSettingsView extends LitElement {
</select>
</label>
<div class="setting-hint">${t("settings_notify_completed_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_title_style", L)}</span>
<select class="notify-title-style" .value=${live(n.title_style || "default")}
@change=${(e: Event) => this._updateSetting("notification_title_style", (e.target as HTMLSelectElement).value)}>
<option value="default">${t("title_style_default", L)}</option>
<option value="object_name">${t("title_style_object_name", L)}</option>
<option value="task_name">${t("title_style_task_name", L)}</option>
</select>
</label>
<div class="setting-hint">${t("settings_title_style_hint", L)}</div>
<div class="notify-rule">
<h4 style="margin: 16px 0 8px; font-size: 14px;">${t("settings_notify_rule", L)}</h4>
@@ -2015,6 +2040,11 @@ export class MaintenanceSettingsView extends LitElement {
${this._importLoading ? "…" : t("settings_import_btn", L)}
</button>
</div>
${this._importUnmatched.length
? html`<div class="import-notice">
${t("settings_import_unmatched_users", L).replace("{names}", this._importUnmatched.join(", "))}
</div>`
: nothing}
</div>
</div>
`;
@@ -2101,7 +2131,8 @@ export class MaintenanceSettingsView extends LitElement {
// (JSON `{`/`[` or YAML `version:`) goes to the structured importer,
// which parses JSON and YAML alike.
const isCsv = content.startsWith("object_name");
const result = await this._ws<{ created: number }>(
this._importUnmatched = [];
const result = await this._ws<{ created: number; unmatched_users?: string[] }>(
isCsv
? { type: "maintenance_supporter/csv/import", csv_content: content }
: { type: "maintenance_supporter/json/import", json_content: content },
@@ -2111,6 +2142,7 @@ export class MaintenanceSettingsView extends LitElement {
if (!result) return;
const count = result.created ?? 0;
this._showToast(t("settings_import_success", this._lang).replace("{count}", String(count)));
this._importUnmatched = Array.isArray(result.unmatched_users) ? result.unmatched_users : [];
this._importCsv = "";
this.dispatchEvent(new CustomEvent("settings-changed"));
}
@@ -2143,13 +2175,18 @@ export class MaintenanceSettingsView extends LitElement {
try {
const form = new FormData();
form.append("file", file, file.name);
const result = await postMultipart<{ blobs_written: number; documents_created: number }>(
this.hass, "/api/maintenance_supporter/documents/archive", form, "docs_archive_too_large",
const result = await postMultipart<{ blobs_written: number; documents_created: number; files_missing?: number }>(
this.hass, "/api/maintenance_supporter/documents/archive", form, "docs_archive_too_large", ["docs_archive_no_manifest"],
);
const restored = t("settings_docs_import_success", this._lang)
.replace("{blobs}", String(result.blobs_written ?? 0))
.replace("{docs}", String(result.documents_created ?? 0));
// Documents whose file the archive did not carry are not created —
// said, so a file left out when zipping again is noticed.
this._showToast(
t("settings_docs_import_success", this._lang)
.replace("{blobs}", String(result.blobs_written ?? 0))
.replace("{docs}", String(result.documents_created ?? 0))
result.files_missing
? `${restored} · ${t("settings_docs_import_missing", this._lang).replace("{n}", String(result.files_missing))}`
: restored,
);
this.dispatchEvent(new CustomEvent("settings-changed"));
} catch (e) {
@@ -2158,7 +2195,9 @@ export class MaintenanceSettingsView extends LitElement {
e instanceof Error && e.message === "docs_archive_too_large"
? t("docs_archive_too_large", this._lang)
.replace("{max}", `${formatNumber(DOCS_ARCHIVE_MAX_BYTES / (1024 * 1024), this._lang)} MB`)
: t("action_error", this._lang),
: e instanceof Error && e.message === "docs_archive_no_manifest"
? t("docs_archive_no_manifest", this._lang)
: t("action_error", this._lang),
);
}
input.value = "";
@@ -2168,6 +2207,12 @@ export class MaintenanceSettingsView extends LitElement {
// --- Styles ---
static styles = [personStyles, css`
.import-notice {
margin-top: 8px; padding: 8px 12px; border-radius: 8px;
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.35);
font-size: 13.5px; line-height: 1.45;
}
.bn-note {
display: flex; align-items: flex-start; gap: 10px;
margin: 6px 0 10px; padding: 10px 12px; border-radius: 8px;
@@ -2331,6 +2376,13 @@ export class MaintenanceSettingsView extends LitElement {
.settings-actions {
display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;
}
/* Heading, hint and buttons stacked — as a row the hint landed between
the heading and the buttons, reading like a caption for neither. */
.docs-archive-block {
flex-direction: column;
align-items: flex-start;
}
.docs-archive-block h4, .docs-archive-block .section-desc { margin: 0; }
.settings-actions button {
padding: 8px 16px;
border-radius: 8px;
@@ -2443,7 +2495,9 @@ export class MaintenanceSettingsView extends LitElement {
}
.vac-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
/* The date fields carry a typing toggle and a clear button (2 × 36px)
next to the picker — 160px left the date itself cut to "11/1". */
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 12px;
margin-bottom: 12px;
}