Compare commits

...

6 Commits

Author SHA1 Message Date
Aidan Timson
ba22a12a20 Fix 2026-03-03 14:46:42 +00:00
Aidan Timson
098b54f749 Fix 2026-03-03 14:46:15 +00:00
Aidan Timson
4c6a7091a6 Filtering 2026-03-03 14:46:15 +00:00
Aidan Timson
322cb35526 More types 2026-03-03 14:46:15 +00:00
Aidan Timson
c34f6bea2b Always show 2026-03-03 14:46:15 +00:00
Aidan Timson
41bf0652b0 Setup log classification 2026-03-03 14:46:15 +00:00
5 changed files with 589 additions and 228 deletions

View File

@@ -7,10 +7,23 @@ export type SystemLogLevel =
| "info"
| "debug";
export type SystemLogErrorType =
| "auth"
| "connection"
| "invalid_response"
| "rate_limit"
| "server"
| "slow_setup"
| "timeout"
| "ssl"
| "statistics"
| "dns";
export interface LoggedError {
name: string;
message: [string];
level: SystemLogLevel;
error_type?: SystemLogErrorType;
source: [string, number];
exception: string;
count: number;

View File

@@ -110,6 +110,13 @@ class DialogSystemLogDetail extends LitElement {
${item.name}<br />
${this.hass.localize("ui.panel.config.logs.detail.source")}:
${item.source.join(":")}
<br />
${this.hass.localize("ui.panel.config.logs.classification")}:
${item.error_type
? this.hass.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)
: this.hass.localize("ui.panel.config.logs.other")}
${integration
? html`
<br />

View File

@@ -1,10 +1,16 @@
import {
mdiDotsVertical,
mdiChevronDown,
mdiChip,
mdiDns,
mdiDownload,
mdiFilterVariant,
mdiFilterVariantRemove,
mdiPackageVariant,
mdiPuzzle,
mdiRadar,
mdiRefresh,
mdiText,
mdiVolumeHigh,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
@@ -17,10 +23,14 @@ import { navigate } from "../../../common/navigate";
import { stringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-button";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-generic-picker";
import "../../../components/ha-icon-button";
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/search-input";
import "../../../components/search-input-outlined";
import type { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
@@ -28,6 +38,7 @@ import "../../../layouts/hass-subpage";
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "./error-log-card";
import "./system-log-card";
import type { SystemLogCard } from "./system-log-card";
@@ -81,13 +92,9 @@ export class HaConfigLogs extends LitElement {
@state() private _logProviders = logProviders;
public connectedCallback() {
super.connectedCallback();
const systemLog = this.systemLog;
if (systemLog && systemLog.loaded) {
systemLog.fetchData();
}
}
@state() private _showSystemLogFilters = false;
@state() private _systemLogFiltersCount = 0;
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
@@ -98,37 +105,140 @@ export class HaConfigLogs extends LitElement {
this._filter = ev.detail.value;
}
protected render(): TemplateResult {
const search = this.narrow
? html`
<div slot="header">
<search-input
class="header"
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`
: html`
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`;
private _toggleSystemLogFilters = () => {
this._showSystemLogFilters = !this._showSystemLogFilters;
};
private _handleSystemLogFiltersChanged(ev: CustomEvent) {
this._showSystemLogFilters = ev.detail.open;
this._systemLogFiltersCount = ev.detail.count;
}
private _downloadSystemLog = () => {
this.systemLog?.downloadLogs();
};
private _refreshSystemLog = () => {
this.systemLog?.fetchData();
};
private _clearSystemLog = () => {
this.systemLog?.clearLogs();
};
private _clearSystemLogFilters = () => {
this.systemLog?.clearFilters();
};
private _handleSystemLogOverflowAction(ev: HaDropdownSelectEvent): void {
if (ev.detail.item.value === "show-full-logs") {
this._showDetail();
}
}
protected render(): TemplateResult {
const showSystemLog = this._selectedLogProvider === "core" && !this._detail;
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
const header =
selectedProvider?.primary ||
this.hass.localize("ui.panel.config.logs.caption");
const searchRow = html`
<div
class="search-row ${showSystemLog
? "with-filters"
: ""} ${showSystemLog && this._showSystemLogFilters && !this.narrow
? "with-pane"
: ""}"
>
${showSystemLog
? this._showSystemLogFilters && !this.narrow
? html`
<div class="filter-controls">
<div class="relative filter-button">
<ha-assist-chip
.label=${this.hass.localize(
"ui.components.subpage-data-table.filters"
)}
active
@click=${this._toggleSystemLogFilters}
>
<ha-svg-icon
slot="icon"
.path=${mdiFilterVariant}
></ha-svg-icon>
</ha-assist-chip>
${this._systemLogFiltersCount
? html`<div class="badge">
${this._systemLogFiltersCount}
</div>`
: nothing}
</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
.label=${this.hass.localize(
"ui.components.subpage-data-table.clear_filter"
)}
.disabled=${!this._systemLogFiltersCount}
@click=${this._clearSystemLogFilters}
></ha-icon-button>
</div>
`
: html`
<div class="relative filter-button">
<ha-assist-chip
.label=${this.hass.localize(
"ui.components.subpage-data-table.filters"
)}
.active=${this._showSystemLogFilters ||
Boolean(this._systemLogFiltersCount)}
@click=${this._toggleSystemLogFilters}
>
<ha-svg-icon
slot="icon"
.path=${mdiFilterVariant}
></ha-svg-icon>
</ha-assist-chip>
${this._systemLogFiltersCount
? html`<div class="badge">
${this._systemLogFiltersCount}
</div>`
: nothing}
</div>
`
: nothing}
<search-input-outlined
class="search-input"
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
@value-changed=${this._filterChanged}
></search-input-outlined>
${showSystemLog
? html`
<ha-assist-chip
class="clear-chip"
.label=${this.hass.localize("ui.panel.config.logs.clear")}
.disabled=${!this.systemLog?.hasItems}
@click=${this._clearSystemLog}
></ha-assist-chip>
`
: nothing}
</div>
`;
const search = this.narrow
? html`<div slot="header">${searchRow}</div>`
: searchRow;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.logs.caption")}
.header=${header}
back-path="/config/system"
>
${isComponentLoaded(this.hass, "hassio") && this._logProviders
@@ -164,17 +274,48 @@ export class HaConfigLogs extends LitElement {
</ha-generic-picker>
`
: nothing}
${showSystemLog
? html`
<ha-icon-button
slot="toolbar-icon"
.path=${mdiDownload}
@click=${this._downloadSystemLog}
.label=${this.hass.localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiRefresh}
@click=${this._refreshSystemLog}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<ha-dropdown
slot="toolbar-icon"
@wa-select=${this._handleSystemLogOverflowAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.menu")}
></ha-icon-button>
<ha-dropdown-item value="show-full-logs">
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.show_full_logs")}
</ha-dropdown-item>
</ha-dropdown>
`
: nothing}
${search}
<div class="content">
${this._selectedLogProvider === "core" && !this._detail
${showSystemLog
? html`
<system-log-card
.hass=${this.hass}
.header=${this._logProviders.find(
(p) => p.key === this._selectedLogProvider
)!.name}
.filter=${this._filter}
@switch-log-view=${this._showDetail}
.showFilters=${this._showSystemLogFilters}
@system-log-filters-changed=${this
._handleSystemLogFiltersChanged}
></system-log-card>
`
: html`<error-log-card
@@ -194,6 +335,7 @@ export class HaConfigLogs extends LitElement {
private _showDetail() {
this._detail = !this._detail;
this._showSystemLogFilters = false;
}
private _openPicker(ev: Event) {
@@ -208,6 +350,8 @@ export class HaConfigLogs extends LitElement {
}
this._selectedLogProvider = provider;
this._filter = "";
this._showSystemLogFilters = false;
this._systemLogFiltersCount = 0;
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
}
@@ -342,24 +486,101 @@ export class HaConfigLogs extends LitElement {
-webkit-user-select: initial;
-moz-user-select: initial;
}
.search {
.search-row {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
height: 56px;
width: 100%;
gap: var(--ha-space-4);
padding: 0 var(--ha-space-4);
background: var(--primary-background-color);
border-bottom: 1px solid var(--divider-color);
box-sizing: border-box;
}
search-input {
.search-row.with-pane {
display: grid;
grid-template-columns:
var(--sidepane-width, 250px) minmax(0, 1fr)
auto;
align-items: center;
gap: 0;
padding: 0;
}
.search-row.with-pane .filter-controls {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0;
width: 100%;
height: 100%;
padding: 0 var(--ha-space-4);
border-inline-end: 1px solid var(--divider-color);
box-sizing: border-box;
}
.search-row.with-pane .search-input {
width: 100%;
min-width: 0;
margin-inline-start: var(--ha-space-4);
}
.search-row.with-pane .clear-chip {
justify-self: end;
margin-inline-start: var(--ha-space-4);
margin-inline-end: var(--ha-space-4);
}
search-input-outlined {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
flex: 1;
}
search-input.header {
--mdc-ripple-color: transparant;
margin-left: -16px;
margin-inline-start: -16px;
margin-inline-end: initial;
.relative {
position: relative;
}
.badge {
position: absolute;
top: -4px;
right: -4px;
inset-inline-end: -4px;
inset-inline-start: initial;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0 2px;
color: var(--text-primary-color);
}
.content {
direction: ltr;
height: calc(
100vh -
1px - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px) -
56px
);
overflow: hidden;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.clear-chip {
white-space: nowrap;
}
ha-generic-picker {
--md-list-item-leading-icon-color: var(--ha-color-primary-50);

View File

@@ -1,21 +1,15 @@
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-filter-states";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import { getSignedPath } from "../../../data/auth";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import { domainToName } from "../../../data/integration";
import type { LoggedError } from "../../../data/system_log";
import type { LoggedError, SystemLogErrorType } from "../../../data/system_log";
import {
fetchSystemLog,
getLoggedErrorIntegration,
@@ -25,7 +19,6 @@ import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("system-log-card")
export class SystemLogCard extends LitElement {
@@ -33,15 +26,45 @@ export class SystemLogCard extends LitElement {
@property() public filter = "";
@property() public header?: string;
public loaded = false;
@property({ type: Boolean, attribute: "show-filters" })
public showFilters = false;
@state() private _items?: LoggedError[];
@state() private _levelFilter: string[] = [];
@state() private _errorTypeFilter: (SystemLogErrorType | "unknown")[] = [];
public async fetchData(): Promise<void> {
this._items = undefined;
this._items = await fetchSystemLog(this.hass!);
this._items = await fetchSystemLog(this.hass);
}
public async clearLogs(): Promise<void> {
await this.hass.callService("system_log", "clear");
this._items = [];
}
public async downloadLogs(): Promise<void> {
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getErrorLogDownloadUrl(this.hass);
const logFileName = `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
public get activeFiltersCount(): number {
return this._levelFilter.length + this._errorTypeFilter.length;
}
public get hasItems(): boolean {
return (this._items?.length || 0) > 0;
}
public clearFilters(): void {
this._levelFilter = [];
this._errorTypeFilter = [];
this._notifyFiltersState();
}
private _timestamp(item: LoggedError): string {
@@ -64,8 +87,30 @@ export class SystemLogCard extends LitElement {
}
private _getFilteredItems = memoizeOne(
(localize: LocalizeFunc, items: LoggedError[], filter: string) =>
(
localize: LocalizeFunc,
items: LoggedError[],
filter: string,
levelFilter: string[],
errorTypeFilter: (SystemLogErrorType | "unknown")[]
) =>
items.filter((item: LoggedError) => {
if (levelFilter.length && !levelFilter.includes(item.level)) {
return false;
}
if (errorTypeFilter.length) {
const matchesKnown =
item.error_type !== undefined &&
errorTypeFilter.includes(item.error_type);
const matchesUnknown =
item.error_type === undefined &&
errorTypeFilter.includes("unknown");
if (!matchesKnown && !matchesUnknown) {
return false;
}
}
if (filter) {
const integration = getLoggedErrorIntegration(item);
return (
@@ -74,6 +119,14 @@ export class SystemLogCard extends LitElement {
) ||
item.source[0].toLowerCase().includes(filter) ||
item.name.toLowerCase().includes(filter) ||
(item.error_type &&
(item.error_type.includes(filter) ||
this.hass
.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)
.toLowerCase()
.includes(filter))) ||
(integration &&
domainToName(localize, integration)
.toLowerCase()
@@ -82,203 +135,203 @@ export class SystemLogCard extends LitElement {
this._multipleMessages(item).toLowerCase().includes(filter)
);
}
return item;
})
);
protected render() {
const filteredItems = this._items
? this._getFilteredItems(
this.hass.localize,
this._items,
this.filter.toLowerCase()
)
: [];
if (this._items === undefined) {
return html`
<div class="loading-container">
<ha-spinner></ha-spinner>
</div>
`;
}
const filteredItems = this._getFilteredItems(
this.hass.localize,
this._items,
this.filter.toLowerCase(),
this._levelFilter,
this._errorTypeFilter
);
const levels = [...new Set(this._items.map((item) => item.level))];
const errorTypes = [
...new Set(
this._items
.map((item) => item.error_type)
.filter((type): type is SystemLogErrorType => Boolean(type))
),
];
const integrations = filteredItems.length
? filteredItems.map((item) => getLoggedErrorIntegration(item))
: [];
return html`
<div class="system-log-intro">
<ha-card outlined>
${this._items === undefined
? html`
<div class="loading-container">
<ha-spinner></ha-spinner>
</div>
`
: html`
<div class="header">
<h1 class="card-header">${this.header || "Logs"}</h1>
<div class="header-buttons">
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadLogs}
.label=${this.hass.localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
<ha-icon-button
.path=${mdiRefresh}
@click=${this.fetchData}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.menu")}
></ha-icon-button>
<ha-dropdown-item value="show-full-logs">
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.logs.show_full_logs"
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
${this._items.length === 0
? html`
<div class="card-content empty-content">
${this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
const hasActiveFilters = this.activeFiltersCount > 0;
const listContent =
this._items.length === 0
? html`
<div class="card-content empty-content">
${this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
`
: filteredItems.length === 0 && (this.filter || hasActiveFilters)
? html`
<div class="card-content">
${this.filter
? this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{
term: this.filter,
}
)
: this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
`
: html`
<div class="list-wrapper">
<ha-list>
${filteredItems.map(
(item, idx) => html`
<ha-list-item
@click=${this._openLog}
.logItem=${item}
twoline
>
${item.message[0]}
<span slot="secondary" class="secondary">
${this._timestamp(item)}
${html`(<span class=${item.level}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level}`
)}</span
>) `}
${item.error_type
? html`(<span class="error-type-text"
>${this.hass.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)}</span
>) `
: nothing}
${integrations[idx]
? `${domainToName(
this.hass.localize,
integrations[idx]!
)}${
isCustomIntegrationError(item)
? ` (${this.hass.localize(
"ui.panel.config.logs.custom_integration"
)})`
: ""
}`
: item.source[0]}
${item.count > 1
? html` - ${this._multipleMessages(item)} `
: nothing}
</span>
</ha-list-item>
`
: filteredItems.length === 0 && this.filter
? html`<div class="card-content">
${this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{ term: this.filter }
)}
</div>`
: html`<ha-list
>${filteredItems.map(
(item, idx) => html`
<ha-list-item
@click=${this._openLog}
.logItem=${item}
twoline
>
${item.message[0]}
<span slot="secondary" class="secondary">
${this._timestamp(item)}
${html`(<span class=${item.level}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level}`
)}</span
>) `}
${integrations[idx]
? `${domainToName(
this.hass!.localize,
integrations[idx]!
)}${
isCustomIntegrationError(item)
? ` (${this.hass.localize(
"ui.panel.config.logs.custom_integration"
)})`
: ""
}`
: item.source[0]}
${item.count > 1
? html` - ${this._multipleMessages(item)} `
: nothing}
</span>
</ha-list-item>
`
)}</ha-list
>`}
)}
</ha-list>
</div>
`;
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="system_log"
service="clear"
>${this.hass.localize(
"ui.panel.config.logs.clear"
)}</ha-call-service-button
>
</div>
`}
</ha-card>
</div>
`;
return this.showFilters
? html`
<div class="content-layout">
<div class="pane">
<div class="pane-content">
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.logs.level_filter"
)}
.states=${levels.map((level) => ({
value: level,
label: this.hass.localize(
`ui.panel.config.logs.level.${level}`
),
}))}
.value=${this._levelFilter}
@data-table-filter-changed=${this._levelFilterChanged}
></ha-filter-states>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.logs.classification"
)}
.states=${[
...errorTypes.map((errorType) => ({
value: errorType,
label: this.hass.localize(
`ui.panel.config.logs.error_type.${errorType}`
),
})),
{
value: "unknown",
label: this.hass.localize("ui.panel.config.logs.other"),
},
]}
.value=${this._errorTypeFilter}
@data-table-filter-changed=${this._errorTypeFilterChanged}
></ha-filter-states>
</div>
</div>
<div class="content-main">${listContent}</div>
</div>
`
: listContent;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
this.fetchData();
this.loaded = true;
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
this._notifyFiltersState();
}
private _levelFilterChanged(ev): void {
this._levelFilter = ev.detail.value || [];
this._notifyFiltersState();
}
private _errorTypeFilterChanged(ev): void {
this._errorTypeFilter = ev.detail.value || [];
this._notifyFiltersState();
}
private _notifyFiltersState(): void {
this.dispatchEvent(
new CustomEvent("system-log-filters-changed", {
detail: {
open: this.showFilters,
count: this.activeFiltersCount,
},
})
);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.domain === "system_log") {
// Do the right thing depending on service
if (ev.detail.service === "clear") {
this._items = [];
}
}
}
private _handleOverflowAction(ev: HaDropdownSelectEvent) {
if (ev.detail.item.value === "show-full-logs") {
// @ts-ignore
fireEvent(this, "switch-log-view");
}
}
private async _downloadLogs() {
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getErrorLogDownloadUrl(this.hass);
const logFileName = `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
private _openLog(ev: Event): void {
const item = (ev.currentTarget as any).logItem;
showSystemLogDetailDialog(this, { item });
}
static styles = css`
ha-card {
padding-top: 8px;
:host {
display: block;
direction: var(--direction);
min-height: 0;
height: 100%;
background: var(--primary-background-color);
}
:host {
direction: var(--direction);
}
ha-list {
direction: ltr;
}
.header {
display: flex;
justify-content: space-between;
padding: 0 16px;
}
.header-buttons {
display: flex;
align-items: flex-start;
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
display: block;
margin-block-start: 0px;
font-weight: var(--ha-font-weight-normal);
}
.system-log-intro {
margin: 16px;
background: var(--card-background-color);
}
.loading-container {
@@ -288,6 +341,39 @@ export class SystemLogCard extends LitElement {
justify-content: center;
}
.content-layout {
display: flex;
min-height: 100%;
height: 100%;
}
.content-main {
flex: 1;
min-width: 0;
background: var(--card-background-color);
}
.list-wrapper {
border-top: 1px solid var(--divider-color);
min-height: 100%;
background: var(--card-background-color);
}
.pane {
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
border-inline-end: 1px solid var(--divider-color);
display: flex;
flex-direction: column;
min-height: 0;
background: var(--primary-background-color);
}
.pane-content {
overflow: auto;
background: var(--primary-background-color);
}
.error {
color: var(--error-color);
}
@@ -296,15 +382,33 @@ export class SystemLogCard extends LitElement {
color: var(--warning-color);
}
.error-type-text {
color: var(--secondary-text-color);
}
.card-content {
border-top: 1px solid var(--divider-color);
padding-top: 16px;
padding-bottom: 16px;
min-height: 100%;
background: var(--card-background-color);
}
.row-secondary {
text-align: left;
}
@media (max-width: 900px) {
.content-layout {
flex-direction: column;
}
.pane {
width: 100%;
border-inline-end: none;
border-top: 1px solid var(--divider-color);
}
}
`;
}

View File

@@ -4195,6 +4195,21 @@
"info": "INFO",
"debug": "DEBUG"
},
"error_type": {
"auth": "Authentication",
"connection": "Connection",
"invalid_response": "Invalid response",
"rate_limit": "Rate limit",
"server": "Server",
"slow_setup": "Slow setup",
"timeout": "Timeout",
"ssl": "TLS/SSL",
"statistics": "Statistics",
"dns": "DNS"
},
"level_filter": "Level",
"classification": "Classification",
"other": "Other",
"custom_integration": "custom integration",
"error_from_custom_integration": "This error originated from a custom integration.",
"show_full_logs": "Show raw logs",
@@ -4219,6 +4234,7 @@
"integration": "[%key:ui::panel::config::integrations::integration%]",
"documentation": "documentation",
"issues": "issues",
"error_type": "Error type",
"first_occurred": "First occurred",
"number_of_occurrences": "{count} {count, plural,\n one {occurrence}\n other {occurrences}\n}",
"last_logged": "Last logged"