Compare commits

...

3 Commits

Author SHA1 Message Date
Petar Petrov 05588d2dc8 Drop pending banner and pending handling from HTTP form
The popup blocks any other interaction while pending exists, so the form
is only reachable when pending is null. Stop fetching/displaying the
pending config and the unconfirmed-config banner.
2026-06-05 19:13:35 +03:00
Petar Petrov 6ff09c95e4 HTTP config: prompt admin to confirm or revert pending changes after restart
After saving HTTP server settings, core now writes the new config as pending,
restarts Home Assistant, and keeps the previous stable config as a recovery
fallback. The frontend now:

- Reads {stable, pending} from http/config and adds a promoteHttpConfig call.
- Asks the admin to confirm before saving (since saving now auto-restarts).
- After reconnect, shows a non-dismissable popup for admins with the changed
  fields, where Confirm promotes pending -> stable and Revert clears pending
  (which triggers another restart).
- Renders an info banner above the form whenever pending is set.
2026-06-05 18:57:54 +03:00
Petar Petrov d99526ff60 Add HTTP server settings to the network panel (#51981)
* Fix focus loss in ha-input-multi when items change

* Add HTTP server settings to the network panel

* Surface fetch errors and validate before saving in HTTP config form

* Update src/panels/config/network/ha-config-http-form.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* Group HTTP form fields into collapsible sections

* Add bottom margin to ha-input-multi add button

* Only apply add-button margin when helper text is present

* Update HTTP config form to new WebSocket API

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-06-04 13:06:14 +03:00
10 changed files with 762 additions and 3 deletions
+5 -2
View File
@@ -80,7 +80,7 @@ class HaInputMulti extends LitElement {
<div class="items">
${repeat(
this._items,
(item, index) => `${item}-${index}`,
(_item, index) => index,
(item, index) => {
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
return html`
@@ -128,7 +128,7 @@ class HaInputMulti extends LitElement {
)}
</div>
</ha-sortable>
<div class="layout horizontal">
<div class="layout horizontal add-row">
<ha-button
size="small"
appearance="filled"
@@ -217,6 +217,9 @@ class HaInputMulti extends LitElement {
margin-bottom: 8px;
--ha-input-padding-bottom: 0;
}
.add-row:has(+ ha-input-helper-text) {
margin-bottom: var(--ha-space-1);
}
ha-icon-button {
display: block;
}
+40
View File
@@ -0,0 +1,40 @@
import type { HomeAssistant } from "../types";
export interface HttpConfig {
server_host?: string[];
server_port?: number;
ssl_certificate?: string;
ssl_peer_certificate?: string;
ssl_key?: string;
cors_allowed_origins?: string[];
use_x_forwarded_for?: boolean;
trusted_proxies?: string[];
use_x_frame_options?: boolean;
ip_ban_enabled?: boolean;
login_attempts_threshold?: number;
ssl_profile?: "modern" | "intermediate";
}
export interface HttpConfigState {
stable: HttpConfig;
pending: HttpConfig | null;
}
export interface SaveHttpConfigResult {
restart: boolean;
}
export const fetchHttpConfig = (hass: HomeAssistant) =>
hass.callWS<HttpConfigState>({ type: "http/config" });
export const saveHttpConfig = (
hass: HomeAssistant,
config: HttpConfig | null
) =>
hass.callWS<SaveHttpConfigResult>({
type: "http/config/configure",
config,
});
export const promoteHttpConfig = (hass: HomeAssistant) =>
hass.callWS<undefined>({ type: "http/config/promote" });
@@ -0,0 +1,229 @@
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog";
import type { HttpConfig } from "../../data/http";
import { promoteHttpConfig, saveHttpConfig } from "../../data/http";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { HttpPendingConfigDialogParams } from "./show-dialog-http-pending-config";
const HTTP_FIELDS: (keyof HttpConfig)[] = [
"server_port",
"server_host",
"ssl_certificate",
"ssl_key",
"ssl_peer_certificate",
"ssl_profile",
"cors_allowed_origins",
"use_x_forwarded_for",
"trusted_proxies",
"use_x_frame_options",
"ip_ban_enabled",
"login_attempts_threshold",
];
@customElement("dialog-http-pending-config")
export class DialogHttpPendingConfig
extends LitElement
implements HassDialog<HttpPendingConfigDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: HttpPendingConfigDialogParams;
@state() private _open = false;
@state() private _busy: "confirm" | "revert" | undefined;
@state() private _error?: string;
public showDialog(params: HttpPendingConfigDialogParams): void {
this._params = params;
this._open = true;
this._busy = undefined;
this._error = undefined;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._busy = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private get _changedFields(): (keyof HttpConfig)[] {
if (!this._params?.state.pending) {
return [];
}
const { stable, pending } = this._params.state;
return HTTP_FIELDS.filter(
(key) => JSON.stringify(stable[key]) !== JSON.stringify(pending[key])
);
}
protected render() {
if (!this._params) {
return nothing;
}
const changes = this._changedFields;
return html`
<ha-dialog
.open=${this._open}
.headerTitle=${this.hass.localize(
"ui.dialogs.http_pending_config.title"
)}
prevent-scrim-close
width="medium"
@closed=${this._dialogClosed}
>
<span slot="headerNavigationIcon"></span>
<div class="content">
<p>
${this.hass.localize("ui.dialogs.http_pending_config.description")}
</p>
${changes.length
? html`
<p class="changes-label">
${this.hass.localize(
"ui.dialogs.http_pending_config.changes_label"
)}
</p>
<ul>
${changes.map(
(key) => html`
<li>
${this.hass.localize(
`ui.panel.config.network.http.fields.${key}` as any
)}
</li>
`
)}
</ul>
`
: nothing}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
.loading=${this._busy === "revert"}
.disabled=${this._busy === "confirm"}
@click=${this._revert}
>
${this.hass.localize("ui.dialogs.http_pending_config.revert")}
</ha-button>
<ha-button
slot="primaryAction"
.loading=${this._busy === "confirm"}
.disabled=${this._busy === "revert"}
@click=${this._confirm}
>
${this.hass.localize("ui.dialogs.http_pending_config.confirm")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private async _confirm(): Promise<void> {
if (this._busy) {
return;
}
this._busy = "confirm";
this._error = undefined;
try {
await promoteHttpConfig(this.hass);
this._notifyResolved();
this._open = false;
} catch (err: any) {
this._error = this.hass.localize(
"ui.dialogs.http_pending_config.confirm_error",
{ error: err.message ?? "" }
);
this._busy = undefined;
}
}
private async _revert(): Promise<void> {
if (this._busy || !this._params) {
return;
}
this._busy = "revert";
this._error = undefined;
try {
await saveHttpConfig(this.hass, null);
this._notifyResolved();
this._open = false;
} catch (err: any) {
// The restart triggered by clearing pending may cut the WS connection
// before we get a reply. The disconnected overlay takes over from here.
if (err?.error?.code === ERR_CONNECTION_LOST) {
this._notifyResolved();
this._open = false;
return;
}
this._error = this.hass.localize(
"ui.dialogs.http_pending_config.revert_error",
{ error: err.message ?? "" }
);
this._busy = undefined;
}
}
private _notifyResolved(): void {
this._params?.onResolved?.();
// The form on Settings > System > Network may be mounted and showing
// stale state; let it know to refetch.
window.dispatchEvent(new Event("http-config-resolved"));
}
static styles = [
haStyleDialog,
css`
.content {
line-height: var(--ha-line-height-normal);
}
p {
margin: 0 0 var(--ha-space-4) 0;
}
.changes-label {
font-weight: var(--ha-font-weight-medium);
margin-bottom: var(--ha-space-2);
}
ul {
margin: 0 0 var(--ha-space-4) 0;
padding-left: var(--ha-space-6);
color: var(--secondary-text-color);
}
li {
margin-bottom: var(--ha-space-1);
}
ha-alert {
display: block;
margin-top: var(--ha-space-4);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-http-pending-config": DialogHttpPendingConfig;
}
}
@@ -0,0 +1,22 @@
import { fireEvent } from "../../common/dom/fire_event";
import type { HttpConfigState } from "../../data/http";
export interface HttpPendingConfigDialogParams {
state: HttpConfigState;
onResolved?: () => void;
}
export const loadHttpPendingConfigDialog = () =>
import("./dialog-http-pending-config");
export const showHttpPendingConfigDialog = (
element: HTMLElement,
dialogParams: HttpPendingConfigDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-http-pending-config",
dialogImport: loadHttpPendingConfigDialog,
dialogParams,
addHistory: false,
});
};
+42 -1
View File
@@ -5,9 +5,12 @@ import { customElement, state } from "lit/decorators";
import { storage } from "../common/decorators/storage";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate";
import { fetchHttpConfig } from "../data/http";
import type { HttpConfigState } from "../data/http";
import type { WindowWithPreloads } from "../data/preloads";
import type { RecorderInfo } from "../data/recorder";
import { getRecorderInfo } from "../data/recorder";
import { showHttpPendingConfigDialog } from "../dialogs/http-pending-config/show-dialog-http-pending-config";
import "../resources/custom-card-support";
import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin";
@@ -39,6 +42,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
@state() private _databaseMigration?: boolean;
private _httpPendingDialogOpen = false;
private _panelUrl: string;
@storage({ key: "ha-version", state: false, subscribe: false })
@@ -70,14 +75,24 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
protected willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this._databaseMigration === undefined &&
changedProps.has("hass") &&
this.hass?.config &&
changedProps.get("hass")?.config !== this.hass?.config
oldHass?.config !== this.hass.config
) {
this.checkDataBaseMigration();
}
// Wait for `hass.user` to populate so the admin guard can run; it arrives
// asynchronously after `hass.config`.
if (
changedProps.has("hass") &&
this.hass?.user &&
oldHass?.user !== this.hass.user
) {
this.checkHttpPendingConfig();
}
}
protected update(changedProps: PropertyValues<this>) {
@@ -208,6 +223,32 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
}
}
protected async checkHttpPendingConfig() {
if (__DEMO__ || this._httpPendingDialogOpen) {
return;
}
if (!this.hass?.user?.is_admin) {
return;
}
let httpConfig: HttpConfigState;
try {
httpConfig = await fetchHttpConfig(this.hass);
} catch (_err) {
// The check re-runs on the next reconnect; ignore transient failures.
return;
}
if (!httpConfig.pending || this._httpPendingDialogOpen) {
return;
}
this._httpPendingDialogOpen = true;
showHttpPendingConfigDialog(this, {
state: httpConfig,
onResolved: () => {
this._httpPendingDialogOpen = false;
},
});
}
protected async checkDataBaseMigration() {
if (__DEMO__) {
this._databaseMigration = false;
@@ -0,0 +1,361 @@
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-form/ha-form";
import type { HaForm } from "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
import type { HttpConfig } from "../../../data/http";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
const SCHEMA = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "server_port",
required: true,
selector: { number: { min: 1, max: 65535, mode: "box" } },
},
{
name: "server_host",
selector: { text: { multiple: true } },
},
{
name: "ssl",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.ssl"),
schema: [
{
name: "ssl_certificate",
selector: { text: {} },
},
{
name: "ssl_key",
selector: { text: {} },
},
{
name: "ssl_peer_certificate",
selector: { text: {} },
},
{
name: "ssl_profile",
selector: {
select: {
options: [
{
value: "modern",
label: localize(
"ui.panel.config.network.http.ssl_profile_modern"
),
},
{
value: "intermediate",
label: localize(
"ui.panel.config.network.http.ssl_profile_intermediate"
),
},
],
},
},
},
],
},
{
name: "reverse_proxy",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.reverse_proxy"),
schema: [
{
name: "use_x_forwarded_for",
selector: { boolean: {} },
},
{
name: "trusted_proxies",
selector: { text: { multiple: true } },
},
],
},
{
name: "ip_banning",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.ip_banning"),
schema: [
{
name: "ip_ban_enabled",
selector: { boolean: {} },
},
{
name: "login_attempts_threshold",
required: true,
selector: { number: { min: -1, max: 1000, mode: "box" } },
},
],
},
{
name: "advanced",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.advanced"),
schema: [
{
name: "cors_allowed_origins",
selector: { text: { multiple: true } },
},
{
name: "use_x_frame_options",
selector: { boolean: {} },
},
],
},
] as const
);
@customElement("ha-config-http-form")
class HaConfigHttpForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _stable?: HttpConfig;
@state() private _config?: HttpConfig;
@state() private _error?: string;
@state() private _fieldErrors: Record<string, string> = {};
@state() private _saving = false;
@state() private _showNoChanges = false;
@query("ha-form") private _form?: HaForm;
@query("ha-alert") private _firstAlert?: HTMLElement;
private _onConfigResolved = () => this._fetchConfig();
protected override firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._fetchConfig();
}
public override connectedCallback() {
super.connectedCallback();
window.addEventListener("http-config-resolved", this._onConfigResolved);
}
public override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("http-config-resolved", this._onConfigResolved);
}
protected render() {
if (!this._stable && !this._error) {
return nothing;
}
const schema = SCHEMA(this.hass.localize);
return html`
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.network.http.caption")}
>
<div class="card-content">
<p class="description">
${this.hass.localize("ui.panel.config.network.http.description")}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${this._showNoChanges
? html`
<ha-alert alert-type="success">
${this.hass.localize(
"ui.panel.config.network.http.save_no_changes"
)}
</ha-alert>
`
: nothing}
${this._config
? html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.error=${this._fieldErrors}
.disabled=${this._saving}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
`
: nothing}
</div>
${this._config
? html`
<div class="card-actions">
<ha-button
@click=${this._save}
.disabled=${this._saving}
.loading=${this._saving}
>
${this.hass.localize("ui.panel.config.network.http.save")}
</ha-button>
</div>
`
: nothing}
</ha-card>
`;
}
private async _fetchConfig(): Promise<void> {
try {
// Pending is exclusively handled by the global confirm/revert dialog, so
// the form only ever displays stable.
const { stable } = await fetchHttpConfig(this.hass);
this._stable = stable;
this._config = { ...stable };
} catch (err: any) {
this._error = err.message;
}
}
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string => {
if ("type" in schema && schema.type === "expandable") {
// Expandable sections render their own title; never label them.
return "";
}
return this.hass.localize(
`ui.panel.config.network.http.fields.${schema.name}` as any
);
};
private _computeHelper = (
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string => {
if ("type" in schema && schema.type === "expandable") {
return "";
}
return (
this.hass.localize(
`ui.panel.config.network.http.helpers.${schema.name}` as any
) || ""
);
};
private _valueChanged(ev: CustomEvent): void {
this._config = ev.detail.value;
this._error = undefined;
this._fieldErrors = {};
this._showNoChanges = false;
}
private async _save(): Promise<void> {
if (!this._config || !this._stable) {
return;
}
if (this._form && !this._form.reportValidity()) {
return;
}
if (JSON.stringify(this._stable) === JSON.stringify(this._config)) {
this._showNoChanges = true;
return;
}
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.network.http.save_confirm.title"
),
text: this.hass.localize(
"ui.panel.config.network.http.save_confirm.text"
),
confirmText: this.hass.localize(
"ui.panel.config.network.http.save_confirm.confirm"
),
});
if (!confirmed) {
return;
}
this._saving = true;
this._error = undefined;
this._fieldErrors = {};
this._showNoChanges = false;
try {
const result = await saveHttpConfig(this.hass, this._config);
if (!result.restart) {
this._showNoChanges = true;
}
// restart === true: a restart is in flight. The reply usually races with
// the connection drop; if we do reach this branch, the disconnected
// overlay will appear in moments. Leave the form as is.
} catch (err: any) {
// The restart kills the WS connection before the ack — that's expected.
if (
err?.error?.code === ERR_CONNECTION_LOST ||
err === ERR_CONNECTION_LOST
) {
return;
}
// voluptuous formats errors as "<message> @ data['<field>']".
// If a field is identified, mark it inline; otherwise show a card-level
// alert.
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
if (fieldMatch) {
this._fieldErrors = { [fieldMatch[1]]: err.message };
} else {
this._error = err.message;
}
} finally {
this._saving = false;
}
await this.updateComplete;
await this._form?.updateComplete;
// Inline field errors render inside ha-form's shadow root, so fall back to
// it when no top-level alert is present.
const target =
this._firstAlert ??
this._form?.shadowRoot?.querySelector<HTMLElement>("ha-alert");
target?.scrollIntoView({ behavior: "smooth", block: "center" });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.description {
margin-top: 0;
color: var(--secondary-text-color);
}
ha-alert {
display: block;
margin-bottom: var(--ha-space-4);
}
.card-actions {
display: flex;
gap: var(--ha-space-2);
justify-content: flex-end;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-http-form": HaConfigHttpForm;
}
}
@@ -8,6 +8,7 @@ import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-icon-next";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-http-form";
import "./ha-config-network";
import "./ha-config-url-form";
import "./supervisor-hostname";
@@ -40,6 +41,7 @@ class HaConfigSectionNetwork extends LitElement {
<supervisor-network .hass=${this.hass}></supervisor-network>`
: ""}
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-http-form .hass=${this.hass}></ha-config-http-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
${NETWORK_BROWSERS.some((component) =>
isComponentLoaded(this.hass.config, component)
@@ -88,6 +90,7 @@ class HaConfigSectionNetwork extends LitElement {
supervisor-hostname,
supervisor-network,
ha-config-url-form,
ha-config-http-form,
ha-config-network,
.discovery-card {
display: block;
+1
View File
@@ -352,6 +352,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
}
this._updateHass({ config });
this.checkDataBaseMigration();
this.checkHttpPendingConfig();
});
}
+3
View File
@@ -36,6 +36,9 @@ export class HassBaseEl extends LitElement {
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected checkDataBaseMigration() {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected checkHttpPendingConfig() {}
protected hassChanged(hass, _oldHass) {
this.__provideHass.forEach((el) => {
(el as any).hass = hass;
+56
View File
@@ -1523,6 +1523,15 @@
"title": "Safe mode",
"text": "Home Assistant is running in safe mode, custom integrations and community frontend modules are not available. Restart Home Assistant to exit safe mode."
},
"http_pending_config": {
"title": "Confirm new network configuration",
"description": "Home Assistant has restarted with new HTTP server settings. Confirm to keep them, or revert to undo the change.",
"changes_label": "Changed settings:",
"confirm": "Confirm",
"revert": "Revert",
"confirm_error": "Failed to confirm the new configuration: {error}",
"revert_error": "Failed to revert the configuration: {error}"
},
"quick-bar": {
"commands_title": "Commands",
"navigate_title": "Navigate",
@@ -8285,6 +8294,53 @@
"zeroconf": "Zeroconf browser",
"zeroconf_info": "Show services discovered using mDNS. Does not include services unknown to Home Assistant."
},
"http": {
"caption": "HTTP server",
"description": "Configure how Home Assistant serves its web interface. Saving restarts Home Assistant.",
"save": "Save",
"save_no_changes": "Nothing changed — no restart needed.",
"save_confirm": {
"title": "Restart required",
"text": "Saving will restart Home Assistant to apply the new HTTP settings.",
"confirm": "Save and restart"
},
"ssl_profile_modern": "Modern",
"ssl_profile_intermediate": "Intermediate",
"sections": {
"ssl": "SSL/TLS",
"reverse_proxy": "Reverse proxy",
"ip_banning": "IP banning",
"advanced": "Advanced"
},
"fields": {
"server_port": "Server port",
"server_host": "Listen addresses",
"ssl_certificate": "SSL certificate path",
"ssl_key": "SSL key path",
"ssl_peer_certificate": "SSL peer certificate path",
"ssl_profile": "SSL profile",
"cors_allowed_origins": "CORS allowed origins",
"use_x_forwarded_for": "Trust X-Forwarded-For",
"trusted_proxies": "Trusted proxies",
"use_x_frame_options": "Send X-Frame-Options",
"ip_ban_enabled": "Enable IP banning",
"login_attempts_threshold": "Login attempts before ban"
},
"helpers": {
"server_port": "The port Home Assistant listens on. Default is 8123.",
"server_host": "IP addresses to bind to. Leave empty to listen on all interfaces.",
"ssl_certificate": "Absolute path to your TLS certificate (for example, /ssl/fullchain.pem).",
"ssl_key": "Absolute path to your TLS private key (for example, /ssl/privkey.pem).",
"ssl_peer_certificate": "Absolute path to a client certificate Home Assistant should require for secure connections.",
"ssl_profile": "Mozilla SSL profile. Use 'Intermediate' only if integrations have SSL handshake issues.",
"cors_allowed_origins": "Origins that may make cross-origin requests. Include the scheme, for example https://example.com.",
"use_x_forwarded_for": "Trust the X-Forwarded-For header behind a reverse proxy. Requires the trusted proxies list below.",
"trusted_proxies": "Reverse-proxy IP addresses or CIDR networks allowed to set X-Forwarded-For. Use a network address, not a host.",
"use_x_frame_options": "Send the X-Frame-Options header to help prevent clickjacking.",
"ip_ban_enabled": "Automatically ban IP addresses after repeated failed logins.",
"login_attempts_threshold": "Failed login attempts before an IP is banned. Set to -1 to disable automatic bans."
}
},
"network_adapter": "Network adapter",
"network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.",
"ip_information": "IP information",