@camstack/addon-notifiers 1.2.19 → 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.
- package/dist/addon.js +308 -32
- package/dist/addon.mjs +308 -32
- 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({
|
|
@@ -13141,7 +13167,23 @@ var NotificationActionSchema = object({
|
|
|
13141
13167
|
* else — see `notification-center/action-token.ts` for what that does and
|
|
13142
13168
|
* does not buy.
|
|
13143
13169
|
*/
|
|
13144
|
-
destructive: boolean().optional()
|
|
13170
|
+
destructive: boolean().optional(),
|
|
13171
|
+
/**
|
|
13172
|
+
* How the tap should REACH the url.
|
|
13173
|
+
*
|
|
13174
|
+
* `navigate` (absent, and every button authored before this field) opens it:
|
|
13175
|
+
* the phone leaves the notification and shows whatever the callback returns.
|
|
13176
|
+
* That is right for a button whose answer the operator wants to read.
|
|
13177
|
+
*
|
|
13178
|
+
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
13179
|
+
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
13180
|
+
* an answer to the notification, and being thrown into a browser tab to
|
|
13181
|
+
* confirm it costs more attention than the notification did. A backend that
|
|
13182
|
+
* cannot do a background call renders it as an ordinary link (the adapters
|
|
13183
|
+
* fall back rather than dropping the button), so this is a preference, never
|
|
13184
|
+
* a requirement.
|
|
13185
|
+
*/
|
|
13186
|
+
mode: _enum(["navigate", "background"]).optional()
|
|
13145
13187
|
});
|
|
13146
13188
|
/**
|
|
13147
13189
|
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
@@ -13323,6 +13365,24 @@ var notificationOutputCapability = {
|
|
|
13323
13365
|
}), _void(), { kind: "mutation" })
|
|
13324
13366
|
}
|
|
13325
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));
|
|
13326
13386
|
var COCO_TO_MACRO = {
|
|
13327
13387
|
mapping: {
|
|
13328
13388
|
person: "person",
|
|
@@ -14120,6 +14180,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14120
14180
|
"alarm-triggered",
|
|
14121
14181
|
"alarm-armed",
|
|
14122
14182
|
"alarm-disarmed",
|
|
14183
|
+
"alarm-arming",
|
|
14184
|
+
"alarm-arm-refused",
|
|
14123
14185
|
"camera-online",
|
|
14124
14186
|
"camera-offline",
|
|
14125
14187
|
"camera-disabled",
|
|
@@ -14156,6 +14218,9 @@ var NcSystemEventConditionSchema = object({
|
|
|
14156
14218
|
nodeIds: array(string().min(1)).min(1).optional(),
|
|
14157
14219
|
packageNames: array(string().min(1)).min(1).optional()
|
|
14158
14220
|
});
|
|
14221
|
+
/** Hard ceiling on a window (24h). A snooze that could not expire would be an
|
|
14222
|
+
* outage the operator asked for once and forgot. */
|
|
14223
|
+
var NC_SNOOZE_MAX_MINUTES = 1440;
|
|
14159
14224
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
14160
14225
|
var NcScheduleSchema = object({
|
|
14161
14226
|
windows: array(object({
|
|
@@ -14532,15 +14597,15 @@ var NcConditionsSchema = object({
|
|
|
14532
14597
|
* (an `immediate` rule naming an `audio-*` class, one notification per
|
|
14533
14598
|
* classified sample) stays exactly as it was for rules that already use it.
|
|
14534
14599
|
*
|
|
14535
|
-
*
|
|
14536
|
-
* rather than an
|
|
14537
|
-
* (`camstack/src/data/notification-center.ts`, guarded by
|
|
14538
|
-
* `scripts/check-viewer-condition-mirror.ts`) and its rule editor
|
|
14600
|
+
* In {@link NC_CONDITION_CATALOG} since P2, and the ORDER it got there is the
|
|
14601
|
+
* rule rather than an accident: the viewer mirrors the descriptor enums BY
|
|
14602
|
+
* HAND (`camstack/src/data/notification-center.ts`, guarded by
|
|
14603
|
+
* `scripts/check-viewer-condition-mirror.ts`) and its rule editor strips the
|
|
14539
14604
|
* condition fields it does not know when a rule is saved from the phone.
|
|
14540
14605
|
* Publishing an editor for a condition the app cannot round-trip is how an
|
|
14541
|
-
* operator loses a rule's conditions by opening it — so the
|
|
14542
|
-
*
|
|
14543
|
-
*
|
|
14606
|
+
* operator loses a rule's conditions by opening it — so the viewer mirror
|
|
14607
|
+
* (P3, shipped) went FIRST, and the descriptor an editor renders from
|
|
14608
|
+
* follows here.
|
|
14544
14609
|
*/
|
|
14545
14610
|
audio: NcAudioConditionSchema.optional()
|
|
14546
14611
|
});
|
|
@@ -14776,6 +14841,30 @@ var NcRuleInputSchema = object({
|
|
|
14776
14841
|
*/
|
|
14777
14842
|
snoozeAllowGlobal: boolean().optional(),
|
|
14778
14843
|
/**
|
|
14844
|
+
* The snooze durations THIS rule's notification offers as buttons, in
|
|
14845
|
+
* minutes.
|
|
14846
|
+
*
|
|
14847
|
+
* Three states, and all three are distinct — which is exactly why this is
|
|
14848
|
+
* `.optional()` and never `.default()`. A Zod default does not run on the
|
|
14849
|
+
* addon cap path (three production failures in one day), so a schema default
|
|
14850
|
+
* would collapse the first two:
|
|
14851
|
+
*
|
|
14852
|
+
* | value | meaning |
|
|
14853
|
+
* | --- | --- |
|
|
14854
|
+
* | absent | the operator never said ⇒ {@link NC_DEFAULT_SNOOZE_MINUTES} |
|
|
14855
|
+
* | `[]` | **no snooze buttons on this rule** — the explicit override |
|
|
14856
|
+
* | a list | these choices, de-duplicated and sorted, at most four |
|
|
14857
|
+
*
|
|
14858
|
+
* `.max(4)` because the notifier's own action budget is small (ntfy allows
|
|
14859
|
+
* three buttons in total) and a rule that spent it all on snooze choices
|
|
14860
|
+
* would push its own tap-through actions off the notification.
|
|
14861
|
+
*
|
|
14862
|
+
* An empty list is NOT an alarm exemption: a rule the alarm is about, or
|
|
14863
|
+
* that arms the panel, is exempt automatically and cannot be silenced by a
|
|
14864
|
+
* window from anywhere (D133).
|
|
14865
|
+
*/
|
|
14866
|
+
snoozeOptions: array(number().int().min(1).max(NC_SNOOZE_MAX_MINUTES)).max(4).optional(),
|
|
14867
|
+
/**
|
|
14779
14868
|
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
14780
14869
|
*
|
|
14781
14870
|
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
@@ -14875,6 +14964,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
14875
14964
|
"device",
|
|
14876
14965
|
"package",
|
|
14877
14966
|
"occupancy",
|
|
14967
|
+
"audio",
|
|
14878
14968
|
"system"
|
|
14879
14969
|
]),
|
|
14880
14970
|
label: string(),
|
|
@@ -14893,6 +14983,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
14893
14983
|
"crossingSelect",
|
|
14894
14984
|
"polygonDraw",
|
|
14895
14985
|
"occupancy",
|
|
14986
|
+
"audio",
|
|
14896
14987
|
"deviceState",
|
|
14897
14988
|
"systemEvent"
|
|
14898
14989
|
]),
|
|
@@ -15044,7 +15135,20 @@ var NcSnoozeInputSchema = object({
|
|
|
15044
15135
|
ruleId: string().optional(),
|
|
15045
15136
|
/** Required when `scope: 'device'`. */
|
|
15046
15137
|
deviceId: number().int().optional(),
|
|
15047
|
-
|
|
15138
|
+
/**
|
|
15139
|
+
* Narrow the window to these subject classes — "the cat, not the person".
|
|
15140
|
+
*
|
|
15141
|
+
* ORTHOGONAL to `scope`, deliberately, and absent means EVERY class: that is
|
|
15142
|
+
* what every window authored before this field meant, so no persisted row
|
|
15143
|
+
* changes meaning and no client has to learn anything to keep working.
|
|
15144
|
+
*
|
|
15145
|
+
* It is what makes the window's real key `(deviceId, classes[])` and lets it
|
|
15146
|
+
* cross rules (D133): the operator points at a camera and a kind of thing,
|
|
15147
|
+
* not at whichever of their four rules happened to produce the notification
|
|
15148
|
+
* they are dismissing.
|
|
15149
|
+
*/
|
|
15150
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
15151
|
+
durationMinutes: number().int().min(1).max(NC_SNOOZE_MAX_MINUTES),
|
|
15048
15152
|
/**
|
|
15049
15153
|
* Silence this for EVERY recipient, not just the caller. Permission is
|
|
15050
15154
|
* checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
|
|
@@ -15069,6 +15173,10 @@ var NcSnoozeSchema = object({
|
|
|
15069
15173
|
scope: NcSnoozeScopeSchema,
|
|
15070
15174
|
ruleId: string().optional(),
|
|
15071
15175
|
deviceId: number().int().optional(),
|
|
15176
|
+
/** Subject classes this window covers. ABSENT = every class — see
|
|
15177
|
+
* {@link NcSnoozeInputSchema.shape.classes}. Lives in the JSON blob and has
|
|
15178
|
+
* no SQLite column: nothing queries a window by class. */
|
|
15179
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
15072
15180
|
startedAt: number(),
|
|
15073
15181
|
/** Exclusive: at exactly this instant the snooze is over. Expiry is a
|
|
15074
15182
|
* COMPARISON, not a job — no sweeper can leave the operator silenced. */
|
|
@@ -17306,6 +17414,22 @@ var detectionFpsField = {
|
|
|
17306
17414
|
default: 10,
|
|
17307
17415
|
step: 1
|
|
17308
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
|
+
*/
|
|
17309
17433
|
var occupancyRecheckSecField = {
|
|
17310
17434
|
min: 0,
|
|
17311
17435
|
max: 300,
|
|
@@ -17507,15 +17631,21 @@ var RunnerCameraConfigSchema = object({
|
|
|
17507
17631
|
*/
|
|
17508
17632
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
17509
17633
|
/**
|
|
17510
|
-
* Master toggle for the occupancy re-check. When `false`
|
|
17511
|
-
*
|
|
17512
|
-
* this is off by default because the recheck re-subscribes a detection session
|
|
17513
|
-
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
17514
|
-
* 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
|
|
17515
17636
|
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
17516
17637
|
* (and only render) when this is enabled.
|
|
17517
|
-
|
|
17518
|
-
|
|
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),
|
|
17519
17649
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
17520
17650
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
17521
17651
|
/**
|
|
@@ -30641,6 +30771,7 @@ Object.freeze({
|
|
|
30641
30771
|
"network-access": "ingress",
|
|
30642
30772
|
"smtp-provider": "email"
|
|
30643
30773
|
});
|
|
30774
|
+
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
30644
30775
|
new Set(["devices", "classes"]);
|
|
30645
30776
|
/** Strip a small, safe subset of Markdown down to plain text. */
|
|
30646
30777
|
function markdownToText(md) {
|
|
@@ -32221,11 +32352,24 @@ function nativePriority$1(prepared) {
|
|
|
32221
32352
|
}
|
|
32222
32353
|
function viewActions(prepared) {
|
|
32223
32354
|
const out = [];
|
|
32224
|
-
for (const action of prepared.actions.slice(0, MAX_ACTIONS$1))
|
|
32225
|
-
action
|
|
32226
|
-
|
|
32227
|
-
|
|
32228
|
-
|
|
32355
|
+
for (const action of prepared.actions.slice(0, MAX_ACTIONS$1)) {
|
|
32356
|
+
if (action.url === void 0 || action.url.length === 0) continue;
|
|
32357
|
+
if (action.mode === "background") {
|
|
32358
|
+
out.push({
|
|
32359
|
+
action: "http",
|
|
32360
|
+
label: action.label,
|
|
32361
|
+
url: action.url,
|
|
32362
|
+
method: "POST",
|
|
32363
|
+
clear: true
|
|
32364
|
+
});
|
|
32365
|
+
continue;
|
|
32366
|
+
}
|
|
32367
|
+
out.push({
|
|
32368
|
+
action: "view",
|
|
32369
|
+
label: action.label,
|
|
32370
|
+
url: action.url
|
|
32371
|
+
});
|
|
32372
|
+
}
|
|
32229
32373
|
return out;
|
|
32230
32374
|
}
|
|
32231
32375
|
/** The single attachment (caps.max = 1), if any. */
|
|
@@ -32280,7 +32424,7 @@ function buildUploadRequest(config, prepared, attachment) {
|
|
|
32280
32424
|
if (tags.length > 0) headers["x-tags"] = tags.join(",");
|
|
32281
32425
|
if (prepared.clickUrl !== null) headers["x-click"] = prepared.clickUrl;
|
|
32282
32426
|
if (attachment.name !== void 0) headers["x-filename"] = attachment.name;
|
|
32283
|
-
if (actions.length > 0) headers["x-actions"] = actions.map((a) => `view, ${a.label}, ${a.url}`).join("; ");
|
|
32427
|
+
if (actions.length > 0) headers["x-actions"] = actions.map((a) => a.action === "http" ? `http, ${a.label}, ${a.url}, method=POST, clear=true` : `view, ${a.label}, ${a.url}`).join("; ");
|
|
32284
32428
|
return {
|
|
32285
32429
|
url: `${serverUrl}/${topic}`,
|
|
32286
32430
|
method: "PUT",
|
|
@@ -32357,6 +32501,21 @@ var ntfyAdapter = {
|
|
|
32357
32501
|
max: 1
|
|
32358
32502
|
},
|
|
32359
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,
|
|
32360
32519
|
levels: [
|
|
32361
32520
|
{
|
|
32362
32521
|
id: "min",
|
|
@@ -33539,17 +33698,123 @@ function userIdsFrom(config) {
|
|
|
33539
33698
|
if (typeof raw === "string") return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
33540
33699
|
return [];
|
|
33541
33700
|
}
|
|
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
|
+
/**
|
|
33778
|
+
* How a `BACKGROUND_CALL` names its request: `METHOD::url`.
|
|
33779
|
+
*
|
|
33780
|
+
* The whole reason the type exists here: a "silence this for 30 minutes"
|
|
33781
|
+
* button that opens a browser tab has already cost more attention than the
|
|
33782
|
+
* notification it was dismissing. `BACKGROUND_CALL` fires the callback and
|
|
33783
|
+
* leaves the phone where it is.
|
|
33784
|
+
*/
|
|
33785
|
+
var BACKGROUND_CALL_PREFIX = "POST::";
|
|
33542
33786
|
function actionsFor(prepared) {
|
|
33543
33787
|
const out = [];
|
|
33544
|
-
for (const action of prepared.actions.slice(0, MAX_ACTIONS))
|
|
33545
|
-
|
|
33546
|
-
|
|
33547
|
-
|
|
33548
|
-
|
|
33549
|
-
|
|
33550
|
-
|
|
33551
|
-
|
|
33552
|
-
|
|
33788
|
+
for (const action of prepared.actions.slice(0, MAX_ACTIONS)) {
|
|
33789
|
+
const symbol = action.icon === void 0 ? void 0 : zentikActionIcon(action.icon);
|
|
33790
|
+
const decoration = {
|
|
33791
|
+
...action.destructive !== void 0 ? { destructive: action.destructive } : {},
|
|
33792
|
+
...symbol !== void 0 ? { icon: symbol } : {}
|
|
33793
|
+
};
|
|
33794
|
+
if (action.url === void 0 || action.url.length === 0) {
|
|
33795
|
+
out.push({
|
|
33796
|
+
type: "OPEN_NOTIFICATION",
|
|
33797
|
+
title: action.label,
|
|
33798
|
+
...decoration
|
|
33799
|
+
});
|
|
33800
|
+
continue;
|
|
33801
|
+
}
|
|
33802
|
+
if (action.mode === "background") {
|
|
33803
|
+
out.push({
|
|
33804
|
+
type: "BACKGROUND_CALL",
|
|
33805
|
+
title: action.label,
|
|
33806
|
+
value: `${BACKGROUND_CALL_PREFIX}${action.url}`,
|
|
33807
|
+
...decoration
|
|
33808
|
+
});
|
|
33809
|
+
continue;
|
|
33810
|
+
}
|
|
33811
|
+
out.push({
|
|
33812
|
+
type: "NAVIGATE",
|
|
33813
|
+
title: action.label,
|
|
33814
|
+
value: action.url,
|
|
33815
|
+
...decoration
|
|
33816
|
+
});
|
|
33817
|
+
}
|
|
33553
33818
|
return out;
|
|
33554
33819
|
}
|
|
33555
33820
|
function attachmentsFor(prepared) {
|
|
@@ -33715,6 +33980,17 @@ var zentikAdapter = {
|
|
|
33715
33980
|
],
|
|
33716
33981
|
format: ["text"],
|
|
33717
33982
|
clickUrl: true,
|
|
33983
|
+
/**
|
|
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.
|
|
33992
|
+
*/
|
|
33993
|
+
actionIcons: true,
|
|
33718
33994
|
sound: true,
|
|
33719
33995
|
ttl: true,
|
|
33720
33996
|
bodyMaxLen: BODY_MAX_LEN
|
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({
|
|
@@ -13114,7 +13140,23 @@ var NotificationActionSchema = object({
|
|
|
13114
13140
|
* else — see `notification-center/action-token.ts` for what that does and
|
|
13115
13141
|
* does not buy.
|
|
13116
13142
|
*/
|
|
13117
|
-
destructive: boolean().optional()
|
|
13143
|
+
destructive: boolean().optional(),
|
|
13144
|
+
/**
|
|
13145
|
+
* How the tap should REACH the url.
|
|
13146
|
+
*
|
|
13147
|
+
* `navigate` (absent, and every button authored before this field) opens it:
|
|
13148
|
+
* the phone leaves the notification and shows whatever the callback returns.
|
|
13149
|
+
* That is right for a button whose answer the operator wants to read.
|
|
13150
|
+
*
|
|
13151
|
+
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
13152
|
+
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
13153
|
+
* an answer to the notification, and being thrown into a browser tab to
|
|
13154
|
+
* confirm it costs more attention than the notification did. A backend that
|
|
13155
|
+
* cannot do a background call renders it as an ordinary link (the adapters
|
|
13156
|
+
* fall back rather than dropping the button), so this is a preference, never
|
|
13157
|
+
* a requirement.
|
|
13158
|
+
*/
|
|
13159
|
+
mode: _enum(["navigate", "background"]).optional()
|
|
13118
13160
|
});
|
|
13119
13161
|
/**
|
|
13120
13162
|
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
@@ -13296,6 +13338,24 @@ var notificationOutputCapability = {
|
|
|
13296
13338
|
}), _void(), { kind: "mutation" })
|
|
13297
13339
|
}
|
|
13298
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));
|
|
13299
13359
|
var COCO_TO_MACRO = {
|
|
13300
13360
|
mapping: {
|
|
13301
13361
|
person: "person",
|
|
@@ -14093,6 +14153,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14093
14153
|
"alarm-triggered",
|
|
14094
14154
|
"alarm-armed",
|
|
14095
14155
|
"alarm-disarmed",
|
|
14156
|
+
"alarm-arming",
|
|
14157
|
+
"alarm-arm-refused",
|
|
14096
14158
|
"camera-online",
|
|
14097
14159
|
"camera-offline",
|
|
14098
14160
|
"camera-disabled",
|
|
@@ -14129,6 +14191,9 @@ var NcSystemEventConditionSchema = object({
|
|
|
14129
14191
|
nodeIds: array(string().min(1)).min(1).optional(),
|
|
14130
14192
|
packageNames: array(string().min(1)).min(1).optional()
|
|
14131
14193
|
});
|
|
14194
|
+
/** Hard ceiling on a window (24h). A snooze that could not expire would be an
|
|
14195
|
+
* outage the operator asked for once and forgot. */
|
|
14196
|
+
var NC_SNOOZE_MAX_MINUTES = 1440;
|
|
14132
14197
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
14133
14198
|
var NcScheduleSchema = object({
|
|
14134
14199
|
windows: array(object({
|
|
@@ -14505,15 +14570,15 @@ var NcConditionsSchema = object({
|
|
|
14505
14570
|
* (an `immediate` rule naming an `audio-*` class, one notification per
|
|
14506
14571
|
* classified sample) stays exactly as it was for rules that already use it.
|
|
14507
14572
|
*
|
|
14508
|
-
*
|
|
14509
|
-
* rather than an
|
|
14510
|
-
* (`camstack/src/data/notification-center.ts`, guarded by
|
|
14511
|
-
* `scripts/check-viewer-condition-mirror.ts`) and its rule editor
|
|
14573
|
+
* In {@link NC_CONDITION_CATALOG} since P2, and the ORDER it got there is the
|
|
14574
|
+
* rule rather than an accident: the viewer mirrors the descriptor enums BY
|
|
14575
|
+
* HAND (`camstack/src/data/notification-center.ts`, guarded by
|
|
14576
|
+
* `scripts/check-viewer-condition-mirror.ts`) and its rule editor strips the
|
|
14512
14577
|
* condition fields it does not know when a rule is saved from the phone.
|
|
14513
14578
|
* Publishing an editor for a condition the app cannot round-trip is how an
|
|
14514
|
-
* operator loses a rule's conditions by opening it — so the
|
|
14515
|
-
*
|
|
14516
|
-
*
|
|
14579
|
+
* operator loses a rule's conditions by opening it — so the viewer mirror
|
|
14580
|
+
* (P3, shipped) went FIRST, and the descriptor an editor renders from
|
|
14581
|
+
* follows here.
|
|
14517
14582
|
*/
|
|
14518
14583
|
audio: NcAudioConditionSchema.optional()
|
|
14519
14584
|
});
|
|
@@ -14749,6 +14814,30 @@ var NcRuleInputSchema = object({
|
|
|
14749
14814
|
*/
|
|
14750
14815
|
snoozeAllowGlobal: boolean().optional(),
|
|
14751
14816
|
/**
|
|
14817
|
+
* The snooze durations THIS rule's notification offers as buttons, in
|
|
14818
|
+
* minutes.
|
|
14819
|
+
*
|
|
14820
|
+
* Three states, and all three are distinct — which is exactly why this is
|
|
14821
|
+
* `.optional()` and never `.default()`. A Zod default does not run on the
|
|
14822
|
+
* addon cap path (three production failures in one day), so a schema default
|
|
14823
|
+
* would collapse the first two:
|
|
14824
|
+
*
|
|
14825
|
+
* | value | meaning |
|
|
14826
|
+
* | --- | --- |
|
|
14827
|
+
* | absent | the operator never said ⇒ {@link NC_DEFAULT_SNOOZE_MINUTES} |
|
|
14828
|
+
* | `[]` | **no snooze buttons on this rule** — the explicit override |
|
|
14829
|
+
* | a list | these choices, de-duplicated and sorted, at most four |
|
|
14830
|
+
*
|
|
14831
|
+
* `.max(4)` because the notifier's own action budget is small (ntfy allows
|
|
14832
|
+
* three buttons in total) and a rule that spent it all on snooze choices
|
|
14833
|
+
* would push its own tap-through actions off the notification.
|
|
14834
|
+
*
|
|
14835
|
+
* An empty list is NOT an alarm exemption: a rule the alarm is about, or
|
|
14836
|
+
* that arms the panel, is exempt automatically and cannot be silenced by a
|
|
14837
|
+
* window from anywhere (D133).
|
|
14838
|
+
*/
|
|
14839
|
+
snoozeOptions: array(number().int().min(1).max(NC_SNOOZE_MAX_MINUTES)).max(4).optional(),
|
|
14840
|
+
/**
|
|
14752
14841
|
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
14753
14842
|
*
|
|
14754
14843
|
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
@@ -14848,6 +14937,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
14848
14937
|
"device",
|
|
14849
14938
|
"package",
|
|
14850
14939
|
"occupancy",
|
|
14940
|
+
"audio",
|
|
14851
14941
|
"system"
|
|
14852
14942
|
]),
|
|
14853
14943
|
label: string(),
|
|
@@ -14866,6 +14956,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
14866
14956
|
"crossingSelect",
|
|
14867
14957
|
"polygonDraw",
|
|
14868
14958
|
"occupancy",
|
|
14959
|
+
"audio",
|
|
14869
14960
|
"deviceState",
|
|
14870
14961
|
"systemEvent"
|
|
14871
14962
|
]),
|
|
@@ -15017,7 +15108,20 @@ var NcSnoozeInputSchema = object({
|
|
|
15017
15108
|
ruleId: string().optional(),
|
|
15018
15109
|
/** Required when `scope: 'device'`. */
|
|
15019
15110
|
deviceId: number().int().optional(),
|
|
15020
|
-
|
|
15111
|
+
/**
|
|
15112
|
+
* Narrow the window to these subject classes — "the cat, not the person".
|
|
15113
|
+
*
|
|
15114
|
+
* ORTHOGONAL to `scope`, deliberately, and absent means EVERY class: that is
|
|
15115
|
+
* what every window authored before this field meant, so no persisted row
|
|
15116
|
+
* changes meaning and no client has to learn anything to keep working.
|
|
15117
|
+
*
|
|
15118
|
+
* It is what makes the window's real key `(deviceId, classes[])` and lets it
|
|
15119
|
+
* cross rules (D133): the operator points at a camera and a kind of thing,
|
|
15120
|
+
* not at whichever of their four rules happened to produce the notification
|
|
15121
|
+
* they are dismissing.
|
|
15122
|
+
*/
|
|
15123
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
15124
|
+
durationMinutes: number().int().min(1).max(NC_SNOOZE_MAX_MINUTES),
|
|
15021
15125
|
/**
|
|
15022
15126
|
* Silence this for EVERY recipient, not just the caller. Permission is
|
|
15023
15127
|
* checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
|
|
@@ -15042,6 +15146,10 @@ var NcSnoozeSchema = object({
|
|
|
15042
15146
|
scope: NcSnoozeScopeSchema,
|
|
15043
15147
|
ruleId: string().optional(),
|
|
15044
15148
|
deviceId: number().int().optional(),
|
|
15149
|
+
/** Subject classes this window covers. ABSENT = every class — see
|
|
15150
|
+
* {@link NcSnoozeInputSchema.shape.classes}. Lives in the JSON blob and has
|
|
15151
|
+
* no SQLite column: nothing queries a window by class. */
|
|
15152
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
15045
15153
|
startedAt: number(),
|
|
15046
15154
|
/** Exclusive: at exactly this instant the snooze is over. Expiry is a
|
|
15047
15155
|
* COMPARISON, not a job — no sweeper can leave the operator silenced. */
|
|
@@ -17279,6 +17387,22 @@ var detectionFpsField = {
|
|
|
17279
17387
|
default: 10,
|
|
17280
17388
|
step: 1
|
|
17281
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
|
+
*/
|
|
17282
17406
|
var occupancyRecheckSecField = {
|
|
17283
17407
|
min: 0,
|
|
17284
17408
|
max: 300,
|
|
@@ -17480,15 +17604,21 @@ var RunnerCameraConfigSchema = object({
|
|
|
17480
17604
|
*/
|
|
17481
17605
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
17482
17606
|
/**
|
|
17483
|
-
* Master toggle for the occupancy re-check. When `false`
|
|
17484
|
-
*
|
|
17485
|
-
* this is off by default because the recheck re-subscribes a detection session
|
|
17486
|
-
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
17487
|
-
* 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
|
|
17488
17609
|
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
17489
17610
|
* (and only render) when this is enabled.
|
|
17490
|
-
|
|
17491
|
-
|
|
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),
|
|
17492
17622
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
17493
17623
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
17494
17624
|
/**
|
|
@@ -30614,6 +30744,7 @@ Object.freeze({
|
|
|
30614
30744
|
"network-access": "ingress",
|
|
30615
30745
|
"smtp-provider": "email"
|
|
30616
30746
|
});
|
|
30747
|
+
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
30617
30748
|
new Set(["devices", "classes"]);
|
|
30618
30749
|
/** Strip a small, safe subset of Markdown down to plain text. */
|
|
30619
30750
|
function markdownToText(md) {
|
|
@@ -32194,11 +32325,24 @@ function nativePriority$1(prepared) {
|
|
|
32194
32325
|
}
|
|
32195
32326
|
function viewActions(prepared) {
|
|
32196
32327
|
const out = [];
|
|
32197
|
-
for (const action of prepared.actions.slice(0, MAX_ACTIONS$1))
|
|
32198
|
-
action
|
|
32199
|
-
|
|
32200
|
-
|
|
32201
|
-
|
|
32328
|
+
for (const action of prepared.actions.slice(0, MAX_ACTIONS$1)) {
|
|
32329
|
+
if (action.url === void 0 || action.url.length === 0) continue;
|
|
32330
|
+
if (action.mode === "background") {
|
|
32331
|
+
out.push({
|
|
32332
|
+
action: "http",
|
|
32333
|
+
label: action.label,
|
|
32334
|
+
url: action.url,
|
|
32335
|
+
method: "POST",
|
|
32336
|
+
clear: true
|
|
32337
|
+
});
|
|
32338
|
+
continue;
|
|
32339
|
+
}
|
|
32340
|
+
out.push({
|
|
32341
|
+
action: "view",
|
|
32342
|
+
label: action.label,
|
|
32343
|
+
url: action.url
|
|
32344
|
+
});
|
|
32345
|
+
}
|
|
32202
32346
|
return out;
|
|
32203
32347
|
}
|
|
32204
32348
|
/** The single attachment (caps.max = 1), if any. */
|
|
@@ -32253,7 +32397,7 @@ function buildUploadRequest(config, prepared, attachment) {
|
|
|
32253
32397
|
if (tags.length > 0) headers["x-tags"] = tags.join(",");
|
|
32254
32398
|
if (prepared.clickUrl !== null) headers["x-click"] = prepared.clickUrl;
|
|
32255
32399
|
if (attachment.name !== void 0) headers["x-filename"] = attachment.name;
|
|
32256
|
-
if (actions.length > 0) headers["x-actions"] = actions.map((a) => `view, ${a.label}, ${a.url}`).join("; ");
|
|
32400
|
+
if (actions.length > 0) headers["x-actions"] = actions.map((a) => a.action === "http" ? `http, ${a.label}, ${a.url}, method=POST, clear=true` : `view, ${a.label}, ${a.url}`).join("; ");
|
|
32257
32401
|
return {
|
|
32258
32402
|
url: `${serverUrl}/${topic}`,
|
|
32259
32403
|
method: "PUT",
|
|
@@ -32330,6 +32474,21 @@ var ntfyAdapter = {
|
|
|
32330
32474
|
max: 1
|
|
32331
32475
|
},
|
|
32332
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,
|
|
32333
32492
|
levels: [
|
|
32334
32493
|
{
|
|
32335
32494
|
id: "min",
|
|
@@ -33512,17 +33671,123 @@ function userIdsFrom(config) {
|
|
|
33512
33671
|
if (typeof raw === "string") return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
33513
33672
|
return [];
|
|
33514
33673
|
}
|
|
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
|
+
/**
|
|
33751
|
+
* How a `BACKGROUND_CALL` names its request: `METHOD::url`.
|
|
33752
|
+
*
|
|
33753
|
+
* The whole reason the type exists here: a "silence this for 30 minutes"
|
|
33754
|
+
* button that opens a browser tab has already cost more attention than the
|
|
33755
|
+
* notification it was dismissing. `BACKGROUND_CALL` fires the callback and
|
|
33756
|
+
* leaves the phone where it is.
|
|
33757
|
+
*/
|
|
33758
|
+
var BACKGROUND_CALL_PREFIX = "POST::";
|
|
33515
33759
|
function actionsFor(prepared) {
|
|
33516
33760
|
const out = [];
|
|
33517
|
-
for (const action of prepared.actions.slice(0, MAX_ACTIONS))
|
|
33518
|
-
|
|
33519
|
-
|
|
33520
|
-
|
|
33521
|
-
|
|
33522
|
-
|
|
33523
|
-
|
|
33524
|
-
|
|
33525
|
-
|
|
33761
|
+
for (const action of prepared.actions.slice(0, MAX_ACTIONS)) {
|
|
33762
|
+
const symbol = action.icon === void 0 ? void 0 : zentikActionIcon(action.icon);
|
|
33763
|
+
const decoration = {
|
|
33764
|
+
...action.destructive !== void 0 ? { destructive: action.destructive } : {},
|
|
33765
|
+
...symbol !== void 0 ? { icon: symbol } : {}
|
|
33766
|
+
};
|
|
33767
|
+
if (action.url === void 0 || action.url.length === 0) {
|
|
33768
|
+
out.push({
|
|
33769
|
+
type: "OPEN_NOTIFICATION",
|
|
33770
|
+
title: action.label,
|
|
33771
|
+
...decoration
|
|
33772
|
+
});
|
|
33773
|
+
continue;
|
|
33774
|
+
}
|
|
33775
|
+
if (action.mode === "background") {
|
|
33776
|
+
out.push({
|
|
33777
|
+
type: "BACKGROUND_CALL",
|
|
33778
|
+
title: action.label,
|
|
33779
|
+
value: `${BACKGROUND_CALL_PREFIX}${action.url}`,
|
|
33780
|
+
...decoration
|
|
33781
|
+
});
|
|
33782
|
+
continue;
|
|
33783
|
+
}
|
|
33784
|
+
out.push({
|
|
33785
|
+
type: "NAVIGATE",
|
|
33786
|
+
title: action.label,
|
|
33787
|
+
value: action.url,
|
|
33788
|
+
...decoration
|
|
33789
|
+
});
|
|
33790
|
+
}
|
|
33526
33791
|
return out;
|
|
33527
33792
|
}
|
|
33528
33793
|
function attachmentsFor(prepared) {
|
|
@@ -33688,6 +33953,17 @@ var zentikAdapter = {
|
|
|
33688
33953
|
],
|
|
33689
33954
|
format: ["text"],
|
|
33690
33955
|
clickUrl: true,
|
|
33956
|
+
/**
|
|
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.
|
|
33965
|
+
*/
|
|
33966
|
+
actionIcons: true,
|
|
33691
33967
|
sound: true,
|
|
33692
33968
|
ttl: true,
|
|
33693
33969
|
bodyMaxLen: BODY_MAX_LEN
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-notifiers",
|
|
3
|
-
"version": "1.2.
|
|
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",
|