Correct backup, onboarding and cloud API types in the frontend (#54286)

Compare the backup, onboarding and cloud data modules with the
handlers that produce them, and fix the mismatches.

Several backup calls declared the wrong result. Deleting a backup
returns agent errors, generating one returns a job id rather than a
backup id, and restoring one returns nothing. The backup list carries
extended content, not the base type.

Add the blocked manager state, which is the state the manager starts
in and replays to new subscribers, along with the event reason fields
and the core_restart restore state.

Correct the onboarding installation type, which listed a value the
backend never sends and omitted one it does, and narrow the cloud
onboarding status to the single field that endpoint returns.

Remove the cloud strict connection preference and its translations.
No such preference exists in core, so the warning it gated could never
render.


Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paulus Schoutsen
2026-09-27 09:35:47 +02:00
committed by GitHub
co-authored by Claude
parent 7c5a7c81e5
commit 4db73f6a3d
19 changed files with 113 additions and 70 deletions
+10 -2
View File
@@ -1,7 +1,6 @@
import type {
BackupAgentsInfo,
BackupConfig,
BackupContent,
BackupInfo,
} from "../../../src/data/backup";
import { BackupScheduleRecurrence } from "../../../src/data/backup";
@@ -28,7 +27,16 @@ const backupInfo: BackupInfo = {
"backup.local": { size: 1024 * 1024 * 512, protected: true },
"cloud.cloud": { size: 1024 * 1024 * 512, protected: true },
},
} as BackupContent,
failed_agent_ids: [],
failed_addons: [],
failed_folders: [],
extra_metadata: {},
addons: [],
database_included: true,
folders: [],
homeassistant_included: true,
homeassistant_version: "2026.9.0",
},
],
agent_errors: {},
last_attempted_automatic_backup: recent,
+7 -3
View File
@@ -20,12 +20,14 @@ const demoWebhooks: Webhook[] = [
domain: "automation",
name: "Front door motion",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
{
webhook_id: "demo_companion_app",
domain: "mobile_app",
name: "Companion app",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
];
@@ -49,12 +51,13 @@ const demoCloudhooks = Object.fromEntries(
// object directly and reset on reload.
const cloudStatus: CloudStatusLoggedIn = {
logged_in: true,
auto_login: null,
cloud: "connected",
cloud_last_disconnect_reason: null,
email: "demo@home-assistant.io",
google_registered: false,
google_local_connected: false,
google_entities: emptyFilter(),
google_domains: ["light", "switch", "climate", "cover"],
alexa_registered: true,
alexa_entities: emptyFilter(),
remote_domain: "demo-instance.ui.nabu.casa",
@@ -75,10 +78,11 @@ const cloudStatus: CloudStatusLoggedIn = {
alexa_enabled: true,
remote_enabled: true,
remote_allow_remote_enable: true,
strict_connection: "disabled",
google_secure_devices_pin: undefined,
google_secure_devices_pin: null,
cloudhooks: demoCloudhooks,
alexa_default_expose: null,
alexa_report_state: true,
google_default_expose: null,
google_report_state: true,
tts_default_voice: ["en-US", "JennyNeural"],
cloud_ice_servers_enabled: false,
+7 -3
View File
@@ -121,12 +121,14 @@ const demoWebhooks: Webhook[] = [
domain: "automation",
name: "Front door motion",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
{
webhook_id: "demo_companion_app",
domain: "mobile_app",
name: "Companion app",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
];
@@ -155,12 +157,13 @@ const buildCloudStatus = (scenario: CloudDemoScenario): CloudStatusLoggedIn => {
: {};
return {
logged_in: true,
auto_login: null,
cloud: "connected",
cloud_last_disconnect_reason: null,
email: "demo@home-assistant.io",
google_registered: scenario.google,
google_local_connected: false,
google_entities: emptyFilter(),
google_domains: ["light", "switch", "climate", "cover"],
alexa_registered: scenario.alexa,
alexa_entities: emptyFilter(),
remote_domain: "demo-instance.ui.nabu.casa",
@@ -181,10 +184,11 @@ const buildCloudStatus = (scenario: CloudDemoScenario): CloudStatusLoggedIn => {
alexa_enabled: scenario.alexa,
remote_enabled: scenario.remote,
remote_allow_remote_enable: true,
strict_connection: "disabled",
google_secure_devices_pin: undefined,
google_secure_devices_pin: null,
cloudhooks,
alexa_default_expose: null,
alexa_report_state: true,
google_default_expose: null,
google_report_state: true,
tts_default_voice: ["en-US", "JennyNeural"],
cloud_ice_servers_enabled: scenario.webrtc,
+2 -2
View File
@@ -2,7 +2,7 @@ import type { HomeAssistant } from "../types";
export interface AlexaEntity {
entity_id: string;
display_categories: string[];
display_categories: string[] | null;
interfaces: string[];
}
@@ -10,7 +10,7 @@ export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<AlexaEntity>({
hass.callWS<undefined>({
type: "cloud/alexa/entities/get",
entity_id,
});
+20 -15
View File
@@ -47,7 +47,7 @@ export interface BackupConfig {
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
next_automatic_backup_additional?: boolean;
next_automatic_backup_additional: boolean;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
@@ -60,7 +60,7 @@ export interface BackupConfig {
retention: Retention;
schedule: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
time: string | null;
days: BackupDay[];
};
agents: BackupAgentsConfig;
@@ -79,9 +79,9 @@ export interface BackupMutableConfig {
};
retention?: Retention;
schedule?: {
recurrence: BackupScheduleRecurrence;
recurrence?: BackupScheduleRecurrence;
time?: string | null;
days?: BackupDay[] | null;
days?: BackupDay[];
};
agents?: BackupAgentsConfig;
}
@@ -114,21 +114,22 @@ export interface BackupContent {
date: string;
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: {
failed_agent_ids: string[];
failed_addons: AddonInfo[];
failed_folders: string[];
extra_metadata: {
"supervisor.addon_update"?: string;
"supervisor.app_update"?: string;
[key: string]: string | boolean | undefined;
};
with_automatic_settings: boolean;
with_automatic_settings: boolean | null;
}
export interface BackupData {
addons: BackupAddon[];
database_included: boolean;
folders: string[];
homeassistant_version: string;
homeassistant_version: string | null;
homeassistant_included: boolean;
}
@@ -141,7 +142,7 @@ export interface BackupAddon {
export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
backups: BackupContentExtended[];
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
@@ -152,6 +153,7 @@ export interface BackupInfo {
}
export interface BackupDetails {
agent_errors: Record<string, string>;
backup: BackupContentExtended;
}
@@ -232,7 +234,10 @@ export const fetchBackupAgentsInfo = (
type: "backup/agents/info",
});
export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
export const deleteBackup = (
hass: HomeAssistant,
id: string
): Promise<{ agent_errors: Record<string, string> }> =>
hass.callWS({
type: "backup/delete",
backup_id: id,
@@ -241,7 +246,7 @@ export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
export const generateBackup = (
hass: HomeAssistant,
params: GenerateBackupParams
): Promise<{ backup_id: string }> =>
): Promise<{ backup_job_id: string }> =>
hass.callWS({
type: "backup/generate",
...params,
@@ -249,7 +254,7 @@ export const generateBackup = (
export const generateBackupWithAutomaticSettings = (
hass: HomeAssistant
): Promise<void> =>
): Promise<{ backup_job_id: string }> =>
hass.callWS({
type: "backup/generate_with_automatic_settings",
});
@@ -257,7 +262,7 @@ export const generateBackupWithAutomaticSettings = (
export const restoreBackup = (
hass: HomeAssistant,
params: RestoreBackupParams
): Promise<{ backup_id: string }> =>
): Promise<void> =>
hass.callWS({
type: "backup/restore",
...params,
+16 -4
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
export type BackupManagerState =
"idle" | "create_backup" | "receive_backup" | "restore_backup";
"idle" | "create_backup" | "receive_backup" | "restore_backup" | "blocked";
export type CreateBackupStage =
| "addon_repositories"
@@ -39,7 +39,8 @@ export type RestoreBackupStage =
| "remove_delta_addons"
| "remove_delta_apps";
export type RestoreBackupState = "completed" | "failed" | "in_progress";
export type RestoreBackupState =
"completed" | "core_restart" | "failed" | "in_progress";
interface IdleEvent {
manager_state: "idle";
@@ -47,22 +48,29 @@ interface IdleEvent {
interface CreateBackupEvent {
manager_state: "create_backup";
reason: string | null;
stage: CreateBackupStage | null;
state: CreateBackupState;
}
interface ReceiveBackupEvent {
manager_state: "receive_backup";
reason: string | null;
stage: ReceiveBackupStage | null;
state: ReceiveBackupState;
}
interface RestoreBackupEvent {
manager_state: "restore_backup";
reason: string | null;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
}
interface BlockedEvent {
manager_state: "blocked";
}
export interface UploadBackupEvent {
manager_state: BackupManagerState;
agent_id: string;
@@ -71,10 +79,14 @@ export interface UploadBackupEvent {
}
export type ManagerState =
"idle" | "create_backup" | "receive_backup" | "restore_backup";
"idle" | "create_backup" | "receive_backup" | "restore_backup" | "blocked";
export type ManagerStateEvent =
IdleEvent | CreateBackupEvent | ReceiveBackupEvent | RestoreBackupEvent;
| IdleEvent
| CreateBackupEvent
| ReceiveBackupEvent
| RestoreBackupEvent
| BlockedEvent;
export type BackupSubscriptionEvent = ManagerStateEvent | UploadBackupEvent;
+1 -1
View File
@@ -8,7 +8,7 @@ import type {
export interface BackupOnboardingInfo {
state: BackupManagerState;
last_action_event?: {
last_action_event: {
manager_state: BackupManagerState;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
+8 -9
View File
@@ -1,8 +1,6 @@
import type { EntityDomainFilter } from "../common/entity/entity_domain_filter";
import type { HomeAssistant } from "../types";
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
export interface CloudAutoLogin {
email: string;
failed: string | null;
@@ -27,10 +25,11 @@ export interface CloudPreferences {
alexa_enabled: boolean;
remote_enabled: boolean;
remote_allow_remote_enable: boolean;
strict_connection: StrictConnectionMode;
google_secure_devices_pin: string | undefined;
google_secure_devices_pin: string | null;
cloudhooks: Record<string, CloudWebhook>;
alexa_default_expose: string[] | null;
alexa_report_state: boolean;
google_default_expose: string[] | null;
google_report_state: boolean;
tts_default_voice: [string, string];
cloud_ice_servers_enabled: boolean;
@@ -43,18 +42,19 @@ export type RemoteCertificateStatus =
export interface CloudStatusLoggedIn {
logged_in: true;
auto_login: null;
cloud: "disconnected" | "connecting" | "connected";
cloud_last_disconnect_reason: { clean: boolean; reason: string } | null;
email: string;
google_registered: boolean;
google_local_connected: boolean;
google_entities: EntityDomainFilter;
google_domains: string[];
alexa_registered: boolean;
alexa_entities: EntityDomainFilter;
prefs: CloudPreferences;
remote_domain: string | undefined;
remote_domain: string | null;
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
remote_certificate: CertificateInformation | null;
remote_certificate_status: RemoteCertificateStatus | null;
http_use_ssl: boolean;
active_subscription: boolean;
@@ -115,7 +115,7 @@ export interface CloudLoginMFA extends CloudLoginBase {
export type CloudEvent =
| { type: "login" | "logout" | "auto_login_cancelled" }
| { type: "auto_login_failed"; translation_key: string };
| { type: "auto_login_failed"; translation_key: string | null };
export const cloudLogin = ({
hass,
@@ -196,7 +196,6 @@ export const updateCloudPref = (
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
strict_connection?: CloudPreferences["strict_connection"];
cloud_ice_servers_enabled?: CloudPreferences["cloud_ice_servers_enabled"];
}
) =>
+1 -1
View File
@@ -4,7 +4,7 @@ export interface GoogleEntity {
entity_id: string;
traits: string[];
might_2fa: boolean;
disable_2fa?: boolean;
disable_2fa?: boolean | null;
}
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
+7 -3
View File
@@ -1,11 +1,11 @@
import type { HomeAssistant } from "../types";
import { handleFetchPromise } from "../util/hass-call-api";
import type { CloudStatus } from "./cloud";
export interface InstallationType {
installation_type:
| "Home Assistant Operating System"
| "Home Assistant OS"
| "Home Assistant Container"
| "Unsupported Third Party Container"
| "Home Assistant Supervised"
| "Home Assistant Core"
| "Unknown";
@@ -49,6 +49,10 @@ export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}
export interface OnboardingCloudStatus {
logged_in: boolean;
}
export const fetchOnboardingOverview = () =>
fetch(`${__HASS_URL__}/api/onboarding`, { credentials: "same-origin" });
@@ -115,7 +119,7 @@ export const loginHaCloud = async (
})
);
export const fetchHaCloudStatus = async (): Promise<CloudStatus> =>
export const fetchHaCloudStatus = async (): Promise<OnboardingCloudStatus> =>
handleFetchPromise(fetch("/api/onboarding/cloud/status"));
export const signOutHaCloud = async () =>
+1
View File
@@ -5,6 +5,7 @@ export interface Webhook {
domain: string;
name: string;
local_only: boolean;
allowed_methods: string[];
}
export interface WebhookError {
code: number;
+2 -2
View File
@@ -13,11 +13,11 @@ import {
type BackupOnboardingConfig,
type BackupOnboardingInfo,
} from "../data/backup_onboarding";
import type { CloudStatus } from "../data/cloud";
import {
fetchHaCloudStatus,
signOutHaCloud,
waitForIntegration,
type OnboardingCloudStatus,
} from "../data/onboarding";
import { showToast } from "../util/toast";
import "./onboarding-loading";
@@ -53,7 +53,7 @@ class OnboardingRestoreBackup extends LitElement {
@state() private _failed?: boolean;
@state() private _cloudStatus?: CloudStatus;
@state() private _cloudStatus?: OnboardingCloudStatus;
@storage({
key: "onboarding-restore-backup-backup-id",
@@ -101,7 +101,10 @@ class HaBackupConfigSchedule extends LitElement {
...this.value,
schedule: {
recurrence: data.recurrence,
time: data.time_option === BackupScheduleTime.CUSTOM ? data.time : null,
time:
data.time_option === BackupScheduleTime.CUSTOM
? (data.time ?? null)
: null,
days:
data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? data.days
@@ -28,7 +28,7 @@ import "./ha-backup-formfield-label";
interface CheckBoxItem {
label: string;
id: string;
version?: string;
version?: string | null;
}
const ITEM_ICONS = {
@@ -93,7 +93,7 @@ export class HaBackupOverviewProgress extends LitElement {
private get _heading() {
const managerState = this.manager.manager_state;
if (managerState === "idle") {
if (managerState === "idle" || managerState === "blocked") {
return "";
}
return this.hass.localize(
@@ -102,7 +102,10 @@ export class HaBackupOverviewProgress extends LitElement {
}
private get _isUploadStage(): boolean {
if (this.manager.manager_state === "idle") {
if (
this.manager.manager_state === "idle" ||
this.manager.manager_state === "blocked"
) {
return false;
}
return this.manager.stage === "upload_to_agents";
@@ -39,7 +39,7 @@ export class CloudRemotePref extends LitElement {
return nothing;
}
const { remote_enabled, remote_allow_remote_enable, strict_connection } =
const { remote_enabled, remote_allow_remote_enable } =
this.cloudStatus.prefs;
const {
@@ -120,17 +120,7 @@ export class CloudRemotePref extends LitElement {
)}
></ha-alert>
`
: strict_connection === "drop_connection"
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
`ui.panel.config.cloud.account.remote.drop_connection_warning_title`
)}
>${this.hass.localize(
`ui.panel.config.cloud.account.remote.drop_connection_warning`
)}</ha-alert
>`
: nothing
: nothing
}
<p>
${this.hass.localize(
-2
View File
@@ -6837,8 +6837,6 @@
"security_options": "Security options",
"external_activation": "Allow external activation of remote access",
"external_activation_secondary": "If you disable remote access on this page, having this setting enabled allows you to reactivate it remotely via your Nabu Casa account.",
"drop_connection_warning_title": "Remote login has been deactivated",
"drop_connection_warning": "The below security options may make it appear the system is not running.",
"certificate_info": "Certificate information",
"certificate_expire": "Certificate renewal at {date}",
"more_info": "More details",
+6 -1
View File
@@ -19,6 +19,7 @@ const makeConfig = (overrides: Partial<BackupConfig> = {}): BackupConfig =>
last_attempted_automatic_backup: null,
last_completed_automatic_backup: null,
next_automatic_backup: null,
next_automatic_backup_additional: false,
create_backup: {
agent_ids: [CLOUD_AGENT],
include_addons: null,
@@ -29,7 +30,11 @@ const makeConfig = (overrides: Partial<BackupConfig> = {}): BackupConfig =>
password: null,
},
retention: {},
schedule: { recurrence: BackupScheduleRecurrence.DAILY, days: [] },
schedule: {
recurrence: BackupScheduleRecurrence.DAILY,
time: null,
days: [],
},
agents: {},
...overrides,
}) satisfies BackupConfig;
@@ -19,6 +19,7 @@ const makeConfig = (overrides: Partial<BackupConfig> = {}): BackupConfig =>
last_attempted_automatic_backup: null,
last_completed_automatic_backup: null,
next_automatic_backup: null,
next_automatic_backup_additional: false,
create_backup: {
agent_ids: [CLOUD_AGENT],
include_addons: null,
@@ -29,7 +30,11 @@ const makeConfig = (overrides: Partial<BackupConfig> = {}): BackupConfig =>
password: null,
},
retention: {},
schedule: { recurrence: BackupScheduleRecurrence.DAILY, days: [] },
schedule: {
recurrence: BackupScheduleRecurrence.DAILY,
time: null,
days: [],
},
agents: {},
...overrides,
}) satisfies BackupConfig;
@@ -48,10 +53,11 @@ const makePrefs = (
alexa_enabled: true,
remote_enabled: true,
remote_allow_remote_enable: true,
strict_connection: "disabled",
google_secure_devices_pin: undefined,
google_secure_devices_pin: null,
cloudhooks: {},
alexa_default_expose: null,
alexa_report_state: true,
google_default_expose: null,
google_report_state: true,
tts_default_voice: ["en-US", "JennyNeural"],
cloud_ice_servers_enabled: true,
@@ -65,18 +71,19 @@ const makeStatus = (
overrides: Partial<CloudStatusLoggedIn> = {}
): CloudStatusLoggedIn => ({
logged_in: true,
auto_login: null,
cloud: "connected",
cloud_last_disconnect_reason: null,
email: "test@home-assistant.io",
google_registered: true,
google_local_connected: false,
google_entities: emptyFilter(),
google_domains: [],
alexa_registered: true,
alexa_entities: emptyFilter(),
prefs: makePrefs(prefsOverrides),
remote_domain: undefined,
remote_domain: null,
remote_connected: true,
remote_certificate: undefined,
remote_certificate: null,
remote_certificate_status: "ready",
http_use_ssl: false,
active_subscription: true,