iobroker.hassemu 1.29.2 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,18 +19,7 @@ Emulates a Home Assistant server so displays that only accept an HA dashboard sh
19
19
 
20
20
  Some smart displays are hardwired to "I only speak Home Assistant". hassemu pretends to be that Home Assistant — the display connects, completes its onboarding, and then shows whatever web URL you choose (VIS, VIS-2, Aura, Grafana, Node-RED, anything HTTP).
21
21
 
22
- ### Tested displays and clients
23
-
24
- | Device / client | Firmware / version | Status |
25
- | --- | --- | --- |
26
- | Shelly Wall Display XL (SAWD-3A1XE10EU2, 10") | 1.x – 2.5.x | works (built-in HA page) |
27
- | Shelly Wall Display XL | **2.6.0+** | works since hassemu 1.29.2 (on-device HA app: browser OAuth2 + mobile-app registration + WebView connection signal) |
28
- | Shelly Wall Display (SAWD1, 4") | 1.x – 2.5.x | works (built-in HA page) |
29
- | Shelly Wall Display X2 / X2i (6.95") | 1.x – 2.5.x | works |
30
- | Home Assistant Companion App (Android) | 2024.x+ | works since hassemu 1.29.x — sideload onto any Android-based wall panel or tablet |
31
- | HA Companion-style WebView (Sonoff NSPanel Pro, generic Android wall panels) | — | works since 1.29.x |
32
-
33
- Other HA-Dashboard-only clients should work too. If you run one that doesn't, open an issue with the failing endpoint trace.
22
+ Typical clients are the Shelly Wall Display family (built-in HA page; on-device HA app on firmware 2.6.0+) and anything running the Home Assistant Companion App (Android wall panels, sideloaded apps). Anything that uses the same HA onboarding flow should work — if yours doesn't, open an issue with the failing endpoint trace.
34
23
 
35
24
  ---
36
25
 
@@ -167,6 +156,15 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
167
156
  Placeholder for the next version (at the beginning of the line):
168
157
  ### **WORK IN PROGRESS**
169
158
  -->
159
+ ### 1.30.0 (2026-05-12)
160
+
161
+ - Internal cleanup based on a source audit. No user-facing changes — except that adding or reconfiguring an Aura adapter now refreshes the URL dropdown automatically instead of requiring a manual refresh.
162
+
163
+ ### 1.29.3 (2026-05-12)
164
+
165
+ - The "Connection to Home Assistant failed" popup on Shelly Wall Display 2.6.0+ also stays away when the landing page is shown (no URL configured yet). v1.29.2 only suppressed it when a target URL was set.
166
+ - Replaced the landing-page emblem with the real ioBroker brand mark (power-button "i" inside a ring).
167
+
170
168
  ### 1.29.2 (2026-05-12)
171
169
 
172
170
  - Shelly Wall Display on firmware 2.6.0+ no longer shows the connection-error popup after the page has loaded.
@@ -181,14 +179,6 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
181
179
 
182
180
  - Shelly Wall Displays running firmware 2.6.0 and newer connect again. The new on-device Home Assistant app uses a browser sign-in flow and a server identity check that the previous adapter version did not answer.
183
181
 
184
- ### 1.28.4 (2026-05-12)
185
-
186
- - The mode dropdowns in admin (global and per-client) no longer crash with "Error in GUI" when opened.
187
-
188
- ### 1.28.3 (2026-05-10)
189
-
190
- - Adapter starts faster on installations with many displays, and a single broken client entry no longer keeps the others from being restored. Plus a security tighten-up around the HA login flow.
191
-
192
182
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
193
183
 
194
184
  ## Support
@@ -366,15 +366,17 @@ class ClientRegistry {
366
366
  async syncUrlDropdown(states) {
367
367
  this.currentUrlStates = states;
368
368
  const merged = this.buildModeStates();
369
- for (const id of this.byId.keys()) {
370
- const stateId = `clients.${id}.mode`;
371
- const existing = await this.adapter.getObjectAsync(stateId);
372
- if (!existing) {
373
- continue;
374
- }
375
- existing.common.states = merged;
376
- await this.adapter.setObjectAsync(stateId, existing);
377
- }
369
+ await Promise.all(
370
+ Array.from(this.byId.keys()).map(async (id) => {
371
+ const stateId = `clients.${id}.mode`;
372
+ const existing = await this.adapter.getObjectAsync(stateId);
373
+ if (!existing) {
374
+ return;
375
+ }
376
+ existing.common.states = merged;
377
+ await this.adapter.setObjectAsync(stateId, existing);
378
+ })
379
+ );
378
380
  }
379
381
  // --- internal ---
380
382
  trackInMemory(record) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/client-registry.ts"],
4
- "sourcesContent": ["/**\n * Client Registry \u2014 persistent multi-client store.\n *\n * Each client gets a channel `clients.<id>` with native.cookie / native.token\n * and states mode / manualUrl / ip / remove. Cookie is the primary identity\n * (auto-sent by browsers on page navigation), IP is only advisory.\n *\n * Registry state is dual-homed: in-memory maps for hot lookups, ioBroker\n * objects for persistence and user-visible config.\n */\n\nimport crypto from 'node:crypto';\nimport {\n buildDropdownStates,\n coerceSafeUrl,\n coerceString,\n coerceUuid,\n isPlainObject,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './constants';\nimport { resolveLabel, tName } from './i18n-states';\nimport { generateClientId } from './network';\nimport type { AdapterInterface, ClientRecord, UrlStates } from './types';\n\n/** Extended adapter interface for registry \u2014 needs object and state operations. */\nexport type RegistryAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n | 'namespace'\n | 'getForeignObjectsAsync'\n | 'getStateAsync'\n | 'getObjectAsync'\n | 'setObjectNotExistsAsync'\n | 'setObjectAsync'\n | 'extendObjectAsync'\n | 'setStateAsync'\n | 'delObjectAsync'\n >;\n\nconst CLIENTS_PREFIX = 'clients.';\n\n/** Provides the default mode value for a freshly created client. */\nexport type NewClientModeProvider = () => string;\n\n/** Persistent multi-client store: cookie \u2192 channel, with in-memory lookup maps. */\nexport class ClientRegistry {\n private readonly adapter: RegistryAdapter;\n private readonly byCookie = new Map<string, ClientRecord>();\n private readonly byId = new Map<string, ClientRecord>();\n private readonly byToken = new Map<string, ClientRecord>();\n private currentUrlStates: UrlStates = {};\n private newClientModeProvider: NewClientModeProvider = () => MODE_GLOBAL;\n /**\n * In-flight client creations keyed by remote IP. Keeps parallel cookieless\n * requests from the same display (typical on first connect: HA clients fire\n * `GET /`, `GET /api/`, `POST /auth/login_flow` almost simultaneously) from\n * each creating a separate client record. The first request starts the\n * create; parallel requests await the same Promise and receive the same\n * client + cookie.\n */\n private readonly pendingByIp = new Map<string, Promise<ClientRecord>>();\n /**\n * Throttle for lastSeen-updates per client. Keyed by client id, value is the\n * last `Date.now()` we wrote `native.lastSeen` to ioBroker. Throttle window\n * is one hour \u2014 saves us extendObject roundtrips on every request.\n */\n private readonly lastSeenFlushedAt = new Map<string, number>();\n /**\n * v1.19.0 (G5): per-IP burst tracking f\u00FCr broken-cookie-Displays.\n * Wenn eine IP > 3 neue Clients in einer Stunde erzeugt, kommt ein\n * einmaliger warn-log mit Hinweis (cookie-Persistenz auf Display kaputt).\n */\n private readonly newClientBurst = new Map<string, { count: number; since: number; warnedAt: number }>();\n\n /** @param adapter Adapter instance used for object/state I/O. */\n constructor(adapter: RegistryAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Wires the default-mode provider used when a new client is registered.\n * Called from main.ts once registry, globalConfig and urlDiscovery exist.\n *\n * @param provider Function returning the desired default mode for a new client.\n */\n setNewClientModeProvider(provider: NewClientModeProvider): void {\n this.newClientModeProvider = provider;\n }\n\n /** Loads existing clients from ioBroker objects into memory. Call once on adapter start. */\n async restore(): Promise<void> {\n let channels: Record<string, ioBroker.ChannelObject> = {};\n try {\n channels =\n (await this.adapter.getForeignObjectsAsync(`${this.adapter.namespace}.clients.*`, 'channel')) ?? {};\n } catch (err) {\n this.adapter.log.debug(`client-registry: restore failed: ${String(err)}`);\n return;\n }\n\n for (const [fullId, obj] of Object.entries(channels)) {\n const id = fullId.substring(`${this.adapter.namespace}.clients.`.length);\n if (!id || id.includes('.')) {\n continue;\n }\n // v1.28.3 (HE1): per-client try/catch \u2014 bei Promise.all auf 4\n // readState-Calls reicht ein einzelner Reject (z.B. corrupted state\n // bei migrating jsonl-store), und ALLE folgenden Clients werden\n // nicht restored. Pro-Client-Wrap entkoppelt: ein broken Client\n // kostet nur sich selbst.\n try {\n const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n // v1.9.0 (D8): vier readState-Calls parallel statt sequenziell.\n // Mit 50 Clients waren das vorher 200 sequenzielle Round-Trips\n // bevor der WebServer up war; jetzt 50 parallele 4er-Gruppen.\n const [modeRaw, manualUrlRaw, ipRaw, hostnameRaw] = await Promise.all([\n this.readState(`${id}.mode`),\n this.readState(`${id}.manualUrl`),\n this.readState(`${id}.ip`),\n this.readState(`${id}.hostname`),\n ]);\n const mode = typeof modeRaw === 'string' ? modeRaw : '';\n const manualUrl = coerceSafeUrl(manualUrlRaw);\n const ip = coerceString(ipRaw);\n const token = coerceUuid(native.token);\n\n // Legacy migration (<=1.1.1): hostname lived in its own state. If present,\n // move the value into common.name and drop the state.\n const legacyHostname = coerceString(hostnameRaw);\n let channelName = coerceString(obj.common?.name);\n if (legacyHostname) {\n if (legacyHostname !== channelName) {\n await this.adapter.extendObjectAsync(`clients.${id}`, { common: { name: legacyHostname } });\n channelName = legacyHostname;\n }\n try {\n await this.adapter.delObjectAsync(`clients.${id}.hostname`);\n } catch {\n /* best effort \u2014 ignore */\n }\n }\n const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;\n\n const record: ClientRecord = { id, cookie, token, mode, manualUrl, ip, hostname };\n this.trackInMemory(record);\n // Legacy clients (v1.1.x) only had `visUrl` + `ip` + `remove` objects;\n // ensure the v1.2.0+ objects (`mode`, `manualUrl`) exist before any\n // state writes from migration land \u2014 otherwise js-controller logs\n // \"State has no existing object\" warnings.\n await this.ensureObjects(record);\n // Promote blank state-value to numeric 0 so the dropdown renders\n // the `0='---'` option as selected. v1.2.0 installs left the value\n // as `''` which doesn't match any common.states entry.\n const modeStateRaw = await this.readState(`${id}.mode`);\n if (modeStateRaw === '' || modeStateRaw === null || modeStateRaw === undefined) {\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n }\n } catch (err) {\n this.adapter.log.debug(`client-registry: skipping ${id} during restore \u2014 ${String(err)}`);\n }\n }\n this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);\n }\n\n /**\n * Find the client for this cookie or create a new one.\n * Creates channel + states on first call and updates IP/hostname if changed.\n *\n * @param cookie Incoming cookie value (may be null/invalid).\n * @param ip Remote IP observed by the server.\n * @param hostname Optional hostname (from reverse DNS), stored for the admin UI.\n * @param userAgent Optional User-Agent header for NAT-collision-Schutz im Pending-Lock.\n */\n async identifyOrCreate(\n cookie: string | null,\n ip: string | null,\n hostname: string | null,\n userAgent: string | null = null,\n ): Promise<ClientRecord> {\n const validCookie = coerceUuid(cookie);\n if (validCookie) {\n const existing = this.byCookie.get(validCookie);\n if (existing) {\n await this.updateIpHostname(existing, ip, hostname);\n this.touchLastSeen(existing);\n return existing;\n }\n }\n // No valid cookie: before spinning up a new client, check whether this\n // IP already has a create in flight. If so, await that Promise \u2014 the\n // parallel request of the same display's initial burst will get the\n // same cookie + client, no more duplicate \"New client\" log entries.\n //\n // v1.17.0 (C8): Bucket-Key kombiniert IP + User-Agent-Hash, sodass\n // zwei verschiedene Displays hinter derselben NAT-IP NICHT in denselben\n // Pending-Lock fallen (vorher: gleicher Cookie/Token/Mode \u2192 Cookie-\n // Klau-Vektor). UA-Hash truncated auf 12 Hex-Chars um Memory-Footprint\n // klein zu halten. Bei UA=null f\u00E4llt der Bucket auf reines IP zur\u00FCck.\n if (ip) {\n const bucketKey = userAgent\n ? `${ip}|${crypto.createHash('sha256').update(userAgent).digest('hex').substring(0, 12)}`\n : ip;\n const pending = this.pendingByIp.get(bucketKey);\n if (pending) {\n // v1.21.0 (D3): pending-promise kann rejecten \u2014 z.B. wenn\n // createClient async failed (broker-disconnect, object-create-\n // error). Wir k\u00F6nnen nicht recover'n (der erste Caller hat eh\n // schon gefailt), aber wir wollen den Fehler diagnostizierbar\n // machen. catch+rethrow sorgt f\u00FCr ein einzelnes log statt\n // unhandled-rejection im fastify-error-handler.\n return pending.catch(err => {\n this.adapter.log.debug(\n `client-registry: pending createClient for ${bucketKey} rejected: ${String(err)}`,\n );\n throw err;\n });\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(bucketKey, promise);\n try {\n return await promise;\n } catch (err) {\n // Tech-Diagnose mit Stack-Detail \u2014 bleibt debug (Maintainer-only).\n this.adapter.log.debug(\n `client-registry: createClient failed for IP ${ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n throw err;\n } finally {\n this.pendingByIp.delete(bucketKey);\n }\n }\n return this.createClient(ip, hostname);\n }\n\n /**\n * Lookup by short client id (channel segment).\n *\n * @param id Client id.\n */\n getById(id: string): ClientRecord | null {\n return this.byId.get(id) ?? null;\n }\n\n /**\n * Lookup by cookie value. Invalid UUIDs return null.\n *\n * @param cookie Raw cookie string.\n */\n getByCookie(cookie: string): ClientRecord | null {\n const v = coerceUuid(cookie);\n return v ? (this.byCookie.get(v) ?? null) : null;\n }\n\n /**\n * Lookup by access token issued during the auth flow.\n *\n * @param token Bearer token.\n */\n getByToken(token: string): ClientRecord | null {\n return this.byToken.get(token) ?? null;\n }\n\n /** Returns a snapshot array of all registered clients. */\n listAll(): ClientRecord[] {\n return [...this.byId.values()];\n }\n\n /**\n * Updates in-memory token and persists to channel.native. Old token is freed.\n *\n * @param id Client id.\n * @param token New bearer token, or null to clear.\n */\n async setToken(id: string, token: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.token) {\n this.byToken.delete(record.token);\n }\n record.token = token;\n if (token) {\n this.byToken.set(token, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { token } });\n }\n\n /**\n * Accept an external mode write on `clients.<id>.mode`.\n *\n * Allowed values: `'global'`, `'manual'`, or any URL that passes\n * {@link coerceSafeUrl}. Empty string clears the choice \u2192 setup page.\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Vorher\n // hatten client-registry und global-config ~80% identische Logik\n // (no-choice, non-string, sentinel, URL-coerce) dupliziert.\n const result = parseModeWrite(rawValue, [MODE_GLOBAL, MODE_MANUAL]);\n switch (result.kind) {\n case 'no-choice':\n record.mode = '';\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n return;\n case 'rejected-non-string':\n // v1.18.0 (G7): debug statt warn \u2014 nicht-string mode-Schreibungen\n // sind UI-Echo, kein Server-Concern.\n this.adapter.log.debug(`client-registry: rejected non-string mode for ${id}`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n return;\n case 'sentinel':\n if (result.value === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `Client ${id}: mode set to \"manual\" but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n return;\n case 'rejected-unsafe-url':\n this.adapter.log.warn(`Client ${id}: rejected unsafe mode value \"${result.raw}\"`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n case 'url':\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n return;\n // 'rejected-disallowed-sentinel' kommt hier nicht vor weil beide\n // Sentinels (global/manual) erlaubt sind. Defensive: revert.\n default:\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n }\n }\n\n /**\n * Accept an external manualUrl write on `clients.<id>.manualUrl`.\n * Free-text \u2014 must pass {@link coerceSafeUrl} or be empty (clears).\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`Client ${id}: rejected unsafe manualUrl value`);\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: record.manualUrl ?? '', ack: true });\n return;\n }\n record.manualUrl = result.safe;\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: result.safe ?? '', ack: true });\n if (record.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(\n `Client ${id}: manualUrl cleared while mode is \"manual\" \u2014 display will see the setup page`,\n );\n }\n }\n\n /**\n * Set every client's `mode` to the same value. Used by the master switch\n * (`global.enabled`) to bulk-sync all displays \u2014 `'global'` when on,\n * the first discovered URL when off.\n *\n * Skips clients whose mode already matches (no spurious state writes).\n *\n * @param value New mode value (sentinel or URL).\n */\n async bulkSetMode(value: string): Promise<void> {\n // v1.8.1 (D7): parallele setStateAsync statt sequenziell. Mit 50 Displays\n // war das vorher 50 Broker-Round-Trips. setStateAsync ist Broker-internal,\n // Parallelism ist safe.\n const writes: Array<Promise<unknown>> = [];\n let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n writes.push(this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true }));\n changed++;\n }\n if (writes.length > 0) {\n await Promise.all(writes);\n }\n if (changed > 0) {\n this.adapter.log.debug(`bulkSetMode applied to ${changed} client(s)`);\n }\n }\n\n /**\n * Removes the client entirely \u2014 channel + states deleted, next visit creates a new entry.\n *\n * @param id Client id to forget.\n */\n async remove(id: string): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n this.byId.delete(id);\n this.byCookie.delete(record.cookie);\n if (record.token) {\n this.byToken.delete(record.token);\n }\n // v1.8.1 (D2): lastSeenFlushedAt war fr\u00FCher nicht aufger\u00E4umt \u2014 bei\n // ID-Reuse (16M-Space, m\u00F6glich nach 100+ Clients \u00FCber Jahre) h\u00E4tte\n // die alte Throttle-Entry den ersten lastSeen-Write des neuen Clients\n // inhibiert. Plus minimal Memory-Leak.\n this.lastSeenFlushedAt.delete(id);\n try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n // Stack-trace level \u2014 Maintainer-Diagnose, EN bleibt.\n this.adapter.log.debug(`client-registry: delObject failed for ${id}: ${String(err)}`);\n }\n this.adapter.log.info(`Client forgotten: ${id}`);\n }\n\n /**\n * Updates the mode dropdown states (`common.states`) on every client's mode datapoint.\n * Adds the `'global'` and `'manual'` sentinels on top of the discovered URLs.\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n this.currentUrlStates = states;\n const merged = this.buildModeStates();\n for (const id of this.byId.keys()) {\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel die nicht mehr discovered werden, blieben sonst im\n // Dropdown stehen (sichtbar nach v1.26\u2192v1.27 URL-Format-Wechsel:\n // alte `vis-2.0/main/index.html`-Keys neben neuen `vis-2/index.html?main`).\n // Object lesen, common.states komplett ersetzen, dann setObjectAsync.\n const stateId = `clients.${id}.mode`;\n const existing = await this.adapter.getObjectAsync(stateId);\n if (!existing) {\n continue;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync(stateId, existing);\n }\n }\n\n // --- internal ---\n\n private trackInMemory(record: ClientRecord): void {\n this.byId.set(record.id, record);\n this.byCookie.set(record.cookie, record);\n if (record.token) {\n this.byToken.set(record.token, record);\n }\n }\n\n private async createClient(ip: string | null, hostname: string | null): Promise<ClientRecord> {\n let id = generateClientId();\n while (this.byId.has(id)) {\n id = generateClientId();\n }\n const cookie = crypto.randomUUID();\n const mode = this.newClientModeProvider();\n const record: ClientRecord = { id, cookie, token: null, mode, manualUrl: null, ip, hostname };\n this.trackInMemory(record);\n await this.createObjects(record);\n this.touchLastSeen(record);\n this.adapter.log.info(ip ? `New client connected: ${id} (${hostname ?? ip})` : `New client connected: ${id}`);\n // v1.19.0 (G5): IP-Burst-Detection f\u00FCr broken-cookie-Displays. Wenn\n // dieselbe IP > 3 neue Clients in einer Stunde erzeugt, ist der Cookie-\n // Mechanismus auf dem Display kaputt (aggressive Privacy, refresh-bug).\n // Einmaliger warn-Hinweis pro IP pro Stunde \u2014 danach bleibt der info-\n // log normal, aber der Operator hat wenigstens einen Anker zur Diagnose.\n if (ip) {\n this.recordNewClientIp(ip);\n }\n return record;\n }\n\n /**\n * v1.19.0 (G5): tracking-only \u2014 wenn eine IP > 3 neue Clients pro Stunde\n * erzeugt, einmaliger warn-log mit Diagnose-Hinweis. Danach 1h cooldown\n * pro IP. Map-Cap 200 (FIFO).\n *\n * @param ip Remote IP that just got a new ClientRecord assigned.\n */\n private recordNewClientIp(ip: string): void {\n const now = Date.now();\n const HOUR = 60 * 60 * 1000;\n const entry = this.newClientBurst.get(ip) ?? { count: 0, since: now, warnedAt: 0 };\n if (now - entry.since > HOUR) {\n // Window expired \u2014 reset.\n entry.count = 0;\n entry.since = now;\n }\n entry.count += 1;\n if (entry.count > 3 && now - entry.warnedAt > HOUR) {\n this.adapter.log.warn(\n `IP ${ip} created ${entry.count} clients within an hour \u2014 display likely is not persisting cookies (privacy mode? refresh bug?)`,\n );\n entry.warnedAt = now;\n }\n this.newClientBurst.set(ip, entry);\n // Soft-cap to keep the map bounded (analog der anderen Caps).\n if (this.newClientBurst.size > 200) {\n const oldest = this.newClientBurst.keys().next().value;\n if (oldest !== undefined) {\n this.newClientBurst.delete(oldest);\n }\n }\n }\n\n /**\n * Updates `native.lastSeen` on the channel, throttled to once per hour per\n * client. Used for the stale-client-GC: clients without token + lastSeen\n * older than 30 days get auto-removed on adapter start.\n *\n * Fire-and-forget \u2014 failures only debug-logged.\n *\n * @param record Client whose lastSeen-timestamp should be refreshed.\n */\n private touchLastSeen(record: ClientRecord): void {\n const now = Date.now();\n const last = this.lastSeenFlushedAt.get(record.id) ?? 0;\n if (now - last < 60 * 60 * 1000) {\n return; // throttle: 1\u00D7 per hour\n }\n this.lastSeenFlushedAt.set(record.id, now);\n this.adapter\n .extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } })\n .catch(err => this.adapter.log.debug(`touchLastSeen failed for ${record.id}: ${String(err)}`));\n }\n\n /**\n * v1.19.0 (F11): zentraler lastSeen-Seed-Pfad. Vorher hatte main.ts\n * gcStaleClients seinen eigenen extendObjectAsync-Call mit identischem\n * native-Format \u2014 DRY-Violation und gef\u00E4hrlich wenn das Format mal \u00E4ndert.\n * Jetzt nutzen beide Pfade diese Methode. Throttle-Map wird auch upgedated,\n * damit der n\u00E4chste touchLastSeen den seed nicht direkt \u00FCberschreibt.\n *\n * @param id Client id (short segment, ohne `clients.`-Prefix).\n * @param now Optionaler Timestamp f\u00FCr tests; default Date.now().\n */\n async seedLastSeen(id: string, now: number = Date.now()): Promise<void> {\n this.lastSeenFlushedAt.set(id, now);\n try {\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { lastSeen: now } });\n } catch (err) {\n this.adapter.log.debug(`seedLastSeen failed for ${id}: ${String(err)}`);\n }\n }\n\n /**\n * Builds the dropdown-states map for `clients.<id>.mode`. Includes the\n * `0='---'` no-choice fallback (analogous to the govee-smart pattern), the\n * `'global'` + `'manual'` sentinels, and all currently discovered URLs.\n */\n private buildModeStates(): UrlStates {\n // v1.20.0 (F4): Helper aus coerce.ts. Vorher dupliziert mit global-config\n // (bis auf den zus\u00E4tzlichen `global`-Sentinel hier, weil clients per\n // `mode='global'` an global delegieren k\u00F6nnen \u2014 global selbst nicht).\n // v1.28.4: Sentinel-Labels als plain-string in adapter.systemLanguage \u2014\n // Admin rendert common.states-VALUES direkt als React-child und crasht\n // bei Translation-Objects mit React Error #31. v1.28.0 hatte tLabel hier\n // via `as unknown as string`-Cast eingeschleust, was den Type-Check still\n // gelassen aber den Admin-Dropdown beim \u00D6ffnen zerlegt hat.\n const lang = this.adapter.systemLanguage;\n return buildDropdownStates(\n {\n [MODE_GLOBAL]: resolveLabel('globalUrl', lang),\n [MODE_MANUAL]: resolveLabel('manualUrl', lang),\n },\n this.currentUrlStates,\n );\n }\n\n /**\n * Idempotently creates all per-client objects (channel + states). Safe to\n * call repeatedly \u2014 uses `setObjectNotExistsAsync` everywhere. Called from\n * both `restore()` (so legacy v1.1.x clients gain the new mode/manualUrl\n * objects before migration writes states) and `createClient()`.\n *\n * @param record Client to create or ensure objects for.\n */\n private async ensureObjects(record: ClientRecord): Promise<void> {\n const { id, cookie, ip, hostname } = record;\n const mergedStates = this.buildModeStates();\n\n // Channel: setObjectNotExistsAsync \u2014 common.name is updated dynamically\n // by updateIpHostname() when reverse-DNS resolves; we must not clobber it.\n await this.adapter.setObjectNotExistsAsync(`clients.${id}`, {\n type: 'channel',\n common: { name: hostname ?? ip ?? id },\n native: { cookie, token: null },\n });\n\n // States:\n // - `clients.<id>.mode` uses get+setObjectAsync (analog `global-config.ts`\n // v1.27.2-Fix): extendObjectAsync deep-merges common.states, weshalb\n // alte i18n-Object-Keys aus pre-v1.28.4-Installs unter den gleichen\n // keys h\u00E4ngen blieben \u2192 React Error #31 beim Admin-Dropdown-Open.\n // setObjectAsync ersetzt common komplett (custom-Custom-Subscriptions\n // wie influxdb.0 bleiben via spread aus existing erhalten).\n // - Repair-Pfad f\u00FCr partial-formed Objects (v1.2.0-Migration-Bug, common\n // ohne top-level type/name/role) ist auch abgedeckt: existing-fields\n // werden von der full schema common komplett \u00FCberschrieben.\n // - `clients.<id>.manualUrl` bleibt extendObjectAsync \u2014 kein states-Feld,\n // daher kein i18n-Object-Risiko.\n const modeFullCommon = {\n name: tName('clientMode') as unknown as string,\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern).\n type: 'mixed' as const,\n role: 'value',\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n };\n const ensureModeObject = async (): Promise<void> => {\n const existing = await this.adapter.getObjectAsync(`clients.${id}.mode`);\n if (existing) {\n existing.common = { ...existing.common, ...modeFullCommon };\n existing.type = 'state';\n await this.adapter.setObjectAsync(`clients.${id}.mode`, existing);\n } else {\n await this.adapter.setObjectAsync(`clients.${id}.mode`, {\n type: 'state',\n common: modeFullCommon,\n native: {},\n } as never);\n }\n };\n await Promise.all([\n ensureModeObject(),\n this.adapter.extendObjectAsync(`clients.${id}.manualUrl`, {\n type: 'state',\n common: {\n name: tName('clientManualUrl') as unknown as string,\n type: 'string',\n role: 'url',\n read: true,\n write: true,\n def: '',\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.ip`, {\n type: 'state',\n common: {\n name: tName('clientIp'),\n type: 'string',\n role: 'info.ip',\n read: true,\n write: false,\n def: '',\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: 'state',\n common: {\n name: tName('clientRemove'),\n type: 'boolean',\n role: 'button',\n read: false,\n write: true,\n def: false,\n },\n native: {},\n }),\n ]);\n }\n\n private async createObjects(record: ClientRecord): Promise<void> {\n await this.ensureObjects(record);\n const { id, mode, ip } = record;\n await Promise.all([\n this.adapter.setStateAsync(`clients.${id}.ip`, { val: ip ?? '', ack: true }),\n this.adapter.setStateAsync(`clients.${id}.mode`, { val: mode, ack: true }),\n this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: '', ack: true }),\n ]);\n }\n\n private async updateIpHostname(record: ClientRecord, ip: string | null, hostname: string | null): Promise<void> {\n if (ip && ip !== record.ip) {\n record.ip = ip;\n await this.adapter.setStateAsync(`clients.${record.id}.ip`, { val: ip, ack: true });\n // If no hostname known yet, common.name falls back to the IP \u2014 keep it current.\n if (!record.hostname) {\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });\n }\n }\n if (hostname && hostname !== record.hostname) {\n record.hostname = hostname;\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: hostname } });\n }\n }\n\n private async readState(subId: string): Promise<unknown> {\n // v1.20.0 (F10): Helper aus coerce.ts \u2014 vorher dupliziert mit\n // global-config.safeGetState (gleicher try/catch-+-null-Fallback,\n // nur Pfad-Prefix anders).\n const s = await safeGetState(this.adapter, `clients.${subId}`);\n return s?.val ?? null;\n }\n}\n\n/**\n * Check whether a full state ID matches a client control datapoint and extract id + kind.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseClientStateId(\n fullId: string,\n namespace: string,\n): { id: string; kind: 'mode' | 'manualUrl' | 'remove' } | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // client-registry seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // global-config.parseGlobalStateId.\n const parts = parseAdapterStateId(fullId, namespace, CLIENTS_PREFIX, 2);\n if (!parts) {\n return null;\n }\n const [id, kind] = parts;\n // v1.9.0 (E5): empty id rejection (`clients..mode` would parse to id='').\n if (!id) {\n return null;\n }\n if (kind !== 'mode' && kind !== 'manualUrl' && kind !== 'remove') {\n return null;\n }\n return { id, kind };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Client Registry \u2014 persistent multi-client store.\n *\n * Each client gets a channel `clients.<id>` with native.cookie / native.token\n * and states mode / manualUrl / ip / remove. Cookie is the primary identity\n * (auto-sent by browsers on page navigation), IP is only advisory.\n *\n * Registry state is dual-homed: in-memory maps for hot lookups, ioBroker\n * objects for persistence and user-visible config.\n */\n\nimport crypto from 'node:crypto';\nimport {\n buildDropdownStates,\n coerceSafeUrl,\n coerceString,\n coerceUuid,\n isPlainObject,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './constants';\nimport { resolveLabel, tName } from './i18n-states';\nimport { generateClientId } from './network';\nimport type { AdapterInterface, ClientRecord, UrlStates } from './types';\n\n/** Extended adapter interface for registry \u2014 needs object and state operations. */\nexport type RegistryAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n | 'namespace'\n | 'getForeignObjectsAsync'\n | 'getStateAsync'\n | 'getObjectAsync'\n | 'setObjectNotExistsAsync'\n | 'setObjectAsync'\n | 'extendObjectAsync'\n | 'setStateAsync'\n | 'delObjectAsync'\n >;\n\nconst CLIENTS_PREFIX = 'clients.';\n\n/** Provides the default mode value for a freshly created client. */\nexport type NewClientModeProvider = () => string;\n\n/** Persistent multi-client store: cookie \u2192 channel, with in-memory lookup maps. */\nexport class ClientRegistry {\n private readonly adapter: RegistryAdapter;\n private readonly byCookie = new Map<string, ClientRecord>();\n private readonly byId = new Map<string, ClientRecord>();\n private readonly byToken = new Map<string, ClientRecord>();\n private currentUrlStates: UrlStates = {};\n private newClientModeProvider: NewClientModeProvider = () => MODE_GLOBAL;\n /**\n * In-flight client creations keyed by remote IP. Keeps parallel cookieless\n * requests from the same display (typical on first connect: HA clients fire\n * `GET /`, `GET /api/`, `POST /auth/login_flow` almost simultaneously) from\n * each creating a separate client record. The first request starts the\n * create; parallel requests await the same Promise and receive the same\n * client + cookie.\n */\n private readonly pendingByIp = new Map<string, Promise<ClientRecord>>();\n /**\n * Throttle for lastSeen-updates per client. Keyed by client id, value is the\n * last `Date.now()` we wrote `native.lastSeen` to ioBroker. Throttle window\n * is one hour \u2014 saves us extendObject roundtrips on every request.\n */\n private readonly lastSeenFlushedAt = new Map<string, number>();\n /**\n * v1.19.0 (G5): per-IP burst tracking f\u00FCr broken-cookie-Displays.\n * Wenn eine IP > 3 neue Clients in einer Stunde erzeugt, kommt ein\n * einmaliger warn-log mit Hinweis (cookie-Persistenz auf Display kaputt).\n */\n private readonly newClientBurst = new Map<string, { count: number; since: number; warnedAt: number }>();\n\n /** @param adapter Adapter instance used for object/state I/O. */\n constructor(adapter: RegistryAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Wires the default-mode provider used when a new client is registered.\n * Called from main.ts once registry, globalConfig and urlDiscovery exist.\n *\n * @param provider Function returning the desired default mode for a new client.\n */\n setNewClientModeProvider(provider: NewClientModeProvider): void {\n this.newClientModeProvider = provider;\n }\n\n /** Loads existing clients from ioBroker objects into memory. Call once on adapter start. */\n async restore(): Promise<void> {\n let channels: Record<string, ioBroker.ChannelObject> = {};\n try {\n channels =\n (await this.adapter.getForeignObjectsAsync(`${this.adapter.namespace}.clients.*`, 'channel')) ?? {};\n } catch (err) {\n this.adapter.log.debug(`client-registry: restore failed: ${String(err)}`);\n return;\n }\n\n for (const [fullId, obj] of Object.entries(channels)) {\n const id = fullId.substring(`${this.adapter.namespace}.clients.`.length);\n if (!id || id.includes('.')) {\n continue;\n }\n // v1.28.3 (HE1): per-client try/catch \u2014 bei Promise.all auf 4\n // readState-Calls reicht ein einzelner Reject (z.B. corrupted state\n // bei migrating jsonl-store), und ALLE folgenden Clients werden\n // nicht restored. Pro-Client-Wrap entkoppelt: ein broken Client\n // kostet nur sich selbst.\n try {\n const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n // v1.9.0 (D8): vier readState-Calls parallel statt sequenziell.\n // Mit 50 Clients waren das vorher 200 sequenzielle Round-Trips\n // bevor der WebServer up war; jetzt 50 parallele 4er-Gruppen.\n const [modeRaw, manualUrlRaw, ipRaw, hostnameRaw] = await Promise.all([\n this.readState(`${id}.mode`),\n this.readState(`${id}.manualUrl`),\n this.readState(`${id}.ip`),\n this.readState(`${id}.hostname`),\n ]);\n const mode = typeof modeRaw === 'string' ? modeRaw : '';\n const manualUrl = coerceSafeUrl(manualUrlRaw);\n const ip = coerceString(ipRaw);\n const token = coerceUuid(native.token);\n\n // Legacy migration (<=1.1.1): hostname lived in its own state. If present,\n // move the value into common.name and drop the state.\n const legacyHostname = coerceString(hostnameRaw);\n let channelName = coerceString(obj.common?.name);\n if (legacyHostname) {\n if (legacyHostname !== channelName) {\n await this.adapter.extendObjectAsync(`clients.${id}`, { common: { name: legacyHostname } });\n channelName = legacyHostname;\n }\n try {\n await this.adapter.delObjectAsync(`clients.${id}.hostname`);\n } catch {\n /* best effort \u2014 ignore */\n }\n }\n const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;\n\n const record: ClientRecord = { id, cookie, token, mode, manualUrl, ip, hostname };\n this.trackInMemory(record);\n // Legacy clients (v1.1.x) only had `visUrl` + `ip` + `remove` objects;\n // ensure the v1.2.0+ objects (`mode`, `manualUrl`) exist before any\n // state writes from migration land \u2014 otherwise js-controller logs\n // \"State has no existing object\" warnings.\n await this.ensureObjects(record);\n // Promote blank state-value to numeric 0 so the dropdown renders\n // the `0='---'` option as selected. v1.2.0 installs left the value\n // as `''` which doesn't match any common.states entry.\n const modeStateRaw = await this.readState(`${id}.mode`);\n if (modeStateRaw === '' || modeStateRaw === null || modeStateRaw === undefined) {\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n }\n } catch (err) {\n this.adapter.log.debug(`client-registry: skipping ${id} during restore \u2014 ${String(err)}`);\n }\n }\n this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);\n }\n\n /**\n * Find the client for this cookie or create a new one.\n * Creates channel + states on first call and updates IP/hostname if changed.\n *\n * @param cookie Incoming cookie value (may be null/invalid).\n * @param ip Remote IP observed by the server.\n * @param hostname Optional hostname (from reverse DNS), stored for the admin UI.\n * @param userAgent Optional User-Agent header for NAT-collision-Schutz im Pending-Lock.\n */\n async identifyOrCreate(\n cookie: string | null,\n ip: string | null,\n hostname: string | null,\n userAgent: string | null = null,\n ): Promise<ClientRecord> {\n const validCookie = coerceUuid(cookie);\n if (validCookie) {\n const existing = this.byCookie.get(validCookie);\n if (existing) {\n await this.updateIpHostname(existing, ip, hostname);\n this.touchLastSeen(existing);\n return existing;\n }\n }\n // No valid cookie: before spinning up a new client, check whether this\n // IP already has a create in flight. If so, await that Promise \u2014 the\n // parallel request of the same display's initial burst will get the\n // same cookie + client, no more duplicate \"New client\" log entries.\n //\n // v1.17.0 (C8): Bucket-Key kombiniert IP + User-Agent-Hash, sodass\n // zwei verschiedene Displays hinter derselben NAT-IP NICHT in denselben\n // Pending-Lock fallen (vorher: gleicher Cookie/Token/Mode \u2192 Cookie-\n // Klau-Vektor). UA-Hash truncated auf 12 Hex-Chars um Memory-Footprint\n // klein zu halten. Bei UA=null f\u00E4llt der Bucket auf reines IP zur\u00FCck.\n if (ip) {\n const bucketKey = userAgent\n ? `${ip}|${crypto.createHash('sha256').update(userAgent).digest('hex').substring(0, 12)}`\n : ip;\n const pending = this.pendingByIp.get(bucketKey);\n if (pending) {\n // v1.21.0 (D3): pending-promise kann rejecten \u2014 z.B. wenn\n // createClient async failed (broker-disconnect, object-create-\n // error). Wir k\u00F6nnen nicht recover'n (der erste Caller hat eh\n // schon gefailt), aber wir wollen den Fehler diagnostizierbar\n // machen. catch+rethrow sorgt f\u00FCr ein einzelnes log statt\n // unhandled-rejection im fastify-error-handler.\n return pending.catch(err => {\n this.adapter.log.debug(\n `client-registry: pending createClient for ${bucketKey} rejected: ${String(err)}`,\n );\n throw err;\n });\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(bucketKey, promise);\n try {\n return await promise;\n } catch (err) {\n // Tech-Diagnose mit Stack-Detail \u2014 bleibt debug (Maintainer-only).\n this.adapter.log.debug(\n `client-registry: createClient failed for IP ${ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n throw err;\n } finally {\n this.pendingByIp.delete(bucketKey);\n }\n }\n return this.createClient(ip, hostname);\n }\n\n /**\n * Lookup by short client id (channel segment).\n *\n * @param id Client id.\n */\n getById(id: string): ClientRecord | null {\n return this.byId.get(id) ?? null;\n }\n\n /**\n * Lookup by cookie value. Invalid UUIDs return null.\n *\n * @param cookie Raw cookie string.\n */\n getByCookie(cookie: string): ClientRecord | null {\n const v = coerceUuid(cookie);\n return v ? (this.byCookie.get(v) ?? null) : null;\n }\n\n /**\n * Lookup by access token issued during the auth flow.\n *\n * @param token Bearer token.\n */\n getByToken(token: string): ClientRecord | null {\n return this.byToken.get(token) ?? null;\n }\n\n /** Returns a snapshot array of all registered clients. */\n listAll(): ClientRecord[] {\n return [...this.byId.values()];\n }\n\n /**\n * Updates in-memory token and persists to channel.native. Old token is freed.\n *\n * @param id Client id.\n * @param token New bearer token, or null to clear.\n */\n async setToken(id: string, token: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.token) {\n this.byToken.delete(record.token);\n }\n record.token = token;\n if (token) {\n this.byToken.set(token, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { token } });\n }\n\n /**\n * Accept an external mode write on `clients.<id>.mode`.\n *\n * Allowed values: `'global'`, `'manual'`, or any URL that passes\n * {@link coerceSafeUrl}. Empty string clears the choice \u2192 setup page.\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Vorher\n // hatten client-registry und global-config ~80% identische Logik\n // (no-choice, non-string, sentinel, URL-coerce) dupliziert.\n const result = parseModeWrite(rawValue, [MODE_GLOBAL, MODE_MANUAL]);\n switch (result.kind) {\n case 'no-choice':\n record.mode = '';\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n return;\n case 'rejected-non-string':\n // v1.18.0 (G7): debug statt warn \u2014 nicht-string mode-Schreibungen\n // sind UI-Echo, kein Server-Concern.\n this.adapter.log.debug(`client-registry: rejected non-string mode for ${id}`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n return;\n case 'sentinel':\n if (result.value === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `Client ${id}: mode set to \"manual\" but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n return;\n case 'rejected-unsafe-url':\n this.adapter.log.warn(`Client ${id}: rejected unsafe mode value \"${result.raw}\"`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n case 'url':\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n return;\n // 'rejected-disallowed-sentinel' kommt hier nicht vor weil beide\n // Sentinels (global/manual) erlaubt sind. Defensive: revert.\n default:\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n }\n }\n\n /**\n * Accept an external manualUrl write on `clients.<id>.manualUrl`.\n * Free-text \u2014 must pass {@link coerceSafeUrl} or be empty (clears).\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`Client ${id}: rejected unsafe manualUrl value`);\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: record.manualUrl ?? '', ack: true });\n return;\n }\n record.manualUrl = result.safe;\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: result.safe ?? '', ack: true });\n if (record.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(\n `Client ${id}: manualUrl cleared while mode is \"manual\" \u2014 display will see the setup page`,\n );\n }\n }\n\n /**\n * Set every client's `mode` to the same value. Used by the master switch\n * (`global.enabled`) to bulk-sync all displays \u2014 `'global'` when on,\n * the first discovered URL when off.\n *\n * Skips clients whose mode already matches (no spurious state writes).\n *\n * @param value New mode value (sentinel or URL).\n */\n async bulkSetMode(value: string): Promise<void> {\n // v1.8.1 (D7): parallele setStateAsync statt sequenziell. Mit 50 Displays\n // war das vorher 50 Broker-Round-Trips. setStateAsync ist Broker-internal,\n // Parallelism ist safe.\n const writes: Array<Promise<unknown>> = [];\n let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n writes.push(this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true }));\n changed++;\n }\n if (writes.length > 0) {\n await Promise.all(writes);\n }\n if (changed > 0) {\n this.adapter.log.debug(`bulkSetMode applied to ${changed} client(s)`);\n }\n }\n\n /**\n * Removes the client entirely \u2014 channel + states deleted, next visit creates a new entry.\n *\n * @param id Client id to forget.\n */\n async remove(id: string): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n this.byId.delete(id);\n this.byCookie.delete(record.cookie);\n if (record.token) {\n this.byToken.delete(record.token);\n }\n // v1.8.1 (D2): lastSeenFlushedAt war fr\u00FCher nicht aufger\u00E4umt \u2014 bei\n // ID-Reuse (16M-Space, m\u00F6glich nach 100+ Clients \u00FCber Jahre) h\u00E4tte\n // die alte Throttle-Entry den ersten lastSeen-Write des neuen Clients\n // inhibiert. Plus minimal Memory-Leak.\n this.lastSeenFlushedAt.delete(id);\n try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n // Stack-trace level \u2014 Maintainer-Diagnose, EN bleibt.\n this.adapter.log.debug(`client-registry: delObject failed for ${id}: ${String(err)}`);\n }\n this.adapter.log.info(`Client forgotten: ${id}`);\n }\n\n /**\n * Updates the mode dropdown states (`common.states`) on every client's mode datapoint.\n * Adds the `'global'` and `'manual'` sentinels on top of the discovered URLs.\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n this.currentUrlStates = states;\n const merged = this.buildModeStates();\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel die nicht mehr discovered werden, blieben sonst im\n // Dropdown stehen (sichtbar nach v1.26\u2192v1.27 URL-Format-Wechsel:\n // alte `vis-2.0/main/index.html`-Keys neben neuen `vis-2/index.html?main`).\n // Object lesen, common.states komplett ersetzen, dann setObjectAsync.\n // v1.30.0 (R4): pro-Client get+set parallel statt sequentiell. Analog\n // gcStaleClients in main.ts (v1.28.3 M5). Sp\u00FCrbar bei Display-Farmen\n // mit 30+ Clients \u2014 vorher 2\u00D7N Broker-Round-Trips sequenziell.\n await Promise.all(\n Array.from(this.byId.keys()).map(async id => {\n const stateId = `clients.${id}.mode`;\n const existing = await this.adapter.getObjectAsync(stateId);\n if (!existing) {\n return;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync(stateId, existing);\n }),\n );\n }\n\n // --- internal ---\n\n private trackInMemory(record: ClientRecord): void {\n this.byId.set(record.id, record);\n this.byCookie.set(record.cookie, record);\n if (record.token) {\n this.byToken.set(record.token, record);\n }\n }\n\n private async createClient(ip: string | null, hostname: string | null): Promise<ClientRecord> {\n let id = generateClientId();\n while (this.byId.has(id)) {\n id = generateClientId();\n }\n const cookie = crypto.randomUUID();\n const mode = this.newClientModeProvider();\n const record: ClientRecord = { id, cookie, token: null, mode, manualUrl: null, ip, hostname };\n this.trackInMemory(record);\n await this.createObjects(record);\n this.touchLastSeen(record);\n this.adapter.log.info(ip ? `New client connected: ${id} (${hostname ?? ip})` : `New client connected: ${id}`);\n // v1.19.0 (G5): IP-Burst-Detection f\u00FCr broken-cookie-Displays. Wenn\n // dieselbe IP > 3 neue Clients in einer Stunde erzeugt, ist der Cookie-\n // Mechanismus auf dem Display kaputt (aggressive Privacy, refresh-bug).\n // Einmaliger warn-Hinweis pro IP pro Stunde \u2014 danach bleibt der info-\n // log normal, aber der Operator hat wenigstens einen Anker zur Diagnose.\n if (ip) {\n this.recordNewClientIp(ip);\n }\n return record;\n }\n\n /**\n * v1.19.0 (G5): tracking-only \u2014 wenn eine IP > 3 neue Clients pro Stunde\n * erzeugt, einmaliger warn-log mit Diagnose-Hinweis. Danach 1h cooldown\n * pro IP. Map-Cap 200 (FIFO).\n *\n * @param ip Remote IP that just got a new ClientRecord assigned.\n */\n private recordNewClientIp(ip: string): void {\n const now = Date.now();\n const HOUR = 60 * 60 * 1000;\n const entry = this.newClientBurst.get(ip) ?? { count: 0, since: now, warnedAt: 0 };\n if (now - entry.since > HOUR) {\n // Window expired \u2014 reset.\n entry.count = 0;\n entry.since = now;\n }\n entry.count += 1;\n if (entry.count > 3 && now - entry.warnedAt > HOUR) {\n this.adapter.log.warn(\n `IP ${ip} created ${entry.count} clients within an hour \u2014 display likely is not persisting cookies (privacy mode? refresh bug?)`,\n );\n entry.warnedAt = now;\n }\n this.newClientBurst.set(ip, entry);\n // Soft-cap to keep the map bounded (analog der anderen Caps).\n if (this.newClientBurst.size > 200) {\n const oldest = this.newClientBurst.keys().next().value;\n if (oldest !== undefined) {\n this.newClientBurst.delete(oldest);\n }\n }\n }\n\n /**\n * Updates `native.lastSeen` on the channel, throttled to once per hour per\n * client. Used for the stale-client-GC: clients without token + lastSeen\n * older than 30 days get auto-removed on adapter start.\n *\n * Fire-and-forget \u2014 failures only debug-logged.\n *\n * @param record Client whose lastSeen-timestamp should be refreshed.\n */\n private touchLastSeen(record: ClientRecord): void {\n const now = Date.now();\n const last = this.lastSeenFlushedAt.get(record.id) ?? 0;\n if (now - last < 60 * 60 * 1000) {\n return; // throttle: 1\u00D7 per hour\n }\n this.lastSeenFlushedAt.set(record.id, now);\n this.adapter\n .extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } })\n .catch(err => this.adapter.log.debug(`touchLastSeen failed for ${record.id}: ${String(err)}`));\n }\n\n /**\n * v1.19.0 (F11): zentraler lastSeen-Seed-Pfad. Vorher hatte main.ts\n * gcStaleClients seinen eigenen extendObjectAsync-Call mit identischem\n * native-Format \u2014 DRY-Violation und gef\u00E4hrlich wenn das Format mal \u00E4ndert.\n * Jetzt nutzen beide Pfade diese Methode. Throttle-Map wird auch upgedated,\n * damit der n\u00E4chste touchLastSeen den seed nicht direkt \u00FCberschreibt.\n *\n * @param id Client id (short segment, ohne `clients.`-Prefix).\n * @param now Optionaler Timestamp f\u00FCr tests; default Date.now().\n */\n async seedLastSeen(id: string, now: number = Date.now()): Promise<void> {\n this.lastSeenFlushedAt.set(id, now);\n try {\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { lastSeen: now } });\n } catch (err) {\n this.adapter.log.debug(`seedLastSeen failed for ${id}: ${String(err)}`);\n }\n }\n\n /**\n * Builds the dropdown-states map for `clients.<id>.mode`. Includes the\n * `0='---'` no-choice fallback (analogous to the govee-smart pattern), the\n * `'global'` + `'manual'` sentinels, and all currently discovered URLs.\n */\n private buildModeStates(): UrlStates {\n // v1.20.0 (F4): Helper aus coerce.ts. Vorher dupliziert mit global-config\n // (bis auf den zus\u00E4tzlichen `global`-Sentinel hier, weil clients per\n // `mode='global'` an global delegieren k\u00F6nnen \u2014 global selbst nicht).\n // v1.28.4: Sentinel-Labels als plain-string in adapter.systemLanguage \u2014\n // Admin rendert common.states-VALUES direkt als React-child und crasht\n // bei Translation-Objects mit React Error #31. v1.28.0 hatte einen\n // helper hier via `as unknown as string`-Cast eingeschleust, der den\n // Type-Check still lie\u00DF aber den Admin-Dropdown beim \u00D6ffnen zerlegt\n // hat. Memory `reference_common_states_plain_string_only`.\n const lang = this.adapter.systemLanguage;\n return buildDropdownStates(\n {\n [MODE_GLOBAL]: resolveLabel('globalUrl', lang),\n [MODE_MANUAL]: resolveLabel('manualUrl', lang),\n },\n this.currentUrlStates,\n );\n }\n\n /**\n * Idempotently creates all per-client objects (channel + states). Safe to\n * call repeatedly \u2014 uses `setObjectNotExistsAsync` everywhere. Called from\n * both `restore()` (so legacy v1.1.x clients gain the new mode/manualUrl\n * objects before migration writes states) and `createClient()`.\n *\n * @param record Client to create or ensure objects for.\n */\n private async ensureObjects(record: ClientRecord): Promise<void> {\n const { id, cookie, ip, hostname } = record;\n const mergedStates = this.buildModeStates();\n\n // Channel: setObjectNotExistsAsync \u2014 common.name is updated dynamically\n // by updateIpHostname() when reverse-DNS resolves; we must not clobber it.\n await this.adapter.setObjectNotExistsAsync(`clients.${id}`, {\n type: 'channel',\n common: { name: hostname ?? ip ?? id },\n native: { cookie, token: null },\n });\n\n // States:\n // - `clients.<id>.mode` uses get+setObjectAsync (analog `global-config.ts`\n // v1.27.2-Fix): extendObjectAsync deep-merges common.states, weshalb\n // alte i18n-Object-Keys aus pre-v1.28.4-Installs unter den gleichen\n // keys h\u00E4ngen blieben \u2192 React Error #31 beim Admin-Dropdown-Open.\n // setObjectAsync ersetzt common komplett (custom-Custom-Subscriptions\n // wie influxdb.0 bleiben via spread aus existing erhalten).\n // - Repair-Pfad f\u00FCr partial-formed Objects (v1.2.0-Migration-Bug, common\n // ohne top-level type/name/role) ist auch abgedeckt: existing-fields\n // werden von der full schema common komplett \u00FCberschrieben.\n // - `clients.<id>.manualUrl` bleibt extendObjectAsync \u2014 kein states-Feld,\n // daher kein i18n-Object-Risiko.\n const modeFullCommon = {\n name: tName('clientMode') as unknown as string,\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern).\n type: 'mixed' as const,\n role: 'value',\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n };\n const ensureModeObject = async (): Promise<void> => {\n const existing = await this.adapter.getObjectAsync(`clients.${id}.mode`);\n if (existing) {\n existing.common = { ...existing.common, ...modeFullCommon };\n existing.type = 'state';\n await this.adapter.setObjectAsync(`clients.${id}.mode`, existing);\n } else {\n await this.adapter.setObjectAsync(`clients.${id}.mode`, {\n type: 'state',\n common: modeFullCommon,\n native: {},\n } as never);\n }\n };\n await Promise.all([\n ensureModeObject(),\n this.adapter.extendObjectAsync(`clients.${id}.manualUrl`, {\n type: 'state',\n common: {\n name: tName('clientManualUrl') as unknown as string,\n type: 'string',\n role: 'url',\n read: true,\n write: true,\n def: '',\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.ip`, {\n type: 'state',\n common: {\n name: tName('clientIp'),\n type: 'string',\n role: 'info.ip',\n read: true,\n write: false,\n def: '',\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: 'state',\n common: {\n name: tName('clientRemove'),\n type: 'boolean',\n role: 'button',\n read: false,\n write: true,\n def: false,\n },\n native: {},\n }),\n ]);\n }\n\n private async createObjects(record: ClientRecord): Promise<void> {\n await this.ensureObjects(record);\n const { id, mode, ip } = record;\n await Promise.all([\n this.adapter.setStateAsync(`clients.${id}.ip`, { val: ip ?? '', ack: true }),\n this.adapter.setStateAsync(`clients.${id}.mode`, { val: mode, ack: true }),\n this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: '', ack: true }),\n ]);\n }\n\n private async updateIpHostname(record: ClientRecord, ip: string | null, hostname: string | null): Promise<void> {\n if (ip && ip !== record.ip) {\n record.ip = ip;\n await this.adapter.setStateAsync(`clients.${record.id}.ip`, { val: ip, ack: true });\n // If no hostname known yet, common.name falls back to the IP \u2014 keep it current.\n if (!record.hostname) {\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });\n }\n }\n if (hostname && hostname !== record.hostname) {\n record.hostname = hostname;\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: hostname } });\n }\n }\n\n private async readState(subId: string): Promise<unknown> {\n // v1.20.0 (F10): Helper aus coerce.ts \u2014 vorher dupliziert mit\n // global-config.safeGetState (gleicher try/catch-+-null-Fallback,\n // nur Pfad-Prefix anders).\n const s = await safeGetState(this.adapter, `clients.${subId}`);\n return s?.val ?? null;\n }\n}\n\n/**\n * Check whether a full state ID matches a client control datapoint and extract id + kind.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseClientStateId(\n fullId: string,\n namespace: string,\n): { id: string; kind: 'mode' | 'manualUrl' | 'remove' } | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // client-registry seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // global-config.parseGlobalStateId.\n const parts = parseAdapterStateId(fullId, namespace, CLIENTS_PREFIX, 2);\n if (!parts) {\n return null;\n }\n const [id, kind] = parts;\n // v1.9.0 (E5): empty id rejection (`clients..mode` would parse to id='').\n if (!id) {\n return null;\n }\n if (kind !== 'mode' && kind !== 'manualUrl' && kind !== 'remove') {\n return null;\n }\n return { id, kind };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["crypto"]
7
7
  }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var external_bridge_exports = {};
20
+ __export(external_bridge_exports, {
21
+ CONNECTION_STATUS_SCRIPT: () => CONNECTION_STATUS_SCRIPT
22
+ });
23
+ module.exports = __toCommonJS(external_bridge_exports);
24
+ const CONNECTION_STATUS_SCRIPT = `<script>
25
+ (function(){
26
+ function notifyConnected(){
27
+ try {
28
+ var v1Payload = JSON.stringify({id:1,type:"connection-status",payload:{event:"connected"}});
29
+ if (window.externalApp && typeof window.externalApp.externalBus === "function") {
30
+ window.externalApp.externalBus(v1Payload);
31
+ return;
32
+ }
33
+ if (window.externalAppV2 && typeof window.externalAppV2.postMessage === "function") {
34
+ window.externalAppV2.postMessage(JSON.stringify({
35
+ type:"externalBus",
36
+ payload:{id:1,type:"connection-status",payload:{event:"connected"}}
37
+ }));
38
+ }
39
+ } catch (e) { /* silent \u2014 bridge not present, this is a regular browser */ }
40
+ }
41
+ notifyConnected();
42
+ setTimeout(notifyConnected, 500);
43
+ setTimeout(notifyConnected, 2000);
44
+ })();
45
+ </script>`;
46
+ // Annotate the CommonJS export names for ESM import in node:
47
+ 0 && (module.exports = {
48
+ CONNECTION_STATUS_SCRIPT
49
+ });
50
+ //# sourceMappingURL=external-bridge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/external-bridge.ts"],
4
+ "sourcesContent": ["/**\n * HA Companion App bridge \u2014 emits a `connection-status: connected` message\n * via either V1 (`window.externalApp.externalBus`) or V2\n * (`window.externalAppV2.postMessage`).\n *\n * The HA Companion App's runtime WebView (FrontendViewModel) starts a 10 s\n * timer when a page begins loading and shows the \"Verbindung zu Home\n * Assistant nicht m\u00F6glich\" popup if it doesn't see this message. Real HA's\n * frontend fires the message after the WebSocket opens \u2014 hassemu doesn't\n * implement WebSocket, so we send the message directly from the loaded HTML.\n *\n * Three call sites at 0 / 500 / 2000 ms cover slow bridge attach in some\n * Companion App builds. After the first successful V1 invocation we early-\n * exit. If neither bridge is present (regular browser tab), the try/catch\n * swallows the no-op.\n *\n * NOTE \u2014 onboarding WebView vs. runtime WebView. The Onboarding flow uses\n * `HAWebViewClient` (error mapping) but **not** `FrontendJsBridge` \u2014 so\n * `window.externalApp` / `window.externalAppV2` are NOT injected on\n * `/auth/authorize`. We therefore don't emit this script from auth-page.ts;\n * the bridge call there would be inert. Sources verified:\n * - home-assistant/android FrontendMessageHandler.kt (parses message)\n * - home-assistant/android FrontendJsBridge.kt (V1+V2 registration)\n * - home-assistant/frontend src/external_app/external_messaging.ts\n * (fires on the `connection-status` DOM event)\n *\n * The constant is intentionally a `<script>` block that can be inlined into\n * any HTML response \u2014 caller pastes it verbatim before `</body>`.\n */\nexport const CONNECTION_STATUS_SCRIPT = `<script>\n(function(){\n function notifyConnected(){\n try {\n var v1Payload = JSON.stringify({id:1,type:\"connection-status\",payload:{event:\"connected\"}});\n if (window.externalApp && typeof window.externalApp.externalBus === \"function\") {\n window.externalApp.externalBus(v1Payload);\n return;\n }\n if (window.externalAppV2 && typeof window.externalAppV2.postMessage === \"function\") {\n window.externalAppV2.postMessage(JSON.stringify({\n type:\"externalBus\",\n payload:{id:1,type:\"connection-status\",payload:{event:\"connected\"}}\n }));\n }\n } catch (e) { /* silent \u2014 bridge not present, this is a regular browser */ }\n }\n notifyConnected();\n setTimeout(notifyConnected, 500);\n setTimeout(notifyConnected, 2000);\n})();\n</script>`;\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BO,MAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/global-config.ts"],
4
- "sourcesContent": ["/**\n * Global redirect override.\n *\n * Holds three datapoints:\n * - `global.enabled` \u2014 master switch. Toggling triggers a bulk-update of all\n * `clients.<id>.mode` (driven from {@link main.ts}, not from this class).\n * - `global.mode` \u2014 dropdown with discovered URLs plus the `'manual'` sentinel.\n * `'global'` is intentionally not allowed here (would be self-referential).\n * - `global.manualUrl` \u2014 free-text URL used when `global.mode === 'manual'`.\n *\n * The resolver delegates: a client whose `mode === 'global'` ends up here.\n */\n\nimport {\n buildDropdownStates,\n coerceBoolean,\n coerceSafeUrl,\n isNoChoice,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './constants';\nimport { resolveLabel } from './i18n-states';\nimport type { AdapterInterface, ClientRecord, UrlStates } from './types';\n\n/** Extended adapter interface \u2014 needs state I/O and object extend. */\nexport type GlobalConfigAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n 'getStateAsync' | 'setStateAsync' | 'getObjectAsync' | 'setObjectAsync' | 'extendObjectAsync'\n >;\n\n/** Kinds of state IDs the GlobalConfig reacts to. */\nexport type GlobalStateKind = 'mode' | 'manualUrl' | 'enabled';\n\n// Re-export f\u00FCr Backwards-Kompatibilit\u00E4t \u2014 Tests importieren direkt von hier.\nexport { MODE_GLOBAL, MODE_MANUAL } from './constants';\n\n/** Holds the runtime state of the global redirect override. */\nexport class GlobalConfig {\n private readonly adapter: GlobalConfigAdapter;\n private mode: string = '';\n private manualUrl: string | null = null;\n private enabled = false;\n\n /** @param adapter Adapter instance used for state and object I/O. */\n constructor(adapter: GlobalConfigAdapter) {\n this.adapter = adapter;\n }\n\n /** Loads the current global.* values from the broker. Call once on adapter start. */\n async restore(): Promise<void> {\n const modeState = await safeGetState(this.adapter, 'global.mode');\n const manualState = await safeGetState(this.adapter, 'global.manualUrl');\n const enabledState = await safeGetState(this.adapter, 'global.enabled');\n this.mode = typeof modeState?.val === 'string' ? modeState.val : '';\n this.manualUrl = coerceSafeUrl(manualState?.val);\n this.enabled = coerceBoolean(enabledState?.val) === true;\n\n // Promote a blank state-value (`''`/null/undefined) to numeric `0` so\n // the admin dropdown renders the `0='---'` option as selected. v1.2.0\n // installs left the value as `''` which doesn't match any common.states\n // entry, so the dropdown showed an empty selection.\n const v = modeState?.val;\n if (v === '' || v === null || v === undefined) {\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n }\n }\n\n /**\n * Resolves the redirect URL for `record`.\n *\n * Delegates via the client's `mode`:\n * - `'global'` \u2192 resolve global mode/manualUrl\n * - `'manual'` \u2192 client's manualUrl\n * - URL string \u2192 that URL\n * - empty / unknown \u2192 null (setup page)\n *\n * @param record Client to resolve for.\n */\n resolveUrlFor(record: ClientRecord): string | null {\n return this.resolveClientMode(record);\n }\n\n private resolveClientMode(record: ClientRecord): string | null {\n const m: unknown = record.mode;\n if (isNoChoice(m)) {\n return null;\n }\n if (m === MODE_GLOBAL) {\n return this.resolveGlobalMode();\n }\n if (m === MODE_MANUAL) {\n return record.manualUrl ?? null;\n }\n return coerceSafeUrl(m);\n }\n\n private resolveGlobalMode(): string | null {\n if (isNoChoice(this.mode)) {\n return null;\n }\n if (this.mode === MODE_MANUAL) {\n return this.manualUrl;\n }\n return coerceSafeUrl(this.mode);\n }\n\n /** Returns whether the master switch is currently active. */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Accept a write on `global.mode`. Allowed values: `'manual'` or a URL that\n * passes {@link coerceSafeUrl}. `'global'` is rejected (would be\n * self-referential). Empty string clears the choice.\n *\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(rawValue: unknown): Promise<void> {\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Erlaubte\n // Sentinels: nur MODE_MANUAL \u2014 MODE_GLOBAL w\u00E4re self-referential\n // (global.mode='global' \u2192 resolve global.mode \u2192 ...).\n const result = parseModeWrite(rawValue, [MODE_MANUAL]);\n switch (result.kind) {\n case 'no-choice':\n this.mode = '';\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n return;\n case 'rejected-non-string':\n this.adapter.log.warn(`global.mode rejected \u2014 non-string value`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode || 0, ack: true });\n return;\n case 'rejected-disallowed-sentinel':\n // MODE_GLOBAL bei global.mode \u2192 self-referential.\n this.adapter.log.warn(\n `global.mode rejected \u2014 \"global\" is not allowed at the global level (self-referential)`,\n );\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n case 'sentinel':\n if (result.value === MODE_MANUAL && !this.manualUrl) {\n this.adapter.log.warn(\n `global.mode is \"manual\" but global.manualUrl is empty \u2014 fill global.manualUrl to redirect`,\n );\n }\n this.mode = result.value;\n await this.adapter.setStateAsync('global.mode', { val: result.value, ack: true });\n return;\n case 'rejected-unsafe-url':\n this.adapter.log.warn(`global.mode rejected \u2014 unsafe URL value \"${result.raw}\"`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n case 'url':\n this.mode = result.value;\n await this.adapter.setStateAsync('global.mode', { val: result.value, ack: true });\n return;\n }\n }\n\n /**\n * Accept a write on `global.manualUrl`. Free-text \u2014 must pass\n * {@link coerceSafeUrl} (or be empty to clear).\n *\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(rawValue: unknown): Promise<void> {\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`global.manualUrl rejected \u2014 unsafe URL`);\n await this.adapter.setStateAsync('global.manualUrl', { val: this.manualUrl ?? '', ack: true });\n return;\n }\n this.manualUrl = result.safe;\n await this.adapter.setStateAsync('global.manualUrl', { val: result.safe ?? '', ack: true });\n if (this.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(\n `global.manualUrl cleared while global.mode is \"manual\" \u2014 clients delegating to global will see the setup page`,\n );\n }\n }\n\n /**\n * Accept a write on `global.enabled`. Persists the value but does NOT trigger\n * the bulk-sync of client modes \u2014 the caller (main.ts) does that, because it\n * holds the registry + url-discovery references needed for the sync.\n *\n * @param rawValue Value written to the state.\n */\n async handleEnabledWrite(rawValue: unknown): Promise<void> {\n const enabled = coerceBoolean(rawValue) === true;\n this.enabled = enabled;\n await this.adapter.setStateAsync('global.enabled', { val: enabled, ack: true });\n }\n\n /**\n * Updates the dropdown states (`common.states`) on `global.mode`.\n * The `'manual'` sentinel is added; `'global'` is NOT (would be self-referential).\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n // v1.20.0 (F4): buildDropdownStates Helper aus coerce.ts \u2014 vorher\n // hatten client-registry und global-config identische `0='---' +\n // sentinels + states`-Composition. Hier nur `manual`-Sentinel weil\n // `global` in global-config self-referential w\u00E4re.\n // v1.28.4: Sentinel-Label als plain-string in adapter.systemLanguage\n // (NICHT tLabel-Translation-Object) \u2014 Admin rendert common.states-VALUES\n // direkt als React-child und crasht bei Objects mit React Error #31.\n const merged = buildDropdownStates(\n { [MODE_MANUAL]: resolveLabel('manualUrl', this.adapter.systemLanguage) },\n states,\n );\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel bleiben drin nach Format-Wechsel (z.B. v1.26\u2192v1.27).\n // Object lesen, common.states ersetzen, setObjectAsync.\n const existing = await this.adapter.getObjectAsync('global.mode');\n if (!existing) {\n return;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync('global.mode', existing);\n }\n\n /**\n * Convenience for migration: set mode + manualUrl together. Skips the\n * write-side validation that {@link handleModeWrite} / {@link handleManualUrlWrite}\n * apply, because migration trusts the legacy values it carries forward.\n *\n * @param mode New mode value.\n * @param manualUrl New manualUrl, or null to clear.\n */\n async migrationSet(mode: string, manualUrl: string | null): Promise<void> {\n // v1.12.0 (C10): trust-Annahme aus dem Caller droppen. Wenn `mode` weder\n // 'manual' noch eine sichere URL ist, defaulten wir auf 'manual' (das\n // legt user-facing den Setup-Pfad nah und vermeidet schreiben von\n // unsicheren values wie `javascript:alert(1)`).\n const safeMode = mode === MODE_MANUAL || coerceSafeUrl(mode) ? mode : MODE_MANUAL;\n const safeManual = manualUrl !== null ? coerceSafeUrl(manualUrl) : null;\n this.mode = safeMode;\n this.manualUrl = safeManual;\n await this.adapter.setStateAsync('global.mode', { val: safeMode, ack: true });\n await this.adapter.setStateAsync('global.manualUrl', { val: safeManual ?? '', ack: true });\n }\n\n // v1.20.0 (F10): private safeGetState war duplicate zu coerce.ts:safeGetState \u2014\n // jetzt direkt importiert.\n}\n\n/**\n * Check whether a full state ID matches a global control datapoint.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseGlobalStateId(fullId: string, namespace: string): GlobalStateKind | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // global-config seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // client-registry.parseClientStateId.\n const parts = parseAdapterStateId(fullId, namespace, 'global.', 1);\n if (!parts) {\n return null;\n }\n const [tail] = parts;\n if (tail === 'mode' || tail === 'manualUrl' || tail === 'enabled') {\n return tail;\n }\n return null;\n}\n"],
4
+ "sourcesContent": ["/**\n * Global redirect override.\n *\n * Holds three datapoints:\n * - `global.enabled` \u2014 master switch. Toggling triggers a bulk-update of all\n * `clients.<id>.mode` (driven from {@link main.ts}, not from this class).\n * - `global.mode` \u2014 dropdown with discovered URLs plus the `'manual'` sentinel.\n * `'global'` is intentionally not allowed here (would be self-referential).\n * - `global.manualUrl` \u2014 free-text URL used when `global.mode === 'manual'`.\n *\n * The resolver delegates: a client whose `mode === 'global'` ends up here.\n */\n\nimport {\n buildDropdownStates,\n coerceBoolean,\n coerceSafeUrl,\n isNoChoice,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './constants';\nimport { resolveLabel } from './i18n-states';\nimport type { AdapterInterface, ClientRecord, UrlStates } from './types';\n\n/** Extended adapter interface \u2014 needs state I/O and object extend. */\nexport type GlobalConfigAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n 'getStateAsync' | 'setStateAsync' | 'getObjectAsync' | 'setObjectAsync' | 'extendObjectAsync'\n >;\n\n/** Kinds of state IDs the GlobalConfig reacts to. */\nexport type GlobalStateKind = 'mode' | 'manualUrl' | 'enabled';\n\n// Re-export f\u00FCr Backwards-Kompatibilit\u00E4t \u2014 Tests importieren direkt von hier.\nexport { MODE_GLOBAL, MODE_MANUAL } from './constants';\n\n/** Holds the runtime state of the global redirect override. */\nexport class GlobalConfig {\n private readonly adapter: GlobalConfigAdapter;\n private mode: string = '';\n private manualUrl: string | null = null;\n private enabled = false;\n\n /** @param adapter Adapter instance used for state and object I/O. */\n constructor(adapter: GlobalConfigAdapter) {\n this.adapter = adapter;\n }\n\n /** Loads the current global.* values from the broker. Call once on adapter start. */\n async restore(): Promise<void> {\n const modeState = await safeGetState(this.adapter, 'global.mode');\n const manualState = await safeGetState(this.adapter, 'global.manualUrl');\n const enabledState = await safeGetState(this.adapter, 'global.enabled');\n this.mode = typeof modeState?.val === 'string' ? modeState.val : '';\n this.manualUrl = coerceSafeUrl(manualState?.val);\n this.enabled = coerceBoolean(enabledState?.val) === true;\n\n // Promote a blank state-value (`''`/null/undefined) to numeric `0` so\n // the admin dropdown renders the `0='---'` option as selected. v1.2.0\n // installs left the value as `''` which doesn't match any common.states\n // entry, so the dropdown showed an empty selection.\n const v = modeState?.val;\n if (v === '' || v === null || v === undefined) {\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n }\n }\n\n /**\n * Resolves the redirect URL for `record`.\n *\n * Delegates via the client's `mode`:\n * - `'global'` \u2192 resolve global mode/manualUrl\n * - `'manual'` \u2192 client's manualUrl\n * - URL string \u2192 that URL\n * - empty / unknown \u2192 null (setup page)\n *\n * @param record Client to resolve for.\n */\n resolveUrlFor(record: ClientRecord): string | null {\n return this.resolveClientMode(record);\n }\n\n private resolveClientMode(record: ClientRecord): string | null {\n const m: unknown = record.mode;\n if (isNoChoice(m)) {\n return null;\n }\n if (m === MODE_GLOBAL) {\n return this.resolveGlobalMode();\n }\n if (m === MODE_MANUAL) {\n return record.manualUrl ?? null;\n }\n return coerceSafeUrl(m);\n }\n\n private resolveGlobalMode(): string | null {\n if (isNoChoice(this.mode)) {\n return null;\n }\n if (this.mode === MODE_MANUAL) {\n return this.manualUrl;\n }\n return coerceSafeUrl(this.mode);\n }\n\n /** Returns whether the master switch is currently active. */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Accept a write on `global.mode`. Allowed values: `'manual'` or a URL that\n * passes {@link coerceSafeUrl}. `'global'` is rejected (would be\n * self-referential). Empty string clears the choice.\n *\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(rawValue: unknown): Promise<void> {\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Erlaubte\n // Sentinels: nur MODE_MANUAL \u2014 MODE_GLOBAL w\u00E4re self-referential\n // (global.mode='global' \u2192 resolve global.mode \u2192 ...).\n const result = parseModeWrite(rawValue, [MODE_MANUAL]);\n switch (result.kind) {\n case 'no-choice':\n this.mode = '';\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n return;\n case 'rejected-non-string':\n this.adapter.log.warn(`global.mode rejected \u2014 non-string value`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode || 0, ack: true });\n return;\n case 'rejected-disallowed-sentinel':\n // MODE_GLOBAL bei global.mode \u2192 self-referential.\n this.adapter.log.warn(\n `global.mode rejected \u2014 \"global\" is not allowed at the global level (self-referential)`,\n );\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n case 'sentinel':\n if (result.value === MODE_MANUAL && !this.manualUrl) {\n this.adapter.log.warn(\n `global.mode is \"manual\" but global.manualUrl is empty \u2014 fill global.manualUrl to redirect`,\n );\n }\n this.mode = result.value;\n await this.adapter.setStateAsync('global.mode', { val: result.value, ack: true });\n return;\n case 'rejected-unsafe-url':\n this.adapter.log.warn(`global.mode rejected \u2014 unsafe URL value \"${result.raw}\"`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n case 'url':\n this.mode = result.value;\n await this.adapter.setStateAsync('global.mode', { val: result.value, ack: true });\n return;\n }\n }\n\n /**\n * Accept a write on `global.manualUrl`. Free-text \u2014 must pass\n * {@link coerceSafeUrl} (or be empty to clear).\n *\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(rawValue: unknown): Promise<void> {\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`global.manualUrl rejected \u2014 unsafe URL`);\n await this.adapter.setStateAsync('global.manualUrl', { val: this.manualUrl ?? '', ack: true });\n return;\n }\n this.manualUrl = result.safe;\n await this.adapter.setStateAsync('global.manualUrl', { val: result.safe ?? '', ack: true });\n if (this.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(\n `global.manualUrl cleared while global.mode is \"manual\" \u2014 clients delegating to global will see the setup page`,\n );\n }\n }\n\n /**\n * Accept a write on `global.enabled`. Persists the value but does NOT trigger\n * the bulk-sync of client modes \u2014 the caller (main.ts) does that, because it\n * holds the registry + url-discovery references needed for the sync.\n *\n * @param rawValue Value written to the state.\n */\n async handleEnabledWrite(rawValue: unknown): Promise<void> {\n const enabled = coerceBoolean(rawValue) === true;\n this.enabled = enabled;\n await this.adapter.setStateAsync('global.enabled', { val: enabled, ack: true });\n }\n\n /**\n * Updates the dropdown states (`common.states`) on `global.mode`.\n * The `'manual'` sentinel is added; `'global'` is NOT (would be self-referential).\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n // v1.20.0 (F4): buildDropdownStates Helper aus coerce.ts \u2014 vorher\n // hatten client-registry und global-config identische `0='---' +\n // sentinels + states`-Composition. Hier nur `manual`-Sentinel weil\n // `global` in global-config self-referential w\u00E4re.\n // v1.28.4: Sentinel-Label als plain-string in adapter.systemLanguage\n // (NICHT als Translation-Object) \u2014 Admin rendert common.states-VALUES\n // direkt als React-child und crasht bei Objects mit React Error #31.\n const merged = buildDropdownStates(\n { [MODE_MANUAL]: resolveLabel('manualUrl', this.adapter.systemLanguage) },\n states,\n );\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel bleiben drin nach Format-Wechsel (z.B. v1.26\u2192v1.27).\n // Object lesen, common.states ersetzen, setObjectAsync.\n const existing = await this.adapter.getObjectAsync('global.mode');\n if (!existing) {\n return;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync('global.mode', existing);\n }\n\n /**\n * Convenience for migration: set mode + manualUrl together. Skips the\n * write-side validation that {@link handleModeWrite} / {@link handleManualUrlWrite}\n * apply, because migration trusts the legacy values it carries forward.\n *\n * @param mode New mode value.\n * @param manualUrl New manualUrl, or null to clear.\n */\n async migrationSet(mode: string, manualUrl: string | null): Promise<void> {\n // v1.12.0 (C10): trust-Annahme aus dem Caller droppen. Wenn `mode` weder\n // 'manual' noch eine sichere URL ist, defaulten wir auf 'manual' (das\n // legt user-facing den Setup-Pfad nah und vermeidet schreiben von\n // unsicheren values wie `javascript:alert(1)`).\n const safeMode = mode === MODE_MANUAL || coerceSafeUrl(mode) ? mode : MODE_MANUAL;\n const safeManual = manualUrl !== null ? coerceSafeUrl(manualUrl) : null;\n this.mode = safeMode;\n this.manualUrl = safeManual;\n await this.adapter.setStateAsync('global.mode', { val: safeMode, ack: true });\n await this.adapter.setStateAsync('global.manualUrl', { val: safeManual ?? '', ack: true });\n }\n\n // v1.20.0 (F10): private safeGetState war duplicate zu coerce.ts:safeGetState \u2014\n // jetzt direkt importiert.\n}\n\n/**\n * Check whether a full state ID matches a global control datapoint.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseGlobalStateId(fullId: string, namespace: string): GlobalStateKind | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // global-config seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // client-registry.parseClientStateId.\n const parts = parseAdapterStateId(fullId, namespace, 'global.', 1);\n if (!parts) {\n return null;\n }\n const [tail] = parts;\n if (tail === 'mode' || tail === 'manualUrl' || tail === 'enabled') {\n return tail;\n }\n return null;\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["import_constants"]
7
7
  }
@@ -23,7 +23,6 @@ __export(i18n_states_exports, {
23
23
  STATE_NAMES: () => STATE_NAMES,
24
24
  resolveLabel: () => resolveLabel,
25
25
  tDesc: () => tDesc,
26
- tLabel: () => tLabel,
27
26
  tName: () => tName
28
27
  });
29
28
  module.exports = __toCommonJS(i18n_states_exports);
@@ -273,9 +272,6 @@ function tName(key) {
273
272
  function tDesc(key) {
274
273
  return STATE_DESCS[key];
275
274
  }
276
- function tLabel(key) {
277
- return STATE_LABELS[key];
278
- }
279
275
  function resolveLabel(key, lang) {
280
276
  var _a, _b;
281
277
  const obj = STATE_LABELS[key];
@@ -292,7 +288,6 @@ function resolveLabel(key, lang) {
292
288
  STATE_NAMES,
293
289
  resolveLabel,
294
290
  tDesc,
295
- tLabel,
296
291
  tName
297
292
  });
298
293
  //# sourceMappingURL=i18n-states.js.map