217 files

This commit is contained in:
Home Assistant Version Control
2026-07-30 23:59:38 +00:00
parent d43a63ad29
commit 7b5e46e702
217 changed files with 15978 additions and 3912 deletions
@@ -9,6 +9,15 @@ import { UserService } from "../user-service";
import { OBJECT_COLUMNS, sanitizeColumns } from "../helpers/object-columns";
import { downloadTextFile } from "../helpers/download";
/** One household member and the notify services they actually resolve to.
* Empty `services` means no Companion device is linked, so their reminders
* fall back to the household notification service. */
interface PersonNotifyTarget {
user_id: string;
name: string;
services: string[];
}
/* Settings response shape from WS maintenance_supporter/settings */
interface SettingsResponse {
features: AdvancedFeatures;
@@ -27,6 +36,8 @@ interface SettingsResponse {
notify_targets?: string[];
panel_enabled: boolean;
panel_title: string;
/** Opt-in copy of the shipped Assist sentences into the config dir. */
install_assist_sentences?: boolean;
};
notifications: {
due_soon_enabled: boolean;
@@ -108,6 +119,8 @@ export class MaintenanceSettingsView extends LitElement {
@state() private _includeHistory = true;
@state() private _toast = "";
@state() private _testingNotification = false;
@state() private _personTargets: PersonNotifyTarget[] = [];
@state() private _testingUser = "";
@state() private _users: HAUser[] = [];
@state() private _savedViews: Array<{ id: string; name: string }> = [];
@@ -169,6 +182,23 @@ export class MaintenanceSettingsView extends LitElement {
} catch {
this._users = [];
}
this._loadNotifyTargets();
}
/** Which notify services each household member resolves to.
*
* Resolved by the backend through the same helper the reminder path uses,
* so the list shown here is the list that will actually be used.
*/
private async _loadNotifyTargets(): Promise<void> {
try {
const res = await this.hass.connection.sendMessagePromise<{ targets: PersonNotifyTarget[] }>({
type: "maintenance_supporter/notify/user_targets",
});
this._personTargets = res.targets || [];
} catch {
this._personTargets = [];
}
}
private async _loadSettings(): Promise<void> {
@@ -220,11 +250,13 @@ export class MaintenanceSettingsView extends LitElement {
}
}
private _sendTestNotification = async (): Promise<void> => {
this._testingNotification = true;
private _sendTestNotification = async (userId?: string): Promise<void> => {
if (userId) this._testingUser = userId;
else this._testingNotification = true;
try {
const res = await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/global/test_notification",
...(userId ? { user_id: userId } : {}),
}) as { success: boolean; message?: string };
const msg = res.message
|| (res.success ? t("test_notification_success", this._lang) : t("test_notification_failed", this._lang));
@@ -232,7 +264,8 @@ export class MaintenanceSettingsView extends LitElement {
} catch {
this._showToast(t("test_notification_failed", this._lang));
} finally {
this._testingNotification = false;
if (userId) this._testingUser = "";
else this._testingNotification = false;
}
};
@@ -570,6 +603,12 @@ export class MaintenanceSettingsView extends LitElement {
@change=${(e: Event) => this._updateSetting("panel_title", (e.target as HTMLInputElement).value.trim())} />
</label>
` : ""}
<label class="setting-row">
<span class="setting-label">${t("settings_install_assist_sentences", L)}</span>
<input type="checkbox" .checked=${g.install_assist_sentences ?? false}
@change=${(e: Event) => this._updateSetting("install_assist_sentences", (e.target as HTMLInputElement).checked)} />
</label>
<div class="setting-hint">${t("settings_install_assist_sentences_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_notifications", L)}</span>
<input type="checkbox" .checked=${g.notifications_enabled}
@@ -588,10 +627,28 @@ export class MaintenanceSettingsView extends LitElement {
<span class="setting-label">${t("test_notification", L)}</span>
<button class="ha-button secondary"
?disabled=${!g.notify_service || this._testingNotification}
@click=${this._sendTestNotification}>
@click=${() => this._sendTestNotification()}>
${this._testingNotification ? t("testing", L) : t("send_test", L)}
</button>
</div>
${this._personTargets.length ? html`
<div class="notify-per-person">
<span class="setting-label">${t("notify_per_person", L)}</span>
${this._personTargets.map((target) => html`
<div class="notify-person-row">
<span class="notify-person-name">${target.name}</span>
<span class="notify-person-target ${target.services.length ? "" : "muted"}">
${target.services.length ? target.services.join(", ") : t("notify_no_own_device", L)}
</span>
<button class="ha-button secondary"
?disabled=${!target.services.length || this._testingUser === target.user_id}
@click=${() => this._sendTestNotification(target.user_id)}>
${this._testingUser === target.user_id ? t("testing", L) : t("send_test", L)}
</button>
</div>
`)}
</div>
` : nothing}
` : nothing}
</div>
`;
@@ -1606,6 +1663,31 @@ export class MaintenanceSettingsView extends LitElement {
cursor: pointer;
gap: 12px;
}
.notify-per-person {
padding: 10px 0;
border-bottom: 1px solid var(--divider-color, #e0e0e0);
}
.notify-person-row {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 0 0;
flex-wrap: wrap;
}
.notify-person-name {
font-weight: 500;
min-width: 120px;
}
.notify-person-target {
flex: 1;
min-width: 160px;
font-size: 0.9em;
word-break: break-word;
color: var(--secondary-text-color, #727272);
}
.notify-person-target.muted {
font-style: italic;
}
/* v2.27: template gallery clustered by category */
.tpl-group { margin-top: 14px; }
.tpl-group-head {