@camstack/addon-agent-ui 1.2.337 → 1.2.338

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.338] - 2026-11-07
11
+
12
+ ### Changed
13
+
14
+ - Rebuilt against updated dependencies.
15
+
10
16
  ## [1.2.337] - 2026-11-07
11
17
 
12
18
  ### Changed
@@ -367,13 +367,13 @@
367
367
  "server/server/packages/addon-agent-ui/src/updates-panel.tsx": "ec6b5b3701322bbb68dbe581703409cd373418cf9227ce829ea0945e70e60d54",
368
368
  "server/server/packages/addon-agent-ui/src/use-pairing-status.ts": "46193a1e83a64250dfff75d417448d74ba0159465e710861b996e903b03e427a",
369
369
  "server/server/packages/sdk/dist/index.js": "89409bfb9632c46ed63ee4882c70b7994c1ff90c67d27a5d4b86b0990c41a131",
370
- "server/server/packages/types/dist/client.mjs": "6de32b198d01e7cd8c25d936effef662421de90c5131c21c3fc3ac1a6fd3e02c",
370
+ "server/server/packages/types/dist/client.mjs": "c931e9ce7ef3cb668698efe68c97842d539f79e753394229677a6df2c0eaca0b",
371
371
  "server/server/packages/types/dist/composition-BmBHuDTQ.mjs": "0fedd1640f42bfdbb8e59f8f11a7aa597a59c35c2574297820bbc026c6236f74",
372
372
  "server/server/packages/types/dist/device-type-D1fH5kaC.mjs": "4002ae7c812789715d7a35271c91b8133511b710eaeb07fea061d285db3dc14a",
373
373
  "server/server/packages/types/dist/event-category-78tcBemo.mjs": "767f1bb576027d9ee79ec036367001330203d84842c2c2111fa465fa434045e2",
374
- "server/server/packages/types/dist/index.mjs": "da32c317f13d7d9f3f978f58e70d4a783300f8c628348282d734e4c1b492d00a",
374
+ "server/server/packages/types/dist/index.mjs": "3e26d4b169f50485bd9753898484e361ff69204aa2ae4eb80caaad00a2afbaab",
375
375
  "server/server/packages/types/dist/node-color.mjs": "942e663074f0a4ed2f8643848ec139eba4b5fd16d0d01c8a23e08a4819a8843b",
376
- "server/server/packages/types/dist/sleep-AEnTN3b4.mjs": "832102f82ade2f54d4fc035a68a55a8b7ca645bdfe6ff5d6a546865ac4d78762",
376
+ "server/server/packages/types/dist/sleep-CQP-REJp.mjs": "ef67ec7f4157dc194b75bc52a2d629a6df7bc15f69d14d8ce69b5208237854cc",
377
377
  "server/server/packages/ui-library/src/composites/adaptive-downlink.ts": "6e9698e97721461b72a3750f930dd7214d049be865491bfb5a64b87c032b0e22",
378
378
  "server/server/packages/ui-library/src/composites/camera-stream-player.tsx": "c04809229b4e091fed09e2bcfe643d6b36cb3a39e294229b49f8a095e4e6ebd3",
379
379
  "server/server/packages/ui-library/src/composites/cap-settings/CameraGridLayoutEditor.tsx": "fae93899c4903183e1b78db39e58bd52a3dd4dc2c11ce85d64e6d2a2bab7516d",
@@ -486,7 +486,7 @@
486
486
  "server/server/packages/ui-library/src/contexts/vod-playback.tsx": "c07bc3ecc786182ef2f27d13bec3f520288e0b47367edd7ddf1c2e7b4a2bbaee",
487
487
  "server/server/packages/ui-library/src/contexts/widget-registry.tsx": "71a2815c645abe96821a9e9e5668e321187527d569414f5ca197d615856cc17a",
488
488
  "server/server/packages/ui-library/src/contexts/zone-editing.tsx": "275f680304ab09dd747fd9e859dc5aa0535df33eb2d33eb669a6de66bdd848d0",
489
- "server/server/packages/ui-library/src/generated/system-hooks.ts": "ff7b9f41e948b3ec798180f0795a0672dc90d6e43d0a7c07abbc4101b4186a5f",
489
+ "server/server/packages/ui-library/src/generated/system-hooks.ts": "e737fa4c3e59db86e15b5df421a30faed6b98e988930f682b094a8ebbdddb20a",
490
490
  "server/server/packages/ui-library/src/hooks/turn-server-cache.ts": "fab22f069c3761e0d268449e056d9c499b6ea63b879684a40023735b34bad061",
491
491
  "server/server/packages/ui-library/src/hooks/use-device-capability.ts": "b74ede60be8039c45f6c712a407ee838ab208610435870ddabaa8598cb7cd7f3",
492
492
  "server/server/packages/ui-library/src/hooks/use-device-detections.ts": "61cc54cf2e7f53864e5aa533c05679e4c204c4004a0296f63ab169b003888611",
package/dist/addon.js CHANGED
@@ -5866,7 +5866,7 @@ var ZodIssueCode = {
5866
5866
  var ZodFirstPartyTypeKind;
5867
5867
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5868
5868
  //#endregion
5869
- //#region ../types/dist/sleep-AEnTN3b4.mjs
5869
+ //#region ../types/dist/sleep-CQP-REJp.mjs
5870
5870
  /**
5871
5871
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5872
5872
  * window to float samples (D455).
@@ -6223,6 +6223,59 @@ Object.fromEntries([
6223
6223
  function isValuelessField(field) {
6224
6224
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
6225
6225
  }
6226
+ /**
6227
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6228
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6229
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6230
+ * pre-resolving) lets callers both read the effective value AND detect
6231
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6232
+ * malformed input yields an empty map, never throws.
6233
+ */
6234
+ function collectHydratedFieldEntries(schema) {
6235
+ const out = /* @__PURE__ */ new Map();
6236
+ if (typeof schema !== "object" || schema === null) return out;
6237
+ const sections = schema.sections;
6238
+ if (!Array.isArray(sections)) return out;
6239
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6240
+ return out;
6241
+ }
6242
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6243
+ function collectFieldEntriesInto(fields, into) {
6244
+ if (!Array.isArray(fields)) return;
6245
+ for (const field of fields) {
6246
+ if (typeof field !== "object" || field === null) continue;
6247
+ const type = field.type;
6248
+ if (type === "group") {
6249
+ collectFieldEntriesInto(field.fields, into);
6250
+ continue;
6251
+ }
6252
+ if (type === "sub-tabs") {
6253
+ const tabs = field.tabs;
6254
+ if (Array.isArray(tabs)) {
6255
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6256
+ }
6257
+ continue;
6258
+ }
6259
+ const key = field.key;
6260
+ if (typeof key !== "string" || key.length === 0) continue;
6261
+ if (!("value" in field)) continue;
6262
+ into.set(key, {
6263
+ value: field.value,
6264
+ default: field.default
6265
+ });
6266
+ }
6267
+ }
6268
+ /**
6269
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6270
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6271
+ * common "just give me the resolved values" case. Built on
6272
+ * {@link collectHydratedFieldEntries}.
6273
+ */
6274
+ function collectHydratedFieldValues(schema) {
6275
+ const out = /* @__PURE__ */ new Map();
6276
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6277
+ return out;
6278
+ }
6226
6279
  function hydrateSchema(schema, values, options = {}) {
6227
6280
  return {
6228
6281
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6918,6 +6971,21 @@ var SCOPED_SETTINGS_SCOPES = [
6918
6971
  "device-defaults",
6919
6972
  "node"
6920
6973
  ];
6974
+ /** The one refusal of a key a scope did not declare. */
6975
+ function assertDeclaredKeys(keys, declared) {
6976
+ const undeclared = keys.filter((key) => !declared.has(key));
6977
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
6978
+ }
6979
+ /** Build the handle over an addon's scoped read, write and unset. */
6980
+ function createScopedSettings(deps) {
6981
+ return {
6982
+ async get() {
6983
+ return deps.schema.parse(await deps.read());
6984
+ },
6985
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
6986
+ unset: (keys) => deps.unset(keys)
6987
+ };
6988
+ }
6921
6989
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
6922
6990
  var KeychainWriteRefusedError = class extends Error {
6923
6991
  fieldPath;
@@ -7493,7 +7561,16 @@ function createHandle(raw, opts) {
7493
7561
  return blob;
7494
7562
  }
7495
7563
  },
7496
- writeAddonStore: writeAddon
7564
+ writeAddonStore: writeAddon,
7565
+ removeClearedKeys: (keys) => serialize("addon", async () => {
7566
+ const stored = await readStoredAddonBlob(raw);
7567
+ index.seen("addon", stored);
7568
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
7569
+ if (removable.length === 0) return;
7570
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
7571
+ await raw.writeAddonStore(patch);
7572
+ index.wrote("addon", patch);
7573
+ })
7497
7574
  };
7498
7575
  }
7499
7576
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8038,6 +8115,151 @@ var BaseAddon = class {
8038
8115
  }
8039
8116
  }
8040
8117
  /**
8118
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8119
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8120
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8121
+ */
8122
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8123
+ return null;
8124
+ }
8125
+ /**
8126
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8127
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8128
+ */
8129
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8130
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8131
+ clusterSettings(cap, schema) {
8132
+ return this.scopedHandle("cluster", cap, schema);
8133
+ }
8134
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8135
+ deviceDefaults(cap, schema) {
8136
+ return this.scopedHandle("device-defaults", cap, schema);
8137
+ }
8138
+ /**
8139
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8140
+ * with no row yields the schema defaults — never a bare-key fallback.
8141
+ */
8142
+ nodeSettings(cap, schema, nodeId) {
8143
+ return this.scopedHandle("node", cap, schema, nodeId);
8144
+ }
8145
+ /**
8146
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8147
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8148
+ * central delete. `nodeId` omitted ⇒ the local node.
8149
+ */
8150
+ nodeState(key, schema, fallback, nodeId) {
8151
+ this._declaredStateKeys.add(key);
8152
+ return createDurableState({
8153
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8154
+ schema,
8155
+ fallback,
8156
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8157
+ write: async (patch) => {
8158
+ await this._ctx?.settings?.writeAddonStore(patch);
8159
+ },
8160
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8161
+ addonId: this._ctx?.id,
8162
+ key: k,
8163
+ error: String(e)
8164
+ } })
8165
+ });
8166
+ }
8167
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8168
+ async declaredScopedSchema(scope, cap) {
8169
+ await this.loadScopedSchemas(cap);
8170
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8171
+ }
8172
+ async requireScopedSchema(scope, cap) {
8173
+ const schema = await this.declaredScopedSchema(scope, cap);
8174
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8175
+ return schema;
8176
+ }
8177
+ scopedKeys(schema) {
8178
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8179
+ }
8180
+ /** `store` narrowed to the keys `schema` declares. */
8181
+ narrowToScope(store, schema) {
8182
+ const keys = this.scopedKeys(schema);
8183
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8184
+ }
8185
+ scopedHandle(scope, cap, schema, nodeId) {
8186
+ return createScopedSettings({
8187
+ schema,
8188
+ read: async () => {
8189
+ const declared = await this.requireScopedSchema(scope, cap);
8190
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8191
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8192
+ return {
8193
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8194
+ ...stored
8195
+ };
8196
+ },
8197
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8198
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8199
+ });
8200
+ }
8201
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8202
+ async writeScoped(scope, cap, patch, nodeId) {
8203
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8204
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8205
+ }
8206
+ /**
8207
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8208
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8209
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8210
+ */
8211
+ async unsetScoped(scope, cap, keys, nodeId) {
8212
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8213
+ const target = scope === "node" ? nodeId : void 0;
8214
+ const tree = this.addonSecretTree();
8215
+ const secretKeys = keys.filter((key) => tree.has(key));
8216
+ const plainKeys = keys.filter((key) => !tree.has(key));
8217
+ let changed = false;
8218
+ if (plainKeys.length > 0) {
8219
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8220
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8221
+ }
8222
+ if (secretKeys.length > 0) {
8223
+ await this.clearSecretKeys(secretKeys, target, cap);
8224
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8225
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8226
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8227
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8228
+ changed = true;
8229
+ }
8230
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8231
+ }
8232
+ /**
8233
+ * The hydrated form of one scope of one capability's settings (any
8234
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8235
+ */
8236
+ async getScopedSettings(scope, cap, nodeId) {
8237
+ const schema = await this.declaredScopedSchema(scope, cap);
8238
+ if (schema === null) return null;
8239
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8240
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8241
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8242
+ }
8243
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8244
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8245
+ this._operatorWriteDepth++;
8246
+ try {
8247
+ await this.writeScoped(scope, cap, patch, nodeId);
8248
+ } finally {
8249
+ this._operatorWriteDepth--;
8250
+ }
8251
+ }
8252
+ /**
8253
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8254
+ * its value. `null` when the capability declares nothing for the scope.
8255
+ */
8256
+ async readScopedValues(scope, cap, nodeId) {
8257
+ const schema = await this.declaredScopedSchema(scope, cap);
8258
+ if (schema === null) return null;
8259
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8260
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8261
+ }
8262
+ /**
8041
8263
  * What the create wizard and the integration edit page render for one instance: the addon's
8042
8264
  * per-instance schema when it declares one, else its global schema (what they always showed).
8043
8265
  */
@@ -8189,6 +8411,9 @@ var BaseAddon = class {
8189
8411
  * a keychain (an older kernel) it writes `''`, as such code always did.
8190
8412
  */
8191
8413
  async clearSecrets(keys, nodeId) {
8414
+ await this.clearSecretKeys(keys, nodeId);
8415
+ }
8416
+ async clearSecretKeys(keys, nodeId, cap) {
8192
8417
  const tree = this.addonSecretTree();
8193
8418
  const keychain = this._ctx?.secrets !== void 0;
8194
8419
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8201,25 +8426,27 @@ var BaseAddon = class {
8201
8426
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8202
8427
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8203
8428
  }
8204
- await this.applyGlobalPatch(patch, nodeId);
8429
+ await this.applyGlobalPatch(patch, nodeId, cap);
8205
8430
  }
8206
- async applyGlobalPatch(barePatch, nodeId) {
8207
- await this.loadScopedSchemas();
8208
- const keys = this.perNodeKeys();
8431
+ /** Resolves `true` when the write changed stored values. */
8432
+ async applyGlobalPatch(barePatch, nodeId, cap) {
8433
+ await this.loadScopedSchemas(cap);
8434
+ const keys = this.perNodeKeys(cap);
8209
8435
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8210
8436
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8211
8437
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8212
8438
  const handle = this._secretSettings;
8213
8439
  if (handle !== null) {
8214
8440
  const { changed } = await handle.writeAddonStore(scoped);
8215
- if (!changed) return;
8441
+ if (!changed) return false;
8216
8442
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8217
- if (target !== localNode) return;
8443
+ if (target !== localNode) return true;
8218
8444
  await this.resolveConfig();
8219
8445
  await this.onConfigChanged();
8220
8446
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8221
8447
  const legacy = this.globalSettingsSchema();
8222
8448
  this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8449
+ return true;
8223
8450
  }
8224
8451
  /**
8225
8452
  * The set of field keys the global settings schema declares `perNode: true`
@@ -43860,6 +44087,8 @@ var SettingsSchemaWithValuesSchema = object({
43860
44087
  });
43861
44088
  /** Patch object — keys are field names, values are the new field values. */
43862
44089
  var SettingsPatchSchema = record(string(), unknown());
44090
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
44091
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
43863
44092
  /** Standard success response for update operations. */
43864
44093
  var SettingsUpdateResultSchema = object({ success: literal(true) });
43865
44094
  method(object({
@@ -43875,6 +44104,25 @@ method(object({
43875
44104
  kind: "mutation",
43876
44105
  auth: "admin"
43877
44106
  }), method(object({
44107
+ addonId: string(),
44108
+ cap: string(),
44109
+ scope: ScopedSettingsScopeSchema,
44110
+ nodeId: string().optional()
44111
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44112
+ addonId: string(),
44113
+ cap: string(),
44114
+ scope: ScopedSettingsScopeSchema,
44115
+ nodeId: string().optional(),
44116
+ patch: SettingsPatchSchema
44117
+ }), SettingsUpdateResultSchema, {
44118
+ kind: "mutation",
44119
+ auth: "admin"
44120
+ }), method(object({
44121
+ addonId: string(),
44122
+ cap: string(),
44123
+ scope: ScopedSettingsScopeSchema,
44124
+ nodeId: string().optional()
44125
+ }), record(string(), unknown()).nullable()), method(object({
43878
44126
  addonId: string(),
43879
44127
  nodeId: string().optional()
43880
44128
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
@@ -75727,6 +75975,20 @@ Object.freeze({
75727
75975
  access: "view",
75728
75976
  role: "admin"
75729
75977
  },
75978
+ "addonSettings.getScopedSettings": {
75979
+ capName: "addon-settings",
75980
+ capScope: "system",
75981
+ addonId: null,
75982
+ access: "view",
75983
+ role: "admin"
75984
+ },
75985
+ "addonSettings.readScopedValues": {
75986
+ capName: "addon-settings",
75987
+ capScope: "system",
75988
+ addonId: null,
75989
+ access: "view",
75990
+ role: "admin"
75991
+ },
75730
75992
  "addonSettings.unsetStoredSettingsKeys": {
75731
75993
  capName: "addon-settings",
75732
75994
  capScope: "system",
@@ -75748,6 +76010,13 @@ Object.freeze({
75748
76010
  access: "create",
75749
76011
  role: "admin"
75750
76012
  },
76013
+ "addonSettings.updateScopedSettings": {
76014
+ capName: "addon-settings",
76015
+ capScope: "system",
76016
+ addonId: null,
76017
+ access: "create",
76018
+ role: "admin"
76019
+ },
75751
76020
  "addonWidgets.listWidgets": {
75752
76021
  capName: "addon-widgets",
75753
76022
  capScope: "system",
@@ -90032,7 +90301,7 @@ var AgentUIAddon = class extends BaseAddon {
90032
90301
  capability: adminUiCapability,
90033
90302
  provider: {
90034
90303
  getStaticDir: async () => ({ staticDir: path.resolve(__dirname) }),
90035
- getVersion: async () => ({ version: "1.2.337" })
90304
+ getVersion: async () => ({ version: "1.2.338" })
90036
90305
  }
90037
90306
  }];
90038
90307
  }
@@ -1 +1 @@
1
- import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{T as a,_ as o,c as s,d as c,l,p as u,t as d}from"./index-C3JZYknO.js";import{MaskShapeCanvas as f}from"./MaskShapeCanvas-BTvl6Rin.js";var p=a(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=n(i(),1),h=n(e(),1),g=110,_=`motion-zones`,v=0,y=[1,2,3],b=1,x=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,S=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function C(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function w(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function T(e,t){return Math.ceil(e/t)}function E(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function D(e,t,n,r){let i=T(t,r),a=T(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=E(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+E(n,t,i)]=!0)}return o}function O(e,t,n,r){let i=T(t,r),a=T(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=E(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+E(n,t,i)]===!0}return o}function k({deviceId:e}){let n=t(),i=c(o().trpcClient,e),[a,E]=(0,m.useState)(null),[k,A]=(0,m.useState)(!1),[j,M]=(0,m.useState)(null),[N,P]=(0,m.useState)(null),[F,I]=(0,m.useState)(b),[L,R]=(0,m.useState)(!1),[z,B]=(0,m.useState)(!1),V=(0,m.useRef)(!1);(0,m.useEffect)(()=>{if(!i)return;let e=!1;return V.current=!1,E(null),A(!1),M(null),P(null),I(b),B(!1),(async()=>{try{let t=await i.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(E(t),V.current)return;let n=await i.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);V.current=!0;let r=n.regions.find(e=>e.shape.kind===`grid`),a=C(r?r.shape.cells:[],t.grid.width,t.grid.height);P(a),M(D(a,t.grid.width,t.grid.height,b))}catch(t){if(e)return;l(t)?A(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[i]);let H=a?a.grid.width:0,U=a?a.grid.height:0,W=T(H,F),G=T(U,F),K=(0,m.useMemo)(()=>j&&a?O(j,H,U,F):null,[j,a,H,U,F]),q=(0,m.useMemo)(()=>K!==null&&N!==null&&!w(K,N),[K,N]),J=(0,m.useMemo)(()=>K?K.reduce((e,t)=>t?e+1:e,0):0,[K]),Y=H*U,X=W*G,Z=(0,m.useCallback)(e=>{X>0&&M(Array.from({length:X},()=>e))},[X]),ee=(0,m.useCallback)(()=>{M(e=>e&&e.map(e=>!e))},[]),te=(0,m.useCallback)(()=>{N&&a&&M(D(N,H,U,F))},[N,a,H,U,F]),ne=(0,m.useCallback)(e=>{e===F||!a||M(t=>{if(!t)return I(e),t;let n=D(O(t,H,U,F),H,U,e);return I(e),n})},[F,a,H,U]),Q=(0,m.useMemo)(()=>a&&j?[{id:v,shape:{kind:`grid`,gridWidth:W,gridHeight:G,cells:[...j]}}]:[],[a,j,W,G]),$=(0,m.useCallback)((e,t)=>{t.kind===`grid`&&M(t.cells)},[]),re=(0,m.useCallback)(async()=>{if(!(!i||!j||!a)){R(!0);try{let e=O(j,a.grid.width,a.grid.height,F),t={kind:`grid`,gridWidth:a.grid.width,gridHeight:a.grid.height,cells:e};await i.motionZones?.setZone({patch:{regions:[{id:v,enabled:!0,shape:t}]}});let n=await i.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=C(e?e.shape.cells:[],a.grid.width,a.grid.height);P(t),M(D(t,a.grid.width,a.grid.height,F))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{R(!1)}}},[i,j,a,F]);d((0,m.useMemo)(()=>z&&!k&&a&&j?{id:_,order:g,node:(0,h.jsx)(f,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:W,height:G},selectedId:v,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[z,k,a,j,Q,$,W,G]));let ie=!k&&a!==null&&j!==null;return i?(0,h.jsx)(u,{title:n(`uiLibrary.motionZones.title`,`Motion Zones`),icon:(0,h.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,h.jsx)(`div`,{className:`flex flex-col gap-3`,children:k?(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:n(`uiLibrary.motionZones.unsupported`,`This camera doesn't expose an on-board motion zones grid.`)}):ie?(0,h.jsxs)(h.Fragment,{children:[(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:(0,h.jsx)(r,{uiKey:`uiLibrary.motionZones.intro`,fallback:`Toggle <0>Edit grid</0> to paint the region directly on the live frame, then <1>Save</1> to push the mask to the camera. Pick a bigger <2>Cell size</2> for quicker, broad-stroke painting — it's resampled to the camera's native {{width}}×{{height}} grid on save (×1 is the finest).`,components:[(0,h.jsx)(`strong`,{className:`text-foreground`},`e`),(0,h.jsx)(`strong`,{className:`text-foreground`},`s`),(0,h.jsx)(`strong`,{className:`text-foreground`},`c`)],values:{width:a.grid.width,height:a.grid.height}})}),(0,h.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,h.jsx)(`button`,{type:`button`,onClick:()=>B(e=>!e),disabled:L,"aria-pressed":z,className:z?S:x,children:z?n(`uiLibrary.motionZones.doneEditing`,`Done editing`):n(`uiLibrary.motionZones.editGrid`,`Edit grid`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>Z(!0),disabled:L,className:x,children:n(`uiLibrary.motionZones.allOn`,`All on`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>Z(!1),disabled:L,className:x,children:n(`uiLibrary.motionZones.allOff`,`All off`)}),(0,h.jsx)(`button`,{type:`button`,onClick:ee,disabled:L,className:x,children:n(`uiLibrary.motionZones.invert`,`Invert`)}),(0,h.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":n(`uiLibrary.motionZones.cellSize`,`Cell size`),children:[(0,h.jsx)(`span`,{className:`${s} mr-0.5`,children:n(`uiLibrary.motionZones.cellSize`,`Cell size`)}),y.map(e=>(0,h.jsxs)(`button`,{type:`button`,onClick:()=>ne(e),disabled:L,"aria-pressed":F===e,title:e===1?n(`uiLibrary.motionZones.cameraGrid`,`Camera grid {{width}}×{{height}} (finest)`,{width:H,height:U}):n(`uiLibrary.motionZones.paintingGrid`,`{{width}}×{{height}} painting grid · cells ×{{factor}} bigger`,{width:T(H,e),height:T(U,e),factor:e}),className:F===e?S:x,children:[`×`,e]},e))]}),(0,h.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[n(`uiLibrary.motionZones.cells`,`{{active}} / {{total}} cells · {{width}}×{{height}}`,{active:J,total:Y,width:a.grid.width,height:a.grid.height}),F===1?``:n(`uiLibrary.motionZones.paintSuffix`,` · paint {{width}}×{{height}}`,{width:W,height:G})]}),(0,h.jsx)(`span`,{className:`flex-1`}),(0,h.jsx)(`button`,{type:`button`,onClick:te,disabled:L||!q,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:n(`uiLibrary.motionZones.revert`,`Revert`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>void re(),disabled:L||!q,className:S,children:L?n(`uiLibrary.motionZones.saving`,`Saving…`):n(`uiLibrary.motionZones.save`,`Save`)})]})]}):(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:n(`uiLibrary.motionZones.loading`,`Loading the camera's grid…`)})})}):null}export{k as MotionZonesSettings};
1
+ import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{T as a,_ as o,c as s,d as c,l,p as u,t as d}from"./index-mE6ihqDj.js";import{MaskShapeCanvas as f}from"./MaskShapeCanvas-BTvl6Rin.js";var p=a(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=n(i(),1),h=n(e(),1),g=110,_=`motion-zones`,v=0,y=[1,2,3],b=1,x=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,S=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function C(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function w(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function T(e,t){return Math.ceil(e/t)}function E(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function D(e,t,n,r){let i=T(t,r),a=T(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=E(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+E(n,t,i)]=!0)}return o}function O(e,t,n,r){let i=T(t,r),a=T(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=E(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+E(n,t,i)]===!0}return o}function k({deviceId:e}){let n=t(),i=c(o().trpcClient,e),[a,E]=(0,m.useState)(null),[k,A]=(0,m.useState)(!1),[j,M]=(0,m.useState)(null),[N,P]=(0,m.useState)(null),[F,I]=(0,m.useState)(b),[L,R]=(0,m.useState)(!1),[z,B]=(0,m.useState)(!1),V=(0,m.useRef)(!1);(0,m.useEffect)(()=>{if(!i)return;let e=!1;return V.current=!1,E(null),A(!1),M(null),P(null),I(b),B(!1),(async()=>{try{let t=await i.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(E(t),V.current)return;let n=await i.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);V.current=!0;let r=n.regions.find(e=>e.shape.kind===`grid`),a=C(r?r.shape.cells:[],t.grid.width,t.grid.height);P(a),M(D(a,t.grid.width,t.grid.height,b))}catch(t){if(e)return;l(t)?A(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[i]);let H=a?a.grid.width:0,U=a?a.grid.height:0,W=T(H,F),G=T(U,F),K=(0,m.useMemo)(()=>j&&a?O(j,H,U,F):null,[j,a,H,U,F]),q=(0,m.useMemo)(()=>K!==null&&N!==null&&!w(K,N),[K,N]),J=(0,m.useMemo)(()=>K?K.reduce((e,t)=>t?e+1:e,0):0,[K]),Y=H*U,X=W*G,Z=(0,m.useCallback)(e=>{X>0&&M(Array.from({length:X},()=>e))},[X]),ee=(0,m.useCallback)(()=>{M(e=>e&&e.map(e=>!e))},[]),te=(0,m.useCallback)(()=>{N&&a&&M(D(N,H,U,F))},[N,a,H,U,F]),ne=(0,m.useCallback)(e=>{e===F||!a||M(t=>{if(!t)return I(e),t;let n=D(O(t,H,U,F),H,U,e);return I(e),n})},[F,a,H,U]),Q=(0,m.useMemo)(()=>a&&j?[{id:v,shape:{kind:`grid`,gridWidth:W,gridHeight:G,cells:[...j]}}]:[],[a,j,W,G]),$=(0,m.useCallback)((e,t)=>{t.kind===`grid`&&M(t.cells)},[]),re=(0,m.useCallback)(async()=>{if(!(!i||!j||!a)){R(!0);try{let e=O(j,a.grid.width,a.grid.height,F),t={kind:`grid`,gridWidth:a.grid.width,gridHeight:a.grid.height,cells:e};await i.motionZones?.setZone({patch:{regions:[{id:v,enabled:!0,shape:t}]}});let n=await i.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=C(e?e.shape.cells:[],a.grid.width,a.grid.height);P(t),M(D(t,a.grid.width,a.grid.height,F))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{R(!1)}}},[i,j,a,F]);d((0,m.useMemo)(()=>z&&!k&&a&&j?{id:_,order:g,node:(0,h.jsx)(f,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:W,height:G},selectedId:v,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[z,k,a,j,Q,$,W,G]));let ie=!k&&a!==null&&j!==null;return i?(0,h.jsx)(u,{title:n(`uiLibrary.motionZones.title`,`Motion Zones`),icon:(0,h.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,h.jsx)(`div`,{className:`flex flex-col gap-3`,children:k?(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:n(`uiLibrary.motionZones.unsupported`,`This camera doesn't expose an on-board motion zones grid.`)}):ie?(0,h.jsxs)(h.Fragment,{children:[(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:(0,h.jsx)(r,{uiKey:`uiLibrary.motionZones.intro`,fallback:`Toggle <0>Edit grid</0> to paint the region directly on the live frame, then <1>Save</1> to push the mask to the camera. Pick a bigger <2>Cell size</2> for quicker, broad-stroke painting — it's resampled to the camera's native {{width}}×{{height}} grid on save (×1 is the finest).`,components:[(0,h.jsx)(`strong`,{className:`text-foreground`},`e`),(0,h.jsx)(`strong`,{className:`text-foreground`},`s`),(0,h.jsx)(`strong`,{className:`text-foreground`},`c`)],values:{width:a.grid.width,height:a.grid.height}})}),(0,h.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,h.jsx)(`button`,{type:`button`,onClick:()=>B(e=>!e),disabled:L,"aria-pressed":z,className:z?S:x,children:z?n(`uiLibrary.motionZones.doneEditing`,`Done editing`):n(`uiLibrary.motionZones.editGrid`,`Edit grid`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>Z(!0),disabled:L,className:x,children:n(`uiLibrary.motionZones.allOn`,`All on`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>Z(!1),disabled:L,className:x,children:n(`uiLibrary.motionZones.allOff`,`All off`)}),(0,h.jsx)(`button`,{type:`button`,onClick:ee,disabled:L,className:x,children:n(`uiLibrary.motionZones.invert`,`Invert`)}),(0,h.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":n(`uiLibrary.motionZones.cellSize`,`Cell size`),children:[(0,h.jsx)(`span`,{className:`${s} mr-0.5`,children:n(`uiLibrary.motionZones.cellSize`,`Cell size`)}),y.map(e=>(0,h.jsxs)(`button`,{type:`button`,onClick:()=>ne(e),disabled:L,"aria-pressed":F===e,title:e===1?n(`uiLibrary.motionZones.cameraGrid`,`Camera grid {{width}}×{{height}} (finest)`,{width:H,height:U}):n(`uiLibrary.motionZones.paintingGrid`,`{{width}}×{{height}} painting grid · cells ×{{factor}} bigger`,{width:T(H,e),height:T(U,e),factor:e}),className:F===e?S:x,children:[`×`,e]},e))]}),(0,h.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[n(`uiLibrary.motionZones.cells`,`{{active}} / {{total}} cells · {{width}}×{{height}}`,{active:J,total:Y,width:a.grid.width,height:a.grid.height}),F===1?``:n(`uiLibrary.motionZones.paintSuffix`,` · paint {{width}}×{{height}}`,{width:W,height:G})]}),(0,h.jsx)(`span`,{className:`flex-1`}),(0,h.jsx)(`button`,{type:`button`,onClick:te,disabled:L||!q,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:n(`uiLibrary.motionZones.revert`,`Revert`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>void re(),disabled:L||!q,className:S,children:L?n(`uiLibrary.motionZones.saving`,`Saving…`):n(`uiLibrary.motionZones.save`,`Save`)})]})]}):(0,h.jsx)(`p`,{className:`${s} leading-relaxed`,children:n(`uiLibrary.motionZones.loading`,`Loading the camera's grid…`)})})}):null}export{k as MotionZonesSettings};
@@ -1 +1 @@
1
- import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{C as ee,T as a,_ as te,c as o,d as ne,l as s,p as c,t as re,v as l,x as u,y as d}from"./index-C3JZYknO.js";import{MaskShapeCanvas as ie}from"./MaskShapeCanvas-BTvl6Rin.js";var ae=a(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),oe=a(`mic-off`,[[`path`,{d:`M12 19v3`,key:`npa21l`}],[`path`,{d:`M15 9.34V5a3 3 0 0 0-5.68-1.33`,key:`1gzdoj`}],[`path`,{d:`M16.95 16.95A7 7 0 0 1 5 12v-2`,key:`cqa7eg`}],[`path`,{d:`M18.89 13.23A7 7 0 0 0 19 12v-2`,key:`16hl24`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M9 9v3a3 3 0 0 0 5.12 2.12`,key:`r2i35w`}]]),f=n(i(),1),p=`Microphone`,m=`The camera encodes no audio at all — live view, recordings, exports and the audio analyzer are all silent.`;function se(e){let t=e.supportsAudioMute===!0,n=t?e.audioEnabled:null,r=e.pendingOn??null;return{label:p,supported:t,on:n===null?null:r??n,available:t&&n!==null,costWhenOff:m}}function ce(e){return!e.available||e.on===null?null:{enabled:!e.on}}var h=n(e(),1),le=120,ue=`privacy-mask`,g=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,_=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function v(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function de(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function fe(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function y({deviceId:e}){let n=t(),i=ne(te().trpcClient,e),[a,p]=(0,f.useState)(null),[m,y]=(0,f.useState)(!1),[b,x]=(0,f.useState)(!1),[S,C]=(0,f.useState)(null),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(!1),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(null),[M,N]=(0,f.useState)(null),[pe,P]=(0,f.useState)(null),[F,I]=(0,f.useState)(null),[L,R]=(0,f.useState)(null),z=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!i)return;let e=!1;return z.current=!1,p(null),y(!1),x(!1),C(null),T(null),k(!1),j(null),N(null),P(null),I(null),R(null),(async()=>{try{let t=await i.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(p(t),z.current)return;let n=await i.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);if(n.regions===null){x(!0),P(n.audioEnabled);return}z.current=!0;let r={enabled:n.enabled,regions:n.regions};T(r),C(r),P(n.audioEnabled)}catch(t){if(e)return;s(t)?y(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[i]);let B=(0,f.useMemo)(()=>S!==null&&w!==null&&(S.enabled!==w.enabled||!fe(S.regions,w.regions)),[S,w]),V=S?S.regions.length:0,H=a?a.maxRegions:0,U=(0,f.useRef)(0);(0,f.useEffect)(()=>{U.current=H},[H]);let W=H>0&&V>=H,me=(0,f.useCallback)(()=>{C(e=>e&&{...e,enabled:!e.enabled})},[]),G=(0,f.useCallback)(e=>{j(typeof e==`number`?e:null)},[]),K=(0,f.useMemo)(()=>S?S.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:n(`uiLibrary.privacyMaskSettings.zoneN`,`Zone {{id}}`,{id:e.id})})):[],[S,n]),q=(0,f.useCallback)((e,t)=>{let n=v(t);n&&C(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),J=(0,f.useCallback)(e=>{let t=v(e);t&&(N(null),C(e=>{if(!e)return e;let n=U.current;if(n>0&&e.regions.length>=n)return e;let r=de(e.regions),i={id:r,enabled:!0,shape:t};return j(r),{...e,regions:[...e.regions,i]}}))},[]),Y=(0,f.useCallback)(e=>{k(!0),j(null),N(e)},[]),he=(0,f.useCallback)(e=>{k(!0),N(null),j(e)},[]),ge=(0,f.useCallback)(e=>{j(t=>t===e?null:t),C(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),_e=(0,f.useCallback)(()=>{N(null),j(null),w&&C({enabled:w.enabled,regions:w.regions})},[w]),X=(0,f.useCallback)(async()=>{if(!(!i||!S)){D(!0);try{await i.privacyMask?.setMask({patch:{enabled:S.enabled,regions:[...S.regions]}});let e=await i.privacyMask?.getStatus({});if(e&&e.regions!==null){let t={enabled:e.enabled,regions:e.regions};T(t),C(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{D(!1)}}},[i,S]),Z=se({supportsAudioMute:a===null?null:a.supportsAudioMute,audioEnabled:pe,pendingOn:F}),ve=(0,f.useCallback)(async()=>{let e=ce(Z);if(!(!i||e===null)){I(e.enabled),R(null);try{await i.privacyMask?.setAudioEnabled(e);let t=await i.privacyMask?.getStatus({});t&&P(t.audioEnabled)}catch(e){console.error(`Privacy microphone write failed — reverting`,e),R(e instanceof Error?e.message:String(e))}finally{I(null)}}},[i,Z]),ye=(0,f.useCallback)(()=>{k(e=>(e&&(N(null),j(null)),!e))},[]),Q=a?.supportedShapes??[];re((0,f.useMemo)(()=>O&&!m&&a&&S?{id:ue,order:le,node:(0,h.jsx)(ie,{transparent:!0,items:K,supportedShapes:Q,polygonVertices:a.polygonVertices,selectedId:A,onSelect:G,onShapeChange:q,onDrawComplete:J,drawingKind:M})}:null,[O,m,a,S,K,Q,A,G,q,J,M]));let be=a?.supportedShapes.includes(`rect`)??!1,xe=a?.supportedShapes.includes(`polygon`)??!1,$=a!==null&&(a.maxRegions<=0||a.supportedShapes.length===0),Se=!m&&!$&&!b&&a!==null&&S!==null;return i?(0,h.jsx)(c,{title:n(`uiLibrary.privacyMaskSettings.title`,`Privacy Mask`),icon:(0,h.jsx)(ee,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,h.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[m||$?(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.unsupported`,`This camera doesn't support an on-board privacy mask.`)}):b?(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.unreadable`,`This camera hasn't answered, so what it currently masks is unknown — the editor stays closed rather than showing an empty frame you could save over. A battery camera answers once it wakes; otherwise check that it is reachable.`)}):Se?(0,h.jsxs)(h.Fragment,{children:[(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:(0,h.jsx)(r,{uiKey:`uiLibrary.privacyMaskSettings.help`,fallback:`Toggle <0>Edit mask</0> to draw blanked-out zones on the live frame, then <1>Save</1> to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`,components:[(0,h.jsx)(`strong`,{className:`text-foreground`},`edit`),(0,h.jsx)(`strong`,{className:`text-foreground`},`save`)]})}),(0,h.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,h.jsx)(`button`,{type:`button`,onClick:ye,disabled:E,"aria-pressed":O,className:O?_:g,children:O?n(`uiLibrary.privacyMaskSettings.doneEditing`,`Done editing`):n(`uiLibrary.privacyMaskSettings.editMask`,`Edit mask`)}),(0,h.jsx)(`button`,{type:`button`,onClick:me,disabled:E,"aria-pressed":S.enabled,className:S.enabled?_:g,children:S.enabled?n(`uiLibrary.privacyMaskSettings.maskOn`,`Mask on`):n(`uiLibrary.privacyMaskSettings.maskOff`,`Mask off`)}),be&&(0,h.jsx)(`button`,{type:`button`,onClick:()=>Y(`rect`),disabled:E||W,"aria-pressed":M===`rect`,className:M===`rect`?_:g,title:W?n(`uiLibrary.privacyMaskSettings.maxZones`,`Maximum zones reached`):n(`uiLibrary.privacyMaskSettings.addRect`,`Add a rectangle zone`),children:n(`uiLibrary.privacyMaskSettings.rectButton`,`+ Rect`)}),xe&&(0,h.jsx)(`button`,{type:`button`,onClick:()=>Y(`polygon`),disabled:E||W,"aria-pressed":M===`polygon`,className:M===`polygon`?_:g,title:W?n(`uiLibrary.privacyMaskSettings.maxZones`,`Maximum zones reached`):n(`uiLibrary.privacyMaskSettings.addPolygon`,`Add a polygon zone`),children:n(`uiLibrary.privacyMaskSettings.polygonButton`,`+ Polygon`)}),(0,h.jsx)(`span`,{className:`${o} ml-1 tabular-nums`,children:n(`uiLibrary.privacyMaskSettings.zoneCount`,`{{active}} / {{max}} zones`,{active:V,max:H})}),(0,h.jsx)(`span`,{className:`flex-1`}),(0,h.jsx)(`button`,{type:`button`,onClick:_e,disabled:E||!B,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:n(`uiLibrary.privacyMaskSettings.revert`,`Revert`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>void X(),disabled:E||!B,className:_,children:E?n(`uiLibrary.privacyMaskSettings.saving`,`Saving…`):n(`uiLibrary.privacyMaskSettings.save`,`Save`)})]}),V>0?(0,h.jsx)(`div`,{className:`flex flex-col gap-1`,children:S.regions.map(e=>{let t=A===e.id,r=e.shape.kind===`polygon`?ae:d;return(0,h.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,h.jsxs)(`button`,{type:`button`,onClick:()=>he(e.id),disabled:E,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,h.jsx)(r,{className:`h-3.5 w-3.5 shrink-0`}),(0,h.jsx)(`span`,{children:n(`uiLibrary.privacyMaskSettings.zoneN`,`Zone {{id}}`,{id:e.id})}),(0,h.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>ge(e.id),disabled:E,"aria-label":n(`uiLibrary.privacyMaskSettings.deleteZoneN`,`Delete zone {{id}}`,{id:e.id}),title:n(`uiLibrary.privacyMaskSettings.deleteZone`,`Delete zone`),className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,h.jsx)(l,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.loading`,`Loading the camera's privacy mask…`)}),Z.supported&&(0,h.jsxs)(`div`,{className:`flex items-start gap-3 rounded-md border border-border bg-surface px-2 py-2`,children:[(0,h.jsx)(`button`,{type:`button`,role:`switch`,"aria-label":Z.label,"aria-checked":Z.on===!0,disabled:!Z.available||F!==null,onClick:()=>void ve(),title:Z.on===null?n(`uiLibrary.privacyMaskSettings.micTitleUnknown`,`Microphone — state unknown; the camera did not answer.`):Z.on?n(`uiLibrary.privacyMaskSettings.micTitleOn`,`The camera is capturing sound. Turning it off: {{cost}}`,{cost:Z.costWhenOff}):n(`uiLibrary.privacyMaskSettings.micTitleOff`,`The camera is silent. {{cost}}`,{cost:Z.costWhenOff}),className:Z.on===!1||Z.on===null?g:_,children:(0,h.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[Z.on===!1?(0,h.jsx)(oe,{className:`h-3.5 w-3.5`}):(0,h.jsx)(u,{className:`h-3.5 w-3.5`}),Z.on===null?n(`uiLibrary.privacyMaskSettings.micUnknown`,`Microphone unknown`):Z.on?n(`uiLibrary.privacyMaskSettings.micOn`,`Microphone on`):n(`uiLibrary.privacyMaskSettings.micOff`,`Microphone off`)]})}),(0,h.jsx)(`p`,{className:`text-[10px] text-foreground-subtle flex-1 leading-relaxed`,children:L===null?Z.on===null?n(`uiLibrary.privacyMaskSettings.micNotReported`,`The camera did not report its microphone, so this state is unknown.`):Z.costWhenOff:n(`uiLibrary.privacyMaskSettings.writeFailed`,`Write failed: {{reason}}`,{reason:L})})]})]})}):null}export{y as PrivacyMaskSettings};
1
+ import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{C as ee,T as a,_ as te,c as o,d as ne,l as s,p as c,t as re,v as l,x as u,y as d}from"./index-mE6ihqDj.js";import{MaskShapeCanvas as ie}from"./MaskShapeCanvas-BTvl6Rin.js";var ae=a(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),oe=a(`mic-off`,[[`path`,{d:`M12 19v3`,key:`npa21l`}],[`path`,{d:`M15 9.34V5a3 3 0 0 0-5.68-1.33`,key:`1gzdoj`}],[`path`,{d:`M16.95 16.95A7 7 0 0 1 5 12v-2`,key:`cqa7eg`}],[`path`,{d:`M18.89 13.23A7 7 0 0 0 19 12v-2`,key:`16hl24`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M9 9v3a3 3 0 0 0 5.12 2.12`,key:`r2i35w`}]]),f=n(i(),1),p=`Microphone`,m=`The camera encodes no audio at all — live view, recordings, exports and the audio analyzer are all silent.`;function se(e){let t=e.supportsAudioMute===!0,n=t?e.audioEnabled:null,r=e.pendingOn??null;return{label:p,supported:t,on:n===null?null:r??n,available:t&&n!==null,costWhenOff:m}}function ce(e){return!e.available||e.on===null?null:{enabled:!e.on}}var h=n(e(),1),le=120,ue=`privacy-mask`,g=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,_=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function v(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function de(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function fe(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function y({deviceId:e}){let n=t(),i=ne(te().trpcClient,e),[a,p]=(0,f.useState)(null),[m,y]=(0,f.useState)(!1),[b,x]=(0,f.useState)(!1),[S,C]=(0,f.useState)(null),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(!1),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(null),[M,N]=(0,f.useState)(null),[pe,P]=(0,f.useState)(null),[F,I]=(0,f.useState)(null),[L,R]=(0,f.useState)(null),z=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!i)return;let e=!1;return z.current=!1,p(null),y(!1),x(!1),C(null),T(null),k(!1),j(null),N(null),P(null),I(null),R(null),(async()=>{try{let t=await i.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(p(t),z.current)return;let n=await i.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);if(n.regions===null){x(!0),P(n.audioEnabled);return}z.current=!0;let r={enabled:n.enabled,regions:n.regions};T(r),C(r),P(n.audioEnabled)}catch(t){if(e)return;s(t)?y(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[i]);let B=(0,f.useMemo)(()=>S!==null&&w!==null&&(S.enabled!==w.enabled||!fe(S.regions,w.regions)),[S,w]),V=S?S.regions.length:0,H=a?a.maxRegions:0,U=(0,f.useRef)(0);(0,f.useEffect)(()=>{U.current=H},[H]);let W=H>0&&V>=H,me=(0,f.useCallback)(()=>{C(e=>e&&{...e,enabled:!e.enabled})},[]),G=(0,f.useCallback)(e=>{j(typeof e==`number`?e:null)},[]),K=(0,f.useMemo)(()=>S?S.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:n(`uiLibrary.privacyMaskSettings.zoneN`,`Zone {{id}}`,{id:e.id})})):[],[S,n]),q=(0,f.useCallback)((e,t)=>{let n=v(t);n&&C(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),J=(0,f.useCallback)(e=>{let t=v(e);t&&(N(null),C(e=>{if(!e)return e;let n=U.current;if(n>0&&e.regions.length>=n)return e;let r=de(e.regions),i={id:r,enabled:!0,shape:t};return j(r),{...e,regions:[...e.regions,i]}}))},[]),Y=(0,f.useCallback)(e=>{k(!0),j(null),N(e)},[]),he=(0,f.useCallback)(e=>{k(!0),N(null),j(e)},[]),ge=(0,f.useCallback)(e=>{j(t=>t===e?null:t),C(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),_e=(0,f.useCallback)(()=>{N(null),j(null),w&&C({enabled:w.enabled,regions:w.regions})},[w]),X=(0,f.useCallback)(async()=>{if(!(!i||!S)){D(!0);try{await i.privacyMask?.setMask({patch:{enabled:S.enabled,regions:[...S.regions]}});let e=await i.privacyMask?.getStatus({});if(e&&e.regions!==null){let t={enabled:e.enabled,regions:e.regions};T(t),C(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{D(!1)}}},[i,S]),Z=se({supportsAudioMute:a===null?null:a.supportsAudioMute,audioEnabled:pe,pendingOn:F}),ve=(0,f.useCallback)(async()=>{let e=ce(Z);if(!(!i||e===null)){I(e.enabled),R(null);try{await i.privacyMask?.setAudioEnabled(e);let t=await i.privacyMask?.getStatus({});t&&P(t.audioEnabled)}catch(e){console.error(`Privacy microphone write failed — reverting`,e),R(e instanceof Error?e.message:String(e))}finally{I(null)}}},[i,Z]),ye=(0,f.useCallback)(()=>{k(e=>(e&&(N(null),j(null)),!e))},[]),Q=a?.supportedShapes??[];re((0,f.useMemo)(()=>O&&!m&&a&&S?{id:ue,order:le,node:(0,h.jsx)(ie,{transparent:!0,items:K,supportedShapes:Q,polygonVertices:a.polygonVertices,selectedId:A,onSelect:G,onShapeChange:q,onDrawComplete:J,drawingKind:M})}:null,[O,m,a,S,K,Q,A,G,q,J,M]));let be=a?.supportedShapes.includes(`rect`)??!1,xe=a?.supportedShapes.includes(`polygon`)??!1,$=a!==null&&(a.maxRegions<=0||a.supportedShapes.length===0),Se=!m&&!$&&!b&&a!==null&&S!==null;return i?(0,h.jsx)(c,{title:n(`uiLibrary.privacyMaskSettings.title`,`Privacy Mask`),icon:(0,h.jsx)(ee,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,h.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[m||$?(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.unsupported`,`This camera doesn't support an on-board privacy mask.`)}):b?(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.unreadable`,`This camera hasn't answered, so what it currently masks is unknown — the editor stays closed rather than showing an empty frame you could save over. A battery camera answers once it wakes; otherwise check that it is reachable.`)}):Se?(0,h.jsxs)(h.Fragment,{children:[(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:(0,h.jsx)(r,{uiKey:`uiLibrary.privacyMaskSettings.help`,fallback:`Toggle <0>Edit mask</0> to draw blanked-out zones on the live frame, then <1>Save</1> to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`,components:[(0,h.jsx)(`strong`,{className:`text-foreground`},`edit`),(0,h.jsx)(`strong`,{className:`text-foreground`},`save`)]})}),(0,h.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,h.jsx)(`button`,{type:`button`,onClick:ye,disabled:E,"aria-pressed":O,className:O?_:g,children:O?n(`uiLibrary.privacyMaskSettings.doneEditing`,`Done editing`):n(`uiLibrary.privacyMaskSettings.editMask`,`Edit mask`)}),(0,h.jsx)(`button`,{type:`button`,onClick:me,disabled:E,"aria-pressed":S.enabled,className:S.enabled?_:g,children:S.enabled?n(`uiLibrary.privacyMaskSettings.maskOn`,`Mask on`):n(`uiLibrary.privacyMaskSettings.maskOff`,`Mask off`)}),be&&(0,h.jsx)(`button`,{type:`button`,onClick:()=>Y(`rect`),disabled:E||W,"aria-pressed":M===`rect`,className:M===`rect`?_:g,title:W?n(`uiLibrary.privacyMaskSettings.maxZones`,`Maximum zones reached`):n(`uiLibrary.privacyMaskSettings.addRect`,`Add a rectangle zone`),children:n(`uiLibrary.privacyMaskSettings.rectButton`,`+ Rect`)}),xe&&(0,h.jsx)(`button`,{type:`button`,onClick:()=>Y(`polygon`),disabled:E||W,"aria-pressed":M===`polygon`,className:M===`polygon`?_:g,title:W?n(`uiLibrary.privacyMaskSettings.maxZones`,`Maximum zones reached`):n(`uiLibrary.privacyMaskSettings.addPolygon`,`Add a polygon zone`),children:n(`uiLibrary.privacyMaskSettings.polygonButton`,`+ Polygon`)}),(0,h.jsx)(`span`,{className:`${o} ml-1 tabular-nums`,children:n(`uiLibrary.privacyMaskSettings.zoneCount`,`{{active}} / {{max}} zones`,{active:V,max:H})}),(0,h.jsx)(`span`,{className:`flex-1`}),(0,h.jsx)(`button`,{type:`button`,onClick:_e,disabled:E||!B,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:n(`uiLibrary.privacyMaskSettings.revert`,`Revert`)}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>void X(),disabled:E||!B,className:_,children:E?n(`uiLibrary.privacyMaskSettings.saving`,`Saving…`):n(`uiLibrary.privacyMaskSettings.save`,`Save`)})]}),V>0?(0,h.jsx)(`div`,{className:`flex flex-col gap-1`,children:S.regions.map(e=>{let t=A===e.id,r=e.shape.kind===`polygon`?ae:d;return(0,h.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,h.jsxs)(`button`,{type:`button`,onClick:()=>he(e.id),disabled:E,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,h.jsx)(r,{className:`h-3.5 w-3.5 shrink-0`}),(0,h.jsx)(`span`,{children:n(`uiLibrary.privacyMaskSettings.zoneN`,`Zone {{id}}`,{id:e.id})}),(0,h.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>ge(e.id),disabled:E,"aria-label":n(`uiLibrary.privacyMaskSettings.deleteZoneN`,`Delete zone {{id}}`,{id:e.id}),title:n(`uiLibrary.privacyMaskSettings.deleteZone`,`Delete zone`),className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,h.jsx)(l,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,h.jsx)(`p`,{className:`${o} leading-relaxed`,children:n(`uiLibrary.privacyMaskSettings.loading`,`Loading the camera's privacy mask…`)}),Z.supported&&(0,h.jsxs)(`div`,{className:`flex items-start gap-3 rounded-md border border-border bg-surface px-2 py-2`,children:[(0,h.jsx)(`button`,{type:`button`,role:`switch`,"aria-label":Z.label,"aria-checked":Z.on===!0,disabled:!Z.available||F!==null,onClick:()=>void ve(),title:Z.on===null?n(`uiLibrary.privacyMaskSettings.micTitleUnknown`,`Microphone — state unknown; the camera did not answer.`):Z.on?n(`uiLibrary.privacyMaskSettings.micTitleOn`,`The camera is capturing sound. Turning it off: {{cost}}`,{cost:Z.costWhenOff}):n(`uiLibrary.privacyMaskSettings.micTitleOff`,`The camera is silent. {{cost}}`,{cost:Z.costWhenOff}),className:Z.on===!1||Z.on===null?g:_,children:(0,h.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[Z.on===!1?(0,h.jsx)(oe,{className:`h-3.5 w-3.5`}):(0,h.jsx)(u,{className:`h-3.5 w-3.5`}),Z.on===null?n(`uiLibrary.privacyMaskSettings.micUnknown`,`Microphone unknown`):Z.on?n(`uiLibrary.privacyMaskSettings.micOn`,`Microphone on`):n(`uiLibrary.privacyMaskSettings.micOff`,`Microphone off`)]})}),(0,h.jsx)(`p`,{className:`text-[10px] text-foreground-subtle flex-1 leading-relaxed`,children:L===null?Z.on===null?n(`uiLibrary.privacyMaskSettings.micNotReported`,`The camera did not report its microphone, so this state is unknown.`):Z.costWhenOff:n(`uiLibrary.privacyMaskSettings.writeFailed`,`Write failed: {{reason}}`,{reason:L})})]})]})}):null}export{y as PrivacyMaskSettings};
@@ -1 +1 @@
1
- import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{S as a,T as o,_ as s,a as c,b as l,c as u,d,f,g as p,h as m,i as h,l as g,m as _,n as v,o as y,r as b,s as x,u as S,v as C,w}from"./index-C3JZYknO.js";import{MaskShapeCanvas as T}from"./MaskShapeCanvas-BTvl6Rin.js";var E=o(`settings-2`,[[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`path`,{d:`M19 7h-9`,key:`6i9tg`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),D=n(i(),1),O=n(e(),1),k=_(`/addon/pipeline-analytics/event-media`);function A(e){return`${k}/m/${e}`}function j(e){let t=new Map;for(let n of e.states)n.references.forEach((e,r)=>{let i=t.get(e.condition)??[];i.push({stateId:n.id,index:r,reference:e}),t.set(e.condition,i)});return[...t.entries()].map(([e,t])=>({condition:e,tiles:[...t].sort((e,t)=>t.reference.capturedAt-e.reference.capturedAt)})).sort((e,t)=>e.condition.localeCompare(t.condition))}function M(e){return e.states.reduce((e,t)=>e+t.references.length,0)}var N=`rounded border border-danger/50 bg-danger/10 px-1 py-0.5 text-[10px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors`;function P({monitor:e,disabled:n,onDelete:r}){let i=t(),a=j(e),o=M(e),s=m();return a.length===0?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:i(`uiLibrary.sceneReferenceGallery.empty`,`No photographs captured yet. Press “Capture daylight” to take the one this scene will judge everything against.`)}):(0,O.jsx)(`div`,{className:`flex flex-col gap-2`,children:a.map(e=>(0,O.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,O.jsxs)(`p`,{className:`text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle`,children:[x(e.condition,i),` · `,e.tiles.length]}),(0,O.jsx)(`ul`,{className:`flex flex-wrap gap-1.5`,children:e.tiles.map(t=>{let a=`${t.stateId}:${String(t.index)}`,c=o===1;return(0,O.jsxs)(`li`,{className:`flex w-24 flex-col gap-1 rounded-md border border-border p-1`,children:[(0,O.jsx)(F,{reference:t.reference}),(0,O.jsx)(`span`,{className:`${u} tabular-nums`,children:I(t.reference.capturedAt)}),(0,O.jsxs)(`button`,{type:`button`,className:N,disabled:n,"aria-label":i(`uiLibrary.sceneReferenceGallery.deleteLabel`,`Delete the {{variant}} photograph from {{date}}`,{variant:x(e.condition,i).toLowerCase(),date:I(t.reference.capturedAt)}),onClick:()=>{(async()=>{c&&!await s({title:i(`uiLibrary.sceneReferenceGallery.deleteTitle`,`Delete the only photograph?`),message:i(`uiLibrary.sceneReferenceGallery.lastWarning`,`This is the only photograph this scene has. Delete it and the scene no longer knows what normal looks like — it stops judging until you capture again.`),confirmLabel:i(`uiLibrary.sceneReferenceGallery.delete`,`Delete`),variant:`danger`})||r({stateId:t.stateId,index:t.index})})()},children:[(0,O.jsx)(C,{className:`mr-1 inline h-3 w-3`}),i(`uiLibrary.sceneReferenceGallery.delete`,`Delete`)]})]},a)})})]},e.condition))})}function F({reference:e}){let n=t(),[r,i]=(0,D.useState)(!1),o=e.thumbnailMediaId;return o===void 0||r?(0,O.jsxs)(`div`,{className:`flex h-16 w-full flex-col items-center justify-center gap-0.5 rounded bg-surface text-foreground-subtle`,title:n(`uiLibrary.sceneReferenceGallery.missingPicture`,`The picture for this reference is no longer stored. The reference itself is intact and still being scored against.`),children:[(0,O.jsx)(a,{className:`h-4 w-4`}),(0,O.jsx)(`span`,{className:`text-[9px]`,children:n(`uiLibrary.sceneReferenceGallery.noPicture`,`no picture`)})]}):(0,O.jsx)(`img`,{src:A(o),alt:``,loading:`lazy`,className:`h-16 w-full rounded object-cover`,onError:()=>i(!0)})}function I(e){let t=new Date(e);return`${t.toLocaleDateString()} ${t.toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`})}`}var L=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50`,R=`text-[10px] font-medium text-foreground-subtle`,z=`flex flex-col gap-2 rounded-md border border-border bg-surface/40 p-2`,B=`text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle`,V=5,H=3600;function U(e,t){switch(e){case`periodic`:return t(`uiLibrary.sceneSettingsForm.triggerHintPeriodic`,`Only on the clock. Predictable, and it notices a change even on a camera nothing ever moves in front of.`);case`on-motion`:return t(`uiLibrary.sceneSettingsForm.triggerHintOnMotion`,`Only after the camera has been still again for the quiet window below. Cheapest, but a change nobody walks past is never seen.`);case`both`:return t(`uiLibrary.sceneSettingsForm.triggerHintBoth`,`Both — the clock is the safety net that lets the motion path miss. This is the right answer for almost every scene.`)}}function W(e,t){switch(e){case`skip`:return t(`uiLibrary.sceneSettingsForm.uncoveredHintSkip`,`Sit that light out. No verdict, no alarm, no flag touched — it picks itself back up when a light it knows returns. This is why the night capture is optional.`);case`judge-anyway`:return t(`uiLibrary.sceneSettingsForm.uncoveredHintJudge`,`Compare against the references you do have. Only correct when the two lights genuinely look alike (a floodlit driveway, an indoor lamp) — against a real infrared frame it will report a change that did not happen.`)}}function G(e){return{label:e.label,checkIntervalSec:e.checkIntervalSec??60,threshold:e.check.mode===`similarity`?e.check.threshold:.8,hysteresisCount:e.check.hysteresisCount,quietSeconds:e.quietSeconds,minObservationSpacingSec:e.minObservationSpacingSec,confirmPrompt:e.confirm?.prompt??``}}function K({monitor:e,disabled:n,onPatch:i}){let a=t(),[o,s]=(0,D.useState)(()=>G(e)),[c,l]=(0,D.useState)(!1);(0,D.useEffect)(()=>{c||s(G(e))},[e,c]);let d=e=>{l(!1),i(e)},f=t=>e.check.mode===`similarity`?{mode:`similarity`,threshold:t.threshold??e.check.threshold,hysteresisCount:t.hysteresisCount??e.check.hysteresisCount}:{...e.check,hysteresisCount:t.hysteresisCount??e.check.hysteresisCount},m=e.confirm?.enabled===!0,h=e.check.mode===`similarity`?e.check.threshold:null;return(0,O.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.whatItWatches`,`What it watches`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.name`,`Name`)}),(0,O.jsx)(`input`,{type:`text`,value:o.label,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,label:e.target.value})),onBlur:()=>{let t=o.label.trim();t.length>0&&t!==e.label?d({label:t}):l(!1)}})]}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:e.enabled,disabled:n,onChange:e=>d({enabled:e.target.checked})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.watching`,`Watching`)})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.watchingHint`,`Turned off, the scene keeps its region, its references and its flag — it simply stops checking. Nothing is lost by pausing one for a while.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.whenToLook`,`When to look`)}),(0,O.jsxs)(p,{value:e.triggerMode,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`periodic`||t===`on-motion`||t===`both`)&&d({triggerMode:t})},children:[(0,O.jsx)(`option`,{value:`both`,children:a(`uiLibrary.sceneSettingsForm.triggerBoth`,`On the clock and after motion settles`)}),(0,O.jsx)(`option`,{value:`periodic`,children:a(`uiLibrary.sceneSettingsForm.triggerPeriodic`,`On the clock only`)}),(0,O.jsx)(`option`,{value:`on-motion`,children:a(`uiLibrary.sceneSettingsForm.triggerOnMotion`,`After motion settles only`)})]})]}),(0,O.jsx)(`p`,{className:u,children:U(e.triggerMode,a)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.checkEvery`,`Check every (seconds)`)}),(0,O.jsx)(`input`,{type:`number`,min:V,max:H,value:o.checkIntervalSec,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,checkIntervalSec:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.checkIntervalSec,V,H);t===e.checkIntervalSec?l(!1):d({checkIntervalSec:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.intervalHint`,`60 s suits “is the bin still out”. Every check costs one photograph and one encode, so going below about 30 s buys latency you almost never need and multiplies the cost by the number of scenes you own.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.howSure`,`How sure it has to be`)}),e.check.mode===`similarity`&&(0,O.jsxs)(O.Fragment,{children:[(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:(0,O.jsx)(r,{uiKey:`uiLibrary.sceneSettingsForm.matchThreshold`,fallback:`Match threshold — <0>{{value}}</0>`,components:[(0,O.jsx)(`span`,{className:`tabular-nums`},`value`)],values:{value:o.threshold.toFixed(2)}})}),(0,O.jsx)(`input`,{type:`range`,min:.3,max:.99,step:.01,value:o.threshold,disabled:n,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,threshold:Number(e.target.value)})),onMouseUp:()=>{o.threshold===h?l(!1):d({check:f({threshold:o.threshold})})},onBlur:()=>{o.threshold===h?l(!1):d({check:f({threshold:o.threshold})})}})]}),(0,O.jsx)(`p`,{className:`text-[10px] text-foreground-subtle`,children:a(`uiLibrary.sceneSettingsForm.thresholdHint`,`How closely the region still has to resemble the photograph to count as unchanged. Higher means fussier: at 0.95 a passing cloud can read as a change; at 0.60 the bin can be taken and the scene will shrug. 0.80 is a sensible start — if it cries wolf, lower it; if it misses the thing you care about, raise it.`)})]}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.agreeingChecks`,`Agreeing checks before it changes its mind`)}),(0,O.jsx)(`input`,{type:`number`,min:1,max:10,value:o.hysteresisCount,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,hysteresisCount:Number(e.target.value)||1})),onBlur:()=>{let t=q(o.hysteresisCount,1,10);t===e.check.hysteresisCount?l(!1):d({check:f({hysteresisCount:t})})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.agreeingHint`,`One bad photograph must never be able to flip a scene. 3 agreeing checks at 60 s is about four minutes of latency and is what makes the answer trustworthy — leaving a state costs exactly the same as entering one.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.quietFor`,`Quiet for (seconds) before a check counts`)}),(0,O.jsx)(`input`,{type:`number`,min:0,max:3600,value:o.quietSeconds,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,quietSeconds:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.quietSeconds,0,3600);t===e.quietSeconds?l(!1):d({quietSeconds:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.quietHint`,`A check taken while something is moving is a photograph of the van parked in front of the bin, so it is discarded rather than merely delayed. 0 disables the gate.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.minGap`,`Minimum gap between counted checks (seconds)`)}),(0,O.jsx)(`input`,{type:`number`,min:0,max:3600,value:o.minObservationSpacingSec,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,minObservationSpacingSec:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.minObservationSpacingSec,0,3600);t===e.minObservationSpacingSec?l(!1):d({minObservationSpacingSec:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.minGapHint`,`Spreads the agreeing checks over real time instead of letting three adjacent polls inside one long occlusion be mistaken for three independent opinions.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.lighting`,`Lighting`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.noReference`,`When a light has no reference of its own`)}),(0,O.jsxs)(p,{value:e.onUncoveredCondition,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`skip`||t===`judge-anyway`)&&d({onUncoveredCondition:t})},children:[(0,O.jsx)(`option`,{value:`skip`,children:a(`uiLibrary.sceneSettingsForm.uncoveredSkip`,`Skip the check (recommended)`)}),(0,O.jsx)(`option`,{value:`judge-anyway`,children:a(`uiLibrary.sceneSettingsForm.uncoveredJudge`,`Judge against the other light anyway`)})]})]}),(0,O.jsx)(`p`,{className:u,children:W(e.onUncoveredCondition,a)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.whatYouGetTold`,`What you get told`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.notificationsFollow`,`Notifications follow`)}),(0,O.jsxs)(p,{value:e.emit,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`latched`||t===`live`)&&d({emit:t})},children:[(0,O.jsx)(`option`,{value:`latched`,children:a(`uiLibrary.sceneSettingsForm.emitLatched`,`The flag — told once, still visible at breakfast`)}),(0,O.jsx)(`option`,{value:`live`,children:a(`uiLibrary.sceneSettingsForm.emitLive`,`The live state — follows the region as it changes`)})]})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.emitHint`,`“Is the bin gone” is a flag: you want to be told once and still see it later. “Is the gate open” is a live state.`)}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:e.autoRestore,disabled:n,onChange:e=>d({autoRestore:e.target.checked})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.autoRestore`,`Clear the flag by itself when things look normal again`)})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.autoRestoreHint`,`Off by default, so an alarm never silently clears itself. The “back to normal” moment is recorded either way, so an automation can still react to it.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.aiDoubleCheck`,`AI double-check`)}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:m,disabled:n||o.confirmPrompt.trim().length===0,onChange:e=>d(e.target.checked?{confirm:{...J(o.confirmPrompt.trim()),enabled:!0}}:{confirm:null})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.askVision`,`Ask a vision model before changing the answer`)})]}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.question`,`The question to ask`)}),(0,O.jsx)(`input`,{type:`text`,value:o.confirmPrompt,placeholder:a(`uiLibrary.sceneSettingsForm.questionPlaceholder`,`e.g. Is the wheelie bin still there?`),disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,confirmPrompt:e.target.value})),onBlur:()=>{let t=o.confirmPrompt.trim();t.length>0&&t!==e.confirm?.prompt?d({confirm:{...J(t),enabled:m}}):l(!1)}})]}),(0,O.jsx)(`p`,{className:u,children:(0,O.jsx)(r,{uiKey:`uiLibrary.sceneSettingsForm.confirmHint`,fallback:`The model is asked <0>only</0> when the region has already changed enough to flip the answer — roughly twice a week for a bin, not once a minute. It reasons about the object rather than the pixels, so rain, shadows and a repositioned bin stop counting as the thing being taken. If it cannot be reached, the change is held rather than announced.`,components:[(0,O.jsx)(`strong`,{},`only`)]})})]})]})}function q(e,t,n){return Math.min(n,Math.max(t,e))}function J(e){return{enabled:!1,prompt:e,timeoutMs:8e3,maxImagePx:448,onTimeout:`hold`}}var Y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,X=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`,Z=`rounded-md border border-danger/50 bg-danger/10 px-2 py-1 text-[11px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors`,Q={alarm:`border-danger/50 bg-danger/15 text-danger`,ok:`border-success/50 bg-success/15 text-success`,unknown:`border-border bg-surface text-foreground-subtle`},ee=.8,te=3;function $({deviceId:e}){let n=t(),r=d(s().trpcClient,e),i=f(r?.state.sceneMonitor),a=S(e,{width:640}),[o,p]=(0,D.useState)(null),[m,_]=(0,D.useState)(!1),[k,A]=(0,D.useState)(null),[j,M]=(0,D.useState)(null),[N,F]=(0,D.useState)(null),[I,L]=(0,D.useState)(null),[R,z]=(0,D.useState)(!1),[B,V]=(0,D.useState)(null),[H,U]=(0,D.useState)(``),W=(0,D.useRef)(!0);(0,D.useEffect)(()=>(W.current=!0,()=>{W.current=!1}),[]);let G=(0,D.useCallback)(async()=>{if(r)try{let e=await r.sceneMonitor?.listScenes({});if(!W.current)return;e&&p(e)}catch(e){if(!W.current)return;g(e)?_(!0):M(e instanceof Error?e.message:String(e))}},[r]);(0,D.useEffect)(()=>{p(null),_(!1),M(null),G()},[G]);let q=i?.monitors??o?.monitors??[],J=Date.now(),$=(0,D.useCallback)(async(e,t)=>{A(e),M(null);try{await t(),await G()}catch(e){W.current&&M(e instanceof Error?e.message:String(e))}finally{W.current&&A(null)}},[G]),ne=(0,D.useMemo)(()=>{let e=q.map(e=>({id:e.id,shape:e.roi,label:e.label,enabled:e.enabled}));return B===null?e:[...e,{id:`__draft__`,shape:B,label:n(`uiLibrary.sceneMonitor.newSceneLabel`,`New scene`)}]},[q,B,n]),re=(0,D.useCallback)((e,t)=>{if(t.kind===`rect`){if(e===`__draft__`){V(t);return}$(`roi-${String(e)}`,async()=>r?.sceneMonitor?.updateScene({monitorId:String(e),patch:{roi:t}}))}},[r,$]),ie=(0,D.useCallback)(e=>{e.kind===`rect`&&V(e)},[]),ae=(0,D.useCallback)(async()=>{B===null||H.trim().length===0||(await $(`create`,async()=>{await r?.sceneMonitor?.createScene({label:H.trim(),roi:B,check:{mode:`similarity`,threshold:ee,hysteresisCount:te}})}),V(null),U(``),z(!1))},[r,B,H,$]);return r?(0,O.jsx)(`div`,{className:`flex flex-col gap-3`,children:m?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.unsupported`,`Scene monitoring is not available for this camera.`)}):(0,O.jsxs)(O.Fragment,{children:[(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.intro`,`A scene watches one region of this camera and tells you when it stops looking like the picture you captured. Draw a box around the thing you care about — a wheelie bin, a gate, a parking space — then capture what “normal” looks like. Daylight is enough to start: the night reference is optional, and without it the scene simply sits the infrared hours out instead of reporting a problem.`)}),(0,O.jsx)(`div`,{className:`relative overflow-hidden rounded-md border border-border`,children:(0,O.jsx)(T,{items:ne,supportedShapes:[`rect`],selectedId:N??(B===null?null:`__draft__`),onSelect:e=>F(e===null?null:String(e)),onShapeChange:re,onDrawComplete:ie,drawingKind:R&&B===null?`rect`:null,backdrop:a.src===null?null:(0,O.jsx)(`img`,{src:a.src,alt:``,className:`h-full w-full object-contain`,draggable:!1})})}),(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,O.jsx)(`button`,{type:`button`,className:R?X:Y,"aria-pressed":R,onClick:()=>{z(e=>!e),V(null)},disabled:k!==null,children:R?n(`uiLibrary.sceneMonitor.cancel`,`Cancel`):n(`uiLibrary.sceneMonitor.newScene`,`+ New scene`)}),(0,O.jsx)(`button`,{type:`button`,className:Y,onClick:a.refresh,disabled:k!==null,children:n(`uiLibrary.sceneMonitor.refresh`,`Refresh frame`)})]}),R&&(0,O.jsxs)(`div`,{className:`flex flex-col gap-2 rounded-md border border-border bg-surface p-2`,children:[(0,O.jsx)(`p`,{className:`text-[10px] text-foreground-subtle`,children:B===null?n(`uiLibrary.sceneMonitor.draftHint`,`Click on the frame to drop a box, then drag its corner to fit it around the thing you want watched.`):n(`uiLibrary.sceneMonitor.nameHint`,`Name it, then create. You capture what “normal” looks like next.`)}),B!==null&&c(B)&&(0,O.jsx)(`p`,{className:`text-[11px] leading-relaxed text-warning`,children:n(`uiLibrary.sceneMonitor.tooLarge`,`That box covers most of the frame. A scene that big is dominated by the weather, the sky and passing cars — it will quietly stop noticing the thing you actually care about. Draw it tighter.`)}),(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`text`,value:H,onChange:e=>U(e.target.value),placeholder:n(`uiLibrary.sceneMonitor.placeholder`,`e.g. Immondizia prelevata`),className:`flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground`}),(0,O.jsx)(`button`,{type:`button`,className:X,onClick:()=>void ae(),disabled:B===null||H.trim().length===0||k!==null,children:n(`uiLibrary.sceneMonitor.create`,`Create`)})]})]}),j!==null&&(0,O.jsx)(`p`,{className:`text-[11px] leading-relaxed text-danger`,children:j}),q.length===0?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.empty`,`No scenes on this camera yet.`)}):(0,O.jsx)(`ul`,{className:`flex flex-col gap-2`,children:q.map(e=>{let t=b(e,n);return(0,O.jsxs)(`li`,{className:`flex flex-col gap-1.5 rounded-md border p-2 ${N===e.id?`border-primary/50`:`border-border`}`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`button`,{type:`button`,className:`flex-1 text-left text-[12px] font-medium text-foreground`,onClick:()=>F(e.id),children:e.label}),(0,O.jsx)(`span`,{className:`rounded border px-1.5 py-0.5 text-[10px] font-medium ${Q[t.tone]}`,children:t.text})]}),(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:y(e,J,n)}),(0,O.jsx)(`p`,{className:`${u} tabular-nums`,children:h(e,n)}),(0,O.jsx)(P,{monitor:e,disabled:k!==null,onDelete:({stateId:t,index:n})=>void $(`refdel-${e.id}-${t}-${String(n)}`,async()=>r.sceneMonitor?.deleteReference({monitorId:e.id,stateId:t,index:n}))}),(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-1.5`,children:[v.map(t=>{let i=t===`ir`;return(0,O.jsxs)(`button`,{type:`button`,className:Y,disabled:k!==null,title:i?n(`uiLibrary.sceneMonitor.optionalTitle`,`Optional. Without a night reference the checks in infrared are simply skipped — the scene keeps working in daylight and nothing is reported as broken.`):n(`uiLibrary.sceneMonitor.captureTitle`,`Take a fresh photo now and store it as the {{variant}} reference`,{variant:x(t,n).toLowerCase()}),onClick:()=>void $(`cap-${e.id}-${t}`,async()=>r.sceneMonitor?.captureReference({monitorId:e.id,condition:t})),children:[(0,O.jsx)(w,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.capture`,`Capture {{variant}}`,{variant:x(t,n).toLowerCase()}),i&&(0,O.jsx)(`span`,{className:`ml-1 text-foreground-subtle/70`,children:n(`uiLibrary.sceneMonitor.optional`,`(optional)`)})]},t)}),(0,O.jsxs)(`button`,{type:`button`,className:X,disabled:k!==null||e.states.length===0,title:n(`uiLibrary.sceneMonitor.resetTitle`,`Clear the flag and re-photograph what normal looks like right now`),onClick:()=>void $(`reset-${e.id}`,async()=>r.sceneMonitor?.resetScene({monitorId:e.id})),children:[(0,O.jsx)(l,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.reset`,`Reset`)]}),(0,O.jsx)(`button`,{type:`button`,className:Y,disabled:k!==null,onClick:()=>void $(`check-${e.id}`,async()=>r.sceneMonitor?.recheckNow({monitorId:e.id})),children:n(`uiLibrary.sceneMonitor.checkNow`,`Check now`)}),(0,O.jsxs)(`button`,{type:`button`,className:I===e.id?X:Y,"aria-pressed":I===e.id,"aria-label":n(`uiLibrary.sceneMonitor.settingsFor`,`Settings for {{label}}`,{label:e.label}),disabled:k!==null,onClick:()=>L(t=>t===e.id?null:e.id),children:[(0,O.jsx)(E,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.settings`,`Settings`)]}),(0,O.jsx)(`button`,{type:`button`,className:Z,disabled:k!==null,onClick:()=>void $(`del-${e.id}`,async()=>r.sceneMonitor?.deleteScene({monitorId:e.id})),children:(0,O.jsx)(C,{className:`h-3 w-3`})})]}),I===e.id&&(0,O.jsx)(K,{monitor:e,disabled:k!==null,onPatch:t=>void $(`tune-${e.id}`,async()=>r.sceneMonitor?.updateScene({monitorId:e.id,patch:t}))})]},e.id)})})]})}):null}export{$ as SceneMonitorEditor};
1
+ import{a as e,i as t,l as n,n as r,o as i}from"./scheduler-0XZJE03d.js";import{S as a,T as o,_ as s,a as c,b as l,c as u,d,f,g as p,h as m,i as h,l as g,m as _,n as v,o as y,r as b,s as x,u as S,v as C,w}from"./index-mE6ihqDj.js";import{MaskShapeCanvas as T}from"./MaskShapeCanvas-BTvl6Rin.js";var E=o(`settings-2`,[[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`path`,{d:`M19 7h-9`,key:`6i9tg`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),D=n(i(),1),O=n(e(),1),k=_(`/addon/pipeline-analytics/event-media`);function A(e){return`${k}/m/${e}`}function j(e){let t=new Map;for(let n of e.states)n.references.forEach((e,r)=>{let i=t.get(e.condition)??[];i.push({stateId:n.id,index:r,reference:e}),t.set(e.condition,i)});return[...t.entries()].map(([e,t])=>({condition:e,tiles:[...t].sort((e,t)=>t.reference.capturedAt-e.reference.capturedAt)})).sort((e,t)=>e.condition.localeCompare(t.condition))}function M(e){return e.states.reduce((e,t)=>e+t.references.length,0)}var N=`rounded border border-danger/50 bg-danger/10 px-1 py-0.5 text-[10px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors`;function P({monitor:e,disabled:n,onDelete:r}){let i=t(),a=j(e),o=M(e),s=m();return a.length===0?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:i(`uiLibrary.sceneReferenceGallery.empty`,`No photographs captured yet. Press “Capture daylight” to take the one this scene will judge everything against.`)}):(0,O.jsx)(`div`,{className:`flex flex-col gap-2`,children:a.map(e=>(0,O.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,O.jsxs)(`p`,{className:`text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle`,children:[x(e.condition,i),` · `,e.tiles.length]}),(0,O.jsx)(`ul`,{className:`flex flex-wrap gap-1.5`,children:e.tiles.map(t=>{let a=`${t.stateId}:${String(t.index)}`,c=o===1;return(0,O.jsxs)(`li`,{className:`flex w-24 flex-col gap-1 rounded-md border border-border p-1`,children:[(0,O.jsx)(F,{reference:t.reference}),(0,O.jsx)(`span`,{className:`${u} tabular-nums`,children:I(t.reference.capturedAt)}),(0,O.jsxs)(`button`,{type:`button`,className:N,disabled:n,"aria-label":i(`uiLibrary.sceneReferenceGallery.deleteLabel`,`Delete the {{variant}} photograph from {{date}}`,{variant:x(e.condition,i).toLowerCase(),date:I(t.reference.capturedAt)}),onClick:()=>{(async()=>{c&&!await s({title:i(`uiLibrary.sceneReferenceGallery.deleteTitle`,`Delete the only photograph?`),message:i(`uiLibrary.sceneReferenceGallery.lastWarning`,`This is the only photograph this scene has. Delete it and the scene no longer knows what normal looks like — it stops judging until you capture again.`),confirmLabel:i(`uiLibrary.sceneReferenceGallery.delete`,`Delete`),variant:`danger`})||r({stateId:t.stateId,index:t.index})})()},children:[(0,O.jsx)(C,{className:`mr-1 inline h-3 w-3`}),i(`uiLibrary.sceneReferenceGallery.delete`,`Delete`)]})]},a)})})]},e.condition))})}function F({reference:e}){let n=t(),[r,i]=(0,D.useState)(!1),o=e.thumbnailMediaId;return o===void 0||r?(0,O.jsxs)(`div`,{className:`flex h-16 w-full flex-col items-center justify-center gap-0.5 rounded bg-surface text-foreground-subtle`,title:n(`uiLibrary.sceneReferenceGallery.missingPicture`,`The picture for this reference is no longer stored. The reference itself is intact and still being scored against.`),children:[(0,O.jsx)(a,{className:`h-4 w-4`}),(0,O.jsx)(`span`,{className:`text-[9px]`,children:n(`uiLibrary.sceneReferenceGallery.noPicture`,`no picture`)})]}):(0,O.jsx)(`img`,{src:A(o),alt:``,loading:`lazy`,className:`h-16 w-full rounded object-cover`,onError:()=>i(!0)})}function I(e){let t=new Date(e);return`${t.toLocaleDateString()} ${t.toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`})}`}var L=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50`,R=`text-[10px] font-medium text-foreground-subtle`,z=`flex flex-col gap-2 rounded-md border border-border bg-surface/40 p-2`,B=`text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle`,V=5,H=3600;function U(e,t){switch(e){case`periodic`:return t(`uiLibrary.sceneSettingsForm.triggerHintPeriodic`,`Only on the clock. Predictable, and it notices a change even on a camera nothing ever moves in front of.`);case`on-motion`:return t(`uiLibrary.sceneSettingsForm.triggerHintOnMotion`,`Only after the camera has been still again for the quiet window below. Cheapest, but a change nobody walks past is never seen.`);case`both`:return t(`uiLibrary.sceneSettingsForm.triggerHintBoth`,`Both — the clock is the safety net that lets the motion path miss. This is the right answer for almost every scene.`)}}function W(e,t){switch(e){case`skip`:return t(`uiLibrary.sceneSettingsForm.uncoveredHintSkip`,`Sit that light out. No verdict, no alarm, no flag touched — it picks itself back up when a light it knows returns. This is why the night capture is optional.`);case`judge-anyway`:return t(`uiLibrary.sceneSettingsForm.uncoveredHintJudge`,`Compare against the references you do have. Only correct when the two lights genuinely look alike (a floodlit driveway, an indoor lamp) — against a real infrared frame it will report a change that did not happen.`)}}function G(e){return{label:e.label,checkIntervalSec:e.checkIntervalSec??60,threshold:e.check.mode===`similarity`?e.check.threshold:.8,hysteresisCount:e.check.hysteresisCount,quietSeconds:e.quietSeconds,minObservationSpacingSec:e.minObservationSpacingSec,confirmPrompt:e.confirm?.prompt??``}}function K({monitor:e,disabled:n,onPatch:i}){let a=t(),[o,s]=(0,D.useState)(()=>G(e)),[c,l]=(0,D.useState)(!1);(0,D.useEffect)(()=>{c||s(G(e))},[e,c]);let d=e=>{l(!1),i(e)},f=t=>e.check.mode===`similarity`?{mode:`similarity`,threshold:t.threshold??e.check.threshold,hysteresisCount:t.hysteresisCount??e.check.hysteresisCount}:{...e.check,hysteresisCount:t.hysteresisCount??e.check.hysteresisCount},m=e.confirm?.enabled===!0,h=e.check.mode===`similarity`?e.check.threshold:null;return(0,O.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.whatItWatches`,`What it watches`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.name`,`Name`)}),(0,O.jsx)(`input`,{type:`text`,value:o.label,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,label:e.target.value})),onBlur:()=>{let t=o.label.trim();t.length>0&&t!==e.label?d({label:t}):l(!1)}})]}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:e.enabled,disabled:n,onChange:e=>d({enabled:e.target.checked})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.watching`,`Watching`)})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.watchingHint`,`Turned off, the scene keeps its region, its references and its flag — it simply stops checking. Nothing is lost by pausing one for a while.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.whenToLook`,`When to look`)}),(0,O.jsxs)(p,{value:e.triggerMode,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`periodic`||t===`on-motion`||t===`both`)&&d({triggerMode:t})},children:[(0,O.jsx)(`option`,{value:`both`,children:a(`uiLibrary.sceneSettingsForm.triggerBoth`,`On the clock and after motion settles`)}),(0,O.jsx)(`option`,{value:`periodic`,children:a(`uiLibrary.sceneSettingsForm.triggerPeriodic`,`On the clock only`)}),(0,O.jsx)(`option`,{value:`on-motion`,children:a(`uiLibrary.sceneSettingsForm.triggerOnMotion`,`After motion settles only`)})]})]}),(0,O.jsx)(`p`,{className:u,children:U(e.triggerMode,a)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.checkEvery`,`Check every (seconds)`)}),(0,O.jsx)(`input`,{type:`number`,min:V,max:H,value:o.checkIntervalSec,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,checkIntervalSec:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.checkIntervalSec,V,H);t===e.checkIntervalSec?l(!1):d({checkIntervalSec:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.intervalHint`,`60 s suits “is the bin still out”. Every check costs one photograph and one encode, so going below about 30 s buys latency you almost never need and multiplies the cost by the number of scenes you own.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.howSure`,`How sure it has to be`)}),e.check.mode===`similarity`&&(0,O.jsxs)(O.Fragment,{children:[(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:(0,O.jsx)(r,{uiKey:`uiLibrary.sceneSettingsForm.matchThreshold`,fallback:`Match threshold — <0>{{value}}</0>`,components:[(0,O.jsx)(`span`,{className:`tabular-nums`},`value`)],values:{value:o.threshold.toFixed(2)}})}),(0,O.jsx)(`input`,{type:`range`,min:.3,max:.99,step:.01,value:o.threshold,disabled:n,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,threshold:Number(e.target.value)})),onMouseUp:()=>{o.threshold===h?l(!1):d({check:f({threshold:o.threshold})})},onBlur:()=>{o.threshold===h?l(!1):d({check:f({threshold:o.threshold})})}})]}),(0,O.jsx)(`p`,{className:`text-[10px] text-foreground-subtle`,children:a(`uiLibrary.sceneSettingsForm.thresholdHint`,`How closely the region still has to resemble the photograph to count as unchanged. Higher means fussier: at 0.95 a passing cloud can read as a change; at 0.60 the bin can be taken and the scene will shrug. 0.80 is a sensible start — if it cries wolf, lower it; if it misses the thing you care about, raise it.`)})]}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.agreeingChecks`,`Agreeing checks before it changes its mind`)}),(0,O.jsx)(`input`,{type:`number`,min:1,max:10,value:o.hysteresisCount,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,hysteresisCount:Number(e.target.value)||1})),onBlur:()=>{let t=q(o.hysteresisCount,1,10);t===e.check.hysteresisCount?l(!1):d({check:f({hysteresisCount:t})})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.agreeingHint`,`One bad photograph must never be able to flip a scene. 3 agreeing checks at 60 s is about four minutes of latency and is what makes the answer trustworthy — leaving a state costs exactly the same as entering one.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.quietFor`,`Quiet for (seconds) before a check counts`)}),(0,O.jsx)(`input`,{type:`number`,min:0,max:3600,value:o.quietSeconds,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,quietSeconds:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.quietSeconds,0,3600);t===e.quietSeconds?l(!1):d({quietSeconds:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.quietHint`,`A check taken while something is moving is a photograph of the van parked in front of the bin, so it is discarded rather than merely delayed. 0 disables the gate.`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.minGap`,`Minimum gap between counted checks (seconds)`)}),(0,O.jsx)(`input`,{type:`number`,min:0,max:3600,value:o.minObservationSpacingSec,disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,minObservationSpacingSec:Number(e.target.value)||0})),onBlur:()=>{let t=q(o.minObservationSpacingSec,0,3600);t===e.minObservationSpacingSec?l(!1):d({minObservationSpacingSec:t})}})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.minGapHint`,`Spreads the agreeing checks over real time instead of letting three adjacent polls inside one long occlusion be mistaken for three independent opinions.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.lighting`,`Lighting`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.noReference`,`When a light has no reference of its own`)}),(0,O.jsxs)(p,{value:e.onUncoveredCondition,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`skip`||t===`judge-anyway`)&&d({onUncoveredCondition:t})},children:[(0,O.jsx)(`option`,{value:`skip`,children:a(`uiLibrary.sceneSettingsForm.uncoveredSkip`,`Skip the check (recommended)`)}),(0,O.jsx)(`option`,{value:`judge-anyway`,children:a(`uiLibrary.sceneSettingsForm.uncoveredJudge`,`Judge against the other light anyway`)})]})]}),(0,O.jsx)(`p`,{className:u,children:W(e.onUncoveredCondition,a)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.whatYouGetTold`,`What you get told`)}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.notificationsFollow`,`Notifications follow`)}),(0,O.jsxs)(p,{value:e.emit,disabled:n,className:L,onChange:e=>{let t=e.target.value;(t===`latched`||t===`live`)&&d({emit:t})},children:[(0,O.jsx)(`option`,{value:`latched`,children:a(`uiLibrary.sceneSettingsForm.emitLatched`,`The flag — told once, still visible at breakfast`)}),(0,O.jsx)(`option`,{value:`live`,children:a(`uiLibrary.sceneSettingsForm.emitLive`,`The live state — follows the region as it changes`)})]})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.emitHint`,`“Is the bin gone” is a flag: you want to be told once and still see it later. “Is the gate open” is a live state.`)}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:e.autoRestore,disabled:n,onChange:e=>d({autoRestore:e.target.checked})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.autoRestore`,`Clear the flag by itself when things look normal again`)})]}),(0,O.jsx)(`p`,{className:u,children:a(`uiLibrary.sceneSettingsForm.autoRestoreHint`,`Off by default, so an alarm never silently clears itself. The “back to normal” moment is recorded either way, so an automation can still react to it.`)})]}),(0,O.jsxs)(`div`,{className:z,children:[(0,O.jsx)(`p`,{className:B,children:a(`uiLibrary.sceneSettingsForm.aiDoubleCheck`,`AI double-check`)}),(0,O.jsxs)(`label`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:m,disabled:n||o.confirmPrompt.trim().length===0,onChange:e=>d(e.target.checked?{confirm:{...J(o.confirmPrompt.trim()),enabled:!0}}:{confirm:null})}),(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.askVision`,`Ask a vision model before changing the answer`)})]}),(0,O.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,O.jsx)(`span`,{className:R,children:a(`uiLibrary.sceneSettingsForm.question`,`The question to ask`)}),(0,O.jsx)(`input`,{type:`text`,value:o.confirmPrompt,placeholder:a(`uiLibrary.sceneSettingsForm.questionPlaceholder`,`e.g. Is the wheelie bin still there?`),disabled:n,className:L,onFocus:()=>l(!0),onChange:e=>s(t=>({...t,confirmPrompt:e.target.value})),onBlur:()=>{let t=o.confirmPrompt.trim();t.length>0&&t!==e.confirm?.prompt?d({confirm:{...J(t),enabled:m}}):l(!1)}})]}),(0,O.jsx)(`p`,{className:u,children:(0,O.jsx)(r,{uiKey:`uiLibrary.sceneSettingsForm.confirmHint`,fallback:`The model is asked <0>only</0> when the region has already changed enough to flip the answer — roughly twice a week for a bin, not once a minute. It reasons about the object rather than the pixels, so rain, shadows and a repositioned bin stop counting as the thing being taken. If it cannot be reached, the change is held rather than announced.`,components:[(0,O.jsx)(`strong`,{},`only`)]})})]})]})}function q(e,t,n){return Math.min(n,Math.max(t,e))}function J(e){return{enabled:!1,prompt:e,timeoutMs:8e3,maxImagePx:448,onTimeout:`hold`}}var Y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,X=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`,Z=`rounded-md border border-danger/50 bg-danger/10 px-2 py-1 text-[11px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors`,Q={alarm:`border-danger/50 bg-danger/15 text-danger`,ok:`border-success/50 bg-success/15 text-success`,unknown:`border-border bg-surface text-foreground-subtle`},ee=.8,te=3;function $({deviceId:e}){let n=t(),r=d(s().trpcClient,e),i=f(r?.state.sceneMonitor),a=S(e,{width:640}),[o,p]=(0,D.useState)(null),[m,_]=(0,D.useState)(!1),[k,A]=(0,D.useState)(null),[j,M]=(0,D.useState)(null),[N,F]=(0,D.useState)(null),[I,L]=(0,D.useState)(null),[R,z]=(0,D.useState)(!1),[B,V]=(0,D.useState)(null),[H,U]=(0,D.useState)(``),W=(0,D.useRef)(!0);(0,D.useEffect)(()=>(W.current=!0,()=>{W.current=!1}),[]);let G=(0,D.useCallback)(async()=>{if(r)try{let e=await r.sceneMonitor?.listScenes({});if(!W.current)return;e&&p(e)}catch(e){if(!W.current)return;g(e)?_(!0):M(e instanceof Error?e.message:String(e))}},[r]);(0,D.useEffect)(()=>{p(null),_(!1),M(null),G()},[G]);let q=i?.monitors??o?.monitors??[],J=Date.now(),$=(0,D.useCallback)(async(e,t)=>{A(e),M(null);try{await t(),await G()}catch(e){W.current&&M(e instanceof Error?e.message:String(e))}finally{W.current&&A(null)}},[G]),ne=(0,D.useMemo)(()=>{let e=q.map(e=>({id:e.id,shape:e.roi,label:e.label,enabled:e.enabled}));return B===null?e:[...e,{id:`__draft__`,shape:B,label:n(`uiLibrary.sceneMonitor.newSceneLabel`,`New scene`)}]},[q,B,n]),re=(0,D.useCallback)((e,t)=>{if(t.kind===`rect`){if(e===`__draft__`){V(t);return}$(`roi-${String(e)}`,async()=>r?.sceneMonitor?.updateScene({monitorId:String(e),patch:{roi:t}}))}},[r,$]),ie=(0,D.useCallback)(e=>{e.kind===`rect`&&V(e)},[]),ae=(0,D.useCallback)(async()=>{B===null||H.trim().length===0||(await $(`create`,async()=>{await r?.sceneMonitor?.createScene({label:H.trim(),roi:B,check:{mode:`similarity`,threshold:ee,hysteresisCount:te}})}),V(null),U(``),z(!1))},[r,B,H,$]);return r?(0,O.jsx)(`div`,{className:`flex flex-col gap-3`,children:m?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.unsupported`,`Scene monitoring is not available for this camera.`)}):(0,O.jsxs)(O.Fragment,{children:[(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.intro`,`A scene watches one region of this camera and tells you when it stops looking like the picture you captured. Draw a box around the thing you care about — a wheelie bin, a gate, a parking space — then capture what “normal” looks like. Daylight is enough to start: the night reference is optional, and without it the scene simply sits the infrared hours out instead of reporting a problem.`)}),(0,O.jsx)(`div`,{className:`relative overflow-hidden rounded-md border border-border`,children:(0,O.jsx)(T,{items:ne,supportedShapes:[`rect`],selectedId:N??(B===null?null:`__draft__`),onSelect:e=>F(e===null?null:String(e)),onShapeChange:re,onDrawComplete:ie,drawingKind:R&&B===null?`rect`:null,backdrop:a.src===null?null:(0,O.jsx)(`img`,{src:a.src,alt:``,className:`h-full w-full object-contain`,draggable:!1})})}),(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,O.jsx)(`button`,{type:`button`,className:R?X:Y,"aria-pressed":R,onClick:()=>{z(e=>!e),V(null)},disabled:k!==null,children:R?n(`uiLibrary.sceneMonitor.cancel`,`Cancel`):n(`uiLibrary.sceneMonitor.newScene`,`+ New scene`)}),(0,O.jsx)(`button`,{type:`button`,className:Y,onClick:a.refresh,disabled:k!==null,children:n(`uiLibrary.sceneMonitor.refresh`,`Refresh frame`)})]}),R&&(0,O.jsxs)(`div`,{className:`flex flex-col gap-2 rounded-md border border-border bg-surface p-2`,children:[(0,O.jsx)(`p`,{className:`text-[10px] text-foreground-subtle`,children:B===null?n(`uiLibrary.sceneMonitor.draftHint`,`Click on the frame to drop a box, then drag its corner to fit it around the thing you want watched.`):n(`uiLibrary.sceneMonitor.nameHint`,`Name it, then create. You capture what “normal” looks like next.`)}),B!==null&&c(B)&&(0,O.jsx)(`p`,{className:`text-[11px] leading-relaxed text-warning`,children:n(`uiLibrary.sceneMonitor.tooLarge`,`That box covers most of the frame. A scene that big is dominated by the weather, the sky and passing cars — it will quietly stop noticing the thing you actually care about. Draw it tighter.`)}),(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`input`,{type:`text`,value:H,onChange:e=>U(e.target.value),placeholder:n(`uiLibrary.sceneMonitor.placeholder`,`e.g. Immondizia prelevata`),className:`flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground`}),(0,O.jsx)(`button`,{type:`button`,className:X,onClick:()=>void ae(),disabled:B===null||H.trim().length===0||k!==null,children:n(`uiLibrary.sceneMonitor.create`,`Create`)})]})]}),j!==null&&(0,O.jsx)(`p`,{className:`text-[11px] leading-relaxed text-danger`,children:j}),q.length===0?(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:n(`uiLibrary.sceneMonitor.empty`,`No scenes on this camera yet.`)}):(0,O.jsx)(`ul`,{className:`flex flex-col gap-2`,children:q.map(e=>{let t=b(e,n);return(0,O.jsxs)(`li`,{className:`flex flex-col gap-1.5 rounded-md border p-2 ${N===e.id?`border-primary/50`:`border-border`}`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`button`,{type:`button`,className:`flex-1 text-left text-[12px] font-medium text-foreground`,onClick:()=>F(e.id),children:e.label}),(0,O.jsx)(`span`,{className:`rounded border px-1.5 py-0.5 text-[10px] font-medium ${Q[t.tone]}`,children:t.text})]}),(0,O.jsx)(`p`,{className:`${u} leading-relaxed`,children:y(e,J,n)}),(0,O.jsx)(`p`,{className:`${u} tabular-nums`,children:h(e,n)}),(0,O.jsx)(P,{monitor:e,disabled:k!==null,onDelete:({stateId:t,index:n})=>void $(`refdel-${e.id}-${t}-${String(n)}`,async()=>r.sceneMonitor?.deleteReference({monitorId:e.id,stateId:t,index:n}))}),(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-1.5`,children:[v.map(t=>{let i=t===`ir`;return(0,O.jsxs)(`button`,{type:`button`,className:Y,disabled:k!==null,title:i?n(`uiLibrary.sceneMonitor.optionalTitle`,`Optional. Without a night reference the checks in infrared are simply skipped — the scene keeps working in daylight and nothing is reported as broken.`):n(`uiLibrary.sceneMonitor.captureTitle`,`Take a fresh photo now and store it as the {{variant}} reference`,{variant:x(t,n).toLowerCase()}),onClick:()=>void $(`cap-${e.id}-${t}`,async()=>r.sceneMonitor?.captureReference({monitorId:e.id,condition:t})),children:[(0,O.jsx)(w,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.capture`,`Capture {{variant}}`,{variant:x(t,n).toLowerCase()}),i&&(0,O.jsx)(`span`,{className:`ml-1 text-foreground-subtle/70`,children:n(`uiLibrary.sceneMonitor.optional`,`(optional)`)})]},t)}),(0,O.jsxs)(`button`,{type:`button`,className:X,disabled:k!==null||e.states.length===0,title:n(`uiLibrary.sceneMonitor.resetTitle`,`Clear the flag and re-photograph what normal looks like right now`),onClick:()=>void $(`reset-${e.id}`,async()=>r.sceneMonitor?.resetScene({monitorId:e.id})),children:[(0,O.jsx)(l,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.reset`,`Reset`)]}),(0,O.jsx)(`button`,{type:`button`,className:Y,disabled:k!==null,onClick:()=>void $(`check-${e.id}`,async()=>r.sceneMonitor?.recheckNow({monitorId:e.id})),children:n(`uiLibrary.sceneMonitor.checkNow`,`Check now`)}),(0,O.jsxs)(`button`,{type:`button`,className:I===e.id?X:Y,"aria-pressed":I===e.id,"aria-label":n(`uiLibrary.sceneMonitor.settingsFor`,`Settings for {{label}}`,{label:e.label}),disabled:k!==null,onClick:()=>L(t=>t===e.id?null:e.id),children:[(0,O.jsx)(E,{className:`mr-1 inline h-3 w-3`}),n(`uiLibrary.sceneMonitor.settings`,`Settings`)]}),(0,O.jsx)(`button`,{type:`button`,className:Z,disabled:k!==null,onClick:()=>void $(`del-${e.id}`,async()=>r.sceneMonitor?.deleteScene({monitorId:e.id})),children:(0,O.jsx)(C,{className:`h-3 w-3`})})]}),I===e.id&&(0,O.jsx)(K,{monitor:e,disabled:k!==null,onPatch:t=>void $(`tune-${e.id}`,async()=>r.sceneMonitor?.updateScene({monitorId:e.id,patch:t}))})]},e.id)})})]})}):null}export{$ as SceneMonitorEditor};