Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## 0.9.5

### Fixed

- **Favorites view blanks after `visibilitychange` restore** — Restored a `_recoverIfNeeded` helper on the panel's visibilitychange handler. It wraps
`_scheduleTabRender` in try/catch **and** verifies `#tab-content` received content; on thrown error or zero child nodes, it makes the initial render and then
retries up to three times with 2s / 4s / 6s backoff (four total renders in the worst case). The Favorites render path clears its container before awaiting
several async build steps (`FavoritesController.build`, `fetchAndBuildHorizonMaps`, `fetchMergedMonitoringStatus`), and when HA's WebSocket drops mid-render
one of those resolved empty or null without throwing, leaving the container blank with no console error. The retry catches the silent bailout. Dashboard (By
Panel) avoided the symptom because its simpler render produces an error message on failure instead of bailing quietly. The helper matches the pre-LitElement
behaviour removed during the `c4154d2` refactor.
- **List row `.list-power-value` min-width shrank the name column for no benefit** — Dropped the 70px `min-width` and `text-align: right` on
`.list-power-value`. Short readings (`1.3A`) were right-aligned inside a 70px cell, leaving a ~40px empty column between the relay control and the reading
that robbed width from the `flex:1 .list-circuit-name`. The value now sizes to content and hugs the preceding relay pill; the freed column flows back into the
name.

### Changed

- **Narrow-viewport list rows fold to a two-row grid** — New `@media (max-width: 520px)` rule switches `.list-row` from flex to grid with `grid-template-areas`
so the circuit name occupies the whole first row (paired with the expand chevron) and `breaker-badge`, `utilization`, shedding icon, status control, power
value, and gear drop to a second row. A `1fr` gap column between the status and power slots keeps the relay pill snug against the reading.
- **By Panel breaker cells fold based on grid width, not viewport** — Made `.panel-grid` a size-query container (`container-type: inline-size`) and added an
`@container (max-width: 760px)` rule on `.circuit-slot`. Each cell is half the grid's width, so truncation kicks in well before any viewport media query would
trigger. The fold uses `display: contents` on `.circuit-header`, `.circuit-info`, `.circuit-controls`, and `.circuit-status` so the leaf elements can be
placed directly via `grid-area` on the outer grid — name spans the full first row, the second row mirrors the list-row layout (badge, util, shed, status,
power, gear), and `.chart-container` stays as a full-width third row.

## 0.9.4

### Added
Expand Down
12 changes: 6 additions & 6 deletions dist/span-panel-card.js

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions dist/span-panel.js

Large diffs are not rendered by default.

121 changes: 119 additions & 2 deletions src/card/card-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ export const CARD_STYLES: string = `
grid-template-columns: 28px 1fr 1fr 28px;
gap: 8px;
align-items: stretch;
/* Mark the grid as a size-query container so individual cells can
fold to a name-on-row-1 layout based on the grid's rendered
width (see .circuit-slot @container rule below). Viewport-based
media queries would mis-fire here because cells are half-width
of the grid, not half-width of the viewport. */
container-type: inline-size;
}

.tab-label {
Expand Down Expand Up @@ -325,6 +331,65 @@ export const CARD_STYLES: string = `

.circuit-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* Narrow-cell fold for By Panel breaker cells. At full width the
cell packs breaker + utilization + name on the left of .circuit-header
and power + toggle on the right, with .circuit-status (shedding +
gear) below. Each cell is half the .panel-grid width, so truncation
kicks in once the grid is narrower than ~760px (cell ~340px) —
well before any viewport-based media query would fire. Query the
grid's own width via a container query and, once crossed, collapse
the nested flex containers into a single grid so the name gets the
whole first row and all readings/controls/gear drop to a second row.
'display: contents' on the wrappers removes them from the layout
tree so the leaf elements can be placed by the outer grid. */
@container (max-width: 760px) {
.circuit-slot {
display: grid;
grid-template-columns: auto auto auto 1fr auto auto auto;
grid-template-areas:
"name name name name name name name"
"badge util shed . status power gear"
"chart chart chart chart chart chart chart";
row-gap: 6px;
column-gap: 8px;
}
.circuit-slot > .circuit-header,
.circuit-slot > .circuit-status,
.circuit-header > .circuit-info,
.circuit-header > .circuit-controls {
display: contents;
}
.circuit-slot .circuit-name {
grid-area: name;
justify-self: start;
}
.circuit-slot .breaker-badge {
grid-area: badge;
}
.circuit-slot .utilization {
grid-area: util;
}
.circuit-slot .shedding-icon,
.circuit-slot .shedding-composite {
grid-area: shed;
}
.circuit-slot .toggle-pill {
grid-area: status;
justify-self: end;
}
.circuit-slot .power-value {
grid-area: power;
justify-self: end;
}
.circuit-slot .gear-icon.circuit-gear {
grid-area: gear;
justify-self: end;
}
.circuit-slot > .chart-container {
grid-area: chart;
}
}

.power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }
.power-value strong { font-weight: 700; font-size: 1.1em; }
.power-unit { font-size: 0.8em; font-weight: 400; color: var(--secondary-text-color, #999); margin-left: 1px; }
Expand Down Expand Up @@ -635,9 +700,13 @@ export const CARD_STYLES: string = `
.list-power-value {
font-size: 0.9em;
font-weight: 600;
min-width: 70px;
text-align: right;
flex-shrink: 0;
/* No min-width / text-align:right: the old 70px right-aligned
cell left a visible blank column for short readings (e.g.
"1.3A" in a 70px slot), which robbed horizontal space from
.list-circuit-name on narrow rows. Let the value hug the
preceding relay control and size to its content so the freed
width flows back into the flex:1 name column. */
}

.list-expand-toggle {
Expand Down Expand Up @@ -669,6 +738,54 @@ export const CARD_STYLES: string = `
color: var(--primary-text-color);
}

/* Narrow-viewport fold: the flat flex row truncates the circuit
name heavily once breaker + utilization + shedding + toggle +
power + gear + chevron are all competing for width. Switch to a
two-row grid so the name gets the full width (paired only with
the expand chevron), and the badges/controls/reading/gear drop
to a secondary row underneath. Named areas keep the CSS readable
despite the flat HTML child order. */
@media (max-width: 520px) {
.list-row {
display: grid;
grid-template-columns: auto auto auto 1fr auto auto auto;
grid-template-areas:
"name name name name name name chevron"
"badge util shed . status power gear";
row-gap: 6px;
column-gap: 8px;
}
.list-row > .list-circuit-name {
grid-area: name;
justify-self: start;
}
.list-row > .list-expand-toggle {
grid-area: chevron;
}
.list-row > .breaker-badge {
grid-area: badge;
}
.list-row > .utilization {
grid-area: util;
}
.list-row > .shedding-icon,
.list-row > .shedding-composite {
grid-area: shed;
}
.list-row > .toggle-pill,
.list-row > .list-status-badge {
grid-area: status;
}
.list-row > .list-power-value {
grid-area: power;
justify-self: end;
}
.list-row > .gear-icon.circuit-gear {
grid-area: gear;
justify-self: end;
}
}

/* ── Expanded circuit content ──────────────────────────── */

.list-expanded-content {
Expand Down
68 changes: 67 additions & 1 deletion src/panel/span-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export class SpanPanelElement extends LitElement {
* column count, horizon edits) the user made inside the sidebar.
*/
private _pendingTabRender = false;
/**
* Pending retry timer for ``_recoverIfNeeded``. Cleared on disconnect
* so a delayed retry cannot fire against a detached element after HA
* has torn down the panel.
*/
private _recoverTimer: ReturnType<typeof setTimeout> | null = null;

private static _shellStyles = css`
:host {
Expand Down Expand Up @@ -230,7 +236,7 @@ export class SpanPanelElement extends LitElement {

this._onVisibilityChange = (): void => {
if (document.visibilityState !== "visible" || !this._discovered || !this.hass) return;
this._scheduleTabRender();
this._recoverIfNeeded();
};
document.addEventListener("visibilitychange", this._onVisibilityChange);

Expand Down Expand Up @@ -267,6 +273,10 @@ export class SpanPanelElement extends LitElement {
clearTimeout(this._persistFavoritesViewStateTimer);
this._persistFavoritesViewStateTimer = null;
}
if (this._recoverTimer) {
clearTimeout(this._recoverTimer);
this._recoverTimer = null;
}
this._errorStore.dispose();
super.disconnectedCallback();
}
Expand Down Expand Up @@ -871,6 +881,62 @@ export class SpanPanelElement extends LitElement {
*/
private readonly _beginRender = makeRenderToken();

/**
* Visibility-restore recovery. When the browser tab is backgrounded
* and HA's WebSocket drops/reconnects, a tab re-render kicked off on
* ``visibilitychange`` can silently bail out mid-flight — a WS call
* resolving empty, a supersession race, or a cache returning null
* without throwing — and the Favorites view in particular is left
* with a blank ``#tab-content`` because it clears the container
* before awaiting its async build steps.
*
* Wrap ``_scheduleTabRender`` in a try/catch **and** verify the
* container produced content afterwards; if either fails, retry
* with backoff so the render can catch a freshly-reconnected WS.
* Mirrors the pre-LitElement ``_recoverIfNeeded`` helper removed
* during the c4154d2 refactor.
*/
private async _recoverIfNeeded(attempt = 0): Promise<void> {
if (!this._discovered || !this.hass) return;
// Retries scheduled after the initial render attempt (``attempt = 0``).
// With ``MAX_RETRIES = 3`` the behaviour is: one initial run plus up to
// three follow-ups at 2s / 4s / 6s backoff — four total renders in the
// worst case. Matches the pre-LitElement helper.
const MAX_RETRIES = 3;
const BACKOFF_BASE_MS = 2000;

const scheduleRetry = (): void => {
if (attempt >= MAX_RETRIES) return;
if (this._recoverTimer) clearTimeout(this._recoverTimer);
this._recoverTimer = setTimeout(
() => {
this._recoverTimer = null;
this._recoverIfNeeded(attempt + 1);
},
BACKOFF_BASE_MS * (attempt + 1)
);
};

try {
await this._scheduleTabRender();
} catch {
scheduleRetry();
return;
}

// A render that completed without throwing but left the tab
// container empty is the symptom we are recovering from. Every
// successful render path produces at least one child node — the
// empty-favorites state appends a ``<p>`` with ``list.no_results``,
// the error paths append a ``<p>`` with the error message, and the
// normal dashboard/activity/area/monitoring renders produce their
// respective headers/grids. Zero children means a silent bailout.
const container = this._root.getElementById("tab-content");
if (container && container.childNodes.length === 0) {
scheduleRetry();
}
}

/**
* Coalesce tab-render requests. If a render is in-flight, remember
* that another was requested and run exactly one follow-up once the
Expand Down
Loading