@riddix/hamh 2.1.0-alpha.756 → 2.1.0-alpha.757

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.
@@ -124630,7 +124630,8 @@ function buildEntityDiagnostics(exposed, failed, warnings) {
124630
124630
  const result = failed.map((f) => ({
124631
124631
  entityId: f.entityId,
124632
124632
  status: "failed",
124633
- reason: f.reason
124633
+ reason: f.reason,
124634
+ failedAt: f.failedAt
124634
124635
  }));
124635
124636
  const failedIds = new Set(failed.map((f) => f.entityId));
124636
124637
  const issuesByEntity = /* @__PURE__ */ new Map();
@@ -127169,7 +127170,8 @@ function healthApi(bridgeService, haClient, version2, startTime) {
127169
127170
  recovery: {
127170
127171
  enabled: bridgeService.autoRecoveryEnabled,
127171
127172
  lastRecoveryAttempt: bridgeService.lastRecoveryAttempt?.toISOString(),
127172
- recoveryCount: bridgeService.recoveryCount
127173
+ recoveryCount: bridgeService.recoveryCount,
127174
+ history: bridgeService.recoveryHistory
127173
127175
  }
127174
127176
  };
127175
127177
  res.json(detailed);
@@ -129336,7 +129338,177 @@ function buildPath(...paths) {
129336
129338
 
129337
129339
  // src/api/settings-api.ts
129338
129340
  import { Router as Router2 } from "express";
129339
- function settingsApi(settingsStorage, envAuth) {
129341
+
129342
+ // src/services/storage/app-settings-storage.ts
129343
+ init_service();
129344
+ import { randomBytes as randomBytes2, scryptSync, timingSafeEqual } from "node:crypto";
129345
+ var SCRYPT_KEYLEN = 64;
129346
+ var SCRYPT_COST_N = 16384;
129347
+ var SCRYPT_BLOCK_R = 8;
129348
+ var SCRYPT_PARALLEL_P = 1;
129349
+ var SALT_BYTES = 16;
129350
+ var DEFAULT_BACKUP_SETTINGS = {
129351
+ autoBackup: true,
129352
+ backupRetentionCount: 5
129353
+ };
129354
+ var MIN_RECOVERY_INTERVAL_MS = 1e4;
129355
+ var MAX_RECOVERY_INTERVAL_MS = 36e5;
129356
+ var DEFAULT_RECOVERY_SETTINGS = {
129357
+ autoRecoveryEnabled: true,
129358
+ recoveryIntervalMs: 6e4
129359
+ };
129360
+ function hashPassword(plain2) {
129361
+ const salt = randomBytes2(SALT_BYTES);
129362
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
129363
+ N: SCRYPT_COST_N,
129364
+ r: SCRYPT_BLOCK_R,
129365
+ p: SCRYPT_PARALLEL_P
129366
+ });
129367
+ return {
129368
+ passwordHash: derived.toString("hex"),
129369
+ passwordSalt: salt.toString("hex")
129370
+ };
129371
+ }
129372
+ function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
129373
+ let expected;
129374
+ let salt;
129375
+ try {
129376
+ expected = Buffer.from(passwordHash, "hex");
129377
+ salt = Buffer.from(passwordSalt, "hex");
129378
+ } catch {
129379
+ return false;
129380
+ }
129381
+ if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
129382
+ return false;
129383
+ }
129384
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
129385
+ N: SCRYPT_COST_N,
129386
+ r: SCRYPT_BLOCK_R,
129387
+ p: SCRYPT_PARALLEL_P
129388
+ });
129389
+ return timingSafeEqual(derived, expected);
129390
+ }
129391
+ function constantTimeStringEquals(a, b) {
129392
+ const bufA = Buffer.from(a);
129393
+ const bufB = Buffer.from(b);
129394
+ if (bufA.length !== bufB.length) {
129395
+ timingSafeEqual(bufA, bufA);
129396
+ return false;
129397
+ }
129398
+ return timingSafeEqual(bufA, bufB);
129399
+ }
129400
+ var AppSettingsStorage = class extends Service {
129401
+ constructor(appStorage) {
129402
+ super("AppSettingsStorage");
129403
+ this.appStorage = appStorage;
129404
+ }
129405
+ appStorage;
129406
+ storage;
129407
+ settings = {};
129408
+ // Caches the last plaintext password that was successfully verified
129409
+ // against the stored hash, keyed by username. This lets repeated basic-auth
129410
+ // calls avoid re-running scrypt on every HTTP request without persisting
129411
+ // the plaintext to disk.
129412
+ verifiedPassword = null;
129413
+ async initialize() {
129414
+ this.storage = this.appStorage.createContext("settings");
129415
+ const stored = await this.storage.get(
129416
+ "data",
129417
+ {}
129418
+ );
129419
+ this.settings = stored ?? {};
129420
+ }
129421
+ get auth() {
129422
+ if (!this.settings.auth) {
129423
+ return void 0;
129424
+ }
129425
+ return { username: this.settings.auth.username };
129426
+ }
129427
+ async setAuth(credentials) {
129428
+ if (!credentials) {
129429
+ this.settings.auth = void 0;
129430
+ this.verifiedPassword = null;
129431
+ } else {
129432
+ const { passwordHash, passwordSalt } = hashPassword(credentials.password);
129433
+ this.settings.auth = {
129434
+ username: credentials.username,
129435
+ passwordHash,
129436
+ passwordSalt
129437
+ };
129438
+ this.verifiedPassword = {
129439
+ username: credentials.username,
129440
+ password: credentials.password
129441
+ };
129442
+ }
129443
+ await this.persist();
129444
+ }
129445
+ verifyAuth(username, password) {
129446
+ const stored = this.settings.auth;
129447
+ if (!stored) {
129448
+ return false;
129449
+ }
129450
+ if (!constantTimeStringEquals(username, stored.username)) {
129451
+ return false;
129452
+ }
129453
+ if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
129454
+ return true;
129455
+ }
129456
+ if (stored.password !== void 0) {
129457
+ const matches = constantTimeStringEquals(password, stored.password);
129458
+ if (matches) {
129459
+ this.verifiedPassword = { username: stored.username, password };
129460
+ void this.migrateLegacyPlaintext(password);
129461
+ }
129462
+ return matches;
129463
+ }
129464
+ if (stored.passwordHash && stored.passwordSalt) {
129465
+ if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
129466
+ this.verifiedPassword = { username: stored.username, password };
129467
+ return true;
129468
+ }
129469
+ }
129470
+ return false;
129471
+ }
129472
+ async migrateLegacyPlaintext(plain2) {
129473
+ const current = this.settings.auth;
129474
+ if (!current || current.password === void 0) {
129475
+ return;
129476
+ }
129477
+ const { passwordHash, passwordSalt } = hashPassword(plain2);
129478
+ this.settings.auth = {
129479
+ username: current.username,
129480
+ passwordHash,
129481
+ passwordSalt
129482
+ };
129483
+ try {
129484
+ await this.persist();
129485
+ } catch {
129486
+ }
129487
+ }
129488
+ get backupSettings() {
129489
+ return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
129490
+ }
129491
+ async setBackupSettings(settings) {
129492
+ this.settings.backup = { ...this.settings.backup, ...settings };
129493
+ await this.persist();
129494
+ }
129495
+ get recoverySettings() {
129496
+ return { ...DEFAULT_RECOVERY_SETTINGS, ...this.settings.recovery };
129497
+ }
129498
+ async setRecoverySettings(settings) {
129499
+ this.settings.recovery = { ...this.settings.recovery, ...settings };
129500
+ await this.persist();
129501
+ }
129502
+ async persist() {
129503
+ await this.storage.set(
129504
+ "data",
129505
+ this.settings
129506
+ );
129507
+ }
129508
+ };
129509
+
129510
+ // src/api/settings-api.ts
129511
+ function settingsApi(settingsStorage, bridgeService, envAuth) {
129340
129512
  const router = Router2();
129341
129513
  router.get("/auth", (_req, res) => {
129342
129514
  if (envAuth) {
@@ -129393,6 +129565,31 @@ function settingsApi(settingsStorage, envAuth) {
129393
129565
  res.json({ enabled: false, source: "none" });
129394
129566
  }
129395
129567
  );
129568
+ router.get("/recovery", (_req, res) => {
129569
+ res.json(settingsStorage.recoverySettings);
129570
+ });
129571
+ router.put(
129572
+ "/recovery",
129573
+ async (req, res) => {
129574
+ const next = { ...settingsStorage.recoverySettings };
129575
+ const { autoRecoveryEnabled, recoveryIntervalMs } = req.body;
129576
+ if (typeof autoRecoveryEnabled === "boolean") {
129577
+ next.autoRecoveryEnabled = autoRecoveryEnabled;
129578
+ }
129579
+ if (typeof recoveryIntervalMs === "number") {
129580
+ if (recoveryIntervalMs < MIN_RECOVERY_INTERVAL_MS || recoveryIntervalMs > MAX_RECOVERY_INTERVAL_MS) {
129581
+ res.status(400).json({
129582
+ error: `recoveryIntervalMs must be between ${MIN_RECOVERY_INTERVAL_MS} and ${MAX_RECOVERY_INTERVAL_MS}`
129583
+ });
129584
+ return;
129585
+ }
129586
+ next.recoveryIntervalMs = recoveryIntervalMs;
129587
+ }
129588
+ await settingsStorage.setRecoverySettings(next);
129589
+ bridgeService.applyRecoverySettings(next);
129590
+ res.json(next);
129591
+ }
129592
+ );
129396
129593
  return router;
129397
129594
  }
129398
129595
 
@@ -129824,7 +130021,10 @@ var WebApi = class extends Service {
129824
130021
  ).use("/bridges", bridgeExportApi(this.bridgeStorage)).use("/bridge-icons", bridgeIconApi(this.props.storageLocation)).use(
129825
130022
  "/device-images",
129826
130023
  deviceImageApi(this.props.storageLocation, this.haRegistry)
129827
- ).use("/entity-mappings", entityMappingApi(this.mappingStorage)).use("/mapping-profiles", mappingProfileApi(this.mappingStorage)).use("/lock-credentials", lockCredentialApi(this.lockCredentialStorage)).use("/settings", settingsApi(this.settingsStorage, this.props.auth)).use(
130024
+ ).use("/entity-mappings", entityMappingApi(this.mappingStorage)).use("/mapping-profiles", mappingProfileApi(this.mappingStorage)).use("/lock-credentials", lockCredentialApi(this.lockCredentialStorage)).use(
130025
+ "/settings",
130026
+ settingsApi(this.settingsStorage, this.bridgeService, this.props.auth)
130027
+ ).use(
129828
130028
  "/backup",
129829
130029
  backupApi(
129830
130030
  this.bridgeStorage,
@@ -130611,13 +130811,23 @@ var BridgeFactory = class extends Service {
130611
130811
  init_esm();
130612
130812
  init_service();
130613
130813
  import crypto3 from "node:crypto";
130614
- var BridgeService = class extends Service {
130814
+ function clampRecoveryInterval(ms) {
130815
+ if (!Number.isFinite(ms)) return 6e4;
130816
+ return Math.min(
130817
+ Math.max(ms, MIN_RECOVERY_INTERVAL_MS),
130818
+ MAX_RECOVERY_INTERVAL_MS
130819
+ );
130820
+ }
130821
+ var BridgeService = class _BridgeService extends Service {
130615
130822
  constructor(bridgeStorage, bridgeFactory, props) {
130616
130823
  super("BridgeService");
130617
130824
  this.bridgeStorage = bridgeStorage;
130618
130825
  this.bridgeFactory = bridgeFactory;
130619
130826
  this.props = props;
130620
130827
  this.autoRecoveryEnabled = props.autoRecovery ?? true;
130828
+ this.recoveryIntervalMs = clampRecoveryInterval(
130829
+ props.recoveryIntervalMs ?? 6e4
130830
+ );
130621
130831
  }
130622
130832
  bridgeStorage;
130623
130833
  bridgeFactory;
@@ -130631,6 +130841,12 @@ var BridgeService = class extends Service {
130631
130841
  // Set by the caller (e.g. start-handler) to broadcast updates via WebSocket.
130632
130842
  onBridgeChanged;
130633
130843
  recoveryInterval;
130844
+ recoveryIntervalMs = 6e4;
130845
+ recoveryInProgress = false;
130846
+ _recoveryHistory = [];
130847
+ lastReactiveRecovery = 0;
130848
+ static MAX_RECOVERY_HISTORY = 20;
130849
+ static REACTIVE_DEBOUNCE_MS = 5e3;
130634
130850
  async initialize() {
130635
130851
  for (const data of this.bridgeStorage.bridges) {
130636
130852
  const normalized = this.normalizeBridgeData(data);
@@ -130653,24 +130869,80 @@ var BridgeService = class extends Service {
130653
130869
  };
130654
130870
  }
130655
130871
  startAutoRecovery() {
130656
- const intervalMs = this.props.recoveryIntervalMs ?? 6e4;
130657
130872
  this.recoveryInterval = setInterval(() => {
130658
130873
  this.attemptRecovery();
130659
- }, intervalMs);
130874
+ }, this.recoveryIntervalMs);
130875
+ }
130876
+ stopAutoRecovery() {
130877
+ if (this.recoveryInterval) {
130878
+ clearInterval(this.recoveryInterval);
130879
+ this.recoveryInterval = void 0;
130880
+ }
130881
+ }
130882
+ get recoverySettings() {
130883
+ return {
130884
+ autoRecoveryEnabled: this.autoRecoveryEnabled,
130885
+ recoveryIntervalMs: this.recoveryIntervalMs
130886
+ };
130887
+ }
130888
+ get recoveryHistory() {
130889
+ return [...this._recoveryHistory];
130890
+ }
130891
+ // Apply persisted recovery settings (at startup) or a change from the UI.
130892
+ applyRecoverySettings(settings) {
130893
+ this.autoRecoveryEnabled = settings.autoRecoveryEnabled;
130894
+ this.recoveryIntervalMs = clampRecoveryInterval(
130895
+ settings.recoveryIntervalMs
130896
+ );
130897
+ this.stopAutoRecovery();
130898
+ if (this.autoRecoveryEnabled) {
130899
+ this.startAutoRecovery();
130900
+ }
130901
+ }
130902
+ // Run a recovery pass right away, e.g. just after Home Assistant reconnects,
130903
+ // instead of waiting for the next interval. Debounced so a burst of reconnect
130904
+ // events only triggers one pass. Still only touches failed bridges.
130905
+ recoverFailedBridgesNow() {
130906
+ if (!this.autoRecoveryEnabled) return;
130907
+ const now = Date.now();
130908
+ if (now - this.lastReactiveRecovery < _BridgeService.REACTIVE_DEBOUNCE_MS) {
130909
+ return;
130910
+ }
130911
+ this.lastReactiveRecovery = now;
130912
+ void this.attemptRecovery();
130660
130913
  }
130661
130914
  async attemptRecovery() {
130915
+ if (this.recoveryInProgress) return;
130662
130916
  const failedBridges = this.bridges.filter(
130663
130917
  (b) => b.data.status === "failed"
130664
130918
  );
130665
130919
  if (failedBridges.length === 0) return;
130920
+ this.recoveryInProgress = true;
130666
130921
  this.lastRecoveryAttempt = /* @__PURE__ */ new Date();
130667
- for (const bridge of failedBridges) {
130668
- try {
130669
- await bridge.start();
130670
- this.recoveryCount++;
130671
- } catch (e) {
130672
- this.log.warn(`Recovery attempt failed for bridge ${bridge.id}:`, e);
130922
+ try {
130923
+ for (const bridge of failedBridges) {
130924
+ try {
130925
+ await bridge.start();
130926
+ this.recoveryCount++;
130927
+ this.recordRecovery(bridge, "success");
130928
+ } catch (e) {
130929
+ this.log.warn(`Recovery attempt failed for bridge ${bridge.id}:`, e);
130930
+ this.recordRecovery(bridge, "failed");
130931
+ }
130673
130932
  }
130933
+ } finally {
130934
+ this.recoveryInProgress = false;
130935
+ }
130936
+ }
130937
+ recordRecovery(bridge, outcome) {
130938
+ this._recoveryHistory.push({
130939
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
130940
+ bridgeId: bridge.id,
130941
+ bridgeName: bridge.data.name,
130942
+ outcome
130943
+ });
130944
+ if (this._recoveryHistory.length > _BridgeService.MAX_RECOVERY_HISTORY) {
130945
+ this._recoveryHistory.shift();
130674
130946
  }
130675
130947
  }
130676
130948
  async restartBridge(bridgeId) {
@@ -130681,9 +130953,7 @@ var BridgeService = class extends Service {
130681
130953
  return true;
130682
130954
  }
130683
130955
  async dispose() {
130684
- if (this.recoveryInterval) {
130685
- clearInterval(this.recoveryInterval);
130686
- }
130956
+ this.stopAutoRecovery();
130687
130957
  await Promise.all(this.bridges.map((bridge) => bridge.dispose()));
130688
130958
  }
130689
130959
  getNextAvailablePort(startPort = 5540) {
@@ -131259,161 +131529,6 @@ function mockDeviceId(entityId) {
131259
131529
  return `e__${hash2}`;
131260
131530
  }
131261
131531
 
131262
- // src/services/storage/app-settings-storage.ts
131263
- init_service();
131264
- import { randomBytes as randomBytes2, scryptSync, timingSafeEqual } from "node:crypto";
131265
- var SCRYPT_KEYLEN = 64;
131266
- var SCRYPT_COST_N = 16384;
131267
- var SCRYPT_BLOCK_R = 8;
131268
- var SCRYPT_PARALLEL_P = 1;
131269
- var SALT_BYTES = 16;
131270
- var DEFAULT_BACKUP_SETTINGS = {
131271
- autoBackup: true,
131272
- backupRetentionCount: 5
131273
- };
131274
- function hashPassword(plain2) {
131275
- const salt = randomBytes2(SALT_BYTES);
131276
- const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
131277
- N: SCRYPT_COST_N,
131278
- r: SCRYPT_BLOCK_R,
131279
- p: SCRYPT_PARALLEL_P
131280
- });
131281
- return {
131282
- passwordHash: derived.toString("hex"),
131283
- passwordSalt: salt.toString("hex")
131284
- };
131285
- }
131286
- function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
131287
- let expected;
131288
- let salt;
131289
- try {
131290
- expected = Buffer.from(passwordHash, "hex");
131291
- salt = Buffer.from(passwordSalt, "hex");
131292
- } catch {
131293
- return false;
131294
- }
131295
- if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
131296
- return false;
131297
- }
131298
- const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
131299
- N: SCRYPT_COST_N,
131300
- r: SCRYPT_BLOCK_R,
131301
- p: SCRYPT_PARALLEL_P
131302
- });
131303
- return timingSafeEqual(derived, expected);
131304
- }
131305
- function constantTimeStringEquals(a, b) {
131306
- const bufA = Buffer.from(a);
131307
- const bufB = Buffer.from(b);
131308
- if (bufA.length !== bufB.length) {
131309
- timingSafeEqual(bufA, bufA);
131310
- return false;
131311
- }
131312
- return timingSafeEqual(bufA, bufB);
131313
- }
131314
- var AppSettingsStorage = class extends Service {
131315
- constructor(appStorage) {
131316
- super("AppSettingsStorage");
131317
- this.appStorage = appStorage;
131318
- }
131319
- appStorage;
131320
- storage;
131321
- settings = {};
131322
- // Caches the last plaintext password that was successfully verified
131323
- // against the stored hash, keyed by username. This lets repeated basic-auth
131324
- // calls avoid re-running scrypt on every HTTP request without persisting
131325
- // the plaintext to disk.
131326
- verifiedPassword = null;
131327
- async initialize() {
131328
- this.storage = this.appStorage.createContext("settings");
131329
- const stored = await this.storage.get(
131330
- "data",
131331
- {}
131332
- );
131333
- this.settings = stored ?? {};
131334
- }
131335
- get auth() {
131336
- if (!this.settings.auth) {
131337
- return void 0;
131338
- }
131339
- return { username: this.settings.auth.username };
131340
- }
131341
- async setAuth(credentials) {
131342
- if (!credentials) {
131343
- this.settings.auth = void 0;
131344
- this.verifiedPassword = null;
131345
- } else {
131346
- const { passwordHash, passwordSalt } = hashPassword(credentials.password);
131347
- this.settings.auth = {
131348
- username: credentials.username,
131349
- passwordHash,
131350
- passwordSalt
131351
- };
131352
- this.verifiedPassword = {
131353
- username: credentials.username,
131354
- password: credentials.password
131355
- };
131356
- }
131357
- await this.persist();
131358
- }
131359
- verifyAuth(username, password) {
131360
- const stored = this.settings.auth;
131361
- if (!stored) {
131362
- return false;
131363
- }
131364
- if (!constantTimeStringEquals(username, stored.username)) {
131365
- return false;
131366
- }
131367
- if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
131368
- return true;
131369
- }
131370
- if (stored.password !== void 0) {
131371
- const matches = constantTimeStringEquals(password, stored.password);
131372
- if (matches) {
131373
- this.verifiedPassword = { username: stored.username, password };
131374
- void this.migrateLegacyPlaintext(password);
131375
- }
131376
- return matches;
131377
- }
131378
- if (stored.passwordHash && stored.passwordSalt) {
131379
- if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
131380
- this.verifiedPassword = { username: stored.username, password };
131381
- return true;
131382
- }
131383
- }
131384
- return false;
131385
- }
131386
- async migrateLegacyPlaintext(plain2) {
131387
- const current = this.settings.auth;
131388
- if (!current || current.password === void 0) {
131389
- return;
131390
- }
131391
- const { passwordHash, passwordSalt } = hashPassword(plain2);
131392
- this.settings.auth = {
131393
- username: current.username,
131394
- passwordHash,
131395
- passwordSalt
131396
- };
131397
- try {
131398
- await this.persist();
131399
- } catch {
131400
- }
131401
- }
131402
- get backupSettings() {
131403
- return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
131404
- }
131405
- async setBackupSettings(settings) {
131406
- this.settings.backup = { ...this.settings.backup, ...settings };
131407
- await this.persist();
131408
- }
131409
- async persist() {
131410
- await this.storage.set(
131411
- "data",
131412
- this.settings
131413
- );
131414
- }
131415
- };
131416
-
131417
131532
  // src/services/storage/app-storage.ts
131418
131533
  init_service();
131419
131534
  var AppStorage = class extends Service {
@@ -163000,7 +163115,11 @@ var EntityIsolationServiceImpl = class {
163000
163115
  return true;
163001
163116
  }
163002
163117
  const reason = `${classification}. Entity isolated to protect bridge stability.`;
163003
- this.isolatedEntities.set(key, { entityId: entityName, reason });
163118
+ this.isolatedEntities.set(key, {
163119
+ entityId: entityName,
163120
+ reason,
163121
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
163122
+ });
163004
163123
  logger231.warn(
163005
163124
  `Isolating entity "${entityName}" from bridge ${bridgeId} due to: ${reason}`
163006
163125
  );
@@ -163088,6 +163207,13 @@ var BridgeEndpointManager = class extends Service {
163088
163207
  const isolated = EntityIsolationService.getIsolatedEntities(this.bridgeId);
163089
163208
  return [...this._failedEntities, ...isolated];
163090
163209
  }
163210
+ addFailedEntity(entityId, reason) {
163211
+ this._failedEntities.push({
163212
+ entityId,
163213
+ reason,
163214
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
163215
+ });
163216
+ }
163091
163217
  wirePluginCallbacks() {
163092
163218
  if (!this.pluginManager) return;
163093
163219
  this.pluginManager.onDeviceRegistered = async (pluginName, device) => {
@@ -163498,10 +163624,10 @@ var BridgeEndpointManager = class extends Service {
163498
163624
  }
163499
163625
  if (memoryLimitReached) {
163500
163626
  if (!existingEndpoints.some((e) => e.entityId === entityId)) {
163501
- this._failedEntities.push({
163627
+ this.addFailedEntity(
163502
163628
  entityId,
163503
- reason: "Skipped due to memory pressure, reduce entities or increase heap size"
163504
- });
163629
+ "Skipped due to memory pressure, reduce entities or increase heap size"
163630
+ );
163505
163631
  }
163506
163632
  continue;
163507
163633
  }
@@ -163519,7 +163645,7 @@ var BridgeEndpointManager = class extends Service {
163519
163645
  if (entityId.length > MAX_ENTITY_ID_LENGTH) {
163520
163646
  const reason = `Entity ID too long (${entityId.length} chars, max ${MAX_ENTITY_ID_LENGTH}). This would cause filesystem errors.`;
163521
163647
  this.log.warn(`Skipping entity: ${entityId}. Reason: ${reason}`);
163522
- this._failedEntities.push({ entityId, reason });
163648
+ this.addFailedEntity(entityId, reason);
163523
163649
  continue;
163524
163650
  }
163525
163651
  let endpoint = existingEndpoints.find((e) => e.entityId === entityId);
@@ -163535,7 +163661,7 @@ var BridgeEndpointManager = class extends Service {
163535
163661
  } catch (e) {
163536
163662
  const reason = this.extractErrorReason(e);
163537
163663
  this.log.warn(`Failed to create device ${entityId}: ${reason}`);
163538
- this._failedEntities.push({ entityId, reason });
163664
+ this.addFailedEntity(entityId, reason);
163539
163665
  continue;
163540
163666
  }
163541
163667
  if (endpoint) {
@@ -163551,10 +163677,7 @@ var BridgeEndpointManager = class extends Service {
163551
163677
  `Failed to add endpoint for ${entityId}: ${errorMessage}`
163552
163678
  );
163553
163679
  this.logDetailedError(entityId, e);
163554
- this._failedEntities.push({
163555
- entityId,
163556
- reason: this.extractErrorReason(e)
163557
- });
163680
+ this.addFailedEntity(entityId, this.extractErrorReason(e));
163558
163681
  }
163559
163682
  }
163560
163683
  }
@@ -166000,6 +166123,14 @@ async function startHandler(startOptions, webUiDist) {
166000
166123
  bridgeService.onBridgeChanged = (bridgeId) => {
166001
166124
  webApi.websocket.broadcastBridgeUpdate(bridgeId);
166002
166125
  };
166126
+ const [haClient, settingsStorage] = await Promise.all([
166127
+ appEnvironment.load(HomeAssistantClient),
166128
+ appEnvironment.load(AppSettingsStorage)
166129
+ ]);
166130
+ bridgeService.applyRecoverySettings(settingsStorage.recoverySettings);
166131
+ haClient.connection.addEventListener("ready", () => {
166132
+ bridgeService.recoverFailedBridgesNow();
166133
+ });
166003
166134
  let shuttingDown = false;
166004
166135
  const gracefulShutdown = async (signal) => {
166005
166136
  if (shuttingDown) return;