@camstack/types 1.2.443 → 1.2.445

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/addon.d.ts +5 -1
  3. package/dist/addon.js +21 -5
  4. package/dist/addon.mjs +17 -6
  5. package/dist/automation/export-entities.d.ts +98 -0
  6. package/dist/capabilities/capability-definition.d.ts +1 -1
  7. package/dist/capabilities/composer.cap.d.ts +34 -0
  8. package/dist/capabilities/consumables.cap.d.ts +4 -5
  9. package/dist/capabilities/core-blocks.cap.d.ts +244 -0
  10. package/dist/capabilities/device-export.cap.d.ts +111 -1
  11. package/dist/capabilities/index.d.ts +6 -4
  12. package/dist/capabilities/metrics-provider.cap.d.ts +16 -16
  13. package/dist/capabilities/nodes.cap.d.ts +2 -2
  14. package/dist/capabilities/pet-fountain.cap.d.ts +138 -0
  15. package/dist/capabilities/pipeline-executor.cap.d.ts +6 -22
  16. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +2 -2
  17. package/dist/capabilities/pipeline-runner.cap.d.ts +2 -2
  18. package/dist/capabilities/pipeline-step-input.d.ts +33 -0
  19. package/dist/capabilities/server-management.cap.d.ts +28 -0
  20. package/dist/client.js +820 -799
  21. package/dist/client.mjs +820 -799
  22. package/dist/composition/composition-items.d.ts +3 -3
  23. package/dist/composition/composition-report.d.ts +17 -0
  24. package/dist/composition/composition.d.ts +68 -1
  25. package/dist/composition/index.d.ts +2 -0
  26. package/dist/composition/presets/pet-fountain.d.ts +36 -0
  27. package/dist/composition/validate-forward.d.ts +31 -0
  28. package/dist/{composition-C0dlyG9U.js → composition-CO6Sak-s.js} +49 -5
  29. package/dist/{composition-bIBW5_13.mjs → composition-kSuOifuG.mjs} +32 -6
  30. package/dist/device/cap-vocabulary/entries/appliances.d.ts +1 -1
  31. package/dist/device/device-control-resolution.d.ts +1 -1
  32. package/dist/device/device-type.d.ts +4 -0
  33. package/dist/{err-msg-BCdlR2Li.mjs → err-msg-DWXWc0Di.mjs} +4 -0
  34. package/dist/{err-msg-COHgq7Mb.js → err-msg-EJhsFHqa.js} +4 -0
  35. package/dist/generated/addon-api.d.ts +148 -32
  36. package/dist/generated/addon-cap-facts.d.ts +1 -1
  37. package/dist/generated/cap-status-types.d.ts +3 -1
  38. package/dist/generated/capability-router-map.d.ts +5 -2
  39. package/dist/generated/collection-array-methods.d.ts +2 -2
  40. package/dist/generated/customize-catalog.d.ts +1 -1
  41. package/dist/generated/device-local-state.d.ts +3 -0
  42. package/dist/generated/device-proxy.d.ts +3 -0
  43. package/dist/generated/device-scoped-caps.d.ts +1 -1
  44. package/dist/generated/history-catalog.d.ts +1 -1
  45. package/dist/generated/method-access-map.d.ts +1 -1
  46. package/dist/generated/method-device-selectors.d.ts +1 -1
  47. package/dist/generated/runtime-state-durability.d.ts +1 -1
  48. package/dist/generated/system-proxy.d.ts +2 -2
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1465 -603
  51. package/dist/index.mjs +1433 -592
  52. package/dist/interfaces/agent.d.ts +6 -2
  53. package/dist/interfaces/event-bus.d.ts +1 -1
  54. package/dist/node.js +2 -2
  55. package/dist/node.mjs +2 -2
  56. package/dist/{sleep-C_u6cEAJ.mjs → sleep-6eG9C8Xl.mjs} +223 -72
  57. package/dist/{sleep-E7qAQwTE.js → sleep-UXDHn-oJ.js} +246 -71
  58. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -697,6 +697,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
697
697
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
698
698
  * integrations sharing the same food/desiccant/hopper surface. */
699
699
  DeviceType["PetFeeder"] = "pet-feeder";
700
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
701
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
702
+ * of a container and is BUILT as a composed device (D967). */
703
+ DeviceType["PetFountain"] = "pet-fountain";
700
704
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
701
705
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
702
706
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -1115,6 +1119,431 @@ function optionalMethod(input, output, options) {
1115
1119
  };
1116
1120
  }
1117
1121
  //#endregion
1122
+ //#region src/capabilities/notification-output.cap.ts
1123
+ /**
1124
+ * notification-output — canonical, capability-gated notification delivery.
1125
+ *
1126
+ * Apprise-derived model (see
1127
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
1128
+ * callers emit ONE canonical `Notification`; each provider declares a
1129
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
1130
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
1131
+ * message to what the kind supports — callers never special-case a service.
1132
+ *
1133
+ * DESIGN DECISIONS (locked):
1134
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
1135
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
1136
+ * cap. Rationale: the admin UI needs one uniform surface across the
1137
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
1138
+ * alternative would fork the UI per addon and cannot host the
1139
+ * discovery→adopt flow.
1140
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
1141
+ * the generated cap-mount auto-`concatCollection`-fans them across every
1142
+ * registered provider (notifiers addon + HA addon) so one catalog is
1143
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
1144
+ * `addonId` the generated collection router extracts from the call input.
1145
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
1146
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
1147
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
1148
+ * base64 fallback needed.
1149
+ *
1150
+ * TODO (deferred, closed-set change — separate decision): add
1151
+ * `providerKind: 'notify'` so notification providers surface on the unified
1152
+ * admin "Integrations" page.
1153
+ */
1154
+ /**
1155
+ * Zentik-derived typed-media enum — the superset across every kind. Each
1156
+ * adapter picks what it supports and the degrade engine filters the rest.
1157
+ */
1158
+ var AttachmentMediaTypeSchema = zod.z.enum([
1159
+ "image",
1160
+ "video",
1161
+ "gif",
1162
+ "audio",
1163
+ "icon"
1164
+ ]);
1165
+ /**
1166
+ * A single attachment. Exactly one of `url` (remote source, most adapters
1167
+ * prefer this) or `bytes` (inline source; required for Pushover-style
1168
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
1169
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
1170
+ */
1171
+ var AttachmentSchema = zod.z.object({
1172
+ mediaType: AttachmentMediaTypeSchema,
1173
+ url: zod.z.string().optional(),
1174
+ bytes: zod.z.instanceof(Uint8Array).optional(),
1175
+ mime: zod.z.string().optional(),
1176
+ name: zod.z.string().optional()
1177
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
1178
+ var NotificationFormatSchema = zod.z.enum([
1179
+ "text",
1180
+ "markdown",
1181
+ "html"
1182
+ ]);
1183
+ /**
1184
+ * The CLOSED icon vocabulary an action button may use.
1185
+ *
1186
+ * A closed set, not a free string, and that is the whole point: an arbitrary
1187
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
1188
+ * notices — the same class of gap as a zone vocabulary nothing produced
1189
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
1190
+ * declares `actionIcons: false` and the degrade engine strips the field.
1191
+ *
1192
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
1193
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
1194
+ * differently.
1195
+ *
1196
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
1197
+ *
1198
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
1199
+ * the whole set onto its own renderer's vocabulary through a
1200
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
1201
+ * fallback, so adding a member here fails every adapter's build until someone
1202
+ * decides its glyph, which is the only place that decision can be made
1203
+ * honestly.
1204
+ *
1205
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
1206
+ * `disarm` straight through; iOS feeds that string to
1207
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
1208
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
1209
+ * mapping, and "the field is documented" is not "the value renders".
1210
+ *
1211
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
1212
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
1213
+ * so an addon that emits a token the running hub does not know does not lose an
1214
+ * icon — its whole `send` fails Zod validation and the notification never
1215
+ * arrives. Never emit a new token from an addon before the train carrying it.
1216
+ */
1217
+ var NotificationActionIconSchema = zod.z.enum([
1218
+ "acknowledge",
1219
+ "dismiss",
1220
+ "silence",
1221
+ "snooze",
1222
+ "view",
1223
+ "play",
1224
+ "open",
1225
+ "close",
1226
+ "lock",
1227
+ "unlock",
1228
+ "arm",
1229
+ "arm-home",
1230
+ "arm-away",
1231
+ "arm-night",
1232
+ "disarm",
1233
+ "light",
1234
+ "alert",
1235
+ "camera"
1236
+ ]);
1237
+ /** A single tap-through action button. */
1238
+ var NotificationActionSchema = zod.z.object({
1239
+ id: zod.z.string(),
1240
+ label: zod.z.string(),
1241
+ url: zod.z.string().optional(),
1242
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
1243
+ icon: NotificationActionIconSchema.optional(),
1244
+ /**
1245
+ * Renders in a warning style where the notifier supports it.
1246
+ *
1247
+ * A HINT, never a gate. The callback's authority is its token and nothing
1248
+ * else — see `notification-center/action-token.ts` for what that does and
1249
+ * does not buy.
1250
+ */
1251
+ destructive: zod.z.boolean().optional(),
1252
+ /**
1253
+ * How the tap should REACH the url.
1254
+ *
1255
+ * `navigate` (absent, and every button authored before this field) opens it:
1256
+ * the phone leaves the notification and shows whatever the callback returns.
1257
+ * That is right for a button whose answer the operator wants to read.
1258
+ *
1259
+ * `background` fires it as a POST and stays put. It exists for the buttons
1260
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
1261
+ * an answer to the notification, and being thrown into a browser tab to
1262
+ * confirm it costs more attention than the notification did. A backend that
1263
+ * cannot do a background call renders it as an ordinary link (the adapters
1264
+ * fall back rather than dropping the button), so this is a preference, never
1265
+ * a requirement.
1266
+ */
1267
+ mode: zod.z.enum(["navigate", "background"]).optional()
1268
+ });
1269
+ /** A delivery parameter value: what a notifier declared it takes. */
1270
+ var DeliveryParamValueSchema = zod.z.union([
1271
+ zod.z.string(),
1272
+ zod.z.number(),
1273
+ zod.z.boolean()
1274
+ ]);
1275
+ /**
1276
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
1277
+ * addresses a target of that kind may set it; a kind that declares nothing is
1278
+ * never handed one. The admin UI derives its control from this descriptor —
1279
+ * no editor knows a service by name.
1280
+ *
1281
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
1282
+ * run on the addon cap path).
1283
+ */
1284
+ var DeliveryParamSchema = zod.z.object({
1285
+ key: zod.z.string().min(1),
1286
+ label: zod.z.string(),
1287
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
1288
+ labelKey: zod.z.string().optional(),
1289
+ type: zod.z.enum([
1290
+ "enum",
1291
+ "number",
1292
+ "boolean",
1293
+ "string"
1294
+ ]),
1295
+ /** `enum` only: the closed set of accepted values. */
1296
+ options: zod.z.array(zod.z.object({
1297
+ value: zod.z.string(),
1298
+ label: zod.z.string()
1299
+ })).optional(),
1300
+ /** `number` only. */
1301
+ min: zod.z.number().optional(),
1302
+ max: zod.z.number().optional(),
1303
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
1304
+ default: DeliveryParamValueSchema.optional()
1305
+ });
1306
+ /**
1307
+ * The canonical notification. `body` is the only hard field (Apprise model).
1308
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
1309
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
1310
+ * the adapter maps this ordinal onto its native level. `level?` is an
1311
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
1312
+ * `priority` for that one target.
1313
+ */
1314
+ var NotificationSchema = zod.z.object({
1315
+ body: zod.z.string(),
1316
+ title: zod.z.string().optional(),
1317
+ format: NotificationFormatSchema.default("text"),
1318
+ priority: zod.z.number().int().min(1).max(5).default(3),
1319
+ level: zod.z.string().optional(),
1320
+ attachments: zod.z.array(AttachmentSchema).optional(),
1321
+ clickUrl: zod.z.string().optional(),
1322
+ actions: zod.z.array(NotificationActionSchema).optional(),
1323
+ sound: zod.z.string().optional(),
1324
+ ttl: zod.z.number().optional(),
1325
+ tag: zod.z.string().optional(),
1326
+ deviceId: zod.z.number().optional(),
1327
+ eventId: zod.z.string().optional(),
1328
+ metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1329
+ /**
1330
+ * Values for the parameters the target's kind DECLARES in
1331
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
1332
+ * declaration by the sender; a notifier reads only the keys it declared.
1333
+ */
1334
+ delivery: zod.z.record(zod.z.string(), DeliveryParamValueSchema).optional()
1335
+ });
1336
+ /** One declared native severity/priority level for a kind. */
1337
+ var TargetKindLevelSchema = zod.z.object({
1338
+ id: zod.z.string(),
1339
+ label: zod.z.string(),
1340
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
1341
+ ordinal: zod.z.number().int().min(1).max(5).nullable(),
1342
+ flags: zod.z.object({
1343
+ critical: zod.z.boolean().optional(),
1344
+ silent: zod.z.boolean().optional(),
1345
+ noPush: zod.z.boolean().optional()
1346
+ }).optional(),
1347
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
1348
+ requires: zod.z.array(zod.z.string()).optional(),
1349
+ description: zod.z.string().optional()
1350
+ });
1351
+ /** Attachment capabilities for a kind (drives the degrade engine + test panel). */
1352
+ var TargetKindAttachmentsCapsSchema = zod.z.object({
1353
+ mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
1354
+ mode: zod.z.enum([
1355
+ "url",
1356
+ "bytes",
1357
+ "both"
1358
+ ]),
1359
+ max: zod.z.number().int().nonnegative(),
1360
+ maxBytes: zod.z.number().int().positive().optional()
1361
+ });
1362
+ /** The full capability block consulted before dispatch. */
1363
+ var TargetKindCapsSchema = zod.z.object({
1364
+ attachments: TargetKindAttachmentsCapsSchema,
1365
+ /** Max action buttons (0 = none). */
1366
+ actions: zod.z.number().int().nonnegative(),
1367
+ /**
1368
+ * Whether this kind renders a per-action ICON.
1369
+ *
1370
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
1371
+ * run on the addon cap path — three production failures in one day taught
1372
+ * this repo that once. Absent is read as false by the degrade engine, which
1373
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
1374
+ * assumed and dropped costs the operator's trust in the field.
1375
+ */
1376
+ actionIcons: zod.z.boolean().optional(),
1377
+ levels: zod.z.array(TargetKindLevelSchema),
1378
+ format: zod.z.array(NotificationFormatSchema),
1379
+ clickUrl: zod.z.boolean(),
1380
+ sound: zod.z.boolean(),
1381
+ ttl: zod.z.boolean(),
1382
+ bodyMaxLen: zod.z.number().int().positive()
1383
+ });
1384
+ /**
1385
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
1386
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
1387
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
1388
+ * the union is large and not meant for runtime validation here; the exported
1389
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
1390
+ */
1391
+ var ConfigSchemaPassthrough = zod.z.unknown();
1392
+ var TargetKindSchema = zod.z.object({
1393
+ kind: zod.z.string(),
1394
+ label: zod.z.string(),
1395
+ icon: zod.z.string(),
1396
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
1397
+ addonId: zod.z.string(),
1398
+ /**
1399
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
1400
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
1401
+ * when the addon bundles no icon for that kind — the client then falls back
1402
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
1403
+ *
1404
+ * Root-relative on purpose: it resolves against whatever origin serves a web
1405
+ * client, and a native client joins it onto its own hub base.
1406
+ *
1407
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
1408
+ * field that survived only because the runtime cap-router forwards provider
1409
+ * output verbatim — so every consumer had to re-declare it by hand to stop
1410
+ * its own Zod parse from stripping it, and the whole arrangement would have
1411
+ * broken silently the moment output validation was tightened anywhere.
1412
+ */
1413
+ iconUrl: zod.z.string().optional(),
1414
+ /**
1415
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
1416
+ *
1417
+ * The server knows this and therefore says it, because the client cannot
1418
+ * safely guess: a React-Native client renders SVG and raster through two
1419
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
1420
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
1421
+ * placeholder glyph for every vector icon while the web build looked fine.
1422
+ *
1423
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
1424
+ * not been updated — a client that cannot determine the type should prefer
1425
+ * its raster path, which is the safe default for an unknown image.
1426
+ */
1427
+ iconMediaType: zod.z.string().optional(),
1428
+ configSchema: ConfigSchemaPassthrough,
1429
+ /**
1430
+ * The config keys that decide WHERE this kind's secrets are sent (server,
1431
+ * base URL, instance, chat, recipient). A save that changes any of them
1432
+ * while leaving a secret redacted is refused by the provider (D634), so an
1433
+ * editor uses this to ask for the secret on exactly those edits instead of
1434
+ * on every edit.
1435
+ *
1436
+ * Optional: absent from a provider that holds no secret (the Home Assistant
1437
+ * kinds) and from a provider older than this field — an editor that finds
1438
+ * it absent must assume ANY edit may need the secret.
1439
+ */
1440
+ destinationKeys: zod.z.array(zod.z.string()).optional(),
1441
+ supportsDiscovery: zod.z.boolean(),
1442
+ /**
1443
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
1444
+ * Absent = none; the rule editor then offers nothing.
1445
+ */
1446
+ deliveryParams: zod.z.array(DeliveryParamSchema).optional(),
1447
+ caps: TargetKindCapsSchema
1448
+ });
1449
+ /**
1450
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
1451
+ * (return a presence marker only) when serving `listTargets` — never
1452
+ * round-trip a stored secret to the UI.
1453
+ */
1454
+ var TargetSchema = zod.z.object({
1455
+ id: zod.z.string(),
1456
+ name: zod.z.string(),
1457
+ kind: zod.z.string(),
1458
+ addonId: zod.z.string(),
1459
+ enabled: zod.z.boolean(),
1460
+ config: zod.z.record(zod.z.string(), zod.z.unknown())
1461
+ });
1462
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
1463
+ var DiscoveredTargetSchema = zod.z.object({
1464
+ kind: zod.z.string(),
1465
+ suggestedName: zod.z.string(),
1466
+ config: zod.z.record(zod.z.string(), zod.z.unknown())
1467
+ });
1468
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
1469
+ var RenderedAsSchema = zod.z.object({
1470
+ level: zod.z.string(),
1471
+ format: NotificationFormatSchema,
1472
+ attachmentsSent: zod.z.number().int().nonnegative(),
1473
+ actionsSent: zod.z.number().int().nonnegative(),
1474
+ truncated: zod.z.boolean(),
1475
+ dropped: zod.z.array(zod.z.string())
1476
+ });
1477
+ var SendResultSchema = zod.z.object({
1478
+ success: zod.z.boolean(),
1479
+ error: zod.z.string().optional(),
1480
+ renderedAs: RenderedAsSchema.optional()
1481
+ });
1482
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
1483
+ var TestResultSchema = SendResultSchema;
1484
+ /**
1485
+ * Pre-save credential probe against a partial (unsaved) target config.
1486
+ * `resolvedUrl` never includes secrets (token stripped from path).
1487
+ */
1488
+ var ProbeTargetConfigResultSchema = zod.z.object({
1489
+ ok: zod.z.boolean(),
1490
+ resolvedUrl: zod.z.string(),
1491
+ httpStatus: zod.z.number().int().optional(),
1492
+ stateInstance: zod.z.string().optional(),
1493
+ error: zod.z.string().optional()
1494
+ });
1495
+ method(zod.z.object({}), zod.z.array(TargetKindSchema)), method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }), method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }), method(zod.z.object({
1496
+ kind: zod.z.string(),
1497
+ config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1498
+ /**
1499
+ * When editing / duplicating, the blank password field never round-trips
1500
+ * the stored token. Pass the source target id so the provider can restore
1501
+ * secrets server-side before dialling (same merge as upsert).
1502
+ */
1503
+ targetId: zod.z.string().optional()
1504
+ }), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }), method(zod.z.object({
1505
+ kind: zod.z.string(),
1506
+ config: zod.z.record(zod.z.string(), zod.z.unknown()),
1507
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
1508
+ targetId: zod.z.string().optional()
1509
+ }), ProbeTargetConfigResultSchema, {
1510
+ kind: "mutation",
1511
+ auth: "admin"
1512
+ }), method(zod.z.object({
1513
+ targetId: zod.z.string(),
1514
+ notification: NotificationSchema
1515
+ }), SendResultSchema, {
1516
+ kind: "mutation",
1517
+ auth: "admin"
1518
+ }), method(zod.z.object({
1519
+ targetId: zod.z.string(),
1520
+ sample: NotificationSchema.optional()
1521
+ }), TestResultSchema, {
1522
+ kind: "mutation",
1523
+ auth: "admin"
1524
+ }), method(zod.z.object({
1525
+ target: TargetSchema,
1526
+ /**
1527
+ * Duplicate flow: create a NEW target id while copying secrets from an
1528
+ * existing row. The UI never holds the stored token; blank / redacted
1529
+ * password fields are restored from this source (D634 still applies if
1530
+ * destinations diverge without a re-typed secret).
1531
+ */
1532
+ cloneSecretsFrom: zod.z.string().optional()
1533
+ }), TargetSchema, {
1534
+ kind: "mutation",
1535
+ auth: "admin"
1536
+ }), method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
1537
+ kind: "mutation",
1538
+ auth: "admin"
1539
+ }), method(zod.z.object({
1540
+ targetId: zod.z.string(),
1541
+ enabled: zod.z.boolean()
1542
+ }), zod.z.void(), {
1543
+ kind: "mutation",
1544
+ auth: "admin"
1545
+ });
1546
+ //#endregion
1118
1547
  //#region src/device/icon-catalog.generated.ts
1119
1548
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
1120
1549
  var ICON_CATEGORY_IDS = [
@@ -20776,827 +21205,402 @@ var ICON_SYNONYMS = {
20776
21205
  store: [
20777
21206
  "negozio",
20778
21207
  "ingresso",
20779
- "vetrina",
20780
- "shop"
20781
- ],
20782
- sun: [
20783
- "sole",
20784
- "luminosita",
20785
- "giorno",
20786
- "esterno",
20787
- "outdoor"
20788
- ],
20789
- "sun-medium": ["sole", "luminosita"],
20790
- sunrise: ["alba"],
20791
- tablet: ["ipad"],
20792
- thermometer: [
20793
- "temperatura",
20794
- "termometro",
20795
- "termostato"
20796
- ],
20797
- "thermometer-snowflake": [
20798
- "freddo",
20799
- "temperatura",
20800
- "bassa"
20801
- ],
20802
- "thermometer-sun": [
20803
- "caldo",
20804
- "temperatura",
20805
- "alta"
20806
- ],
20807
- timer: ["conto", "rovescia"],
20808
- "toggle-left": [
20809
- "interruttore",
20810
- "switch",
20811
- "toggle"
20812
- ],
20813
- "toggle-right": [
20814
- "interruttore",
20815
- "switch",
20816
- "toggle",
20817
- "acceso"
20818
- ],
20819
- tractor: [
20820
- "trattore",
20821
- "tosaerba",
20822
- "giardino"
20823
- ],
20824
- "train-front": ["treno"],
20825
- "tree-pine": [
20826
- "albero",
20827
- "pino",
20828
- "giardino"
20829
- ],
20830
- trees: [
20831
- "giardino",
20832
- "alberi",
20833
- "esterno",
20834
- "parco",
20835
- "garden"
20836
- ],
20837
- truck: [
20838
- "camion",
20839
- "furgone",
20840
- "consegna"
20841
- ],
20842
- tv: ["televisore"],
20843
- umbrella: [
20844
- "tenda",
20845
- "markise",
20846
- "ombrellone",
20847
- "pergola",
20848
- "veranda",
20849
- "terrace"
20850
- ],
20851
- user: ["persona", "utente"],
20852
- users: [
20853
- "persone",
20854
- "gruppo",
20855
- "famiglia"
20856
- ],
20857
- utensils: [
20858
- "cucina",
20859
- "posate",
20860
- "sala",
20861
- "pranzo",
20862
- "kitchen"
20863
- ],
20864
- vibrate: [
20865
- "vibrazione",
20866
- "urto",
20867
- "tamper"
20868
- ],
20869
- video: [],
20870
- "video-off": ["video", "spento"],
20871
- "volume-2": [
20872
- "volume",
20873
- "audio",
20874
- "altoparlante",
20875
- "suono"
20876
- ],
20877
- "volume-x": [
20878
- "muto",
20879
- "silenziato",
20880
- "volume",
20881
- "spento"
20882
- ],
20883
- warehouse: [
20884
- "magazzino",
20885
- "capanno",
20886
- "deposito",
20887
- "cantina",
20888
- "warehouse"
20889
- ],
20890
- "washing-machine": [
20891
- "lavatrice",
20892
- "lavastoviglie",
20893
- "asciugatrice"
20894
- ],
20895
- waves: [
20896
- "onde",
20897
- "acqua",
20898
- "vibrazione"
20899
- ],
20900
- "waves-ladder": [
20901
- "piscina",
20902
- "vasca",
20903
- "pool"
20904
- ],
20905
- wifi: ["rete", "segnale"],
20906
- "wifi-off": [
20907
- "wifi",
20908
- "assente",
20909
- "offline",
20910
- "disconnesso"
20911
- ],
20912
- wind: [
20913
- "vento",
20914
- "aria",
20915
- "gas",
20916
- "monossido",
20917
- "co"
20918
- ],
20919
- workflow: [
20920
- "automazione",
20921
- "flusso",
20922
- "scenario"
21208
+ "vetrina",
21209
+ "shop"
20923
21210
  ],
20924
- wrench: [
20925
- "officina",
20926
- "manutenzione",
20927
- "attrezzi",
20928
- "utenze"
21211
+ sun: [
21212
+ "sole",
21213
+ "luminosita",
21214
+ "giorno",
21215
+ "esterno",
21216
+ "outdoor"
20929
21217
  ],
20930
- zap: [
20931
- "energia",
20932
- "corrente",
20933
- "consumo",
20934
- "watt",
20935
- "elettricita"
20936
- ]
20937
- };
20938
- /**
20939
- * Spellings an operator may already have PERSISTED that no lucide package
20940
- * accepts any more, and the catalog key they draw. They resolve
20941
- * (`canonicalIconKey`) but are never offered. `gauge2` was this repo's own
20942
- * pre-catalog spelling of `gauge`.
20943
- */
20944
- var LEGACY_ICON_ALIASES = { gauge2: "gauge" };
20945
- //#endregion
20946
- //#region src/device/icon-catalog.ts
20947
- /**
20948
- * icon-catalog — THE icon vocabulary (D854).
20949
- *
20950
- * Every place that picks or stores an icon — a device's display override, a
20951
- * location, a site zone, a viewer accessory, an `icon` settings field — names
20952
- * it by a key of this catalog and nothing else. There is no curated subset and
20953
- * no second list: the catalog is every lucide icon that both surfaces can draw
20954
- * (the admin UI's `lucide-react` and the viewer's `lucide-react-native`), the
20955
- * intersection computed by `scripts/generate-icon-catalog.ts` into the
20956
- * committed `icon-catalog.generated.ts`.
20957
- *
20958
- * Like the vocabularies it replaces, this module holds STRINGS only — no React,
20959
- * no lucide import, no glyph. Each surface owns its glyph table (the web one
20960
- * lives in `ui-library`, loaded lazily), and a test on each side pins that it
20961
- * covers {@link ICON_KEYS}.
20962
- *
20963
- * ## What is persisted
20964
- *
20965
- * The canonical kebab-case lucide id ({@link IconKey}). Spellings that a lucide
20966
- * package still accepts (`home` for `house`) and this repo's own legacy ones
20967
- * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
20968
- * stale, but they are never OFFERED by search.
20969
- *
20970
- * ## Search
20971
- *
20972
- * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
20973
- * the caller adds, and lucide's English tags — in that order of weight.
20974
- */
20975
- var KEY_SET$1 = new Set(ICON_KEYS);
20976
- var ALIAS_TO_KEY = new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
20977
- /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
20978
- function isIconKey(value) {
20979
- return typeof value === "string" && KEY_SET$1.has(value);
20980
- }
20981
- /**
20982
- * A stored value as a catalog key: the key itself, the key a still-accepted or
20983
- * legacy spelling stands for, or `undefined` — never a guess.
20984
- */
20985
- function canonicalIconKey(value) {
20986
- if (typeof value !== "string") return void 0;
20987
- if (isIconKey(value)) return value;
20988
- return ALIAS_TO_KEY.get(value);
20989
- }
20990
- /**
20991
- * A stored icon value as a catalog key: its canonical key when it is (or
20992
- * aliases) one, `fallback` otherwise. For an `icon` settings field whose stored
20993
- * value may predate the catalog (a free-text lucide name) — the field's default
20994
- * is drawn rather than nothing.
20995
- */
20996
- function resolveIconKey(value, fallback) {
20997
- return canonicalIconKey(value) ?? fallback;
20998
- }
20999
- /**
21000
- * Wire/storage schema for one icon key.
21001
- *
21002
- * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
21003
- * into every cap router, type and API-reference row that carries an icon. The
21004
- * check is membership in the catalog and the type is {@link IconKey}.
21005
- */
21006
- var IconKeySchema = zod.z.custom(isIconKey, "Not an icon of the catalog");
21007
- var ENTRIES = ICON_KEYS.map((key, index) => {
21008
- const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
21009
- const categories = categoryIndexes.flatMap((i) => {
21010
- const id = ICON_CATEGORY_IDS[i];
21011
- return id === void 0 ? [] : [id];
21012
- });
21013
- return {
21014
- key,
21015
- categories,
21016
- primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
21017
- tags,
21018
- synonyms: ICON_SYNONYMS[key] ?? []
21019
- };
21020
- });
21021
- var ENTRY_BY_KEY = new Map(ENTRIES.map((e) => [e.key, e]));
21022
- /** The entry for a canonical key, or `undefined`. */
21023
- function iconEntry(key) {
21024
- return ENTRY_BY_KEY.get(key);
21025
- }
21026
- /** Every entry, in key order. */
21027
- var ICON_ENTRIES = ENTRIES;
21028
- /** Categories a picker offers first: the ones this product's icons mostly live in. */
21029
- var LEADING_CATEGORIES = [
21030
- "devices",
21031
- "home",
21032
- "security",
21033
- "buildings",
21034
- "connectivity",
21035
- "multimedia",
21036
- "nature",
21037
- "weather",
21038
- "transportation",
21039
- "animals"
21040
- ];
21041
- var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
21042
- /**
21043
- * The categories a picker files icons under, in picker order: the leading ones,
21044
- * then the rest alphabetically. Only categories that are some icon's PRIMARY
21045
- * one — a category lucide lists only as a secondary has no section of its own.
21046
- */
21047
- var ICON_CATEGORIES = [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
21048
- /** The icons filed under a category as their PRIMARY one, in key order. */
21049
- function iconsInCategory(category) {
21050
- return ENTRIES.filter((e) => e.primaryCategory === category);
21051
- }
21052
- function normalise(text) {
21053
- return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[_-]+/g, " ").trim();
21054
- }
21055
- /** Does `needle` appear in `haystack` in order, though not adjacently? */
21056
- function isSubsequence(needle, haystack) {
21057
- let i = 0;
21058
- for (const ch of haystack) {
21059
- if (ch === needle[i]) i += 1;
21060
- if (i === needle.length) return true;
21061
- }
21062
- return needle.length === 0;
21063
- }
21064
- /**
21065
- * How well one word answers the query. 0 = not at all. An exact word beats a
21066
- * prefix, a prefix beats a word STARTING inside the phrase, and a loose
21067
- * subsequence comes last because it matches almost anything.
21068
- */
21069
- function scoreWord(query, word, loose) {
21070
- if (word === query) return 100;
21071
- if (word.startsWith(query)) return 80;
21072
- if (word.split(" ").some((w) => w.startsWith(query))) return 60;
21073
- if (word.includes(query)) return 40;
21074
- if (loose && isSubsequence(query, word)) return 15;
21075
- return 0;
21076
- }
21077
- var WORDS_CACHE = /* @__PURE__ */ new Map();
21078
- /** Normalised once per entry — search runs on every keystroke over ~1,700 entries. */
21079
- function wordsOf(entry) {
21080
- const cached = WORDS_CACHE.get(entry.key);
21081
- if (cached !== void 0) return cached;
21082
- const built = {
21083
- key: normalise(entry.key),
21084
- synonyms: entry.synonyms.map(normalise),
21085
- tags: entry.tags.map(normalise)
21086
- };
21087
- WORDS_CACHE.set(entry.key, built);
21088
- return built;
21089
- }
21090
- /**
21091
- * Rank the catalog against a free-text query.
21092
- *
21093
- * An empty query returns the WHOLE catalog in key order, because a picker's
21094
- * resting state is the full list. `extraWords` lets a caller add the words IT
21095
- * can translate without this module knowing what a locale is; they score like a
21096
- * synonym. The key outranks a synonym, a synonym outranks a lucide tag, and
21097
- * ties keep key order so the list does not reshuffle between keystrokes.
21098
- */
21099
- function searchIcons(query, extraWords) {
21100
- const q = normalise(query);
21101
- if (q.length === 0) return ENTRIES.map((entry) => ({
21102
- entry,
21103
- score: 0
21104
- }));
21105
- const hits = [];
21106
- for (const entry of ENTRIES) {
21107
- const words = wordsOf(entry);
21108
- const keyScore = scoreWord(q, words.key, true);
21109
- let best = keyScore > 0 ? keyScore + 2 : 0;
21110
- for (const word of [...words.synonyms, ...(extraWords?.get(entry.key) ?? []).map(normalise)]) {
21111
- const score = scoreWord(q, word, true);
21112
- if (score > 0 && score + 1 > best) best = score + 1;
21113
- }
21114
- for (const word of words.tags) {
21115
- const score = scoreWord(q, word, false);
21116
- if (score > best) best = score;
21117
- }
21118
- if (best > 0) hits.push({
21119
- entry,
21120
- score: best
21121
- });
21122
- }
21123
- return hits.toSorted((a, b) => b.score - a.score);
21124
- }
21125
- /** A readable label derived from a lucide category id (`food-beverage` → `Food beverage`). */
21126
- function iconCategoryLabel(category) {
21127
- const spaced = category.replace(/-/g, " ");
21128
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
21129
- }
21130
- //#endregion
21131
- //#region src/device/location-icon.ts
21132
- /**
21133
- * What a location's icon is, and the ONE key its label joins on.
21134
- *
21135
- * A location is not an entity — it is a free-form label (`PersistedDeviceMeta.
21136
- * location`) plus an operator-curated registry of labels in the device-manager
21137
- * addon store. The icon is a second map beside that registry, keyed the same
21138
- * way the registry matches everywhere else: {@link locationIconKey}, i.e.
21139
- * trimmed + lower-cased.
21140
- *
21141
- * The icon itself is a key of the ONE icon catalog (`icon-catalog.ts`, D854):
21142
- * a location may wear any icon both surfaces can draw. There used to be a
21143
- * closed 20-icon subset here (D511); it was a second vocabulary on the same two
21144
- * surfaces and is gone. The names below keep their meaning:
21145
- *
21146
- * DEGRADATION IS A FALLBACK, NEVER A HOLE. A surface that cannot draw a stored
21147
- * value — a persisted value from a newer hub, a hand-edited settings row —
21148
- * draws {@link LOCATION_ICON_FALLBACK} (`map-pin`, the one every surface has
21149
- * always had). Read a stored/wire value through
21150
- * `resolveIconKey(value, LOCATION_ICON_FALLBACK)` and that is what you get;
21151
- * nothing anywhere renders an empty box.
21152
- */
21153
- /**
21154
- * The id every surface is guaranteed to draw, and what an unrecognised value
21155
- * degrades to. It has been in both icon sets since before the catalog existed
21156
- * (the Locations page's own page icon), so "the fallback is missing" is not a
21157
- * state a build can reach.
21158
- */
21159
- var LOCATION_ICON_FALLBACK = "map-pin";
21160
- zod.z.object({
21161
- locationKey: zod.z.string(),
21162
- icon: IconKeySchema
21163
- });
21218
+ "sun-medium": ["sole", "luminosita"],
21219
+ sunrise: ["alba"],
21220
+ tablet: ["ipad"],
21221
+ thermometer: [
21222
+ "temperatura",
21223
+ "termometro",
21224
+ "termostato"
21225
+ ],
21226
+ "thermometer-snowflake": [
21227
+ "freddo",
21228
+ "temperatura",
21229
+ "bassa"
21230
+ ],
21231
+ "thermometer-sun": [
21232
+ "caldo",
21233
+ "temperatura",
21234
+ "alta"
21235
+ ],
21236
+ timer: ["conto", "rovescia"],
21237
+ "toggle-left": [
21238
+ "interruttore",
21239
+ "switch",
21240
+ "toggle"
21241
+ ],
21242
+ "toggle-right": [
21243
+ "interruttore",
21244
+ "switch",
21245
+ "toggle",
21246
+ "acceso"
21247
+ ],
21248
+ tractor: [
21249
+ "trattore",
21250
+ "tosaerba",
21251
+ "giardino"
21252
+ ],
21253
+ "train-front": ["treno"],
21254
+ "tree-pine": [
21255
+ "albero",
21256
+ "pino",
21257
+ "giardino"
21258
+ ],
21259
+ trees: [
21260
+ "giardino",
21261
+ "alberi",
21262
+ "esterno",
21263
+ "parco",
21264
+ "garden"
21265
+ ],
21266
+ truck: [
21267
+ "camion",
21268
+ "furgone",
21269
+ "consegna"
21270
+ ],
21271
+ tv: ["televisore"],
21272
+ umbrella: [
21273
+ "tenda",
21274
+ "markise",
21275
+ "ombrellone",
21276
+ "pergola",
21277
+ "veranda",
21278
+ "terrace"
21279
+ ],
21280
+ user: ["persona", "utente"],
21281
+ users: [
21282
+ "persone",
21283
+ "gruppo",
21284
+ "famiglia"
21285
+ ],
21286
+ utensils: [
21287
+ "cucina",
21288
+ "posate",
21289
+ "sala",
21290
+ "pranzo",
21291
+ "kitchen"
21292
+ ],
21293
+ vibrate: [
21294
+ "vibrazione",
21295
+ "urto",
21296
+ "tamper"
21297
+ ],
21298
+ video: [],
21299
+ "video-off": ["video", "spento"],
21300
+ "volume-2": [
21301
+ "volume",
21302
+ "audio",
21303
+ "altoparlante",
21304
+ "suono"
21305
+ ],
21306
+ "volume-x": [
21307
+ "muto",
21308
+ "silenziato",
21309
+ "volume",
21310
+ "spento"
21311
+ ],
21312
+ warehouse: [
21313
+ "magazzino",
21314
+ "capanno",
21315
+ "deposito",
21316
+ "cantina",
21317
+ "warehouse"
21318
+ ],
21319
+ "washing-machine": [
21320
+ "lavatrice",
21321
+ "lavastoviglie",
21322
+ "asciugatrice"
21323
+ ],
21324
+ waves: [
21325
+ "onde",
21326
+ "acqua",
21327
+ "vibrazione"
21328
+ ],
21329
+ "waves-ladder": [
21330
+ "piscina",
21331
+ "vasca",
21332
+ "pool"
21333
+ ],
21334
+ wifi: ["rete", "segnale"],
21335
+ "wifi-off": [
21336
+ "wifi",
21337
+ "assente",
21338
+ "offline",
21339
+ "disconnesso"
21340
+ ],
21341
+ wind: [
21342
+ "vento",
21343
+ "aria",
21344
+ "gas",
21345
+ "monossido",
21346
+ "co"
21347
+ ],
21348
+ workflow: [
21349
+ "automazione",
21350
+ "flusso",
21351
+ "scenario"
21352
+ ],
21353
+ wrench: [
21354
+ "officina",
21355
+ "manutenzione",
21356
+ "attrezzi",
21357
+ "utenze"
21358
+ ],
21359
+ zap: [
21360
+ "energia",
21361
+ "corrente",
21362
+ "consumo",
21363
+ "watt",
21364
+ "elettricita"
21365
+ ]
21366
+ };
21164
21367
  /**
21165
- * The join key for a location label: trimmed, lower-cased. The ONE place this
21166
- * derivation lives — a caller that re-spells it is a rename away from an
21167
- * orphaned icon.
21368
+ * Spellings an operator may already have PERSISTED that no lucide package
21369
+ * accepts any more, and the catalog key they draw. They resolve
21370
+ * (`canonicalIconKey`) but are never offered. `gauge2` was this repo's own
21371
+ * pre-catalog spelling of `gauge`.
21168
21372
  */
21169
- function locationIconKey(label) {
21170
- return label.trim().toLowerCase();
21171
- }
21172
- zod.z.object({
21173
- locationKey: zod.z.string(),
21174
- description: zod.z.string()
21175
- });
21373
+ var LEGACY_ICON_ALIASES = { gauge2: "gauge" };
21176
21374
  //#endregion
21177
- //#region src/capabilities/notification-output.cap.ts
21178
- /**
21179
- * notification-output — canonical, capability-gated notification delivery.
21180
- *
21181
- * Apprise-derived model (see
21182
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
21183
- * callers emit ONE canonical `Notification`; each provider declares a
21184
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
21185
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
21186
- * message to what the kind supports — callers never special-case a service.
21187
- *
21188
- * DESIGN DECISIONS (locked):
21189
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
21190
- * `setTargetEnabled`), each provider persisting via the `settings-store`
21191
- * cap. Rationale: the admin UI needs one uniform surface across the
21192
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
21193
- * alternative would fork the UI per addon and cannot host the
21194
- * discovery→adopt flow.
21195
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
21196
- * the generated cap-mount auto-`concatCollection`-fans them across every
21197
- * registered provider (notifiers addon + HA addon) so one catalog is
21198
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
21199
- * `addonId` the generated collection router extracts from the call input.
21200
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
21201
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
21202
- * `storage` / `storage-provider` / `recording` caps over the same path. No
21203
- * base64 fallback needed.
21204
- *
21205
- * TODO (deferred, closed-set change — separate decision): add
21206
- * `providerKind: 'notify'` so notification providers surface on the unified
21207
- * admin "Integrations" page.
21208
- */
21209
- /**
21210
- * Zentik-derived typed-media enum — the superset across every kind. Each
21211
- * adapter picks what it supports and the degrade engine filters the rest.
21212
- */
21213
- var AttachmentMediaTypeSchema = zod.z.enum([
21214
- "image",
21215
- "video",
21216
- "gif",
21217
- "audio",
21218
- "icon"
21219
- ]);
21220
- /**
21221
- * A single attachment. Exactly one of `url` (remote source, most adapters
21222
- * prefer this) or `bytes` (inline source; required for Pushover-style
21223
- * bytes-only kinds) MUST be present — the degrade engine expresses a
21224
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
21225
- */
21226
- var AttachmentSchema = zod.z.object({
21227
- mediaType: AttachmentMediaTypeSchema,
21228
- url: zod.z.string().optional(),
21229
- bytes: zod.z.instanceof(Uint8Array).optional(),
21230
- mime: zod.z.string().optional(),
21231
- name: zod.z.string().optional()
21232
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
21233
- var NotificationFormatSchema = zod.z.enum([
21234
- "text",
21235
- "markdown",
21236
- "html"
21237
- ]);
21375
+ //#region src/device/icon-catalog.ts
21238
21376
  /**
21239
- * The CLOSED icon vocabulary an action button may use.
21240
- *
21241
- * A closed set, not a free string, and that is the whole point: an arbitrary
21242
- * icon name is one that ntfy renders, zentik silently drops, and nobody
21243
- * notices — the same class of gap as a zone vocabulary nothing produced
21244
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
21245
- * declares `actionIcons: false` and the degrade engine strips the field.
21377
+ * icon-catalog — THE icon vocabulary (D854).
21246
21378
  *
21247
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
21248
- * renderer's icon set; "acknowledge" survives an adapter that draws it
21249
- * differently.
21379
+ * Every place that picks or stores an icon — a device's display override, a
21380
+ * location, a site zone, a viewer accessory, an `icon` settings field — names
21381
+ * it by a key of this catalog and nothing else. There is no curated subset and
21382
+ * no second list: the catalog is every lucide icon that both surfaces can draw
21383
+ * (the admin UI's `lucide-react` and the viewer's `lucide-react-native`), the
21384
+ * intersection computed by `scripts/generate-icon-catalog.ts` into the
21385
+ * committed `icon-catalog.generated.ts`.
21250
21386
  *
21251
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
21387
+ * Like the vocabularies it replaces, this module holds STRINGS only — no React,
21388
+ * no lucide import, no glyph. Each surface owns its glyph table (the web one
21389
+ * lives in `ui-library`, loaded lazily), and a test on each side pins that it
21390
+ * covers {@link ICON_KEYS}.
21252
21391
  *
21253
- * These names are for US. **No adapter may forward one verbatim.** Each maps
21254
- * the whole set onto its own renderer's vocabulary through a
21255
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
21256
- * fallback, so adding a member here fails every adapter's build until someone
21257
- * decides its glyph, which is the only place that decision can be made
21258
- * honestly.
21392
+ * ## What is persisted
21259
21393
  *
21260
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
21261
- * `disarm` straight through; iOS feeds that string to
21262
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
21263
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
21264
- * mapping, and "the field is documented" is not "the value renders".
21394
+ * The canonical kebab-case lucide id ({@link IconKey}). Spellings that a lucide
21395
+ * package still accepts (`home` for `house`) and this repo's own legacy ones
21396
+ * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
21397
+ * stale, but they are never OFFERED by search.
21265
21398
  *
21266
- * Adding a member is TRAIN-BOUND. The enum lives in the published
21267
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
21268
- * so an addon that emits a token the running hub does not know does not lose an
21269
- * icon — its whole `send` fails Zod validation and the notification never
21270
- * arrives. Never emit a new token from an addon before the train carrying it.
21399
+ * ## Search
21400
+ *
21401
+ * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
21402
+ * the caller adds, and lucide's English tags — in that order of weight.
21271
21403
  */
21272
- var NotificationActionIconSchema = zod.z.enum([
21273
- "acknowledge",
21274
- "dismiss",
21275
- "silence",
21276
- "snooze",
21277
- "view",
21278
- "play",
21279
- "open",
21280
- "close",
21281
- "lock",
21282
- "unlock",
21283
- "arm",
21284
- "arm-home",
21285
- "arm-away",
21286
- "arm-night",
21287
- "disarm",
21288
- "light",
21289
- "alert",
21290
- "camera"
21291
- ]);
21292
- /** A single tap-through action button. */
21293
- var NotificationActionSchema = zod.z.object({
21294
- id: zod.z.string(),
21295
- label: zod.z.string(),
21296
- url: zod.z.string().optional(),
21297
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
21298
- icon: NotificationActionIconSchema.optional(),
21299
- /**
21300
- * Renders in a warning style where the notifier supports it.
21301
- *
21302
- * A HINT, never a gate. The callback's authority is its token and nothing
21303
- * else — see `notification-center/action-token.ts` for what that does and
21304
- * does not buy.
21305
- */
21306
- destructive: zod.z.boolean().optional(),
21307
- /**
21308
- * How the tap should REACH the url.
21309
- *
21310
- * `navigate` (absent, and every button authored before this field) opens it:
21311
- * the phone leaves the notification and shows whatever the callback returns.
21312
- * That is right for a button whose answer the operator wants to read.
21313
- *
21314
- * `background` fires it as a POST and stays put. It exists for the buttons
21315
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
21316
- * an answer to the notification, and being thrown into a browser tab to
21317
- * confirm it costs more attention than the notification did. A backend that
21318
- * cannot do a background call renders it as an ordinary link (the adapters
21319
- * fall back rather than dropping the button), so this is a preference, never
21320
- * a requirement.
21321
- */
21322
- mode: zod.z.enum(["navigate", "background"]).optional()
21323
- });
21324
- /** A delivery parameter value: what a notifier declared it takes. */
21325
- var DeliveryParamValueSchema = zod.z.union([
21326
- zod.z.string(),
21327
- zod.z.number(),
21328
- zod.z.boolean()
21329
- ]);
21404
+ var KEY_SET$1 = new Set(ICON_KEYS);
21405
+ var ALIAS_TO_KEY = new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
21406
+ /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
21407
+ function isIconKey(value) {
21408
+ return typeof value === "string" && KEY_SET$1.has(value);
21409
+ }
21330
21410
  /**
21331
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
21332
- * addresses a target of that kind may set it; a kind that declares nothing is
21333
- * never handed one. The admin UI derives its control from this descriptor —
21334
- * no editor knows a service by name.
21411
+ * A stored value as a catalog key: the key itself, the key a still-accepted or
21412
+ * legacy spelling stands for, or `undefined` — never a guess.
21413
+ */
21414
+ function canonicalIconKey(value) {
21415
+ if (typeof value !== "string") return void 0;
21416
+ if (isIconKey(value)) return value;
21417
+ return ALIAS_TO_KEY.get(value);
21418
+ }
21419
+ /**
21420
+ * A stored icon value as a catalog key: its canonical key when it is (or
21421
+ * aliases) one, `fallback` otherwise. For an `icon` settings field whose stored
21422
+ * value may predate the catalog (a free-text lucide name) — the field's default
21423
+ * is drawn rather than nothing.
21424
+ */
21425
+ function resolveIconKey(value, fallback) {
21426
+ return canonicalIconKey(value) ?? fallback;
21427
+ }
21428
+ /**
21429
+ * Wire/storage schema for one icon key.
21335
21430
  *
21336
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
21337
- * run on the addon cap path).
21431
+ * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
21432
+ * into every cap router, type and API-reference row that carries an icon. The
21433
+ * check is membership in the catalog and the type is {@link IconKey}.
21338
21434
  */
21339
- var DeliveryParamSchema = zod.z.object({
21340
- key: zod.z.string().min(1),
21341
- label: zod.z.string(),
21342
- /** i18n key of the label, resolved by the declaring addon's namespace. */
21343
- labelKey: zod.z.string().optional(),
21344
- type: zod.z.enum([
21345
- "enum",
21346
- "number",
21347
- "boolean",
21348
- "string"
21349
- ]),
21350
- /** `enum` only: the closed set of accepted values. */
21351
- options: zod.z.array(zod.z.object({
21352
- value: zod.z.string(),
21353
- label: zod.z.string()
21354
- })).optional(),
21355
- /** `number` only. */
21356
- min: zod.z.number().optional(),
21357
- max: zod.z.number().optional(),
21358
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
21359
- default: DeliveryParamValueSchema.optional()
21435
+ var IconKeySchema = zod.z.custom(isIconKey, "Not an icon of the catalog");
21436
+ var ENTRIES = ICON_KEYS.map((key, index) => {
21437
+ const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
21438
+ const categories = categoryIndexes.flatMap((i) => {
21439
+ const id = ICON_CATEGORY_IDS[i];
21440
+ return id === void 0 ? [] : [id];
21441
+ });
21442
+ return {
21443
+ key,
21444
+ categories,
21445
+ primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
21446
+ tags,
21447
+ synonyms: ICON_SYNONYMS[key] ?? []
21448
+ };
21360
21449
  });
21450
+ var ENTRY_BY_KEY = new Map(ENTRIES.map((e) => [e.key, e]));
21451
+ /** The entry for a canonical key, or `undefined`. */
21452
+ function iconEntry(key) {
21453
+ return ENTRY_BY_KEY.get(key);
21454
+ }
21455
+ /** Every entry, in key order. */
21456
+ var ICON_ENTRIES = ENTRIES;
21457
+ /** Categories a picker offers first: the ones this product's icons mostly live in. */
21458
+ var LEADING_CATEGORIES = [
21459
+ "devices",
21460
+ "home",
21461
+ "security",
21462
+ "buildings",
21463
+ "connectivity",
21464
+ "multimedia",
21465
+ "nature",
21466
+ "weather",
21467
+ "transportation",
21468
+ "animals"
21469
+ ];
21470
+ var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
21361
21471
  /**
21362
- * The canonical notification. `body` is the only hard field (Apprise model).
21363
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
21364
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
21365
- * the adapter maps this ordinal onto its native level. `level?` is an
21366
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
21367
- * `priority` for that one target.
21472
+ * The categories a picker files icons under, in picker order: the leading ones,
21473
+ * then the rest alphabetically. Only categories that are some icon's PRIMARY
21474
+ * one — a category lucide lists only as a secondary has no section of its own.
21368
21475
  */
21369
- var NotificationSchema = zod.z.object({
21370
- body: zod.z.string(),
21371
- title: zod.z.string().optional(),
21372
- format: NotificationFormatSchema.default("text"),
21373
- priority: zod.z.number().int().min(1).max(5).default(3),
21374
- level: zod.z.string().optional(),
21375
- attachments: zod.z.array(AttachmentSchema).optional(),
21376
- clickUrl: zod.z.string().optional(),
21377
- actions: zod.z.array(NotificationActionSchema).optional(),
21378
- sound: zod.z.string().optional(),
21379
- ttl: zod.z.number().optional(),
21380
- tag: zod.z.string().optional(),
21381
- deviceId: zod.z.number().optional(),
21382
- eventId: zod.z.string().optional(),
21383
- metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
21384
- /**
21385
- * Values for the parameters the target's kind DECLARES in
21386
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
21387
- * declaration by the sender; a notifier reads only the keys it declared.
21388
- */
21389
- delivery: zod.z.record(zod.z.string(), DeliveryParamValueSchema).optional()
21390
- });
21391
- /** One declared native severity/priority level for a kind. */
21392
- var TargetKindLevelSchema = zod.z.object({
21393
- id: zod.z.string(),
21394
- label: zod.z.string(),
21395
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
21396
- ordinal: zod.z.number().int().min(1).max(5).nullable(),
21397
- flags: zod.z.object({
21398
- critical: zod.z.boolean().optional(),
21399
- silent: zod.z.boolean().optional(),
21400
- noPush: zod.z.boolean().optional()
21401
- }).optional(),
21402
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
21403
- requires: zod.z.array(zod.z.string()).optional(),
21404
- description: zod.z.string().optional()
21405
- });
21406
- /** Attachment capabilities for a kind (drives the degrade engine + test panel). */
21407
- var TargetKindAttachmentsCapsSchema = zod.z.object({
21408
- mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
21409
- mode: zod.z.enum([
21410
- "url",
21411
- "bytes",
21412
- "both"
21413
- ]),
21414
- max: zod.z.number().int().nonnegative(),
21415
- maxBytes: zod.z.number().int().positive().optional()
21416
- });
21417
- /** The full capability block consulted before dispatch. */
21418
- var TargetKindCapsSchema = zod.z.object({
21419
- attachments: TargetKindAttachmentsCapsSchema,
21420
- /** Max action buttons (0 = none). */
21421
- actions: zod.z.number().int().nonnegative(),
21422
- /**
21423
- * Whether this kind renders a per-action ICON.
21424
- *
21425
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
21426
- * run on the addon cap path — three production failures in one day taught
21427
- * this repo that once. Absent is read as false by the degrade engine, which
21428
- * is the safe direction: an icon that is not rendered costs nothing, an icon
21429
- * assumed and dropped costs the operator's trust in the field.
21430
- */
21431
- actionIcons: zod.z.boolean().optional(),
21432
- levels: zod.z.array(TargetKindLevelSchema),
21433
- format: zod.z.array(NotificationFormatSchema),
21434
- clickUrl: zod.z.boolean(),
21435
- sound: zod.z.boolean(),
21436
- ttl: zod.z.boolean(),
21437
- bodyMaxLen: zod.z.number().int().positive()
21438
- });
21476
+ var ICON_CATEGORIES = [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
21477
+ /** The icons filed under a category as their PRIMARY one, in key order. */
21478
+ function iconsInCategory(category) {
21479
+ return ENTRIES.filter((e) => e.primaryCategory === category);
21480
+ }
21481
+ function normalise(text) {
21482
+ return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[_-]+/g, " ").trim();
21483
+ }
21484
+ /** Does `needle` appear in `haystack` in order, though not adjacently? */
21485
+ function isSubsequence(needle, haystack) {
21486
+ let i = 0;
21487
+ for (const ch of haystack) {
21488
+ if (ch === needle[i]) i += 1;
21489
+ if (i === needle.length) return true;
21490
+ }
21491
+ return needle.length === 0;
21492
+ }
21493
+ /**
21494
+ * How well one word answers the query. 0 = not at all. An exact word beats a
21495
+ * prefix, a prefix beats a word STARTING inside the phrase, and a loose
21496
+ * subsequence comes last because it matches almost anything.
21497
+ */
21498
+ function scoreWord(query, word, loose) {
21499
+ if (word === query) return 100;
21500
+ if (word.startsWith(query)) return 80;
21501
+ if (word.split(" ").some((w) => w.startsWith(query))) return 60;
21502
+ if (word.includes(query)) return 40;
21503
+ if (loose && isSubsequence(query, word)) return 15;
21504
+ return 0;
21505
+ }
21506
+ var WORDS_CACHE = /* @__PURE__ */ new Map();
21507
+ /** Normalised once per entry — search runs on every keystroke over ~1,700 entries. */
21508
+ function wordsOf(entry) {
21509
+ const cached = WORDS_CACHE.get(entry.key);
21510
+ if (cached !== void 0) return cached;
21511
+ const built = {
21512
+ key: normalise(entry.key),
21513
+ synonyms: entry.synonyms.map(normalise),
21514
+ tags: entry.tags.map(normalise)
21515
+ };
21516
+ WORDS_CACHE.set(entry.key, built);
21517
+ return built;
21518
+ }
21439
21519
  /**
21440
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
21441
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
21442
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
21443
- * the union is large and not meant for runtime validation here; the exported
21444
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
21520
+ * Rank the catalog against a free-text query.
21521
+ *
21522
+ * An empty query returns the WHOLE catalog in key order, because a picker's
21523
+ * resting state is the full list. `extraWords` lets a caller add the words IT
21524
+ * can translate without this module knowing what a locale is; they score like a
21525
+ * synonym. The key outranks a synonym, a synonym outranks a lucide tag, and
21526
+ * ties keep key order so the list does not reshuffle between keystrokes.
21445
21527
  */
21446
- var ConfigSchemaPassthrough = zod.z.unknown();
21447
- var TargetKindSchema = zod.z.object({
21448
- kind: zod.z.string(),
21449
- label: zod.z.string(),
21450
- icon: zod.z.string(),
21451
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
21452
- addonId: zod.z.string(),
21453
- /**
21454
- * URL of the kind's bundled BRAND icon, served by the providing addon over
21455
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
21456
- * when the addon bundles no icon for that kind — the client then falls back
21457
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
21458
- *
21459
- * Root-relative on purpose: it resolves against whatever origin serves a web
21460
- * client, and a native client joins it onto its own hub base.
21461
- *
21462
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
21463
- * field that survived only because the runtime cap-router forwards provider
21464
- * output verbatim — so every consumer had to re-declare it by hand to stop
21465
- * its own Zod parse from stripping it, and the whole arrangement would have
21466
- * broken silently the moment output validation was tightened anywhere.
21467
- */
21468
- iconUrl: zod.z.string().optional(),
21469
- /**
21470
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
21471
- *
21472
- * The server knows this and therefore says it, because the client cannot
21473
- * safely guess: a React-Native client renders SVG and raster through two
21474
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
21475
- * not decode SVG on iOS/Android), so without this it silently fell back to a
21476
- * placeholder glyph for every vector icon while the web build looked fine.
21477
- *
21478
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
21479
- * not been updated — a client that cannot determine the type should prefer
21480
- * its raster path, which is the safe default for an unknown image.
21481
- */
21482
- iconMediaType: zod.z.string().optional(),
21483
- configSchema: ConfigSchemaPassthrough,
21484
- /**
21485
- * The config keys that decide WHERE this kind's secrets are sent (server,
21486
- * base URL, instance, chat, recipient). A save that changes any of them
21487
- * while leaving a secret redacted is refused by the provider (D634), so an
21488
- * editor uses this to ask for the secret on exactly those edits instead of
21489
- * on every edit.
21490
- *
21491
- * Optional: absent from a provider that holds no secret (the Home Assistant
21492
- * kinds) and from a provider older than this field — an editor that finds
21493
- * it absent must assume ANY edit may need the secret.
21494
- */
21495
- destinationKeys: zod.z.array(zod.z.string()).optional(),
21496
- supportsDiscovery: zod.z.boolean(),
21497
- /**
21498
- * Extra delivery parameters a rule may set for a target of this kind (D938).
21499
- * Absent = none; the rule editor then offers nothing.
21500
- */
21501
- deliveryParams: zod.z.array(DeliveryParamSchema).optional(),
21502
- caps: TargetKindCapsSchema
21503
- });
21528
+ function searchIcons(query, extraWords) {
21529
+ const q = normalise(query);
21530
+ if (q.length === 0) return ENTRIES.map((entry) => ({
21531
+ entry,
21532
+ score: 0
21533
+ }));
21534
+ const hits = [];
21535
+ for (const entry of ENTRIES) {
21536
+ const words = wordsOf(entry);
21537
+ const keyScore = scoreWord(q, words.key, true);
21538
+ let best = keyScore > 0 ? keyScore + 2 : 0;
21539
+ for (const word of [...words.synonyms, ...(extraWords?.get(entry.key) ?? []).map(normalise)]) {
21540
+ const score = scoreWord(q, word, true);
21541
+ if (score > 0 && score + 1 > best) best = score + 1;
21542
+ }
21543
+ for (const word of words.tags) {
21544
+ const score = scoreWord(q, word, false);
21545
+ if (score > best) best = score;
21546
+ }
21547
+ if (best > 0) hits.push({
21548
+ entry,
21549
+ score: best
21550
+ });
21551
+ }
21552
+ return hits.toSorted((a, b) => b.score - a.score);
21553
+ }
21554
+ /** A readable label derived from a lucide category id (`food-beverage` → `Food beverage`). */
21555
+ function iconCategoryLabel(category) {
21556
+ const spaced = category.replace(/-/g, " ");
21557
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
21558
+ }
21559
+ //#endregion
21560
+ //#region src/device/location-icon.ts
21504
21561
  /**
21505
- * A persisted target. `config` holds secrets; providers REDACT secret fields
21506
- * (return a presence marker only) when serving `listTargets` — never
21507
- * round-trip a stored secret to the UI.
21562
+ * What a location's icon is, and the ONE key its label joins on.
21563
+ *
21564
+ * A location is not an entity — it is a free-form label (`PersistedDeviceMeta.
21565
+ * location`) plus an operator-curated registry of labels in the device-manager
21566
+ * addon store. The icon is a second map beside that registry, keyed the same
21567
+ * way the registry matches everywhere else: {@link locationIconKey}, i.e.
21568
+ * trimmed + lower-cased.
21569
+ *
21570
+ * The icon itself is a key of the ONE icon catalog (`icon-catalog.ts`, D854):
21571
+ * a location may wear any icon both surfaces can draw. There used to be a
21572
+ * closed 20-icon subset here (D511); it was a second vocabulary on the same two
21573
+ * surfaces and is gone. The names below keep their meaning:
21574
+ *
21575
+ * DEGRADATION IS A FALLBACK, NEVER A HOLE. A surface that cannot draw a stored
21576
+ * value — a persisted value from a newer hub, a hand-edited settings row —
21577
+ * draws {@link LOCATION_ICON_FALLBACK} (`map-pin`, the one every surface has
21578
+ * always had). Read a stored/wire value through
21579
+ * `resolveIconKey(value, LOCATION_ICON_FALLBACK)` and that is what you get;
21580
+ * nothing anywhere renders an empty box.
21508
21581
  */
21509
- var TargetSchema = zod.z.object({
21510
- id: zod.z.string(),
21511
- name: zod.z.string(),
21512
- kind: zod.z.string(),
21513
- addonId: zod.z.string(),
21514
- enabled: zod.z.boolean(),
21515
- config: zod.z.record(zod.z.string(), zod.z.unknown())
21516
- });
21517
- /** A discovery-surfaced candidate (config is partial + non-secret). */
21518
- var DiscoveredTargetSchema = zod.z.object({
21519
- kind: zod.z.string(),
21520
- suggestedName: zod.z.string(),
21521
- config: zod.z.record(zod.z.string(), zod.z.unknown())
21522
- });
21523
- /** The degrade engine's report — what was resolved / dropped / degraded. */
21524
- var RenderedAsSchema = zod.z.object({
21525
- level: zod.z.string(),
21526
- format: NotificationFormatSchema,
21527
- attachmentsSent: zod.z.number().int().nonnegative(),
21528
- actionsSent: zod.z.number().int().nonnegative(),
21529
- truncated: zod.z.boolean(),
21530
- dropped: zod.z.array(zod.z.string())
21531
- });
21532
- var SendResultSchema = zod.z.object({
21533
- success: zod.z.boolean(),
21534
- error: zod.z.string().optional(),
21535
- renderedAs: RenderedAsSchema.optional()
21536
- });
21537
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
21538
- var TestResultSchema = SendResultSchema;
21539
21582
  /**
21540
- * Pre-save credential probe against a partial (unsaved) target config.
21541
- * `resolvedUrl` never includes secrets (token stripped from path).
21583
+ * The id every surface is guaranteed to draw, and what an unrecognised value
21584
+ * degrades to. It has been in both icon sets since before the catalog existed
21585
+ * (the Locations page's own page icon), so "the fallback is missing" is not a
21586
+ * state a build can reach.
21542
21587
  */
21543
- var ProbeTargetConfigResultSchema = zod.z.object({
21544
- ok: zod.z.boolean(),
21545
- resolvedUrl: zod.z.string(),
21546
- httpStatus: zod.z.number().int().optional(),
21547
- stateInstance: zod.z.string().optional(),
21548
- error: zod.z.string().optional()
21588
+ var LOCATION_ICON_FALLBACK = "map-pin";
21589
+ zod.z.object({
21590
+ locationKey: zod.z.string(),
21591
+ icon: IconKeySchema
21549
21592
  });
21550
- method(zod.z.object({}), zod.z.array(TargetKindSchema)), method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }), method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }), method(zod.z.object({
21551
- kind: zod.z.string(),
21552
- config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
21553
- /**
21554
- * When editing / duplicating, the blank password field never round-trips
21555
- * the stored token. Pass the source target id so the provider can restore
21556
- * secrets server-side before dialling (same merge as upsert).
21557
- */
21558
- targetId: zod.z.string().optional()
21559
- }), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }), method(zod.z.object({
21560
- kind: zod.z.string(),
21561
- config: zod.z.record(zod.z.string(), zod.z.unknown()),
21562
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
21563
- targetId: zod.z.string().optional()
21564
- }), ProbeTargetConfigResultSchema, {
21565
- kind: "mutation",
21566
- auth: "admin"
21567
- }), method(zod.z.object({
21568
- targetId: zod.z.string(),
21569
- notification: NotificationSchema
21570
- }), SendResultSchema, {
21571
- kind: "mutation",
21572
- auth: "admin"
21573
- }), method(zod.z.object({
21574
- targetId: zod.z.string(),
21575
- sample: NotificationSchema.optional()
21576
- }), TestResultSchema, {
21577
- kind: "mutation",
21578
- auth: "admin"
21579
- }), method(zod.z.object({
21580
- target: TargetSchema,
21581
- /**
21582
- * Duplicate flow: create a NEW target id while copying secrets from an
21583
- * existing row. The UI never holds the stored token; blank / redacted
21584
- * password fields are restored from this source (D634 still applies if
21585
- * destinations diverge without a re-typed secret).
21586
- */
21587
- cloneSecretsFrom: zod.z.string().optional()
21588
- }), TargetSchema, {
21589
- kind: "mutation",
21590
- auth: "admin"
21591
- }), method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
21592
- kind: "mutation",
21593
- auth: "admin"
21594
- }), method(zod.z.object({
21595
- targetId: zod.z.string(),
21596
- enabled: zod.z.boolean()
21597
- }), zod.z.void(), {
21598
- kind: "mutation",
21599
- auth: "admin"
21593
+ /**
21594
+ * The join key for a location label: trimmed, lower-cased. The ONE place this
21595
+ * derivation lives — a caller that re-spells it is a rename away from an
21596
+ * orphaned icon.
21597
+ */
21598
+ function locationIconKey(label) {
21599
+ return label.trim().toLowerCase();
21600
+ }
21601
+ zod.z.object({
21602
+ locationKey: zod.z.string(),
21603
+ description: zod.z.string()
21600
21604
  });
21601
21605
  //#endregion
21602
21606
  //#region src/catalogs/audio-classmap.ts
@@ -25798,7 +25802,8 @@ var TopologyNodeSchema = zod.z.object({
25798
25802
  isHub: zod.z.boolean(),
25799
25803
  isOnline: zod.z.boolean(),
25800
25804
  cpuPercent: zod.z.number(),
25801
- memoryPercent: zod.z.number(),
25805
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
25806
+ memoryPercent: zod.z.number().nullable(),
25802
25807
  uptime: zod.z.number(),
25803
25808
  lastSeen: zod.z.string(),
25804
25809
  localIps: zod.z.array(zod.z.string()).readonly(),
@@ -27017,6 +27022,14 @@ function createDeviceProxy(api, binding, opts) {
27017
27022
  setVolume: (input) => dispatch("pet-feeder", "petFeeder", "setVolume", "mutation", input),
27018
27023
  getStatus: (input) => dispatch("pet-feeder", "petFeeder", "getStatus", "query", input)
27019
27024
  };
27025
+ const petFountainInterface = {
27026
+ setPump: (input) => dispatch("pet-fountain", "petFountain", "setPump", "mutation", input),
27027
+ setPumpInterval: (input) => dispatch("pet-fountain", "petFountain", "setPumpInterval", "mutation", input),
27028
+ setPumpOnMinutes: (input) => dispatch("pet-fountain", "petFountain", "setPumpOnMinutes", "mutation", input),
27029
+ setPumpOffMinutes: (input) => dispatch("pet-fountain", "petFountain", "setPumpOffMinutes", "mutation", input),
27030
+ resetDailyConsumption: (input) => dispatch("pet-fountain", "petFountain", "resetDailyConsumption", "mutation", input),
27031
+ getStatus: (input) => dispatch("pet-fountain", "petFountain", "getStatus", "query", input)
27032
+ };
27020
27033
  const pipelineAnalyticsInterface = {
27021
27034
  getActiveTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getActiveTracks", "query", input),
27022
27035
  getTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrack", "query", input),
@@ -27485,6 +27498,7 @@ function createDeviceProxy(api, binding, opts) {
27485
27498
  notifier: createSliceHandle(stateSource, binding.deviceId, "notifier"),
27486
27499
  numericSensor: createSliceHandle(stateSource, binding.deviceId, "numeric-sensor"),
27487
27500
  petFeeder: createSliceHandle(stateSource, binding.deviceId, "pet-feeder"),
27501
+ petFountain: createSliceHandle(stateSource, binding.deviceId, "pet-fountain"),
27488
27502
  powerMeter: createSliceHandle(stateSource, binding.deviceId, "power-meter"),
27489
27503
  presence: createSliceHandle(stateSource, binding.deviceId, "presence"),
27490
27504
  pressureSensor: createSliceHandle(stateSource, binding.deviceId, "pressure-sensor"),
@@ -27575,6 +27589,7 @@ function createDeviceProxy(api, binding, opts) {
27575
27589
  ...bound("numeric-sensor") ? { numericSensor: numericSensorInterface } : {},
27576
27590
  ...bound("osd") ? { osd: osdInterface } : {},
27577
27591
  ...bound("pet-feeder") ? { petFeeder: petFeederInterface } : {},
27592
+ ...bound("pet-fountain") ? { petFountain: petFountainInterface } : {},
27578
27593
  ...bound("pipeline-analytics") ? { pipelineAnalytics: pipelineAnalyticsInterface } : {},
27579
27594
  ...bound("power-meter") ? { powerMeter: powerMeterInterface } : {},
27580
27595
  ...bound("presence") ? { presence: presenceInterface } : {},
@@ -28501,7 +28516,11 @@ function createSystemProxy(api) {
28501
28516
  listSupportedDeviceKinds: (input) => dispatch("deviceExport", "listSupportedDeviceKinds", "query", input),
28502
28517
  listExposedDevices: (input) => dispatch("deviceExport", "listExposedDevices", "query", input),
28503
28518
  exposeDevice: (input) => dispatch("deviceExport", "exposeDevice", "mutation", input),
28504
- unexposeDevice: (input) => dispatch("deviceExport", "unexposeDevice", "mutation", input)
28519
+ unexposeDevice: (input) => dispatch("deviceExport", "unexposeDevice", "mutation", input),
28520
+ listSupportedEntityKinds: (input) => dispatch("deviceExport", "listSupportedEntityKinds", "query", input),
28521
+ listExposedEntities: (input) => dispatch("deviceExport", "listExposedEntities", "query", input),
28522
+ exposeEntity: (input) => dispatch("deviceExport", "exposeEntity", "mutation", input),
28523
+ unexposeEntity: (input) => dispatch("deviceExport", "unexposeEntity", "mutation", input)
28505
28524
  },
28506
28525
  deviceHistory: {
28507
28526
  listNodeSeries: (input) => dispatch("deviceHistory", "listNodeSeries", "query", input),
@@ -28951,7 +28970,9 @@ function createSystemProxy(api) {
28951
28970
  checkServerUpdate: (input) => dispatch("serverManagement", "checkServerUpdate", "mutation", input),
28952
28971
  applyServerUpdate: (input) => dispatch("serverManagement", "applyServerUpdate", "mutation", input),
28953
28972
  rollbackServerUpdate: (input) => dispatch("serverManagement", "rollbackServerUpdate", "mutation", input),
28954
- restartServer: (input) => dispatch("serverManagement", "restartServer", "mutation", input)
28973
+ restartServer: (input) => dispatch("serverManagement", "restartServer", "mutation", input),
28974
+ getLaunchOverrides: (input) => dispatch("serverManagement", "getLaunchOverrides", "query", input),
28975
+ setLaunchOverrides: (input) => dispatch("serverManagement", "setLaunchOverrides", "mutation", input)
28955
28976
  },
28956
28977
  settingsStore: {
28957
28978
  get: (input) => dispatch("settingsStore", "get", "query", input),