iobroker.hassemu 1.3.1 → 1.3.3

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
@@ -147,44 +147,27 @@ Reverse DNS on a home LAN depends on your router/DHCP server and often fails. Th
147
147
  ---
148
148
 
149
149
  ## Changelog
150
- ### 1.3.1 (2026-04-30)
150
+ ### 1.3.3 (2026-05-01)
151
+ - Documentation: rewrote release notes for v1.1.4–v1.3.2 in user-friendly style across all languages.
151
152
 
152
- - Hotfix for legacy v1.1.x clients: their `visUrl` channel did not have `mode` / `manualUrl` objects. The v1.2.0 migration wrote states without the matching objects, which the broker logged as `State has no existing object` and rendered the `mode` datapoint without a name or dropdown in the object browser. `ClientRegistry.restore()` now calls an idempotent `ensureObjects()` for every client, so the v1.2.0+ object shapes exist before any migration writes happen.
153
- - Mode dropdown gains a numeric `0 = "---"` no-choice fallback (analogous to govee-smart's pattern). Existing displays keep their setting; new displays start at `0` and the resolver falls back to the landing page until a real choice is made.
153
+ ### 1.3.2 (2026-04-30)
154
+ - Fix: dropdown default `---` now applied correctly on upgrades from older v1.1.x clients (was empty after migration).
154
155
 
155
- ### 1.3.0 (2026-04-30)
156
+ ### 1.3.1 (2026-04-30)
157
+ - Fix: legacy v1.1.x clients without `mode`/`manualUrl` objects now get migrated correctly on first start.
158
+ - Mode dropdown gains a `0 = "---"` no-choice fallback — new displays start without a target until a real choice is made.
156
159
 
157
- - Security: brute-force lockout on `/auth/login_flow/:flowId` — after 5 failed credential attempts an IP is rejected with HTTP 429 for 15 min. Successful login resets the counter.
158
- - DRY refactor: shared `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold are now named constants instead of magic numbers.
159
- - Dead-code cleanup: `resolveBindToReachable`, `coerceUuid` strict-V4 parameter, `DEFAULT_REFRESH_DEBOUNCE_MS` export, internal `getMode`/`getManualUrl` test affordances — all removed; tests rewritten to assert observable behaviour.
160
- - New `landing-page` test suite with XSS-escape coverage and 11-language fallback verification.
161
- - Emulated Home Assistant version bumped from 2026.3.1 to 2026.4.0.
160
+ ### 1.3.0 (2026-04-30)
161
+ - Security: brute-force lockout on login (5 failed attempts IP blocked for 15 min, successful login resets the counter).
162
+ - Emulated Home Assistant version bumped to 2026.4.0.
162
163
 
163
164
  ### 1.2.0 (2026-04-29)
164
-
165
- - Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Migration runs automatically.
166
- - Master switch `global.enabled` syncs every display: on → all follow the global URL, off → each display picks up its own again.
165
+ - **Breaking:** Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Existing setups auto-migrated.
166
+ - Master switch `global.enabled` syncs every display (on all follow global URL, off each display picks up its own).
167
167
  - Idle displays without auth token are auto-removed after 30 days.
168
168
  - Security hardening of the auth flow.
169
169
  - `web` adapter declared as dependency.
170
170
 
171
- ### 1.1.6 (2026-04-28)
172
-
173
- - Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:
174
- - Test setup migrated: tests now live next to source as `src/lib/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`
175
- - `@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: ">=20"`
176
- - Dependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`
177
- - `nyc` config + `coverage` script added
178
- - Orphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)
179
-
180
- ### 1.1.5 (2026-04-26)
181
-
182
- - Process-level `unhandledRejection` / `uncaughtException` handlers added as last-line-of-defence against fire-and-forget rejections.
183
- - Stop shipping the `manual-review` release-script plugin — adapter-only consequence.
184
- - Audit-driven boilerplate sync with the other krobi adapters (`.vscode` json5 schemas, `tsconfig.test` looser test rules).
185
- - Min js-controller correction: was `>=7.0.0`, restored to repochecker-recommended `>=6.0.11` (Source: `ioBroker.repochecker/lib/M1000_IOPackageJson.js`).
186
- - `@types/iobroker` bumped to `^7.1.1`.
187
-
188
171
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
189
172
 
190
173
  ## Support
@@ -113,6 +113,10 @@ class ClientRegistry {
113
113
  const record = { id, cookie, token, mode, manualUrl, ip, hostname };
114
114
  this.trackInMemory(record);
115
115
  await this.ensureObjects(record);
116
+ const modeStateRaw = await this.readState(`${id}.mode`);
117
+ if (modeStateRaw === "" || modeStateRaw === null || modeStateRaw === void 0) {
118
+ await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });
119
+ }
116
120
  }
117
121
  this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);
118
122
  }
@@ -397,20 +401,18 @@ class ClientRegistry {
397
401
  var _a;
398
402
  const { id, cookie, ip, hostname } = record;
399
403
  const mergedStates = this.buildModeStates();
404
+ await this.adapter.setObjectNotExistsAsync(`clients.${id}`, {
405
+ type: "channel",
406
+ common: { name: (_a = hostname != null ? hostname : ip) != null ? _a : id },
407
+ native: { cookie, token: null }
408
+ });
400
409
  await Promise.all([
401
- this.adapter.setObjectNotExistsAsync(`clients.${id}`, {
402
- type: "channel",
403
- common: { name: (_a = hostname != null ? hostname : ip) != null ? _a : id },
404
- native: { cookie, token: null }
405
- }),
406
- this.adapter.setObjectNotExistsAsync(`clients.${id}.mode`, {
410
+ this.adapter.extendObjectAsync(`clients.${id}.mode`, {
407
411
  type: "state",
408
412
  common: {
409
413
  name: "Redirect mode",
410
414
  // 'mixed' future-proofs against the upcoming js-controller
411
- // strict-type cast (see govee-smart v1.11.0 pattern). User
412
- // can write Number/String/Sentinel from Blockly/scripts
413
- // without "expects type X but received Y" warnings.
415
+ // strict-type cast (see govee-smart v1.11.0 pattern).
414
416
  type: "mixed",
415
417
  role: "value",
416
418
  read: true,
@@ -420,7 +422,7 @@ class ClientRegistry {
420
422
  },
421
423
  native: {}
422
424
  }),
423
- this.adapter.setObjectNotExistsAsync(`clients.${id}.manualUrl`, {
425
+ this.adapter.extendObjectAsync(`clients.${id}.manualUrl`, {
424
426
  type: "state",
425
427
  common: {
426
428
  name: "Manual URL",
@@ -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 { coerceString, coerceUuid, coerceSafeUrl, isPlainObject, parseManualUrlWrite } from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './global-config';\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 | 'setObjectNotExistsAsync'\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 /** @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 const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n const modeRaw = await this.readState(`${id}.mode`);\n const mode = typeof modeRaw === 'string' ? modeRaw : '';\n const manualUrl = coerceSafeUrl(await this.readState(`${id}.manualUrl`));\n const ip = coerceString(await this.readState(`${id}.ip`));\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(await this.readState(`${id}.hostname`));\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 }\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 */\n async identifyOrCreate(cookie: string | null, ip: string | null, hostname: string | null): 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 if (ip) {\n const pending = this.pendingByIp.get(ip);\n if (pending) {\n return pending;\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(ip, promise);\n try {\n return await promise;\n } finally {\n this.pendingByIp.delete(ip);\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 // No-choice marker: numeric 0 (default), string '0', or empty string \u2014\n // all clear the choice and trigger the landing page.\n if (rawValue === 0 || rawValue === '0' || rawValue === '') {\n record.mode = '';\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n return;\n }\n if (typeof rawValue !== 'string') {\n this.adapter.log.warn(`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 }\n if (rawValue === MODE_GLOBAL || rawValue === MODE_MANUAL) {\n if (rawValue === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `client-registry: ${id} mode set to 'manual' but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = rawValue;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: rawValue, ack: true });\n return;\n }\n const safe = coerceSafeUrl(rawValue);\n if (!safe) {\n this.adapter.log.warn(`client-registry: rejected unsafe mode value for ${id}: '${rawValue}'`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n }\n record.mode = safe;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: safe, ack: true });\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-registry: rejected unsafe manualUrl for ${id}`);\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-registry: ${id} manualUrl cleared while mode='manual' \u2014 display will hit 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 let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n await this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true });\n changed++;\n }\n if (changed > 0) {\n this.adapter.log.info(`client-registry: bulk-set mode='${value}' on ${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 try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n this.adapter.log.warn(`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 await this.adapter.extendObjectAsync(`clients.${id}.mode`, {\n common: { states: merged },\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(`New client registered: ${id}${ip ? ` (${hostname ?? ip})` : ''}, mode='${mode}'`);\n return record;\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 * 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 return {\n 0: '---',\n [MODE_GLOBAL]: 'Global URL',\n [MODE_MANUAL]: 'Manual URL',\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 await Promise.all([\n this.adapter.setObjectNotExistsAsync(`clients.${id}`, {\n type: 'channel',\n common: { name: hostname ?? ip ?? id },\n native: { cookie, token: null },\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.mode`, {\n type: 'state',\n common: {\n name: 'Redirect mode',\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern). User\n // can write Number/String/Sentinel from Blockly/scripts\n // without \"expects type X but received Y\" warnings.\n type: 'mixed',\n role: 'value',\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.manualUrl`, {\n type: 'state',\n common: {\n name: 'Manual URL',\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: { name: 'Client IP', type: 'string', role: 'info.ip', read: true, write: false, def: '' },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: 'state',\n common: {\n name: 'Forget this client',\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 try {\n const s = await this.adapter.getStateAsync(`clients.${subId}`);\n return s?.val ?? null;\n } catch {\n return null;\n }\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 const prefix = `${namespace}.${CLIENTS_PREFIX}`;\n if (!fullId.startsWith(prefix)) {\n return null;\n }\n const tail = fullId.substring(prefix.length);\n const parts = tail.split('.');\n if (parts.length !== 2) {\n return null;\n }\n const [id, kind] = parts;\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 { coerceString, coerceUuid, coerceSafeUrl, isPlainObject, parseManualUrlWrite } from './coerce';\nimport { MODE_GLOBAL, MODE_MANUAL } from './global-config';\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 | 'setObjectNotExistsAsync'\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 /** @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 const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n const modeRaw = await this.readState(`${id}.mode`);\n const mode = typeof modeRaw === 'string' ? modeRaw : '';\n const manualUrl = coerceSafeUrl(await this.readState(`${id}.manualUrl`));\n const ip = coerceString(await this.readState(`${id}.ip`));\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(await this.readState(`${id}.hostname`));\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 }\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 */\n async identifyOrCreate(cookie: string | null, ip: string | null, hostname: string | null): 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 if (ip) {\n const pending = this.pendingByIp.get(ip);\n if (pending) {\n return pending;\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(ip, promise);\n try {\n return await promise;\n } finally {\n this.pendingByIp.delete(ip);\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 // No-choice marker: numeric 0 (default), string '0', or empty string \u2014\n // all clear the choice and trigger the landing page.\n if (rawValue === 0 || rawValue === '0' || rawValue === '') {\n record.mode = '';\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n return;\n }\n if (typeof rawValue !== 'string') {\n this.adapter.log.warn(`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 }\n if (rawValue === MODE_GLOBAL || rawValue === MODE_MANUAL) {\n if (rawValue === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `client-registry: ${id} mode set to 'manual' but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = rawValue;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: rawValue, ack: true });\n return;\n }\n const safe = coerceSafeUrl(rawValue);\n if (!safe) {\n this.adapter.log.warn(`client-registry: rejected unsafe mode value for ${id}: '${rawValue}'`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n }\n record.mode = safe;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: safe, ack: true });\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-registry: rejected unsafe manualUrl for ${id}`);\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-registry: ${id} manualUrl cleared while mode='manual' \u2014 display will hit 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 let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n await this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true });\n changed++;\n }\n if (changed > 0) {\n this.adapter.log.info(`client-registry: bulk-set mode='${value}' on ${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 try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n this.adapter.log.warn(`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 await this.adapter.extendObjectAsync(`clients.${id}.mode`, {\n common: { states: merged },\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(`New client registered: ${id}${ip ? ` (${hostname ?? ip})` : ''}, mode='${mode}'`);\n return record;\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 * 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 return {\n 0: '---',\n [MODE_GLOBAL]: 'Global URL',\n [MODE_MANUAL]: 'Manual URL',\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: extendObjectAsync \u2014 REPAIRS partial objects from the v1.2.0\n // migration bug (extendObjectAsync was called with only common.type:'mixed',\n // creating an object without top-level type/name/role/read/write/def).\n // Using extend instead of setObjectNotExists merges missing properties\n // onto the existing partial object so js-controller stops warning\n // \"obj.type has to exist\" and the dropdown renders correctly.\n await Promise.all([\n this.adapter.extendObjectAsync(`clients.${id}.mode`, {\n type: 'state',\n common: {\n name: 'Redirect mode',\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern).\n type: 'mixed',\n role: 'value',\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n },\n native: {},\n }),\n this.adapter.extendObjectAsync(`clients.${id}.manualUrl`, {\n type: 'state',\n common: {\n name: 'Manual URL',\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: { name: 'Client IP', type: 'string', role: 'info.ip', read: true, write: false, def: '' },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: 'state',\n common: {\n name: 'Forget this client',\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 try {\n const s = await this.adapter.getStateAsync(`clients.${subId}`);\n return s?.val ?? null;\n } catch {\n return null;\n }\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 const prefix = `${namespace}.${CLIENTS_PREFIX}`;\n if (!fullId.startsWith(prefix)) {\n return null;\n }\n const tail = fullId.substring(prefix.length);\n const parts = tail.split('.');\n if (parts.length !== 2) {\n return null;\n }\n const [id, kind] = parts;\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
  }
@@ -44,6 +44,10 @@ class GlobalConfig {
44
44
  this.mode = typeof (modeState == null ? void 0 : modeState.val) === "string" ? modeState.val : "";
45
45
  this.manualUrl = (0, import_coerce.coerceSafeUrl)(manualState == null ? void 0 : manualState.val);
46
46
  this.enabled = (0, import_coerce.coerceBoolean)(enabledState == null ? void 0 : enabledState.val) === true;
47
+ const v = modeState == null ? void 0 : modeState.val;
48
+ if (v === "" || v === null || v === void 0) {
49
+ await this.adapter.setStateAsync("global.mode", { val: 0, ack: true });
50
+ }
47
51
  }
48
52
  /**
49
53
  * Resolves the redirect URL for `record`.
@@ -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 { coerceBoolean, coerceSafeUrl, parseManualUrlWrite } from './coerce';\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<ioBroker.Adapter, 'getStateAsync' | 'setStateAsync' | 'extendObjectAsync'>;\n\n/** Kinds of state IDs the GlobalConfig reacts to. */\nexport type GlobalStateKind = 'mode' | 'manualUrl' | 'enabled';\n\n/** Sentinel value: client delegates to global. Not legal as `global.mode`. */\nexport const MODE_GLOBAL = 'global';\n/** Sentinel value: use the matching manualUrl datapoint. */\nexport const MODE_MANUAL = 'manual';\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 this.safeGetState('global.mode');\n const manualState = await this.safeGetState('global.manualUrl');\n const enabledState = await this.safeGetState('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\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 // No-choice markers: numeric 0, string '0', empty string \u2014 all \u2192 null\n if (m === 0 || m === '0' || 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 const m: unknown = this.mode;\n if (m === 0 || m === '0' || m === '') {\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 // No-choice markers: numeric 0, string '0', empty string \u2014 all clear the choice\n if (rawValue === 0 || rawValue === '0' || rawValue === '') {\n this.mode = '';\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n return;\n }\n if (typeof rawValue !== 'string') {\n this.adapter.log.warn('global-config: rejected non-string global.mode');\n await this.adapter.setStateAsync('global.mode', { val: this.mode || 0, ack: true });\n return;\n }\n if (rawValue === MODE_GLOBAL) {\n this.adapter.log.warn(\"global-config: 'global' is not allowed as global.mode (self-referential)\");\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n }\n if (rawValue === MODE_MANUAL) {\n if (!this.manualUrl) {\n this.adapter.log.warn(\n \"global-config: global.mode set to 'manual' but global.manualUrl is empty \u2014 fill it to redirect\",\n );\n }\n this.mode = MODE_MANUAL;\n await this.adapter.setStateAsync('global.mode', { val: MODE_MANUAL, ack: true });\n return;\n }\n const safe = coerceSafeUrl(rawValue);\n if (!safe) {\n this.adapter.log.warn(`global-config: rejected unsafe global.mode value '${rawValue}'`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n }\n this.mode = safe;\n await this.adapter.setStateAsync('global.mode', { val: safe, ack: true });\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-config: rejected unsafe global.manualUrl');\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-config: global.manualUrl cleared while global.mode='manual' \u2014 clients delegating to global will hit 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 // 0='---' is the no-choice fallback (analogous to govee-smart pattern).\n // 'global' is intentionally NOT in this map \u2014 it would be self-referential.\n const merged: UrlStates = { 0: '---', [MODE_MANUAL]: 'Manual URL', ...states };\n await this.adapter.extendObjectAsync('global.mode', {\n common: { states: merged },\n });\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 this.mode = mode;\n this.manualUrl = manualUrl;\n await this.adapter.setStateAsync('global.mode', { val: mode, ack: true });\n await this.adapter.setStateAsync('global.manualUrl', { val: manualUrl ?? '', ack: true });\n }\n\n private async safeGetState(id: string): Promise<ioBroker.State | null> {\n try {\n return (await this.adapter.getStateAsync(id)) ?? null;\n } catch {\n return null;\n }\n }\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 const prefix = `${namespace}.global.`;\n if (!fullId.startsWith(prefix)) {\n return null;\n }\n const tail = fullId.substring(prefix.length);\n if (tail === 'mode' || tail === 'manualUrl' || tail === 'enabled') {\n return tail;\n }\n return null;\n}\n"],
5
- "mappings": "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",
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 { coerceBoolean, coerceSafeUrl, parseManualUrlWrite } from './coerce';\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<ioBroker.Adapter, 'getStateAsync' | 'setStateAsync' | 'extendObjectAsync'>;\n\n/** Kinds of state IDs the GlobalConfig reacts to. */\nexport type GlobalStateKind = 'mode' | 'manualUrl' | 'enabled';\n\n/** Sentinel value: client delegates to global. Not legal as `global.mode`. */\nexport const MODE_GLOBAL = 'global';\n/** Sentinel value: use the matching manualUrl datapoint. */\nexport const MODE_MANUAL = 'manual';\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 this.safeGetState('global.mode');\n const manualState = await this.safeGetState('global.manualUrl');\n const enabledState = await this.safeGetState('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 // No-choice markers: numeric 0, string '0', empty string \u2014 all \u2192 null\n if (m === 0 || m === '0' || 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 const m: unknown = this.mode;\n if (m === 0 || m === '0' || m === '') {\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 // No-choice markers: numeric 0, string '0', empty string \u2014 all clear the choice\n if (rawValue === 0 || rawValue === '0' || rawValue === '') {\n this.mode = '';\n await this.adapter.setStateAsync('global.mode', { val: 0, ack: true });\n return;\n }\n if (typeof rawValue !== 'string') {\n this.adapter.log.warn('global-config: rejected non-string global.mode');\n await this.adapter.setStateAsync('global.mode', { val: this.mode || 0, ack: true });\n return;\n }\n if (rawValue === MODE_GLOBAL) {\n this.adapter.log.warn(\"global-config: 'global' is not allowed as global.mode (self-referential)\");\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n }\n if (rawValue === MODE_MANUAL) {\n if (!this.manualUrl) {\n this.adapter.log.warn(\n \"global-config: global.mode set to 'manual' but global.manualUrl is empty \u2014 fill it to redirect\",\n );\n }\n this.mode = MODE_MANUAL;\n await this.adapter.setStateAsync('global.mode', { val: MODE_MANUAL, ack: true });\n return;\n }\n const safe = coerceSafeUrl(rawValue);\n if (!safe) {\n this.adapter.log.warn(`global-config: rejected unsafe global.mode value '${rawValue}'`);\n await this.adapter.setStateAsync('global.mode', { val: this.mode, ack: true });\n return;\n }\n this.mode = safe;\n await this.adapter.setStateAsync('global.mode', { val: safe, ack: true });\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-config: rejected unsafe global.manualUrl');\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-config: global.manualUrl cleared while global.mode='manual' \u2014 clients delegating to global will hit 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 // 0='---' is the no-choice fallback (analogous to govee-smart pattern).\n // 'global' is intentionally NOT in this map \u2014 it would be self-referential.\n const merged: UrlStates = { 0: '---', [MODE_MANUAL]: 'Manual URL', ...states };\n await this.adapter.extendObjectAsync('global.mode', {\n common: { states: merged },\n });\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 this.mode = mode;\n this.manualUrl = manualUrl;\n await this.adapter.setStateAsync('global.mode', { val: mode, ack: true });\n await this.adapter.setStateAsync('global.manualUrl', { val: manualUrl ?? '', ack: true });\n }\n\n private async safeGetState(id: string): Promise<ioBroker.State | null> {\n try {\n return (await this.adapter.getStateAsync(id)) ?? null;\n } catch {\n return null;\n }\n }\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 const prefix = `${namespace}.global.`;\n if (!fullId.startsWith(prefix)) {\n return null;\n }\n const tail = fullId.substring(prefix.length);\n if (tail === 'mode' || tail === 'manualUrl' || tail === 'enabled') {\n return tail;\n }\n return null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -68,6 +68,7 @@ class HassEmu extends utils.Adapter {
68
68
  await this.registry.restore();
69
69
  await this.migrateLegacyDefaultVisUrl();
70
70
  await this.migrateVisUrlToMode();
71
+ await this.repairGlobalSchemas();
71
72
  await this.gcStaleClients();
72
73
  const instanceUuid = import_node_crypto.default.randomUUID();
73
74
  this.log.debug(
@@ -221,13 +222,49 @@ class HassEmu extends utils.Adapter {
221
222
  } catch {
222
223
  }
223
224
  }
225
+ }
226
+ /**
227
+ * Repairs partial-formed `global.mode` / `global.manualUrl` objects from
228
+ * the v1.2.0 migration bug (extendObjectAsync was called with only
229
+ * `common.type:'mixed'` — leaving the object without top-level `type`,
230
+ * name, role, read, write, def). `extendObjectAsync` here merges the full
231
+ * instanceObjects schema onto the existing partial object so js-controller
232
+ * stops warning "obj.type has to exist" and the dropdown renders correctly.
233
+ *
234
+ * Idempotent — extending an already-complete object is a no-op write.
235
+ */
236
+ async repairGlobalSchemas() {
224
237
  try {
225
238
  await this.extendObjectAsync("global.mode", {
226
239
  type: "state",
227
- common: { type: "mixed" }
240
+ common: {
241
+ name: "Global redirect mode",
242
+ type: "mixed",
243
+ role: "value",
244
+ read: true,
245
+ write: true,
246
+ def: 0
247
+ },
248
+ native: {}
249
+ });
250
+ } catch (err) {
251
+ this.log.debug(`repair global.mode failed: ${String(err)}`);
252
+ }
253
+ try {
254
+ await this.extendObjectAsync("global.manualUrl", {
255
+ type: "state",
256
+ common: {
257
+ name: "Global manual URL (used when mode='manual')",
258
+ type: "string",
259
+ role: "url",
260
+ read: true,
261
+ write: true,
262
+ def: ""
263
+ },
264
+ native: {}
228
265
  });
229
266
  } catch (err) {
230
- this.log.debug(`Migration: extend global.mode failed: ${String(err)}`);
267
+ this.log.debug(`repair global.manualUrl failed: ${String(err)}`);
231
268
  }
232
269
  }
233
270
  /**
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import crypto from 'node:crypto';\nimport * as utils from '@iobroker/adapter-core';\nimport { ClientRegistry, parseClientStateId } from './lib/client-registry';\nimport { coerceSafeUrl } from './lib/coerce';\nimport { GlobalConfig, MODE_GLOBAL, MODE_MANUAL, parseGlobalStateId } from './lib/global-config';\nimport { MDNSService } from './lib/mdns';\nimport { UrlDiscovery } from './lib/url-discovery';\nimport { WebServer } from './lib/webserver';\nimport type { AdapterConfig } from './lib/types';\n\n/** Stale-Client-GC threshold: clients without token + lastSeen older are auto-removed. */\nconst STALE_CLIENT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\nclass HassEmu extends utils.Adapter {\n private mdnsService: MDNSService | null = null;\n private webServer: WebServer | null = null;\n private registry: ClientRegistry | null = null;\n private globalConfig: GlobalConfig | null = null;\n private urlDiscovery: UrlDiscovery | null = null;\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n\n declare config: AdapterConfig;\n\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: 'hassemu' });\n\n this.on('ready', () => {\n this.onReady().catch(err => this.log.error(`onReady unhandled: ${String(err)}`));\n });\n this.on('stateChange', (id, state) => {\n this.onStateChange(id, state).catch(err => this.log.error(`stateChange unhandled: ${String(err)}`));\n });\n this.on('objectChange', () => {\n // Foreign object changed \u2014 refresh URL dropdown (debounced inside discovery)\n this.urlDiscovery?.scheduleRefresh();\n });\n this.on('unload', this.onUnload.bind(this));\n\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler wrappers cover documented async\n // paths; this catches anything that slips past during refactors.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.message}`);\n };\n process.on('unhandledRejection', this.unhandledRejectionHandler);\n process.on('uncaughtException', this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n await this.setState('info.connection', { val: false, ack: true });\n\n this.globalConfig = new GlobalConfig(this);\n await this.globalConfig.restore();\n\n this.registry = new ClientRegistry(this);\n await this.registry.restore();\n\n // Migrations run before subscriptions / webserver \u2014 first the legacy\n // 1.0.x-style native config, then the visUrl \u2192 mode/manualUrl move.\n await this.migrateLegacyDefaultVisUrl();\n await this.migrateVisUrlToMode();\n\n // Garbage-collect stale clients (no token + lastSeen older than 30 days).\n await this.gcStaleClients();\n\n const instanceUuid = crypto.randomUUID();\n this.log.debug(\n `Config: port=${this.config.port}, auth=${this.config.authRequired}, mdns=${this.config.mdnsEnabled}`,\n );\n\n this.urlDiscovery = new UrlDiscovery(this, async states => {\n await this.globalConfig?.syncUrlDropdown(states);\n await this.registry?.syncUrlDropdown(states);\n });\n await this.urlDiscovery.collect();\n\n // After discovery: wire the default-mode provider for new clients.\n // - global.enabled=true \u2192 new clients default to 'global' (follow master)\n // - global.enabled=false \u2192 first discovered URL, fallback 'manual'\n this.registry.setNewClientModeProvider(() => this.computeNewClientMode());\n\n // Watch broker state for new/removed instances, VIS projects and client/global writes\n await this.subscribeForeignObjectsAsync('system.adapter.*');\n await this.subscribeStatesAsync('clients.*');\n await this.subscribeStatesAsync('global.*');\n\n const systemLanguage = await this.readSystemLanguage();\n\n try {\n this.webServer = new WebServer(\n this,\n this.config,\n this.registry,\n this.globalConfig,\n instanceUuid,\n systemLanguage,\n );\n await this.webServer.start();\n } catch (err) {\n this.log.error(`Web server failed to start: ${String(err)}`);\n return;\n }\n\n if (this.config.mdnsEnabled) {\n this.mdnsService = new MDNSService(this, this.config, instanceUuid);\n this.mdnsService.start();\n } else {\n this.log.debug('mDNS disabled \u2014 clients must enter the URL manually.');\n }\n\n await this.setState('info.connection', { val: true, ack: true });\n const bindAddr = this.config.bindAddress || '0.0.0.0';\n this.log.info(\n `HA emulation running on ${bindAddr}:${this.config.port}${this.config.mdnsEnabled ? ', mDNS active' : ''}`,\n );\n }\n\n /**\n * Default mode for newly registered clients. Respects the master switch:\n * - `global.enabled=true` \u2192 `'global'` (follow master)\n * - `global.enabled=false` \u2192 first discovered URL, fallback `'manual'`\n */\n private computeNewClientMode(): string {\n if (this.globalConfig?.isEnabled()) {\n return MODE_GLOBAL;\n }\n const first = this.urlDiscovery?.getFirstDiscoveredUrl();\n return first ?? MODE_MANUAL;\n }\n\n /**\n * Read the ioBroker system language (set in Admin \u2192 Main Settings).\n * Used for the landing page so the end-user sees the same language as\n * their admin UI. Falls back to `en` when `system.config` can't be read\n * or holds a language we don't translate. Read once on startup \u2014 a\n * language switch at runtime only takes effect after an adapter restart,\n * which is fine for a setup-hint page that most users see once.\n */\n private async readSystemLanguage(): Promise<string> {\n try {\n const cfg = await this.getForeignObjectAsync('system.config');\n const lang = (cfg?.common as { language?: string } | undefined)?.language;\n return typeof lang === 'string' && lang.length > 0 ? lang : 'en';\n } catch {\n return 'en';\n }\n }\n\n /**\n * 1.0.x / 1.1.0 \u2192 1.1.1 migration \u2014 move the legacy `defaultVisUrl` from\n * instance native into `global.visUrl` + `global.enabled=true` and drop it\n * from native. Subsequent migrations (`migrateVisUrlToMode`) then move\n * `global.visUrl` into the mode/manualUrl model.\n */\n private async migrateLegacyDefaultVisUrl(): Promise<void> {\n const legacy = this.config as AdapterConfig & { defaultVisUrl?: string; visUrl?: string };\n const url = legacy.defaultVisUrl || legacy.visUrl;\n if (!url) {\n return;\n }\n this.log.info('Migrating legacy native.defaultVisUrl/visUrl \u2192 global.visUrl');\n // We cannot call globalConfig.handleVisUrlWrite \u2014 that method is gone in\n // v1.2.0. Write the legacy state directly so migrateVisUrlToMode picks it up.\n await this.setStateAsync('global.visUrl', { val: url, ack: true }).catch(() => {\n // global.visUrl object may not exist anymore (v1.2.0 instanceObjects);\n // create it transparently for the migration step.\n });\n try {\n const id = `system.adapter.${this.namespace}`;\n const obj = await this.getForeignObjectAsync(id);\n if (obj?.native) {\n delete obj.native.defaultVisUrl;\n delete obj.native.visUrl;\n await this.setForeignObjectAsync(id, obj);\n }\n } catch (err) {\n this.log.warn(`Legacy config cleanup failed: ${String(err)}`);\n }\n }\n\n /**\n * 1.x \u2192 1.2.0 migration \u2014 move legacy per-client `visUrl`-states to the\n * `mode`/`manualUrl` model, plus the global `visUrl` to `global.mode` +\n * `global.manualUrl`. Old datapoints are removed, type of mode-states\n * upgraded to 'mixed'. Idempotent \u2014 does nothing on subsequent starts.\n */\n private async migrateVisUrlToMode(): Promise<void> {\n // 1) Global visUrl \u2192 mode + manualUrl\n try {\n const legacyGlobal = await this.getStateAsync('global.visUrl');\n if (\n legacyGlobal &&\n legacyGlobal.val !== undefined &&\n legacyGlobal.val !== null &&\n legacyGlobal.val !== ''\n ) {\n const safe = coerceSafeUrl(legacyGlobal.val);\n if (safe) {\n await this.globalConfig!.migrationSet(MODE_MANUAL, safe);\n this.log.info(`Migration: global.visUrl \u2192 mode='manual', manualUrl='${safe}'`);\n } else {\n await this.globalConfig!.migrationSet(MODE_MANUAL, null);\n this.log.warn(`Migration: legacy global.visUrl rejected as unsafe \u2014 set global.manualUrl manually`);\n }\n }\n } catch {\n /* state didn't exist \u2014 fresh install or already migrated */\n }\n try {\n await this.delObjectAsync('global.visUrl');\n } catch {\n /* didn't exist */\n }\n\n // 2) Per-client visUrl \u2192 mode='manual' + manualUrl\n const records = this.registry?.listAll() ?? [];\n for (const record of records) {\n try {\n const legacy = await this.getStateAsync(`clients.${record.id}.visUrl`);\n if (legacy && legacy.val !== undefined && legacy.val !== null && legacy.val !== '') {\n const safe = coerceSafeUrl(legacy.val);\n if (safe) {\n record.mode = MODE_MANUAL;\n record.manualUrl = safe;\n await this.setStateAsync(`clients.${record.id}.mode`, { val: MODE_MANUAL, ack: true });\n await this.setStateAsync(`clients.${record.id}.manualUrl`, { val: safe, ack: true });\n this.log.info(\n `Migration: client ${record.id} visUrl='${safe}' \u2192 mode='manual', manualUrl='${safe}'`,\n );\n } else {\n this.log.warn(\n `Migration: client ${record.id} legacy visUrl rejected as unsafe \u2014 set clients.${record.id}.manualUrl manually`,\n );\n }\n }\n } catch {\n /* state didn't exist for this client */\n }\n try {\n await this.delObjectAsync(`clients.${record.id}.visUrl`);\n } catch {\n /* didn't exist */\n }\n }\n\n // 3) Upgrade existing global.mode object to type:'mixed' + ensure\n // top-level type:'state' (instanceObjects only initialise on first\n // install \u2014 older adapters may have a partial object).\n try {\n await this.extendObjectAsync('global.mode', {\n type: 'state',\n common: { type: 'mixed' as ioBroker.CommonType },\n });\n } catch (err) {\n this.log.debug(`Migration: extend global.mode failed: ${String(err)}`);\n }\n // Per-client mode objects are already created with the right shape via\n // ClientRegistry.ensureObjects() during restore() \u2014 no extend needed here.\n }\n\n /**\n * Removes clients that are clearly stale: no auth token (= never authenticated\n * or revoked) AND `native.lastSeen` older than {@link STALE_CLIENT_TTL_MS}.\n * Clients without `lastSeen` (pre-1.2.0) get the timestamp seeded on this run\n * \u2014 GC kicks in only on subsequent restarts.\n */\n private async gcStaleClients(): Promise<void> {\n const now = Date.now();\n const records = this.registry?.listAll() ?? [];\n let removed = 0;\n for (const record of records) {\n if (record.token) {\n continue;\n }\n try {\n const obj = await this.getObjectAsync(`clients.${record.id}`);\n const native = (obj?.native as { lastSeen?: number } | undefined) ?? {};\n const lastSeen = typeof native.lastSeen === 'number' ? native.lastSeen : 0;\n if (lastSeen === 0) {\n // Pre-v1.2.0 client \u2014 seed timestamp, GC waits one cycle.\n await this.extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } });\n continue;\n }\n if (now - lastSeen > STALE_CLIENT_TTL_MS) {\n await this.registry!.remove(record.id);\n removed++;\n }\n } catch (err) {\n this.log.debug(`Stale-GC: failed for ${record.id}: ${String(err)}`);\n }\n }\n if (removed > 0) {\n this.log.info(`Stale-Client-GC: removed ${removed} client(s) (no token + idle >30 days)`);\n }\n }\n\n /**\n * Master-switch action: when `global.enabled` flips, propagate to every\n * client's `mode`. true \u2192 all clients follow `'global'`. false \u2192 fall back\n * to the first discovered URL, or `'manual'` if discovery is empty.\n *\n * @param enabled New value of `global.enabled`.\n */\n private async applyMasterSwitch(enabled: boolean): Promise<void> {\n if (!this.registry) {\n return;\n }\n if (enabled) {\n await this.registry.bulkSetMode(MODE_GLOBAL);\n return;\n }\n const first = this.urlDiscovery?.getFirstDiscoveredUrl();\n if (first) {\n await this.registry.bulkSetMode(first);\n } else {\n await this.registry.bulkSetMode(MODE_MANUAL);\n this.log.warn(\n \"global.enabled=false but no discovered VIS URL \u2014 clients set to 'manual'; \" +\n 'fill clients.<id>.manualUrl per client',\n );\n }\n }\n\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack) {\n return;\n }\n const clientParsed = this.registry ? parseClientStateId(id, this.namespace) : null;\n if (clientParsed) {\n if (clientParsed.kind === 'mode') {\n await this.registry!.handleModeWrite(clientParsed.id, state.val);\n // B4: if the user picked 'global' but global resolves to nothing,\n // give them a one-shot heads-up so the cause of the empty redirect\n // is obvious without digging through the resolver code.\n const record = this.registry!.getById(clientParsed.id);\n if (record?.mode === MODE_GLOBAL && this.globalConfig!.resolveUrlFor(record) === null) {\n this.log.warn(\n `Client ${record.id}: mode='global' but global has no resolvable URL \u2014 ` +\n 'fill global.mode/manualUrl, or pick a different client mode',\n );\n }\n } else if (clientParsed.kind === 'manualUrl') {\n await this.registry!.handleManualUrlWrite(clientParsed.id, state.val);\n } else if (clientParsed.kind === 'remove' && state.val === true) {\n await this.registry!.remove(clientParsed.id);\n }\n return;\n }\n const globalParsed = this.globalConfig ? parseGlobalStateId(id, this.namespace) : null;\n if (globalParsed === 'mode') {\n await this.globalConfig!.handleModeWrite(state.val);\n } else if (globalParsed === 'manualUrl') {\n await this.globalConfig!.handleManualUrlWrite(state.val);\n } else if (globalParsed === 'enabled') {\n await this.globalConfig!.handleEnabledWrite(state.val);\n await this.applyMasterSwitch(this.globalConfig!.isEnabled());\n }\n }\n\n private onUnload(callback: () => void): void {\n try {\n this.urlDiscovery?.cancelRefresh();\n this.urlDiscovery = null;\n\n if (this.mdnsService) {\n this.mdnsService.stop();\n this.mdnsService = null;\n }\n\n if (this.webServer) {\n this.webServer.stop().catch((err: Error) => this.log.error(`Server stop error: ${err.message}`));\n this.webServer = null;\n }\n\n this.registry = null;\n this.globalConfig = null;\n\n // Detach process-level last-line-of-defence handlers\n if (this.unhandledRejectionHandler) {\n process.off('unhandledRejection', this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off('uncaughtException', this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n\n void this.setState('info.connection', { val: false, ack: true });\n } catch (error) {\n const err = error as Error;\n this.log.error(`Shutdown error: ${err.message}`);\n } finally {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HassEmu(options);\n} else {\n (() => new HassEmu())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import crypto from 'node:crypto';\nimport * as utils from '@iobroker/adapter-core';\nimport { ClientRegistry, parseClientStateId } from './lib/client-registry';\nimport { coerceSafeUrl } from './lib/coerce';\nimport { GlobalConfig, MODE_GLOBAL, MODE_MANUAL, parseGlobalStateId } from './lib/global-config';\nimport { MDNSService } from './lib/mdns';\nimport { UrlDiscovery } from './lib/url-discovery';\nimport { WebServer } from './lib/webserver';\nimport type { AdapterConfig } from './lib/types';\n\n/** Stale-Client-GC threshold: clients without token + lastSeen older are auto-removed. */\nconst STALE_CLIENT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\nclass HassEmu extends utils.Adapter {\n private mdnsService: MDNSService | null = null;\n private webServer: WebServer | null = null;\n private registry: ClientRegistry | null = null;\n private globalConfig: GlobalConfig | null = null;\n private urlDiscovery: UrlDiscovery | null = null;\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n\n declare config: AdapterConfig;\n\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: 'hassemu' });\n\n this.on('ready', () => {\n this.onReady().catch(err => this.log.error(`onReady unhandled: ${String(err)}`));\n });\n this.on('stateChange', (id, state) => {\n this.onStateChange(id, state).catch(err => this.log.error(`stateChange unhandled: ${String(err)}`));\n });\n this.on('objectChange', () => {\n // Foreign object changed \u2014 refresh URL dropdown (debounced inside discovery)\n this.urlDiscovery?.scheduleRefresh();\n });\n this.on('unload', this.onUnload.bind(this));\n\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler wrappers cover documented async\n // paths; this catches anything that slips past during refactors.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.message}`);\n };\n process.on('unhandledRejection', this.unhandledRejectionHandler);\n process.on('uncaughtException', this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n await this.setState('info.connection', { val: false, ack: true });\n\n this.globalConfig = new GlobalConfig(this);\n await this.globalConfig.restore();\n\n this.registry = new ClientRegistry(this);\n await this.registry.restore();\n\n // Migrations run before subscriptions / webserver \u2014 first the legacy\n // 1.0.x-style native config, then the visUrl \u2192 mode/manualUrl move,\n // then a defensive schema repair for users upgrading from v1.2.0+\n // (where the partial-formed mode-object from the v1.2.0 extend-bug\n // persists since `legacy.visUrl` is already gone and migrate doesn't trigger).\n await this.migrateLegacyDefaultVisUrl();\n await this.migrateVisUrlToMode();\n await this.repairGlobalSchemas();\n\n // Garbage-collect stale clients (no token + lastSeen older than 30 days).\n await this.gcStaleClients();\n\n const instanceUuid = crypto.randomUUID();\n this.log.debug(\n `Config: port=${this.config.port}, auth=${this.config.authRequired}, mdns=${this.config.mdnsEnabled}`,\n );\n\n this.urlDiscovery = new UrlDiscovery(this, async states => {\n await this.globalConfig?.syncUrlDropdown(states);\n await this.registry?.syncUrlDropdown(states);\n });\n await this.urlDiscovery.collect();\n\n // After discovery: wire the default-mode provider for new clients.\n // - global.enabled=true \u2192 new clients default to 'global' (follow master)\n // - global.enabled=false \u2192 first discovered URL, fallback 'manual'\n this.registry.setNewClientModeProvider(() => this.computeNewClientMode());\n\n // Watch broker state for new/removed instances, VIS projects and client/global writes\n await this.subscribeForeignObjectsAsync('system.adapter.*');\n await this.subscribeStatesAsync('clients.*');\n await this.subscribeStatesAsync('global.*');\n\n const systemLanguage = await this.readSystemLanguage();\n\n try {\n this.webServer = new WebServer(\n this,\n this.config,\n this.registry,\n this.globalConfig,\n instanceUuid,\n systemLanguage,\n );\n await this.webServer.start();\n } catch (err) {\n this.log.error(`Web server failed to start: ${String(err)}`);\n return;\n }\n\n if (this.config.mdnsEnabled) {\n this.mdnsService = new MDNSService(this, this.config, instanceUuid);\n this.mdnsService.start();\n } else {\n this.log.debug('mDNS disabled \u2014 clients must enter the URL manually.');\n }\n\n await this.setState('info.connection', { val: true, ack: true });\n const bindAddr = this.config.bindAddress || '0.0.0.0';\n this.log.info(\n `HA emulation running on ${bindAddr}:${this.config.port}${this.config.mdnsEnabled ? ', mDNS active' : ''}`,\n );\n }\n\n /**\n * Default mode for newly registered clients. Respects the master switch:\n * - `global.enabled=true` \u2192 `'global'` (follow master)\n * - `global.enabled=false` \u2192 first discovered URL, fallback `'manual'`\n */\n private computeNewClientMode(): string {\n if (this.globalConfig?.isEnabled()) {\n return MODE_GLOBAL;\n }\n const first = this.urlDiscovery?.getFirstDiscoveredUrl();\n return first ?? MODE_MANUAL;\n }\n\n /**\n * Read the ioBroker system language (set in Admin \u2192 Main Settings).\n * Used for the landing page so the end-user sees the same language as\n * their admin UI. Falls back to `en` when `system.config` can't be read\n * or holds a language we don't translate. Read once on startup \u2014 a\n * language switch at runtime only takes effect after an adapter restart,\n * which is fine for a setup-hint page that most users see once.\n */\n private async readSystemLanguage(): Promise<string> {\n try {\n const cfg = await this.getForeignObjectAsync('system.config');\n const lang = (cfg?.common as { language?: string } | undefined)?.language;\n return typeof lang === 'string' && lang.length > 0 ? lang : 'en';\n } catch {\n return 'en';\n }\n }\n\n /**\n * 1.0.x / 1.1.0 \u2192 1.1.1 migration \u2014 move the legacy `defaultVisUrl` from\n * instance native into `global.visUrl` + `global.enabled=true` and drop it\n * from native. Subsequent migrations (`migrateVisUrlToMode`) then move\n * `global.visUrl` into the mode/manualUrl model.\n */\n private async migrateLegacyDefaultVisUrl(): Promise<void> {\n const legacy = this.config as AdapterConfig & { defaultVisUrl?: string; visUrl?: string };\n const url = legacy.defaultVisUrl || legacy.visUrl;\n if (!url) {\n return;\n }\n this.log.info('Migrating legacy native.defaultVisUrl/visUrl \u2192 global.visUrl');\n // We cannot call globalConfig.handleVisUrlWrite \u2014 that method is gone in\n // v1.2.0. Write the legacy state directly so migrateVisUrlToMode picks it up.\n await this.setStateAsync('global.visUrl', { val: url, ack: true }).catch(() => {\n // global.visUrl object may not exist anymore (v1.2.0 instanceObjects);\n // create it transparently for the migration step.\n });\n try {\n const id = `system.adapter.${this.namespace}`;\n const obj = await this.getForeignObjectAsync(id);\n if (obj?.native) {\n delete obj.native.defaultVisUrl;\n delete obj.native.visUrl;\n await this.setForeignObjectAsync(id, obj);\n }\n } catch (err) {\n this.log.warn(`Legacy config cleanup failed: ${String(err)}`);\n }\n }\n\n /**\n * 1.x \u2192 1.2.0 migration \u2014 move legacy per-client `visUrl`-states to the\n * `mode`/`manualUrl` model, plus the global `visUrl` to `global.mode` +\n * `global.manualUrl`. Old datapoints are removed, type of mode-states\n * upgraded to 'mixed'. Idempotent \u2014 does nothing on subsequent starts.\n */\n private async migrateVisUrlToMode(): Promise<void> {\n // 1) Global visUrl \u2192 mode + manualUrl\n try {\n const legacyGlobal = await this.getStateAsync('global.visUrl');\n if (\n legacyGlobal &&\n legacyGlobal.val !== undefined &&\n legacyGlobal.val !== null &&\n legacyGlobal.val !== ''\n ) {\n const safe = coerceSafeUrl(legacyGlobal.val);\n if (safe) {\n await this.globalConfig!.migrationSet(MODE_MANUAL, safe);\n this.log.info(`Migration: global.visUrl \u2192 mode='manual', manualUrl='${safe}'`);\n } else {\n await this.globalConfig!.migrationSet(MODE_MANUAL, null);\n this.log.warn(`Migration: legacy global.visUrl rejected as unsafe \u2014 set global.manualUrl manually`);\n }\n }\n } catch {\n /* state didn't exist \u2014 fresh install or already migrated */\n }\n try {\n await this.delObjectAsync('global.visUrl');\n } catch {\n /* didn't exist */\n }\n\n // 2) Per-client visUrl \u2192 mode='manual' + manualUrl\n const records = this.registry?.listAll() ?? [];\n for (const record of records) {\n try {\n const legacy = await this.getStateAsync(`clients.${record.id}.visUrl`);\n if (legacy && legacy.val !== undefined && legacy.val !== null && legacy.val !== '') {\n const safe = coerceSafeUrl(legacy.val);\n if (safe) {\n record.mode = MODE_MANUAL;\n record.manualUrl = safe;\n await this.setStateAsync(`clients.${record.id}.mode`, { val: MODE_MANUAL, ack: true });\n await this.setStateAsync(`clients.${record.id}.manualUrl`, { val: safe, ack: true });\n this.log.info(\n `Migration: client ${record.id} visUrl='${safe}' \u2192 mode='manual', manualUrl='${safe}'`,\n );\n } else {\n this.log.warn(\n `Migration: client ${record.id} legacy visUrl rejected as unsafe \u2014 set clients.${record.id}.manualUrl manually`,\n );\n }\n }\n } catch {\n /* state didn't exist for this client */\n }\n try {\n await this.delObjectAsync(`clients.${record.id}.visUrl`);\n } catch {\n /* didn't exist */\n }\n }\n\n // 3) global.mode + global.manualUrl repair handled by repairGlobalSchemas()\n // (called separately in onReady so it ALSO runs for users upgrading from\n // v1.2.0/v1.3.0/v1.3.1 where the legacy visUrl is already gone but the\n // partial-formed mode-object from the v1.2.0 extendObject-bug persists).\n }\n\n /**\n * Repairs partial-formed `global.mode` / `global.manualUrl` objects from\n * the v1.2.0 migration bug (extendObjectAsync was called with only\n * `common.type:'mixed'` \u2014 leaving the object without top-level `type`,\n * name, role, read, write, def). `extendObjectAsync` here merges the full\n * instanceObjects schema onto the existing partial object so js-controller\n * stops warning \"obj.type has to exist\" and the dropdown renders correctly.\n *\n * Idempotent \u2014 extending an already-complete object is a no-op write.\n */\n private async repairGlobalSchemas(): Promise<void> {\n try {\n await this.extendObjectAsync('global.mode', {\n type: 'state',\n common: {\n name: 'Global redirect mode',\n type: 'mixed',\n role: 'value',\n read: true,\n write: true,\n def: 0,\n },\n native: {},\n });\n } catch (err) {\n this.log.debug(`repair global.mode failed: ${String(err)}`);\n }\n try {\n await this.extendObjectAsync('global.manualUrl', {\n type: 'state',\n common: {\n name: \"Global manual URL (used when mode='manual')\",\n type: 'string',\n role: 'url',\n read: true,\n write: true,\n def: '',\n },\n native: {},\n });\n } catch (err) {\n this.log.debug(`repair global.manualUrl failed: ${String(err)}`);\n }\n }\n\n /**\n * Removes clients that are clearly stale: no auth token (= never authenticated\n * or revoked) AND `native.lastSeen` older than {@link STALE_CLIENT_TTL_MS}.\n * Clients without `lastSeen` (pre-1.2.0) get the timestamp seeded on this run\n * \u2014 GC kicks in only on subsequent restarts.\n */\n private async gcStaleClients(): Promise<void> {\n const now = Date.now();\n const records = this.registry?.listAll() ?? [];\n let removed = 0;\n for (const record of records) {\n if (record.token) {\n continue;\n }\n try {\n const obj = await this.getObjectAsync(`clients.${record.id}`);\n const native = (obj?.native as { lastSeen?: number } | undefined) ?? {};\n const lastSeen = typeof native.lastSeen === 'number' ? native.lastSeen : 0;\n if (lastSeen === 0) {\n // Pre-v1.2.0 client \u2014 seed timestamp, GC waits one cycle.\n await this.extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } });\n continue;\n }\n if (now - lastSeen > STALE_CLIENT_TTL_MS) {\n await this.registry!.remove(record.id);\n removed++;\n }\n } catch (err) {\n this.log.debug(`Stale-GC: failed for ${record.id}: ${String(err)}`);\n }\n }\n if (removed > 0) {\n this.log.info(`Stale-Client-GC: removed ${removed} client(s) (no token + idle >30 days)`);\n }\n }\n\n /**\n * Master-switch action: when `global.enabled` flips, propagate to every\n * client's `mode`. true \u2192 all clients follow `'global'`. false \u2192 fall back\n * to the first discovered URL, or `'manual'` if discovery is empty.\n *\n * @param enabled New value of `global.enabled`.\n */\n private async applyMasterSwitch(enabled: boolean): Promise<void> {\n if (!this.registry) {\n return;\n }\n if (enabled) {\n await this.registry.bulkSetMode(MODE_GLOBAL);\n return;\n }\n const first = this.urlDiscovery?.getFirstDiscoveredUrl();\n if (first) {\n await this.registry.bulkSetMode(first);\n } else {\n await this.registry.bulkSetMode(MODE_MANUAL);\n this.log.warn(\n \"global.enabled=false but no discovered VIS URL \u2014 clients set to 'manual'; \" +\n 'fill clients.<id>.manualUrl per client',\n );\n }\n }\n\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack) {\n return;\n }\n const clientParsed = this.registry ? parseClientStateId(id, this.namespace) : null;\n if (clientParsed) {\n if (clientParsed.kind === 'mode') {\n await this.registry!.handleModeWrite(clientParsed.id, state.val);\n // B4: if the user picked 'global' but global resolves to nothing,\n // give them a one-shot heads-up so the cause of the empty redirect\n // is obvious without digging through the resolver code.\n const record = this.registry!.getById(clientParsed.id);\n if (record?.mode === MODE_GLOBAL && this.globalConfig!.resolveUrlFor(record) === null) {\n this.log.warn(\n `Client ${record.id}: mode='global' but global has no resolvable URL \u2014 ` +\n 'fill global.mode/manualUrl, or pick a different client mode',\n );\n }\n } else if (clientParsed.kind === 'manualUrl') {\n await this.registry!.handleManualUrlWrite(clientParsed.id, state.val);\n } else if (clientParsed.kind === 'remove' && state.val === true) {\n await this.registry!.remove(clientParsed.id);\n }\n return;\n }\n const globalParsed = this.globalConfig ? parseGlobalStateId(id, this.namespace) : null;\n if (globalParsed === 'mode') {\n await this.globalConfig!.handleModeWrite(state.val);\n } else if (globalParsed === 'manualUrl') {\n await this.globalConfig!.handleManualUrlWrite(state.val);\n } else if (globalParsed === 'enabled') {\n await this.globalConfig!.handleEnabledWrite(state.val);\n await this.applyMasterSwitch(this.globalConfig!.isEnabled());\n }\n }\n\n private onUnload(callback: () => void): void {\n try {\n this.urlDiscovery?.cancelRefresh();\n this.urlDiscovery = null;\n\n if (this.mdnsService) {\n this.mdnsService.stop();\n this.mdnsService = null;\n }\n\n if (this.webServer) {\n this.webServer.stop().catch((err: Error) => this.log.error(`Server stop error: ${err.message}`));\n this.webServer = null;\n }\n\n this.registry = null;\n this.globalConfig = null;\n\n // Detach process-level last-line-of-defence handlers\n if (this.unhandledRejectionHandler) {\n process.off('unhandledRejection', this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off('uncaughtException', this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n\n void this.setState('info.connection', { val: false, ack: true });\n } catch (error) {\n const err = error as Error;\n this.log.error(`Shutdown error: ${err.message}`);\n } finally {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HassEmu(options);\n} else {\n (() => new HassEmu())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["crypto"]
7
7
  }
package/io-package.json CHANGED
@@ -1,98 +1,98 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "hassemu",
4
- "version": "1.3.1",
4
+ "version": "1.3.3",
5
5
  "news": {
6
+ "1.3.3": {
7
+ "en": "Documentation: rewrote release notes in user-friendly style across all languages.",
8
+ "de": "Dokumentation: Release-Notes in alle Sprachen User-orientiert neu geschrieben.",
9
+ "ru": "Документация: примечания к релизу переписаны в дружественном к пользователю стиле на всех языках.",
10
+ "pt": "Documentação: notas de release reescritas em estilo amigável ao usuário em todos os idiomas.",
11
+ "nl": "Documentatie: release-notes herschreven in gebruikersvriendelijke stijl in alle talen.",
12
+ "fr": "Documentation : notes de version réécrites dans un style adapté à l'utilisateur dans toutes les langues.",
13
+ "it": "Documentazione: note di release riscritte in stile user-friendly in tutte le lingue.",
14
+ "es": "Documentación: notas de versión reescritas en estilo amigable para el usuario en todos los idiomas.",
15
+ "pl": "Dokumentacja: notatki wydania przepisane w przyjaznym dla użytkownika stylu we wszystkich językach.",
16
+ "uk": "Документація: нотатки до релізу переписано у дружньому до користувача стилі усіма мовами.",
17
+ "zh-cn": "文档:所有语言的发布说明已重写为用户友好的风格。"
18
+ },
19
+ "1.3.2": {
20
+ "en": "Fix: dropdown default `---` now applied correctly on upgrades from older v1.1.x clients (was empty after migration).",
21
+ "de": "Fix: Dropdown-Standard `---` wird bei Upgrades von älteren v1.1.x-Clients jetzt korrekt gesetzt (war nach der Migration leer).",
22
+ "ru": "Fix: значение по умолчанию `---` в dropdown теперь правильно применяется при обновлении со старых клиентов v1.1.x (было пустым после миграции).",
23
+ "pt": "Fix: valor padrão `---` do dropdown agora é aplicado corretamente em atualizações de clientes v1.1.x antigos (estava vazio após a migração).",
24
+ "nl": "Fix: dropdown-standaard `---` wordt nu correct toegepast bij upgrades van oudere v1.1.x-clients (was leeg na de migratie).",
25
+ "fr": "Fix : la valeur par défaut `---` du dropdown est désormais correctement appliquée lors des mises à niveau depuis les anciens clients v1.1.x (était vide après la migration).",
26
+ "it": "Fix: il valore predefinito `---` del dropdown ora viene applicato correttamente sugli aggiornamenti da client v1.1.x più vecchi (era vuoto dopo la migrazione).",
27
+ "es": "Fix: el valor predeterminado `---` del dropdown ahora se aplica correctamente en actualizaciones desde clientes v1.1.x antiguos (estaba vacío tras la migración).",
28
+ "pl": "Fix: wartość domyślna `---` w dropdown jest teraz poprawnie ustawiana przy aktualizacjach ze starszych klientów v1.1.x (była pusta po migracji).",
29
+ "uk": "Fix: значення за замовчуванням `---` у dropdown тепер коректно застосовується при оновленнях зі старих клієнтів v1.1.x (було порожнім після міграції).",
30
+ "zh-cn": "修复:从旧的 v1.1.x 客户端升级时,下拉框默认值 `---` 现在能正确显示(迁移后曾为空)。"
31
+ },
6
32
  "1.3.1": {
7
- "en": "Hotfix for legacy v1.1.x clients: their `visUrl` channel did not have `mode` / `manualUrl` objects. The v1.2.0 migration wrote states without the matching objects, which the broker logged as `State has no existing object` and rendered the `mode` datapoint without a name or dropdown in the object browser. `ClientRegistry.restore()` now calls an idempotent `ensureObjects()` for every client, so the v1.2.0+ object shapes exist before any migration writes happen.\nMode dropdown gains a numeric `0 = \"---\"` no-choice fallback (analogous to govee-smart's pattern). Existing displays keep their setting; new displays start at `0` and the resolver falls back to the landing page until a real choice is made.",
8
- "de": "Hotfix für ältere v1.1.x-Clients: ihr `visUrl`-Kanal hatte keine `mode` / `manualUrl`-Objekte. Die v1.2.0 Migration schrieb Zustände ohne die passenden Objekte, die der als `State eingeloggte Broker hat kein bestehendes Objekt` und den `mode`-Datenpunkt ohne Namen oder Dropdown im Objektbrowser. `ClientRegistry.restore()` ruft nun für jeden Client ein idempotent `ensureObjects()` auf, so dass die v1.2.0+ Objektformen existieren, bevor Migrationsschreiben passieren.\nModus Dropdown gewinnt einen numerischen `0 = \"--\"` no-choice fallback (analog zu govee-smart's Muster). Vorhandene Displays halten ihre Einstellung; neue Displays starten bei `0` und der Resolver fällt auf die Landingpage zurück, bis eine echte Wahl getroffen wird.",
9
- "ru": "Hotfix для устаревших клиентов v1.1.x: их канал «visUrl» не имел объектов «режима» / «ручного Урла». Миграция v1.2.0 писала состояния без соответствующих объектов, которые брокер зарегистрировал как «Государство не имеет существующего объекта», и отображала точку данных «режима» без имени или выпадения в браузере объекта. «ClientRegistry.restore()» теперь называет идемпотентом «ensureObjects()» для каждого клиента, поэтому формы объектов v1.2.0+ существуют до того, как произойдет какая-либо миграция.\nРежим выпадения получает числовое '0 = '--'' без выбора запасного варианта (аналогично шаблону Govee-smart). Существующие дисплеи сохраняют свою настройку; новые дисплеи начинаются с 0, и решатель возвращается на целевую страницу, пока не будет сделан реальный выбор.",
10
- "pt": "Hotfix para clientes legados v1.1.x: seu canal 'visUrl' não tinha objetos 'modo' / 'manualUrl'. A migração v1.2.0 escreveu estados sem os objetos correspondentes, que o corretor logou como `Estado não tem objeto existente' e renderizou o ponto de dados `modo' sem um nome ou dropdown no navegador objeto. `ClientRegistry.restore()` agora chama um idempotent `ensureObjects()` para cada cliente, então as formas do objeto v1.20+ existem antes que qualquer migração escreva.\nO dropdown do modo ganha um valor numérico `0 = \"---\"` sem escolha (análogo para o padrão govee-smart). Os displays existentes mantêm sua configuração; os novos displays começam em `0` e o resolvedor volta para a landing page até que uma escolha real seja feita.",
11
- "nl": "Hotfix voor legacy v1.1.x clients: hun De v1.2.0 migratie schreef staten zonder de bijbehorende objecten, die de makelaar aangemeld als Staat heeft geen bestaand object heeft en gaf de \"mode\" datapoint zonder een naam of dropdown in de object browser. .\nMode dropdown krijgt een numerieke Bestaande displays houden hun instelling; nieuwe displays beginnen bij .",
12
- "fr": "Hotfix pour les clients left v1.1.x: leur canal `visUrl` n'avait pas d'objets `mode` / `manuelUrl`. La migration v1.2.0 a écrit des états sans les objets correspondants, que le courtier connecté comme `État n'a pas d'objet existant` et a rendu le point de données `mode` sans nom ou déroulant dans le navigateur objet. `ClientRegistry.restore()` appelle maintenant un idémpotent `ensurerObjects()` pour chaque client, de sorte que les formes d'objet v1.2.0+ existent avant toute migration écrite.\nLe mode déroulant gagne un résultat numérique `0 = \"---\"` sans choix (analogue au modèle govee-smart). Les affichages existants gardent leur réglage; les nouveaux affichages commencent à `0` et le résolveur retourne à la page d'atterrissage jusqu'à ce qu'un choix réel soit fait.",
13
- "it": "Hotfix per client v1.1.x legacy: il loro canale `visUrl` non ha avuto oggetti `mode` / `manualUrl`. La migrazione v1.2.0 ha scritto stati senza gli oggetti corrispondenti, che il broker ha registrato come `State non ha oggetto esistente` e ha reso il `mode` datapoint senza un nome o dropdown nel browser dell'oggetto. `ClientRegistry.restore()` ora chiama un idempotent `ensureObjects()` per ogni cliente, in modo che le forme dell'oggetto v1.2.0+ esistano prima che qualsiasi migrazione scriva.\nLa mode dropdown ottiene un `0 numerico = \"---\"` non-choice Fallback (analogo del modello di govee-smart). I display esistenti mantengono la loro impostazione; i nuovi display iniziano a `0` e il risolutore torna alla pagina di atterraggio fino a quando non viene fatta una vera scelta.",
14
- "es": "Hotfix for legacy v1.1.x clients: their `visUrl` channel did not have `mode` / `manualUrl` objects. La migración v1.2.0 escribió estados sin los objetos coincidentes, que el corredor registró como `Estado no tiene objeto existente ' y emitió el punto de datos de `mode ' sin un nombre o desplegable en el navegador objeto. `ClientRegistry.restore()` ahora llama un \"objetos de seguridad\" idempotente para cada cliente, por lo que las formas de objetos v1.2.0+ existen antes de que ocurra cualquier migración escribe.\nLa caída del modo gana un numérico `0 = \"---\"` retroceso sin elección (análogo para el patrón de govee-smart). Las pantallas existentes mantienen su configuración; las nuevas pantallas comienzan en `0` y el solucionador vuelve a la página de aterrizaje hasta que se haga una elección real.",
15
- "pl": "Hotfix dla dotychczasowych klientów v1.1.x: ich kanał 'visUrl' nie miał obiektów 'mode' / 'manualUrl'. Migracja v1.2.0 napisała stany bez dopasowujących się obiektów, które broker zalogował jako 'Stan nie ma istniejącego obiektu', a w przeglądarce obiektu dodano punkt 'mode' bez nazwy lub zrzutu. 'ClientRegistry.recovery ()' teraz nazywa idemstrong 'ensureObjects ()' dla każdego klienta, tak więc kształty obiektu v1.2.0 + istnieją zanim jakaś migracja się wydarzy.\nMode dropdown zyskuje numeryczny '0 = \"---\"' no-choice fallback (analogiczny do wzorca govee- smart). Istniejące wyświetlacze utrzymują ustawienia; nowe wyświetlacze rozpoczynają się na '0', a rozdzielczość wraca do strony docelowej do momentu dokonania prawdziwego wyboru.",
16
- "uk": "Hotfix для Legacy v1.1.x клієнтів: їх `visUrl` канал не мав `mode` / `manualUrl` об'єкти. Переміщення v1.2.0 писали стани без відповідних об'єктів, які були зареєстровані як `Державний не має наявних об'єктів і надано `mode` datapoint без назви або випадання в браузері об'єкта. `ClientRegistry.restore()` тепер називає idempotent `ensureObjects()` для кожного клієнта, тобто v1.2.0+ об’єктів існують до будь-яких міграційних записів.\nПохибка в режимі реального часу отримує нумерний `0 = \"--\"``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` Випробувано нові покази на сайті `0` і вирішувач повертається на сторінку посадки, доки не буде виконано реальний вибір.",
17
- "zh-cn": "V1.1.x客户端的热补:他们的`visUrl ' 频道没有`mode ' /`manualUrl ' 对象。 v1.2.0 迁移时写了没有匹配对象的状态,经纪人将其记录为`状态没有现有对象 ' ,并在对象浏览器中没有名称或下拉时使“模式”数据点。 `ClientRegistry.restore()'现在将每个客户称为“确保对象()”,因此在任何迁移发生之前,v1.2.0+对象形状就已存在.\n模式降级获得一个数字 `0 = \"--'' `不选择回落' (反感到gove-smart的图案). 现有显示器保持其设置;新的显示器开始于`0',解析器回到着陆页,直到作出真正的选择."
33
+ "en": "Fix: legacy v1.1.x clients without mode/manualUrl objects now get migrated correctly on first start.",
34
+ "de": "Fix: Legacy-v1.1.x-Clients ohne mode/manualUrl-Objekte werden beim ersten Start jetzt korrekt migriert.",
35
+ "ru": "Fix: устаревшие клиенты v1.1.x без объектов mode/manualUrl теперь корректно мигрируются при первом запуске.",
36
+ "pt": "Fix: clientes v1.1.x antigos sem objetos mode/manualUrl agora são migrados corretamente na primeira inicialização.",
37
+ "nl": "Fix: oude v1.1.x-clients zonder mode/manualUrl-objecten worden nu correct gemigreerd bij de eerste start.",
38
+ "fr": "Fix : les anciens clients v1.1.x sans objets mode/manualUrl sont désormais correctement migrés au premier démarrage.",
39
+ "it": "Fix: i client v1.1.x legacy senza oggetti mode/manualUrl ora vengono migrati correttamente al primo avvio.",
40
+ "es": "Fix: los clientes v1.1.x antiguos sin objetos mode/manualUrl ahora se migran correctamente en el primer inicio.",
41
+ "pl": "Fix: starsze klienty v1.1.x bez obiektów mode/manualUrl teraz poprawnie migrowane przy pierwszym uruchomieniu.",
42
+ "uk": "Fix: застарілі клієнти v1.1.x без об'єктів mode/manualUrl тепер коректно мігрують при першому запуску.",
43
+ "zh-cn": "修复:缺少 mode/manualUrl 对象的旧版 v1.1.x 客户端现在能在首次启动时正确迁移。"
18
44
  },
19
45
  "1.3.0": {
20
- "en": "Security: brute-force lockout on `/auth/login_flow/:flowId` — after 5 failed credential attempts an IP is rejected with HTTP 429 for 15 min. Successful login resets the counter.\nDRY refactor: shared `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold are now named constants instead of magic numbers.\nDead-code cleanup: `resolveBindToReachable`, `coerceUuid` strict-V4 parameter, `DEFAULT_REFRESH_DEBOUNCE_MS` export, internal `getMode`/`getManualUrl` test affordances — all removed; tests rewritten to assert observable behaviour.\nNew `landing-page` test suite with XSS-escape coverage and 11-language fallback verification.\nEmulated Home Assistant version bumped from 2026.3.1 to 2026.4.0.",
21
- "de": "Sicherheit: brute-force lockout auf `/auth/login flow/:flow Id` — nach 5 fehlgeschlagenen Anmeldeversuchen wird eine IP mit HTTP 429 für 15 min abgelehnt. Erfolgreiche Anmeldung setzt den Zähler zurück.\nDRY Refactor: geteilter `parseManualUrlWrite` Helfer zwischen Client + global config; FIFO-cap-Helfer im WebServer; OAuth Access-token TTL + Lockout-Fenster/Threshold werden jetzt als Konstanten anstelle von magischen Zahlen bezeichnet.\nTotcode-Reinigung: `resolveBindToReachable`, `coerceUuid` streng-V4-Parameter, `DEFAULT REFRESH DEBOUNCE MS` Export, interne `getMode`/`getManualUrl` Test Provisionances - alle entfernt; Tests neu geschrieben, um beobachtbares Verhalten zu behaupten.\nNeue `landing-page` Testsuite mit XSS-Escape-Abdeckung und 11-sprachiger Fallback-Verifikation.\nEmulated Home Assistant Version von 2026.3.1 bis 2026.4.0.",
22
- "ru": "Безопасность: блокировка грубой силы на `/auth/login flow/:flow Id' - после 5 неудачных попыток проверки подлинности IP отклоняется с помощью HTTP 429 в течение 15 минут. Успешный логин сбрасывает счетчик.\nРефактор DRY: общий помощник «parseManualUrlWrite» между клиентом + глобальной конфигурацией; помощник FIFO-кап в WebServer; токен доступа OAuth TTL + окно / порог блокировки теперь называются константами вместо магических чисел.\nОчистка от мертвого кода: «resolveBindToReachable», «coerceUuid» строгий параметр V4, «DEFAULT REFRESH DEBOUNCE MS» экспорт, внутренние «getMode» / «getManualUrl» доступ к тесту — все удалены; тесты переписаны, чтобы утверждать наблюдаемое поведение.\nНовый тестовый пакет «посадочная страница» с покрытием XSS-escape и проверкой резервного копирования на 11 языках.\nЭмулированная версия Home Assistant выросла с 2026.3.1 до 2026.4.0.",
23
- "pt": "Segurança: bloqueio de força bruta em `/auth/login flow/:flow Id` — após 5 tentativas de credencial falhadas, um IP é rejeitado com HTTP 429 por 15 min. O login bem- sucedido reinicia o contador.\nDRY refator: compartilhado `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold são agora chamadas constantes em vez de números mágicos.\nLimpeza de código morto: `resolveBindToReachable`, `coerceUuid` strict-V4 parameter, `DEFAULT REFRESH DEBOUNCIE MS' export, interna `getMode`/`getManualUrl` test affordances — todos removidos; testes reescritos para afirmar comportamento observável.\nNovo conjunto de testes `landing-page` com cobertura XSS-scape e verificação de retorno de 11 idiomas.\nEmulated Home Assistant versão bateu de 2026.3.1 para 2026.4.0.",
24
- "nl": "Beveiliging: brute-force lockout op Id Succesvolle login resetten de teller.\nDRY-refactor: gedeelde .\nDead-code cleanup: .\nNieuwe testsuite met XSS-escape dekking en 11-taal fallback verificatie.\nEmulated Home Assistant versie stootte van 2026.3.1 naar 2026.4.0.",
25
- "fr": "Sécurité: verrouillage de la force brute sur `/auth/login flow/:flow Id` — après 5 tentatives de reconnaissance ratées, une IP est rejetée avec HTTP 429 pendant 15 min. Une connexion réussie réinitialise le compteur.\nRefactor DRY: helper partagé `parseManualUrlWrite` entre client + config global; helper FIFO-cap dans WebServer; OAuth access-token TTL + lockout window/threshold sont maintenant nommés constantes au lieu de nombres magiques.\nNettoyage en code mort: `resolveBindToReachable`, paramètre `coerceUuid` strict-V4, `DEFAULT REFRESH DEBOUNCE MS` export, interne `getMode`/`getManualUrl` test provideances - tous supprimés; tests réécrits pour affirmer un comportement observable.\nNouvelle suite d ' essais < < atterrissage-page > > avec couverture XSS-escape et vérification en 11 langues.\nLa version de l'Emuled Home Assistant est tombée de 2026.3.1 à 2026.4.0.",
26
- "it": "Sicurezza: blocco forza bruta su `/auth/login flow/:flow Id` — dopo 5 tentativi di credenziali falliti un IP viene respinto con HTTP 429 per 15 min. Il successo del login reimposta il contatore.\nDRY refactor: condiviso `parseManualUrlWrite` helper tra client + configurazione globale; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout finestra / soglia sono ora chiamate costanti invece di numeri magici.\nPulitura codice morto: `resolveBindToReachable`, `coerceUuid` parametro rigoroso-V4, `DEFAULT REFRESH DEBOUNCE MS` export, `getMode`/`getManualUrl` offerte di prova — tutti rimossi; test riscritti per affermare il comportamento osservabile.\nNuova suite di prova `landing-page` con copertura XSS-escape e verifica di caduta in 11 lingue.\nEmulated Home Assistant versione urtata da 2026.3.1 a 2026.4.0.",
27
- "es": "Seguridad: bloqueo de fuerza bruta en `/auth/login flow/:flow Id` - después de 5 intentos de credencial fallidos una IP es rechazada con HTTP 429 por 15 min. Acceso exitoso restaura el contador.\nDRY refactor: compartido `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold are now named constants instead of magic numbers.\nLimpieza del código muerto: `resolveBindToReachable`, `coerceUuid` strict-V4 parámetro, `DEFAULT REFRESH DEBOUNCE MS` export, internal `getMode`/`getManualUrl` test affordances — all removed; tests rewritten to assert observable behaviour.\nNew `landing-page` test suite with XSS-escape coverage and 11-language fallback verification.\nLa versión Emulated Home Assistant bajó de 2026.3.1 a 2026.4.0.",
28
- "pl": "Bezpieczeństwo: zablokowanie siły brutalnej na '/ auth / login _ flow /: flow Id' - po 5 nieudanych próbach kredytowych IP jest odrzucane przez HTTP 429 przez 15 min. Udane logowanie resetuje licznik.\nReczynnik DRY: wspólny \"parseManualUrlWrite\" pomocnik pomiędzy klientem + globalny config; helper FIFO- cap w WebServer; OAuth access- token TTL + blockout okno / próg są teraz nazywane stałe zamiast magicznych liczb.\nDead- code cleanup: 'resolveBindToReavable', 'coerceUuid' parametr strict- V4, 'DEFAULT _ REFRESH _ DEBOUNCE _ MS' export, 'internal' getMode '/' getManualUrl 'test foredances - all removed; tests rewrited to assert observable behavior.\nNowy zestaw testowy 'Landing- page' z pokryciem XSS- escape i weryfikacją 11- językową.\nWersja Emulated Home Assistant spadła z 2026.3.1 do 2026.4.0.",
29
- "uk": "Безпека: броньований замок на `/auth/login flow/:flow Id` — після 5 невдалих спроб IP відхилений HTTP 429 за 15 хв. Успішний логін скидає лічильник.\nРефактор DRY: загальний `parseManualUrlWrite` помічник між клієнтом + глобальним налаштуванням; помічник FIFO-cap в WebServer; OAuth access-token TTL + вікно блокування / поле тепер іменуються константи замість чарівних чисел.\nВидалення коду: `resolveBindToReachable`, `coerceUuid` строгий-V4 параметр, `DEFAULT REFRESH DEBOUNCE MS` експорт, внутрішня `getMode`/`getManualUrl` test allowances — всі видалені; тести, що повторюються, щоб стверджувати спостережну поведінку.\nНовий `landing-page` тест-люкс з висвітленням S-escape і 11-мовою перевіркою повернення.\nВбудований домашній помічник варіант з 2026.3.1 до 2026.4.0.",
30
- "zh-cn": "安全: \" /auth/login flow/:flow \" 上的野蛮武力封锁 Id ' - 在5次证书尝试失败后,一个IP在15分钟内被HTTP 429拒绝。 成功登录重置计数器 .\nDRY Refactor:在客户端+全局配置之间共享“parseManualUrlWrite”帮助器;WebServer中的FIFO-cap帮助器;OAuth访问-token TTL+锁定窗口/阈值现在被命名为常数而不是魔法数.\n死码清理:`溶解BindToeachable ' 、`coerceUuid ' 严格V4参数、`DEFAULT REFRESH DEBOUNCE MS ' 输出、内部`GetMode'/`GetManualUrl ' 测试支付能力-全部取消;测试重写以申明可观察到的行为.\n新的 \" 登陆页 \" 测试套件,包括XSS-escape覆盖范围和11种语言的回落核查.\n模拟家庭助理版本从2026.3.1到2026.4.0."
46
+ "en": "Security: brute-force lockout on login (5 failed attempts IP blocked for 15 min). Emulated Home Assistant version bumped to 2026.4.0.",
47
+ "de": "Sicherheit: Brute-Force-Sperre beim Login (5 Fehlversuche IP für 15 Min gesperrt). Emulierte Home-Assistant-Version auf 2026.4.0 erhöht.",
48
+ "ru": "Безопасность: блокировка от brute-force при входе (5 неудачных попыток IP блокируется на 15 мин). Версия эмулируемого Home Assistant повышена до 2026.4.0.",
49
+ "pt": "Segurança: bloqueio brute-force no login (5 tentativas falhadas IP bloqueado por 15 min). Versão emulada do Home Assistant atualizada para 2026.4.0.",
50
+ "nl": "Beveiliging: brute-force-blokkering bij login (5 mislukte pogingen IP 15 min geblokkeerd). Geëmuleerde Home Assistant-versie naar 2026.4.0.",
51
+ "fr": "Sécurité : verrouillage brute-force sur la connexion (5 tentatives échouées IP bloquée 15 min). Version Home Assistant émulée portée à 2026.4.0.",
52
+ "it": "Sicurezza: blocco brute-force al login (5 tentativi falliti IP bloccato per 15 min). Versione Home Assistant emulata aggiornata a 2026.4.0.",
53
+ "es": "Seguridad: bloqueo brute-force al iniciar sesión (5 intentos fallidos IP bloqueada 15 min). Versión emulada de Home Assistant actualizada a 2026.4.0.",
54
+ "pl": "Bezpieczeństwo: blokada brute-force przy logowaniu (5 nieudanych prób IP zablokowane na 15 min). Wersja emulowanego Home Assistant podniesiona do 2026.4.0.",
55
+ "uk": "Безпека: блокування brute-force при вході (5 невдалих спроб IP заблоковано на 15 хв). Версія емульованого Home Assistant оновлена до 2026.4.0.",
56
+ "zh-cn": "安全:登录暴力破解防护(5 次失败 IP 阻断 15 分钟)。模拟的 Home Assistant 版本提升至 2026.4.0"
31
57
  },
32
58
  "1.2.0": {
33
- "en": "Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Migration runs automatically.\nMaster switch `global.enabled` syncs every display: on → all follow the global URL, off → each display picks up its own again.\nIdle displays without auth token are auto-removed after 30 days.\nSecurity hardening of the auth flow.\n`web` adapter declared as dependency.",
34
- "de": "Redirect-Ziel jetzt über `mode` (Dropdown) + `manualUrl` (Freitext) statt des alten `visUrl`. Migration läuft automatisch.\nMaster-Switch `global.enabled` synct jedes Display: an → alle folgen der globalen URL, aus → jedes Display nutzt wieder seine eigene.\nDisplay-Channels ohne Auth-Token werden nach 30 Tagen Leerlauf automatisch entfernt.\nAuth-Flow gehärtet.\n`web`-Adapter als Abhängigkeit eingetragen.",
35
- "ru": "Цель перенаправления теперь настраивается через `mode` (выпадающий список) + `manualUrl` (свободный текст) вместо старого `visUrl`. Миграция выполняется автоматически.\nГлавный переключатель `global.enabled` синхронизирует все дисплеи: вкл → все следуют глобальному URL, выкл → каждый дисплей использует свой.\nПростаивающие дисплеи без токена авторизации удаляются автоматически через 30 дней.\nУсилена безопасность потока авторизации.\nАдаптер `web` объявлен как зависимость.",
36
- "pt": "Destino do redirecionamento agora configurado via `mode` (dropdown) + `manualUrl` (texto livre) em vez do antigo `visUrl`. Migração executa automaticamente.\nInterruptor mestre `global.enabled` sincroniza cada display: ligado → todos seguem o URL global, desligado → cada display usa o seu próprio.\nDisplays inativos sem token de autenticação são removidos automaticamente após 30 dias.\nFluxo de autenticação reforçado.\nAdaptador `web` declarado como dependência.",
37
- "nl": "Redirect-doel wordt nu geconfigureerd via `mode` (dropdown) + `manualUrl` (vrije tekst) in plaats van de oude `visUrl`. Migratie verloopt automatisch.\nHoofdschakelaar `global.enabled` synchroniseert elk display: aan → allemaal volgen de globale URL, uit → elk display gebruikt zijn eigen.\nInactieve displays zonder auth-token worden na 30 dagen automatisch verwijderd.\nAuth-flow versterkt.\n`web`-adapter als afhankelijkheid gedeclareerd.",
38
- "fr": "Cible de redirection désormais configurée via `mode` (liste déroulante) + `manualUrl` (texte libre) au lieu de l'ancien `visUrl`. La migration est automatique.\nL'interrupteur principal `global.enabled` synchronise chaque écran : activé → tous suivent l'URL globale, désactivé → chaque écran utilise la sienne.\nLes écrans inactifs sans jeton d'authentification sont supprimés automatiquement après 30 jours.\nFlux d'authentification renforcé.\nAdaptateur `web` déclaré comme dépendance.",
39
- "it": "Destinazione del redirect ora configurata tramite `mode` (menu a discesa) + `manualUrl` (testo libero) invece del vecchio `visUrl`. La migrazione è automatica.\nInterruttore principale `global.enabled` sincronizza ogni display: on → tutti seguono l'URL globale, off → ogni display usa il proprio.\nI display inattivi senza token di autenticazione vengono rimossi automaticamente dopo 30 giorni.\nFlusso di autenticazione rafforzato.\nAdattatore `web` dichiarato come dipendenza.",
40
- "es": "Destino de redirección ahora configurado vía `mode` (desplegable) + `manualUrl` (texto libre) en lugar del antiguo `visUrl`. La migración es automática.\nInterruptor principal `global.enabled` sincroniza cada pantalla: activado → todas siguen la URL global, desactivado → cada pantalla usa la suya.\nPantallas inactivas sin token de autenticación se eliminan automáticamente tras 30 días.\nFlujo de autenticación reforzado.\nAdaptador `web` declarado como dependencia.",
41
- "pl": "Cel przekierowania jest teraz konfigurowany przez `mode` (lista rozwijana) + `manualUrl` (tekst dowolny) zamiast starego `visUrl`. Migracja przebiega automatycznie.\nGłówny przełącznik `global.enabled` synchronizuje każdy wyświetlacz: wł → wszystkie podążają za globalnym URL, wył → każdy używa własnego.\nNieaktywne wyświetlacze bez tokena uwierzytelniania usuwane automatycznie po 30 dniach.\nWzmocniono bezpieczeństwo procesu uwierzytelniania.\nAdapter `web` zadeklarowany jako zależność.",
42
- "uk": "Ціль перенаправлення тепер налаштовується через `mode` (випадаючий список) + `manualUrl` (вільний текст) замість старого `visUrl`. Міграція виконується автоматично.\nГоловний перемикач `global.enabled` синхронізує кожен дисплей: увімк → усі слідують глобальній URL, вимк → кожен дисплей використовує власну.\nНеактивні дисплеї без токена автентифікації автоматично видаляються через 30 днів.\nПотік автентифікації посилено.\nАдаптер `web` оголошено залежністю.",
43
- "zh-cn": "重定向目标现在通过 `mode`(下拉菜单)+ `manualUrl`(自由文本)配置,取代旧的 `visUrl`。迁移自动运行。\n主开关 `global.enabled` 同步每个显示器:开 → 全部跟随全局 URL,关 → 每个显示器使用自己的。\n无认证令牌且闲置 30 天的显示器会自动移除。\n认证流程已加固。\n`web` 适配器已声明为依赖。"
59
+ "en": "Breaking: redirect target now configured via mode dropdown + manualUrl free text instead of the old visUrl. Existing setups auto-migrated.",
60
+ "de": "Breaking: Weiterleitungs-Ziel jetzt über mode-Dropdown + manualUrl-Freitext konfiguriert statt altem visUrl. Bestehende Setups werden automatisch migriert.",
61
+ "ru": "Breaking: цель перенаправления теперь настраивается через dropdown mode + свободный текст manualUrl вместо старого visUrl. Существующие настройки мигрируют автоматически.",
62
+ "pt": "Breaking: alvo de redirecionamento agora configurado via dropdown mode + texto livre manualUrl em vez do antigo visUrl. Configurações existentes são migradas automaticamente.",
63
+ "nl": "Breaking: redirect-doel nu via mode-dropdown + manualUrl-vrije tekst geconfigureerd in plaats van het oude visUrl. Bestaande setups worden automatisch gemigreerd.",
64
+ "fr": "Breaking : la cible de redirection se configure désormais via le dropdown mode + le texte libre manualUrl au lieu de l'ancien visUrl. Les configurations existantes sont migrées automatiquement.",
65
+ "it": "Breaking: obiettivo di redirect ora configurato tramite dropdown mode + testo libero manualUrl invece del vecchio visUrl. Le configurazioni esistenti vengono migrate automaticamente.",
66
+ "es": "Breaking: el destino de redirección ahora se configura mediante el dropdown mode + el texto libre manualUrl en lugar del antiguo visUrl. Las configuraciones existentes se migran automáticamente.",
67
+ "pl": "Breaking: cel przekierowania jest teraz konfigurowany przez dropdown mode + wolny tekst manualUrl zamiast starego visUrl. Istniejące konfiguracjemigrowane automatycznie.",
68
+ "uk": "Breaking: ціль перенаправлення тепер налаштовується через dropdown mode + вільний текст manualUrl замість старого visUrl. Існуючі налаштування мігрують автоматично.",
69
+ "zh-cn": "Breaking:重定向目标现在通过 mode 下拉框 + manualUrl 自由文本配置,取代旧的 visUrl。现有配置会自动迁移。"
44
70
  },
45
71
  "1.1.6": {
46
- "en": "Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:\nTest setup migrated: tests now live next to source as `src/lib/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: \">=20\"`\nDependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script added\nOrphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)",
47
- "de": "Audit-Aufräumung gegen den vorgeschalteten `ioBroker.example/TypeScript` Vollstandard:\nTest-Setup migriert: Tests leben jetzt neben der Quelle als `src/lib/*.test.ts` und laufen direkt über `ts-node/register`. Entfernen `tsconfig.test.json` + `build-test/`, add `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rollte zurück von `^25.6.0` zu `^20.19.24` so type defs match `engines.node: \">=20\"``\nDependabot ignoriert nun wichtige Beulen für `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` skript hinzugefügt\nOrphan `.github/auto-merge.yml` entfernt (aktiver Workflow ist `automerge-dependabot.yml` mit `gh pr merge`)",
48
- "ru": "Очистка аудита по полному стандарту «ioBroker.example/TypeScript»:\nНастройка теста мигрировала: тесты теперь живут рядом с источником как «src/lib/*.test.ts» и запускаются непосредственно через «ts-узел/регистр». Снято \"tsconfig.test.json\" + \"build-test/\", добавлено \"test/mocharc.custom.json\" + \"test/mocha.setup.js\" + \"test/tsconfig.json\" + \"test/.eslintrc.json\"\n'@types/node' откатился от '^25.6.0' к '^20.19.24', так что type defs соответствуют 'engines.node: '>=20'\nDependabot теперь игнорирует основные проблемы для «@types/node», «typescript», «eslint», «actions/checkout», «actions/setup-node»\n«nyc» config + скрипт «coverage»\nOrphan '.github/auto-merge.yml' removed (активный рабочий процесс - 'automerge-dependabot.yml' using 'gh pr merge')",
49
- "pt": "Limpeza de auditoria contra o padrão completo 'ioBroker.example/TypeScript` upstream:\nTest setup migrated: tests now live next to source as `src/lib/*.test.ts` e execute diretamente via `ts-node/register`. Removido `tsconfig.test.json` + `build-test/`, adicionado `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json'\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` então tipo defs match `engines.node: \">=20\"`\nDependabot agora ignora grandes saliências para `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node'\n'nyc' config + 'coverage' script adicionado\nÓrfão `.github/auto-merge.yml` removido (fluxo de trabalho ativo é `automerge-dependabot.yml` usando `gh pr merge`)",
50
- "nl": "Audit cleanup tegen de upstream .example/TypeScript\nTestopstelling gemigreerd: de tests leven nu naast de bron als Verwijderd \nhet type defs komt overeen met de motors.node: \">=20\"\nDependabot negeert nu de grote hobbels voor \n`nyc` config + `coverage` script added\nOrphan ",
51
- "fr": "Nettoyage de l'audit par rapport à la norme en amont `ioBroker.example/TypeScript`:\nConfiguration de test migrée: teste maintenant en direct à côté de source comme `src/lib/*.test.ts` et fonctionne directement via `ts-node/register`. Supprimé `tsconfig.test.json` + `build-test/`, ajouté `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.estintrc.json`\n`@types/node` retourné de `^25.6.0` à `^20.19.24` de sorte que le type defs correspond `moteurs.node: \">=20\"`\nDependabot ignore maintenant les principales bosses pour `@types/node`, `typescript`, `estint`, `actions/checkout`, `actions/setup-node`\nscript `nyc` config + `coverage` ajouté\nOrphan `.github/auto-merge.yml` supprimé (le workflow actif est `automerge-dependabot.yml` en utilisant `gh pr merge`)",
52
- "it": "Audit cleanup contro il `ioBroker.example/TypeScript` standard completo:\nConfigurazione del test migrata: i test ora vivono accanto alla sorgente come `src/lib/*.test.ts` e vengono eseguiti direttamente tramite `ts-node/register`. `tsconfig.test.json` + `build-test/`, aggiunto `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rotolato indietro da `^25.6.0` a `^20.19.24` così tipo defs match `engines.node: \">=20\"`\nDependabot ora ignora urti principali per `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script aggiunto\nOrphan `.github/auto-merge.yml` rimosso (il flusso di lavoro attivo è `automerge-dependabot.yml` utilizzando `gh pr merge`)",
53
- "es": "Limpieza de auditorías contra el estándar completo del `ioBroker.example/TypeScript`:\nSe migraron las pruebas: las pruebas ahora viven al lado de la fuente como `src/lib/*.test.ts` y se ejecutan directamente a través de `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: \"consign=20\"\nDependabot hace caso omiso de los principales golpes de `@tipos/nodo`, `tiposcript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script añadido\nOrphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)",
54
- "pl": "Oczyszczanie audytów w stosunku do pełnego standardu 'iBroker.example / TypeScript':\nUstawienie testowe migrowane: testy są teraz dostępne obok źródła jako 'src / lib / * .test.ts' i uruchamiane bezpośrednio przez 'ts- node / register'. Usunięto 'tsconfig.test.json' + 'build- test /', dodano 'test / mocharc.customi.json' + 'test / mocha.setup.js' + 'test / tsconfig.json' + 'test / .eslintrc.json'\n'@ types / node' rolled back from '^ 25.6.0' to '^ 20.19.24' so type defs match 'engins.node: \"> = 20\"'\nDependabot ignoruje teraz główne przeszkody dla '@ types / node', 'typescript', 'eslint', 'actions / checkout', 'actions / setup-node'\ndodano skrypt 'nyc' config + 'cover'\nUsunięty Orphan '.github / auto- merge.yml' (aktywny przepływ pracy to 'automaterge- dependiabot.yml' przy użyciu 'gh pr merge')",
55
- "uk": "Аудиторське очищення від потоку `ioBroker.example/TypeScript`\nТестові налаштування мігровані: тести тепер живуть поруч з джерелом як `src/lib/*.test.ts` і запустити безпосередньо через `ts-node/register`. Вилучено `tsconfig.test.json` + `build-test/``, додано `test/mocharc.custom.json` + `test/mocha.setup.js` +`test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` від `^25.6.0`` до `^20.19.24` так типу defs match `engines.node: \">=20\"`\nВ залежності від того, як `slint`, `slint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + скрипт `coverage` додано\nOrphan `.github/auto-merge.yml` вилучено (активний робочий процес `automerge-залежніabot.yml` за допомогою `gh pr злиття`)",
56
- "zh-cn": "对照上游`ioBroker.example/TypeScript ' 全面标准进行审计清理:\n测试设置迁移:现在测试作为`src/lib/*.test.ts ' 在源头旁边进行,直接通过`ts-node/register ' 运行。 删除`tsconfig.test.json ' +`building-test/ ' ,增加`test/mocharc.custom.json ' +`test/mocha.setup.js ' +`test/tsconfig.json ' +`test/.eslintrac.json'\n`@型/节点`从`^25.6.0`回滚至`^20.19.24`,因此 type型与 型相同\n现在,依赖忽略了`Q ' 类型/节点 ' 、`字典 ' 、`slint ' 、`行动/检查 ' 、`行动/设置节点 ' 的重大颠峰\n添加了 \" nyc \" 配置 + \" 覆盖 \" 文字\n孤儿`.github/自动合并.yml ' 被删除(使用`gh pr 合并 ' 的主动工作流程为`自动合并-依赖.yml')"
72
+ "en": "Internal cleanup. No user-facing changes.",
73
+ "de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
74
+ "ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
75
+ "pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
76
+ "nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
77
+ "fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
78
+ "it": "Pulizia interna. Nessuna modifica visibile all'utente.",
79
+ "es": "Limpieza interna. Sin cambios visibles para el usuario.",
80
+ "pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
81
+ "uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
82
+ "zh-cn": "内部清理。对用户无可见变化。"
57
83
  },
58
84
  "1.1.5": {
59
- "en": "Process-level unhandledRejection/uncaughtException handlers added as last-line-of-defence. Stop shipping the manual-review release-script plugin. Audit-driven boilerplate sync with the other krobi adapters. Min js-controller correction: was >=7.0.0, restored to repochecker-recommended >=6.0.11 (Source: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker bumped to ^7.1.1.",
60
- "de": "Process-level unhandledRejection/uncaughtException-Handler als Last-Line-of-Defence hinzugefügt. Das manual-review-Release-Script-Plugin entfällt. Audit-getriebener Boilerplate-Sync gegenüber den anderen krobi-Adaptern. js-controller-Korrektur: war >=7.0.0, korrigiert auf Repochecker-recommended >=6.0.11 (Quelle: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker auf ^7.1.1 angehoben.",
61
- "ru": "Process-level обробники unhandledRejection/uncaughtException додано як last-line-of-defence. Manual-review release-script plugin більше не постачається. Audit-керована синхронізація boilerplate з іншими krobi-адаптерами. Корекція js-controller: було >=7.0.0, відновлено на repochecker-recommended >=6.0.11 (Джерело: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker оновлено до ^7.1.1.",
62
- "pt": "Handlers process-level unhandledRejection/uncaughtException adicionados como last-line-of-defence. O plugin manual-review do release-script foi removido. Sincronização de boilerplate orientada por audit com os outros adaptadores krobi. Correção js-controller: era >=7.0.0, restaurada para repochecker-recommended >=6.0.11 (Fonte: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker atualizado para ^7.1.1.",
63
- "nl": "Process-level unhandledRejection/uncaughtException-handlers toegevoegd als last-line-of-defence. Het manual-review release-script plugin wordt niet meer meegeleverd. Audit-gestuurde boilerplate-sync met de andere krobi-adapters. js-controller-correctie: was >=7.0.0, hersteld naar repochecker-recommended >=6.0.11 (Bron: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker verhoogd naar ^7.1.1.",
64
- "fr": "Handlers process-level unhandledRejection/uncaughtException ajoutés en last-line-of-defence. Le plugin manual-review du release-script n'est plus livré. Synchronisation boilerplate audit-driven avec les autres adaptateurs krobi. Correction js-controller: était >=7.0.0, restaurée à repochecker-recommended >=6.0.11 (Source: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker porté à ^7.1.1.",
65
- "it": "Handler process-level unhandledRejection/uncaughtException aggiunti come last-line-of-defence. Il plugin manual-review del release-script non viene più distribuito. Sincronizzazione boilerplate audit-driven con gli altri adapter krobi. Correzione js-controller: era >=7.0.0, ripristinata a repochecker-recommended >=6.0.11 (Fonte: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker alzato a ^7.1.1.",
66
- "es": "Handlers process-level unhandledRejection/uncaughtException añadidos como last-line-of-defence. El plugin manual-review del release-script ya no se incluye. Sincronización de boilerplate guiada por audit con los demás adaptadores krobi. Corrección js-controller: era >=7.0.0, restaurada a repochecker-recommended >=6.0.11 (Fuente: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker elevado a ^7.1.1.",
67
- "pl": "Handlery process-level unhandledRejection/uncaughtException dodane jako last-line-of-defence. Plugin manual-review release-script nie jest już dostarczany. Synchronizacja boilerplate'u audit-driven z pozostałymi adapterami krobi. Korekcja js-controller: było >=7.0.0, przywrócono do repochecker-recommended >=6.0.11 (Źródło: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker podniesione do ^7.1.1.",
68
- "uk": "Process-level обробники unhandledRejection/uncaughtException додано як last-line-of-defence. Manual-review release-script plugin більше не постачається. Audit-керована синхронізація boilerplate з іншими krobi-адаптерами. js-controller-корекція: було >=7.0.0, відновлено на repochecker-recommended >=6.0.11 (Джерело: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker оновлено до ^7.1.1.",
69
- "zh-cn": "新增进程级 unhandledRejection/uncaughtException 处理器作为 last-line-of-defence。manual-review release-script 插件不再随包发布。与其他 krobi 适配器进行 audit 驱动的 boilerplate 同步。js-controller 更正:从 >=7.0.0 恢复为 repochecker 推荐的 >=6.0.11(来源:ioBroker.repochecker/lib/M1000_IOPackageJson.js)。@types/iobroker 升级至 ^7.1.1。"
70
- },
71
- "1.1.4": {
72
- "en": "Separate test-build output (`build-test/`) from production `build/` — `npm test` no longer risks leaving duplicated `build/src` + `build/test` trees in the published package. No runtime change.",
73
- "de": "Separate Test-Building-Ausgang (`Building-Test/`) von der Produktion `Building/` — `npm-Test` nicht mehr Risiken verlassen duplizierte `Building/Src` + `Building/test` Bäume im veröffentlichten Paket. Keine Laufzeitänderung.",
74
- "ru": "Отдельная продукция испытательной постройки (‘build-test/’) от производства ‘build/’ — ‘npm test’ больше не рискует оставить дублированные деревья ‘build/src’ + ‘build/test’ в опубликованном пакете. Никаких изменений.",
75
- "pt": "Saída de construção de teste separada (`build-test/`) da produção `build/` — `npm test` já não corre o risco de deixar árvores duplicadas `build/src` + `build/test` no pacote publicado. Nenhuma mudança de tempo de execução.",
76
- "nl": "Aparte test-build output van de productie Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build-Build- Geen verandering in de looptijd.",
77
- "fr": "La sortie séparée de la construction d'essai (`build-test/`) de la production `build/` — `npm test` ne risque plus de laisser des arbres dupliqués `build/src` + `build/test` dans le paquet publié. Pas de changement d'exécution.",
78
- "it": "Uscita separata di test-build (`build-test/`) dalla produzione `build/` — `npm test` non rischia più di lasciare duplicati `build/src` + `build/test` alberi nel pacchetto pubblicato. Nessun cambiamento di runtime.",
79
- "es": "La producción separada de la producción de pruebas ( \" pruebas impresas \" ) de la producción \" , \" prueba npm \" ya no corre el riesgo de dejar árboles duplicados \" construidos/src \" + \" construidos/test \" en el paquete publicado. No hay cambio de horario.",
80
- "pl": "Oddzielna produkcja testowa (\"build-test /\") z produkcji \"build /\" - \"npm test\" nie może już pozostawiać powielonych \"build / src\" + \"build / test\" drzew w opublikowanym pakiecie. Bez zmian.",
81
- "uk": "Окремий тестово-будівельний вихід (`build-test/``) від виробництва `build/`` — `npm test’ не більше ризиків, що залишають дублікатом `build/src` + `build/test` дерев у опублікованому пакеті. Немає змін робочого часу.",
82
- "zh-cn": "将试验-建设产出(`建设-测试/')与生产`建设/'——`npm测试 ' 分开,不再有将`建设/src ' +`建设/测试 ' 树木复制到已公布的成套材料的风险。 无运行时间变化 ."
83
- },
84
- "1.1.3": {
85
- "en": "Fix duplicate client registration on initial connect — HA displays fire several parallel cookieless requests, each used to create a separate client record. Now locked per IP so the first create wins and the rest of the burst attach to the same client. Setup page redesigned: big green OK banner, localized into all 11 adapter languages (auto-picked from the ioBroker system language), IP shown alongside the device ID, responsive layout with dark-mode support.",
86
- "de": "Doppelte Client-Registrierung beim ersten Verbinden behoben — HA-Displays feuern mehrere parallele Requests ohne Cookie, jeder legte bisher einen eigenen Client an. Jetzt pro IP gesperrt: der erste Create gewinnt, die restlichen Parallel-Requests werden dem gleichen Client zugeordnet. Setup-Seite überarbeitet: großes grünes OK-Banner, Lokalisierung in alle 11 Adapter-Sprachen (automatisch nach ioBroker-Systemsprache), IP wird neben der Device-ID angezeigt, responsive Layout mit Dark-Mode.",
87
- "ru": "Исправлено дублирование регистрации клиента при первом подключении — HA-дисплеи шлют несколько параллельных запросов без cookie. Теперь блокировка по IP. Страница настройки переработана, локализована, показана IP.",
88
- "pt": "Corrigido registro duplicado de cliente na conexão inicial — displays HA enviam várias requisições paralelas sem cookie. Agora bloqueadas por IP. Página de configuração redesenhada, traduzida, com IP.",
89
- "nl": "Dubbele client-registratie bij eerste verbinding opgelost — HA-displays sturen meerdere parallelle requests zonder cookie. Nu vergrendeld per IP. Setup-pagina vernieuwd, gelokaliseerd, met IP.",
90
- "fr": "Correction de l'enregistrement client dupliqué à la première connexion — les écrans HA envoient plusieurs requêtes parallèles sans cookie. Verrouillé par IP désormais. Page de configuration redessinée, traduite, IP affichée.",
91
- "it": "Corretta la registrazione duplicata del client alla prima connessione — i display HA inviano più richieste parallele senza cookie. Ora bloccato per IP. Pagina di setup ridisegnata, localizzata, con IP.",
92
- "es": "Corregido el registro duplicado de cliente en la conexión inicial — los displays HA envían varias solicitudes paralelas sin cookie. Ahora bloqueado por IP. Página de configuración rediseñada, traducida, con IP.",
93
- "pl": "Naprawiono zduplikowaną rejestrację klienta przy pierwszym połączeniu — wyświetlacze HA wysyłają kilka równoległych żądań bez cookie. Teraz blokada per IP. Strona konfiguracji przeprojektowana, przetłumaczona, z IP.",
94
- "uk": "Виправлено дублювання реєстрації клієнта при першому підключенні — HA-дисплеї надсилають кілька паралельних запитів без cookie. Тепер блокування за IP. Сторінка налаштування перероблена, перекладена, з IP.",
95
- "zh-cn": "修复首次连接时客户端重复注册 — HA 显示器并行发送多个无 cookie 请求,之前每个都会创建独立客户端记录。现在按 IP 加锁。设置页面重新设计,已本地化,显示 IP。"
85
+ "en": "Crash defense: process-level error handlers. Min js-controller restored to >=6.0.11.",
86
+ "de": "Crash-Schutz: process-level Error-Handler. Min js-controller zurück auf >=6.0.11.",
87
+ "ru": "Защита от сбоев: process-level обработчики ошибок. Мин. js-controller восстановлен на >=6.0.11.",
88
+ "pt": "Proteção contra falhas: handlers de erro process-level. Mín. js-controller restaurado para >=6.0.11.",
89
+ "nl": "Crashbescherming: process-level error-handlers. Min. js-controller hersteld op >=6.0.11.",
90
+ "fr": "Défense contre les crashs : handlers d'erreur process-level. Min js-controller rétabli à >=6.0.11.",
91
+ "it": "Difesa anti-crash: handler di errore process-level. Min js-controller ripristinato a >=6.0.11.",
92
+ "es": "Defensa contra fallos: handlers de error process-level. Mín. js-controller restaurado a >=6.0.11.",
93
+ "pl": "Ochrona przed crashem: handlery błędów process-level. Min. js-controller przywrócony do >=6.0.11.",
94
+ "uk": "Захист від збоїв: process-level обробники помилок. Мін. js-controller повернено на >=6.0.11.",
95
+ "zh-cn": "崩溃防护:process-level 错误处理器。最低 js-controller 恢复为 >=6.0.11。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.hassemu",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Emulates a minimal Home Assistant server so devices expecting a Home Assistant dashboard can display any custom web URL.",
5
5
  "author": {
6
6
  "name": "krobi",