mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.29.0: 'new device' flag with a red dot
- auto devices/light groups appearing after install get a server-side 'new' flag (settings.new_device_ids) and a red dot top-right of the icon; opening the device's editor clears it for every client - known_devices baseline seeded silently on first run — upgrades never flood the plan with dots; hand-made markers never flagged - pure diffNewDevices + unit tests; backend schema; smoke_new_device.mjs; TESTING.md row
This commit is contained in:
+50
-2
@@ -17,7 +17,7 @@ import {
|
||||
pointOnBoundary, mergeRooms, splitRoom, polygonArea, closestPointOnBoundary,
|
||||
snapToWall, openingAmount,
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
||||
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
||||
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.28.1';
|
||||
const CARD_VERSION = '1.29.0';
|
||||
const LS_KEY = 'houseplan_card_layout_v1';
|
||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||
@@ -76,6 +76,7 @@ class HouseplanCard extends LitElement {
|
||||
private _devices: DevItem[] = [];
|
||||
private _regSignature = '';
|
||||
private _defPos: Record<string, { x: number; y: number }> = {};
|
||||
private _newSyncKey = '';
|
||||
private _tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null } | null = null;
|
||||
private _selId: string | null = null;
|
||||
private _toast = '';
|
||||
@@ -549,6 +550,51 @@ class HouseplanCard extends LitElement {
|
||||
iconRules: this._iconRules,
|
||||
});
|
||||
this._defPos = this._defaultPositions();
|
||||
this._syncNewDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* "New device" flag (server-side, shared by every client): an auto device
|
||||
* that appears after the known baseline was recorded gets a red dot until
|
||||
* someone opens its editor. The baseline is seeded silently on first run,
|
||||
* so an upgrade never floods the plan with dots.
|
||||
*/
|
||||
private _syncNewDevices(): void {
|
||||
if (!this._norm || !this._loadOk || !this._serverCfg) return;
|
||||
// only auto-appearing icons: area devices and light groups; markers are user-made
|
||||
const autoIds = this._devices.filter((d) => !d.marker && !d.virtual).map((d) => d.id).sort();
|
||||
const key = autoIds.join(',');
|
||||
if (key === this._newSyncKey) return; // same registry picture — nothing to do
|
||||
this._newSyncKey = key;
|
||||
const st: any = this._settings;
|
||||
const { fresh, known } = diffNewDevices(autoIds, st.known_devices);
|
||||
if (!Array.isArray(st.known_devices) || fresh.length) {
|
||||
const newIds = [...new Set([...(st.new_device_ids || []), ...fresh])];
|
||||
this._serverCfg = {
|
||||
...this._serverCfg,
|
||||
settings: { ...st, known_devices: known, new_device_ids: newIds },
|
||||
};
|
||||
// best-effort persist: non-admins under admin_only just keep the local view
|
||||
this._saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/** Ids currently flagged as new (drawn with the red dot). */
|
||||
private get _newIds(): Set<string> {
|
||||
const list = (this._settings as any).new_device_ids;
|
||||
return new Set(Array.isArray(list) ? list : []);
|
||||
}
|
||||
|
||||
/** First visit to the device's editor acknowledges its "new" flag. */
|
||||
private _ackNewDevice(id: string): void {
|
||||
if (!this._newIds.has(id) || !this._serverCfg) return;
|
||||
const st: any = this._settings;
|
||||
this._serverCfg = {
|
||||
...this._serverCfg,
|
||||
settings: { ...st, new_device_ids: (st.new_device_ids || []).filter((x: string) => x !== id) },
|
||||
};
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Curation + light groups + overrides + virtual devices. */
|
||||
@@ -1548,6 +1594,7 @@ class HouseplanCard extends LitElement {
|
||||
// ================= DEVICE EDITOR (markers) =================
|
||||
|
||||
private _openMarkerDialog(d?: DevItem): void {
|
||||
if (d) this._ackNewDevice(d.id);
|
||||
if (!this._norm) {
|
||||
this._showToast(this._t('toast.marker_needs_server'));
|
||||
return;
|
||||
@@ -2583,6 +2630,7 @@ class HouseplanCard extends LitElement {
|
||||
${ripple
|
||||
? html`<span class="ripple ${active ? 'active' : ''}"><i></i><i></i><i></i></span>`
|
||||
: nothing}
|
||||
${this._newIds.has(d.id) ? html`<span class="newdot" title=${this._t('device.new')}></span>` : nothing}
|
||||
${valText != null
|
||||
? html`<span class="valtext">${valText}</span>`
|
||||
: disp !== 'ripple'
|
||||
|
||||
+2
-1
@@ -244,5 +244,6 @@
|
||||
"mode.plan": "Plan",
|
||||
"mode.devices": "Devices",
|
||||
"display.value": "Value instead of an icon",
|
||||
"marker.subarea": "no area, manual"
|
||||
"marker.subarea": "no area, manual",
|
||||
"device.new": "New device — open its editor to dismiss"
|
||||
}
|
||||
|
||||
+2
-1
@@ -244,5 +244,6 @@
|
||||
"mode.plan": "План",
|
||||
"mode.devices": "Устройства",
|
||||
"display.value": "Значение вместо иконки",
|
||||
"marker.subarea": "без зоны, вручную"
|
||||
"marker.subarea": "без зоны, вручную",
|
||||
"device.new": "Новое устройство — откройте его редактор, чтобы снять отметку"
|
||||
}
|
||||
|
||||
@@ -732,3 +732,20 @@ export function parseRoomRef(
|
||||
}
|
||||
return { space, area: rest, roomId: null };
|
||||
}
|
||||
|
||||
// ---------------- new-device detection ----------------
|
||||
|
||||
/**
|
||||
* Which auto-appearing device ids are NEW against the known baseline.
|
||||
* No baseline yet (first run / upgrade) → nothing is new: every current id
|
||||
* becomes the baseline silently, so an update never floods the plan with dots.
|
||||
*/
|
||||
export function diffNewDevices(
|
||||
currentIds: string[],
|
||||
known: string[] | null | undefined,
|
||||
): { fresh: string[]; known: string[] } {
|
||||
if (!Array.isArray(known)) return { fresh: [], known: [...currentIds] };
|
||||
const knownSet = new Set(known);
|
||||
const fresh = currentIds.filter((id) => !knownSet.has(id));
|
||||
return { fresh, known: fresh.length ? [...known, ...fresh] : known };
|
||||
}
|
||||
|
||||
@@ -526,6 +526,18 @@ export const cardStyles = css`
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dev.alarm::after { animation: none; opacity: 0.9; }
|
||||
}
|
||||
.dev .newdot {
|
||||
position: absolute;
|
||||
top: calc(var(--icon-size, 2.5cqw) * -0.12);
|
||||
right: calc(var(--icon-size, 2.5cqw) * -0.12);
|
||||
width: calc(var(--icon-size, 2.5cqw) * 0.34);
|
||||
height: calc(var(--icon-size, 2.5cqw) * 0.34);
|
||||
border-radius: 50%;
|
||||
background: #f0301f;
|
||||
border: 2px solid var(--card-background-color, var(--hp-bg));
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.devlayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user