@camstack/addon-notifiers 1.2.20 → 1.2.21

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 (3) hide show
  1. package/dist/addon.js +177 -15
  2. package/dist/addon.mjs +177 -15
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -13111,11 +13111,33 @@ var NotificationFormatSchema = _enum([
13111
13111
  * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13112
13112
  * renderer's icon set; "acknowledge" survives an adapter that draws it
13113
13113
  * differently.
13114
+ *
13115
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13116
+ *
13117
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13118
+ * the whole set onto its own renderer's vocabulary through a
13119
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13120
+ * fallback, so adding a member here fails every adapter's build until someone
13121
+ * decides its glyph, which is the only place that decision can be made
13122
+ * honestly.
13123
+ *
13124
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13125
+ * `disarm` straight through; iOS feeds that string to
13126
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13127
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13128
+ * mapping, and "the field is documented" is not "the value renders".
13129
+ *
13130
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13131
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13132
+ * so an addon that emits a token the running hub does not know does not lose an
13133
+ * icon — its whole `send` fails Zod validation and the notification never
13134
+ * arrives. Never emit a new token from an addon before the train carrying it.
13114
13135
  */
13115
13136
  var NotificationActionIconSchema = _enum([
13116
13137
  "acknowledge",
13117
13138
  "dismiss",
13118
13139
  "silence",
13140
+ "snooze",
13119
13141
  "view",
13120
13142
  "play",
13121
13143
  "open",
@@ -13123,9 +13145,13 @@ var NotificationActionIconSchema = _enum([
13123
13145
  "lock",
13124
13146
  "unlock",
13125
13147
  "arm",
13148
+ "arm-home",
13149
+ "arm-away",
13150
+ "arm-night",
13126
13151
  "disarm",
13127
13152
  "light",
13128
- "alert"
13153
+ "alert",
13154
+ "camera"
13129
13155
  ]);
13130
13156
  /** A single tap-through action button. */
13131
13157
  var NotificationActionSchema = object({
@@ -13339,6 +13365,24 @@ var notificationOutputCapability = {
13339
13365
  }), _void(), { kind: "mutation" })
13340
13366
  }
13341
13367
  };
13368
+ new Set([
13369
+ {
13370
+ id: "person",
13371
+ name: "Person"
13372
+ },
13373
+ {
13374
+ id: "vehicle",
13375
+ name: "Vehicle"
13376
+ },
13377
+ {
13378
+ id: "animal",
13379
+ name: "Animal"
13380
+ },
13381
+ {
13382
+ id: "package",
13383
+ name: "Package"
13384
+ }
13385
+ ].map((l) => l.id));
13342
13386
  var COCO_TO_MACRO = {
13343
13387
  mapping: {
13344
13388
  person: "person",
@@ -14136,6 +14180,8 @@ var NcSystemEventKindSchema = _enum([
14136
14180
  "alarm-triggered",
14137
14181
  "alarm-armed",
14138
14182
  "alarm-disarmed",
14183
+ "alarm-arming",
14184
+ "alarm-arm-refused",
14139
14185
  "camera-online",
14140
14186
  "camera-offline",
14141
14187
  "camera-disabled",
@@ -17368,6 +17414,22 @@ var detectionFpsField = {
17368
17414
  default: 10,
17369
17415
  step: 1
17370
17416
  };
17417
+ /**
17418
+ * The occupancy re-check interval. DEFAULT 300 s (2026-08-13 — was 30 s).
17419
+ *
17420
+ * The recheck is now on by default (a parked car is invisible to occupancy
17421
+ * rules until the stationary registry has been rebuilt by motion, which after a
17422
+ * restart may be never on a quiet camera). Each cycle re-subscribes a detection
17423
+ * session — an RTSP re-dial — so the switch is only affordable at a WIDE
17424
+ * interval: 300 s is ~12 re-dials an hour per camera, against 120 at the old
17425
+ * 30 s. A parked car is therefore counted within 5 minutes of a restart.
17426
+ *
17427
+ * Why not wider: `max` is 300 and raising it is TRAIN-BOUND, not addon-bound —
17428
+ * the host validates `attachCamera` against ITS copy of this schema, so a
17429
+ * runner asked for 600 would be rejected by the hub until a `@camstack/server`
17430
+ * carrying the wider bound is installed everywhere. 300 is the widest value
17431
+ * that ships with an addon deploy.
17432
+ */
17371
17433
  var occupancyRecheckSecField = {
17372
17434
  min: 0,
17373
17435
  max: 300,
@@ -17569,15 +17631,21 @@ var RunnerCameraConfigSchema = object({
17569
17631
  */
17570
17632
  onboardMotionDrivesAnalyzer: boolean().default(true),
17571
17633
  /**
17572
- * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
17573
- * never arms the periodic recheck timer, regardless of `occupancyRecheckSec`
17574
- * this is off by default because the recheck re-subscribes a detection session
17575
- * every N seconds while `watching`, a major source of pull-decoder re-dial
17576
- * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
17634
+ * Master toggle for the occupancy re-check. When `false` the runner never arms
17635
+ * the periodic recheck timer, regardless of `occupancyRecheckSec`; the
17577
17636
  * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
17578
17637
  * (and only render) when this is enabled.
17579
- */
17580
- occupancyRecheckEnabled: boolean().default(false),
17638
+ *
17639
+ * DEFAULT `true` since 2026-08-13 (was `false`). It was off because the
17640
+ * recheck re-subscribes a detection session every N seconds while `watching`
17641
+ * — each cycle creates+tears a session ⇒ an RTSP re-dial ⇒ latency, a major
17642
+ * pull-decoder churn source. What that bought was a blind spot: a STATIONARY
17643
+ * object is counted only while the stationary registry holds it, and the
17644
+ * registry rebuilds from motion, so after a restart a parked car was invisible
17645
+ * to every occupancy rule until something moved in front of it. The churn is
17646
+ * now paid on the interval instead — see `occupancyRecheckSecField`.
17647
+ */
17648
+ occupancyRecheckEnabled: boolean().default(true),
17581
17649
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
17582
17650
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
17583
17651
  /**
@@ -32433,6 +32501,21 @@ var ntfyAdapter = {
32433
32501
  max: 1
32434
32502
  },
32435
32503
  actions: MAX_ACTIONS$1,
32504
+ /**
32505
+ * ntfy has NO per-action icon, and this says so out loud.
32506
+ *
32507
+ * Its action object is `{action, label, url, clear, headers, body}` and the
32508
+ * `X-Actions` header form is `action, label, url[, extras]` — there is no
32509
+ * icon field in either, and the `icon` ntfy does have is the NOTIFICATION's
32510
+ * icon (an attachment URL, handled above), not a per-button one.
32511
+ *
32512
+ * Left EXPLICIT rather than absent: absent and `false` behave identically
32513
+ * (the degrade engine reads a missing value as unsupported), but only one
32514
+ * of them proves somebody checked. The degrade engine then strips the icon
32515
+ * centrally and puts `actionIcons` in `renderedAs.dropped`, so the loss is
32516
+ * reported instead of being discovered on a phone.
32517
+ */
32518
+ actionIcons: false,
32436
32519
  levels: [
32437
32520
  {
32438
32521
  id: "min",
@@ -33616,6 +33699,82 @@ function userIdsFrom(config) {
33616
33699
  return [];
33617
33700
  }
33618
33701
  /**
33702
+ * Canonical icon token → the string Zentik's client actually renders.
33703
+ *
33704
+ * ── EVIDENCE (read from the Zentik sources, not guessed) ────────────
33705
+ * `NotificationActionDto.icon` is `@IsOptional() @IsString()` — the BACKEND
33706
+ * validates nothing, so a wrong value is accepted, stored and delivered. The
33707
+ * rendering happens on the client, and both iOS paths do the same thing
33708
+ * (`ZentikNotificationService/NotificationService.swift`,
33709
+ * `ZentikNotificationContentExtension/NotificationViewController.swift`):
33710
+ *
33711
+ * let actualIconName = iconName.hasPrefix("sfsymbols:")
33712
+ * ? String(iconName.dropFirst("sfsymbols:".count)) : iconName
33713
+ * icon = UNNotificationActionIcon(systemImageName: actualIconName)
33714
+ *
33715
+ * So the value is an **SF Symbol name**, with an optional `sfsymbols:` prefix
33716
+ * that is stripped. Zentik's own UI confirms the convention: its icon field
33717
+ * placeholder is "e.g., house.fill, heart, gear" and its picker writes
33718
+ * `sfsymbols:${iconName}`.
33719
+ *
33720
+ * `UNNotificationActionIcon(systemImageName:)` with a name iOS does not know
33721
+ * renders NOTHING — silently. That is the whole failure: we sent `silence`,
33722
+ * `arm` and `disarm`, none of which are SF Symbols, and every button arrived
33723
+ * blank with nothing anywhere saying why.
33724
+ *
33725
+ * The prefix is emitted explicitly even though bare names work, because the
33726
+ * bare form is indistinguishable from an un-translated intent token in a
33727
+ * captured payload — and telling those two apart is what cost the time.
33728
+ *
33729
+ * A `Record` over the closed enum, NOT a lookup with a fallback: adding a
33730
+ * vocabulary member without deciding its glyph fails the build here.
33731
+ *
33732
+ * Android: Zentik's Android client does not render per-action icons at all
33733
+ * (an Android notification action needs a bundled drawable, not a name), so
33734
+ * this is an iOS/watchOS enrichment and a no-op elsewhere — which is a
33735
+ * cosmetic loss, not a wrong glyph.
33736
+ */
33737
+ var ZENTIK_ACTION_ICONS = {
33738
+ acknowledge: "checkmark.circle",
33739
+ dismiss: "xmark.circle",
33740
+ silence: "bell.slash",
33741
+ snooze: "zzz",
33742
+ view: "eye",
33743
+ play: "play.fill",
33744
+ open: "door.left.hand.open",
33745
+ close: "door.left.hand.closed",
33746
+ lock: "lock.fill",
33747
+ unlock: "lock.open.fill",
33748
+ arm: "shield.fill",
33749
+ "arm-home": "house.fill",
33750
+ "arm-away": "figure.walk",
33751
+ "arm-night": "moon.fill",
33752
+ disarm: "shield.slash.fill",
33753
+ light: "lightbulb.fill",
33754
+ alert: "exclamationmark.triangle.fill",
33755
+ camera: "video.fill"
33756
+ };
33757
+ /** The `sfsymbols:` prefix Zentik's clients strip before resolving the symbol. */
33758
+ var SFSYMBOLS_PREFIX = "sfsymbols:";
33759
+ /**
33760
+ * The Zentik value for one icon token, or `undefined` when there is none.
33761
+ *
33762
+ * Typed on `string`, not on `NotificationActionIcon`, because that is what this
33763
+ * boundary actually receives: an addon deployed against a hub whose vocabulary
33764
+ * moved on hands over a token this build has never heard of. Omitting it is the
33765
+ * honest answer — forwarding it raw would put a string iOS ignores on the wire
33766
+ * and produce exactly the blank button this map exists to fix.
33767
+ *
33768
+ * `Object.hasOwn` and not a truthiness check: an inherited `toString` must not
33769
+ * read as a mapping.
33770
+ */
33771
+ function zentikActionIcon(icon) {
33772
+ const table = ZENTIK_ACTION_ICONS;
33773
+ if (!Object.hasOwn(ZENTIK_ACTION_ICONS, icon)) return void 0;
33774
+ const symbol = table[icon];
33775
+ return symbol === void 0 ? void 0 : `${SFSYMBOLS_PREFIX}${symbol}`;
33776
+ }
33777
+ /**
33619
33778
  * How a `BACKGROUND_CALL` names its request: `METHOD::url`.
33620
33779
  *
33621
33780
  * The whole reason the type exists here: a "silence this for 30 minutes"
@@ -33627,9 +33786,10 @@ var BACKGROUND_CALL_PREFIX = "POST::";
33627
33786
  function actionsFor(prepared) {
33628
33787
  const out = [];
33629
33788
  for (const action of prepared.actions.slice(0, MAX_ACTIONS)) {
33789
+ const symbol = action.icon === void 0 ? void 0 : zentikActionIcon(action.icon);
33630
33790
  const decoration = {
33631
33791
  ...action.destructive !== void 0 ? { destructive: action.destructive } : {},
33632
- ...action.icon !== void 0 ? { icon: action.icon } : {}
33792
+ ...symbol !== void 0 ? { icon: symbol } : {}
33633
33793
  };
33634
33794
  if (action.url === void 0 || action.url.length === 0) {
33635
33795
  out.push({
@@ -33821,12 +33981,14 @@ var zentikAdapter = {
33821
33981
  format: ["text"],
33822
33982
  clickUrl: true,
33823
33983
  /**
33824
- * `NotificationActionDto.icon` is a documented field, so the engine hands
33825
- * the icon over instead of stripping it centrally. What Zentik's client
33826
- * DRAWS for a given name is not documented so the intent name travels
33827
- * verbatim, exactly as the webhook kind forwards it, and a name the app
33828
- * does not know renders as no icon rather than as a wrong one. This is a
33829
- * pass-through claim, not a rendering claim.
33984
+ * A RENDERING claim, which it was not before.
33985
+ *
33986
+ * This used to say the intent name "travels verbatim a name the app does
33987
+ * not know renders as no icon rather than as a wrong one" — true, and it
33988
+ * described a feature that never once drew an icon, because NO intent name
33989
+ * is an SF Symbol. The engine handed the icon over and the adapter put it
33990
+ * on the wire untranslated. `ZENTIK_ACTION_ICONS` is what makes this claim
33991
+ * honest; keep it exhaustive or this reverts to a pass-through that lies.
33830
33992
  */
33831
33993
  actionIcons: true,
33832
33994
  sound: true,
package/dist/addon.mjs CHANGED
@@ -13084,11 +13084,33 @@ var NotificationFormatSchema = _enum([
13084
13084
  * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13085
13085
  * renderer's icon set; "acknowledge" survives an adapter that draws it
13086
13086
  * differently.
13087
+ *
13088
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13089
+ *
13090
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13091
+ * the whole set onto its own renderer's vocabulary through a
13092
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13093
+ * fallback, so adding a member here fails every adapter's build until someone
13094
+ * decides its glyph, which is the only place that decision can be made
13095
+ * honestly.
13096
+ *
13097
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13098
+ * `disarm` straight through; iOS feeds that string to
13099
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13100
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13101
+ * mapping, and "the field is documented" is not "the value renders".
13102
+ *
13103
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13104
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13105
+ * so an addon that emits a token the running hub does not know does not lose an
13106
+ * icon — its whole `send` fails Zod validation and the notification never
13107
+ * arrives. Never emit a new token from an addon before the train carrying it.
13087
13108
  */
13088
13109
  var NotificationActionIconSchema = _enum([
13089
13110
  "acknowledge",
13090
13111
  "dismiss",
13091
13112
  "silence",
13113
+ "snooze",
13092
13114
  "view",
13093
13115
  "play",
13094
13116
  "open",
@@ -13096,9 +13118,13 @@ var NotificationActionIconSchema = _enum([
13096
13118
  "lock",
13097
13119
  "unlock",
13098
13120
  "arm",
13121
+ "arm-home",
13122
+ "arm-away",
13123
+ "arm-night",
13099
13124
  "disarm",
13100
13125
  "light",
13101
- "alert"
13126
+ "alert",
13127
+ "camera"
13102
13128
  ]);
13103
13129
  /** A single tap-through action button. */
13104
13130
  var NotificationActionSchema = object({
@@ -13312,6 +13338,24 @@ var notificationOutputCapability = {
13312
13338
  }), _void(), { kind: "mutation" })
13313
13339
  }
13314
13340
  };
13341
+ new Set([
13342
+ {
13343
+ id: "person",
13344
+ name: "Person"
13345
+ },
13346
+ {
13347
+ id: "vehicle",
13348
+ name: "Vehicle"
13349
+ },
13350
+ {
13351
+ id: "animal",
13352
+ name: "Animal"
13353
+ },
13354
+ {
13355
+ id: "package",
13356
+ name: "Package"
13357
+ }
13358
+ ].map((l) => l.id));
13315
13359
  var COCO_TO_MACRO = {
13316
13360
  mapping: {
13317
13361
  person: "person",
@@ -14109,6 +14153,8 @@ var NcSystemEventKindSchema = _enum([
14109
14153
  "alarm-triggered",
14110
14154
  "alarm-armed",
14111
14155
  "alarm-disarmed",
14156
+ "alarm-arming",
14157
+ "alarm-arm-refused",
14112
14158
  "camera-online",
14113
14159
  "camera-offline",
14114
14160
  "camera-disabled",
@@ -17341,6 +17387,22 @@ var detectionFpsField = {
17341
17387
  default: 10,
17342
17388
  step: 1
17343
17389
  };
17390
+ /**
17391
+ * The occupancy re-check interval. DEFAULT 300 s (2026-08-13 — was 30 s).
17392
+ *
17393
+ * The recheck is now on by default (a parked car is invisible to occupancy
17394
+ * rules until the stationary registry has been rebuilt by motion, which after a
17395
+ * restart may be never on a quiet camera). Each cycle re-subscribes a detection
17396
+ * session — an RTSP re-dial — so the switch is only affordable at a WIDE
17397
+ * interval: 300 s is ~12 re-dials an hour per camera, against 120 at the old
17398
+ * 30 s. A parked car is therefore counted within 5 minutes of a restart.
17399
+ *
17400
+ * Why not wider: `max` is 300 and raising it is TRAIN-BOUND, not addon-bound —
17401
+ * the host validates `attachCamera` against ITS copy of this schema, so a
17402
+ * runner asked for 600 would be rejected by the hub until a `@camstack/server`
17403
+ * carrying the wider bound is installed everywhere. 300 is the widest value
17404
+ * that ships with an addon deploy.
17405
+ */
17344
17406
  var occupancyRecheckSecField = {
17345
17407
  min: 0,
17346
17408
  max: 300,
@@ -17542,15 +17604,21 @@ var RunnerCameraConfigSchema = object({
17542
17604
  */
17543
17605
  onboardMotionDrivesAnalyzer: boolean().default(true),
17544
17606
  /**
17545
- * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
17546
- * never arms the periodic recheck timer, regardless of `occupancyRecheckSec`
17547
- * this is off by default because the recheck re-subscribes a detection session
17548
- * every N seconds while `watching`, a major source of pull-decoder re-dial
17549
- * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
17607
+ * Master toggle for the occupancy re-check. When `false` the runner never arms
17608
+ * the periodic recheck timer, regardless of `occupancyRecheckSec`; the
17550
17609
  * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
17551
17610
  * (and only render) when this is enabled.
17552
- */
17553
- occupancyRecheckEnabled: boolean().default(false),
17611
+ *
17612
+ * DEFAULT `true` since 2026-08-13 (was `false`). It was off because the
17613
+ * recheck re-subscribes a detection session every N seconds while `watching`
17614
+ * — each cycle creates+tears a session ⇒ an RTSP re-dial ⇒ latency, a major
17615
+ * pull-decoder churn source. What that bought was a blind spot: a STATIONARY
17616
+ * object is counted only while the stationary registry holds it, and the
17617
+ * registry rebuilds from motion, so after a restart a parked car was invisible
17618
+ * to every occupancy rule until something moved in front of it. The churn is
17619
+ * now paid on the interval instead — see `occupancyRecheckSecField`.
17620
+ */
17621
+ occupancyRecheckEnabled: boolean().default(true),
17554
17622
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
17555
17623
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
17556
17624
  /**
@@ -32406,6 +32474,21 @@ var ntfyAdapter = {
32406
32474
  max: 1
32407
32475
  },
32408
32476
  actions: MAX_ACTIONS$1,
32477
+ /**
32478
+ * ntfy has NO per-action icon, and this says so out loud.
32479
+ *
32480
+ * Its action object is `{action, label, url, clear, headers, body}` and the
32481
+ * `X-Actions` header form is `action, label, url[, extras]` — there is no
32482
+ * icon field in either, and the `icon` ntfy does have is the NOTIFICATION's
32483
+ * icon (an attachment URL, handled above), not a per-button one.
32484
+ *
32485
+ * Left EXPLICIT rather than absent: absent and `false` behave identically
32486
+ * (the degrade engine reads a missing value as unsupported), but only one
32487
+ * of them proves somebody checked. The degrade engine then strips the icon
32488
+ * centrally and puts `actionIcons` in `renderedAs.dropped`, so the loss is
32489
+ * reported instead of being discovered on a phone.
32490
+ */
32491
+ actionIcons: false,
32409
32492
  levels: [
32410
32493
  {
32411
32494
  id: "min",
@@ -33589,6 +33672,82 @@ function userIdsFrom(config) {
33589
33672
  return [];
33590
33673
  }
33591
33674
  /**
33675
+ * Canonical icon token → the string Zentik's client actually renders.
33676
+ *
33677
+ * ── EVIDENCE (read from the Zentik sources, not guessed) ────────────
33678
+ * `NotificationActionDto.icon` is `@IsOptional() @IsString()` — the BACKEND
33679
+ * validates nothing, so a wrong value is accepted, stored and delivered. The
33680
+ * rendering happens on the client, and both iOS paths do the same thing
33681
+ * (`ZentikNotificationService/NotificationService.swift`,
33682
+ * `ZentikNotificationContentExtension/NotificationViewController.swift`):
33683
+ *
33684
+ * let actualIconName = iconName.hasPrefix("sfsymbols:")
33685
+ * ? String(iconName.dropFirst("sfsymbols:".count)) : iconName
33686
+ * icon = UNNotificationActionIcon(systemImageName: actualIconName)
33687
+ *
33688
+ * So the value is an **SF Symbol name**, with an optional `sfsymbols:` prefix
33689
+ * that is stripped. Zentik's own UI confirms the convention: its icon field
33690
+ * placeholder is "e.g., house.fill, heart, gear" and its picker writes
33691
+ * `sfsymbols:${iconName}`.
33692
+ *
33693
+ * `UNNotificationActionIcon(systemImageName:)` with a name iOS does not know
33694
+ * renders NOTHING — silently. That is the whole failure: we sent `silence`,
33695
+ * `arm` and `disarm`, none of which are SF Symbols, and every button arrived
33696
+ * blank with nothing anywhere saying why.
33697
+ *
33698
+ * The prefix is emitted explicitly even though bare names work, because the
33699
+ * bare form is indistinguishable from an un-translated intent token in a
33700
+ * captured payload — and telling those two apart is what cost the time.
33701
+ *
33702
+ * A `Record` over the closed enum, NOT a lookup with a fallback: adding a
33703
+ * vocabulary member without deciding its glyph fails the build here.
33704
+ *
33705
+ * Android: Zentik's Android client does not render per-action icons at all
33706
+ * (an Android notification action needs a bundled drawable, not a name), so
33707
+ * this is an iOS/watchOS enrichment and a no-op elsewhere — which is a
33708
+ * cosmetic loss, not a wrong glyph.
33709
+ */
33710
+ var ZENTIK_ACTION_ICONS = {
33711
+ acknowledge: "checkmark.circle",
33712
+ dismiss: "xmark.circle",
33713
+ silence: "bell.slash",
33714
+ snooze: "zzz",
33715
+ view: "eye",
33716
+ play: "play.fill",
33717
+ open: "door.left.hand.open",
33718
+ close: "door.left.hand.closed",
33719
+ lock: "lock.fill",
33720
+ unlock: "lock.open.fill",
33721
+ arm: "shield.fill",
33722
+ "arm-home": "house.fill",
33723
+ "arm-away": "figure.walk",
33724
+ "arm-night": "moon.fill",
33725
+ disarm: "shield.slash.fill",
33726
+ light: "lightbulb.fill",
33727
+ alert: "exclamationmark.triangle.fill",
33728
+ camera: "video.fill"
33729
+ };
33730
+ /** The `sfsymbols:` prefix Zentik's clients strip before resolving the symbol. */
33731
+ var SFSYMBOLS_PREFIX = "sfsymbols:";
33732
+ /**
33733
+ * The Zentik value for one icon token, or `undefined` when there is none.
33734
+ *
33735
+ * Typed on `string`, not on `NotificationActionIcon`, because that is what this
33736
+ * boundary actually receives: an addon deployed against a hub whose vocabulary
33737
+ * moved on hands over a token this build has never heard of. Omitting it is the
33738
+ * honest answer — forwarding it raw would put a string iOS ignores on the wire
33739
+ * and produce exactly the blank button this map exists to fix.
33740
+ *
33741
+ * `Object.hasOwn` and not a truthiness check: an inherited `toString` must not
33742
+ * read as a mapping.
33743
+ */
33744
+ function zentikActionIcon(icon) {
33745
+ const table = ZENTIK_ACTION_ICONS;
33746
+ if (!Object.hasOwn(ZENTIK_ACTION_ICONS, icon)) return void 0;
33747
+ const symbol = table[icon];
33748
+ return symbol === void 0 ? void 0 : `${SFSYMBOLS_PREFIX}${symbol}`;
33749
+ }
33750
+ /**
33592
33751
  * How a `BACKGROUND_CALL` names its request: `METHOD::url`.
33593
33752
  *
33594
33753
  * The whole reason the type exists here: a "silence this for 30 minutes"
@@ -33600,9 +33759,10 @@ var BACKGROUND_CALL_PREFIX = "POST::";
33600
33759
  function actionsFor(prepared) {
33601
33760
  const out = [];
33602
33761
  for (const action of prepared.actions.slice(0, MAX_ACTIONS)) {
33762
+ const symbol = action.icon === void 0 ? void 0 : zentikActionIcon(action.icon);
33603
33763
  const decoration = {
33604
33764
  ...action.destructive !== void 0 ? { destructive: action.destructive } : {},
33605
- ...action.icon !== void 0 ? { icon: action.icon } : {}
33765
+ ...symbol !== void 0 ? { icon: symbol } : {}
33606
33766
  };
33607
33767
  if (action.url === void 0 || action.url.length === 0) {
33608
33768
  out.push({
@@ -33794,12 +33954,14 @@ var zentikAdapter = {
33794
33954
  format: ["text"],
33795
33955
  clickUrl: true,
33796
33956
  /**
33797
- * `NotificationActionDto.icon` is a documented field, so the engine hands
33798
- * the icon over instead of stripping it centrally. What Zentik's client
33799
- * DRAWS for a given name is not documented so the intent name travels
33800
- * verbatim, exactly as the webhook kind forwards it, and a name the app
33801
- * does not know renders as no icon rather than as a wrong one. This is a
33802
- * pass-through claim, not a rendering claim.
33957
+ * A RENDERING claim, which it was not before.
33958
+ *
33959
+ * This used to say the intent name "travels verbatim a name the app does
33960
+ * not know renders as no icon rather than as a wrong one" — true, and it
33961
+ * described a feature that never once drew an icon, because NO intent name
33962
+ * is an SF Symbol. The engine handed the icon over and the adapter put it
33963
+ * on the wire untranslated. `ZENTIK_ACTION_ICONS` is what makes this claim
33964
+ * honest; keep it exhaustive or this reverts to a pass-through that lies.
33803
33965
  */
33804
33966
  actionIcons: true,
33805
33967
  sound: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.20",
3
+ "version": "1.2.21",
4
4
  "description": "System notifiers addon for CamStack — a `notification-output` collection provider hosting per-kind notifier adapters (ntfy, pushover, gotify, telegram, discord, webhook, zentik).",
5
5
  "keywords": [
6
6
  "camstack",