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