@camstack/addon-notifiers 1.2.55 → 1.2.57

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 CHANGED
@@ -7851,7 +7851,8 @@ var OpsLogReasonSchema = _enum([
7851
7851
  "quota",
7852
7852
  "manual",
7853
7853
  "operator",
7854
- "maintenance"
7854
+ "maintenance",
7855
+ "orphaned-device"
7855
7856
  ]);
7856
7857
  /** One audit row, shared verbatim by both domains. */
7857
7858
  var OpsLogEntrySchema = object({
@@ -12923,10 +12924,39 @@ var DevicePersistConfigPayloadSchema = object({
12923
12924
  deviceId: number(),
12924
12925
  data: record(string(), unknown())
12925
12926
  });
12927
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12928
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12929
+ var MigrateSwitchOutcomeSchema = _enum([
12930
+ "off",
12931
+ "on",
12932
+ "not-offered",
12933
+ "unreachable"
12934
+ ]);
12935
+ var MigrateSwitchReportSchema = object({
12936
+ deviceId: number(),
12937
+ switchId: CameraSwitchIdSchema,
12938
+ outcome: MigrateSwitchOutcomeSchema,
12939
+ detail: string().optional()
12940
+ });
12941
+ var MigrateDeviceResultSchema = object({
12942
+ sourceId: number(),
12943
+ targetId: number(),
12944
+ switches: array(MigrateSwitchReportSchema).readonly(),
12945
+ /** Switches that could NOT be turned off on the source. Empty is the only
12946
+ * value meaning the replaced hardware is quiet. */
12947
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12948
+ swapped: boolean()
12949
+ });
12926
12950
  method(object({
12927
12951
  addonId: string(),
12928
12952
  stableId: string()
12929
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12953
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12954
+ sourceId: number(),
12955
+ targetId: number()
12956
+ }), MigrateDeviceResultSchema, {
12957
+ kind: "mutation",
12958
+ auth: "admin"
12959
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12930
12960
  deviceId: number(),
12931
12961
  name: string()
12932
12962
  }), _void(), {
@@ -19037,6 +19067,34 @@ var KeyEventsForDeviceSchema = object({
19037
19067
  deviceId: number(),
19038
19068
  events: array(KeyEventSchema).readonly()
19039
19069
  });
19070
+ /** One non-empty bucket of the timeline histogram. */
19071
+ var EventDensityBucketSchema = object({
19072
+ bucketStart: number(),
19073
+ motion: number().int(),
19074
+ object: number().int(),
19075
+ audio: number().int()
19076
+ });
19077
+ /**
19078
+ * One camera's row in a `getEventDensityBatch` answer.
19079
+ *
19080
+ * TWO facts, and the timeline draws them differently:
19081
+ *
19082
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19083
+ * claim: read, and nothing happened in the window.
19084
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19085
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19086
+ * not quiet.
19087
+ *
19088
+ * Fanned out per camera the difference was free — one query rejected while the
19089
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19090
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19091
+ * got eleven cannot tell which one is missing, or that any is.
19092
+ */
19093
+ var EventDensityForDeviceSchema = object({
19094
+ deviceId: number(),
19095
+ read: _enum(["read", "unreadable"]),
19096
+ buckets: array(EventDensityBucketSchema).readonly()
19097
+ });
19040
19098
  object({
19041
19099
  trackId: string(),
19042
19100
  className: string(),
@@ -19347,12 +19405,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19347
19405
  since: number(),
19348
19406
  until: number(),
19349
19407
  bucketMs: number().int().positive()
19350
- }), array(object({
19351
- bucketStart: number(),
19352
- motion: number().int(),
19353
- object: number().int(),
19354
- audio: number().int()
19355
- })).readonly()), method(object({
19408
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19409
+ deviceIds: array(number()).min(1).max(200),
19410
+ since: number(),
19411
+ until: number(),
19412
+ bucketMs: number().int().positive()
19413
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19356
19414
  deviceId: number(),
19357
19415
  cutoffMs: number()
19358
19416
  }), object({
@@ -26662,6 +26720,39 @@ var RecordingDaysSchema = object({
26662
26720
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26663
26721
  days: array(number())
26664
26722
  });
26723
+ /**
26724
+ * One camera's row in a `getAvailabilityBatch` answer.
26725
+ *
26726
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26727
+ * batch collapses the transport, not the work — plus the one thing the singular
26728
+ * method never had to say:
26729
+ *
26730
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26731
+ * no footage in the window", which is a real claim.
26732
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26733
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26734
+ * that emptiness means NOTHING.
26735
+ *
26736
+ * A timeline that renders the second as the first tells an operator there is no
26737
+ * footage when the truth is that nobody looked.
26738
+ */
26739
+ var RecordingAvailabilityForDeviceSchema = object({
26740
+ deviceId: number(),
26741
+ read: _enum(["read", "unreadable"]),
26742
+ ranges: array(RecordingRangeSchema).readonly()
26743
+ });
26744
+ /**
26745
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26746
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26747
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26748
+ * the date-picker's day dots must not spell them the same.
26749
+ */
26750
+ var RecordingDaysForDeviceSchema = object({
26751
+ deviceId: number(),
26752
+ read: _enum(["read", "unreadable"]),
26753
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26754
+ days: array(number()).readonly()
26755
+ });
26665
26756
  var RecordingManifestSchema = object({
26666
26757
  deviceId: number(),
26667
26758
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26896,6 +26987,13 @@ method(object({
26896
26987
  }), RecordingAvailabilitySchema, {
26897
26988
  kind: "query",
26898
26989
  auth: "protected"
26990
+ }), method(object({
26991
+ deviceIds: array(number()).min(1).max(200),
26992
+ fromMs: number(),
26993
+ toMs: number()
26994
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26995
+ kind: "query",
26996
+ auth: "protected"
26899
26997
  }), method(object({
26900
26998
  deviceId: number(),
26901
26999
  fromMs: number(),
@@ -26904,6 +27002,14 @@ method(object({
26904
27002
  }), RecordingDaysSchema, {
26905
27003
  kind: "query",
26906
27004
  auth: "protected"
27005
+ }), method(object({
27006
+ deviceIds: array(number()).min(1).max(200),
27007
+ fromMs: number(),
27008
+ toMs: number(),
27009
+ tzOffsetMinutes: number()
27010
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
27011
+ kind: "query",
27012
+ auth: "protected"
26907
27013
  }), method(object({
26908
27014
  deviceId: number(),
26909
27015
  fromMs: number(),
@@ -30367,6 +30473,12 @@ Object.freeze({
30367
30473
  addonId: null,
30368
30474
  access: "view"
30369
30475
  },
30476
+ "deviceManager.migrateDevice": {
30477
+ capName: "device-manager",
30478
+ capScope: "system",
30479
+ addonId: null,
30480
+ access: "create"
30481
+ },
30370
30482
  "deviceManager.persistConfig": {
30371
30483
  capName: "device-manager",
30372
30484
  capScope: "system",
@@ -32101,6 +32213,12 @@ Object.freeze({
32101
32213
  addonId: null,
32102
32214
  access: "view"
32103
32215
  },
32216
+ "pipelineAnalytics.getEventDensityBatch": {
32217
+ capName: "pipeline-analytics",
32218
+ capScope: "device",
32219
+ addonId: null,
32220
+ access: "view"
32221
+ },
32104
32222
  "pipelineAnalytics.getEventMedia": {
32105
32223
  capName: "pipeline-analytics",
32106
32224
  capScope: "device",
@@ -33223,12 +33341,24 @@ Object.freeze({
33223
33341
  addonId: null,
33224
33342
  access: "view"
33225
33343
  },
33344
+ "recording.getAvailabilityBatch": {
33345
+ capName: "recording",
33346
+ capScope: "system",
33347
+ addonId: null,
33348
+ access: "view"
33349
+ },
33226
33350
  "recording.getDaysWithRecordings": {
33227
33351
  capName: "recording",
33228
33352
  capScope: "system",
33229
33353
  addonId: null,
33230
33354
  access: "view"
33231
33355
  },
33356
+ "recording.getDaysWithRecordingsBatch": {
33357
+ capName: "recording",
33358
+ capScope: "system",
33359
+ addonId: null,
33360
+ access: "view"
33361
+ },
33232
33362
  "recording.getDeviceConfig": {
33233
33363
  capName: "recording",
33234
33364
  capScope: "system",
@@ -35833,6 +35963,11 @@ Object.freeze({
35833
35963
  form: "single",
35834
35964
  optional: false
35835
35965
  }],
35966
+ "pipelineAnalytics.getEventDensityBatch": [{
35967
+ name: "deviceIds",
35968
+ form: "array",
35969
+ optional: false
35970
+ }],
35836
35971
  "pipelineAnalytics.getEventMedia": [{
35837
35972
  name: "deviceId",
35838
35973
  form: "single",
@@ -36248,11 +36383,21 @@ Object.freeze({
36248
36383
  form: "single",
36249
36384
  optional: false
36250
36385
  }],
36386
+ "recording.getAvailabilityBatch": [{
36387
+ name: "deviceIds",
36388
+ form: "array",
36389
+ optional: false
36390
+ }],
36251
36391
  "recording.getDaysWithRecordings": [{
36252
36392
  name: "deviceId",
36253
36393
  form: "single",
36254
36394
  optional: false
36255
36395
  }],
36396
+ "recording.getDaysWithRecordingsBatch": [{
36397
+ name: "deviceIds",
36398
+ form: "array",
36399
+ optional: false
36400
+ }],
36256
36401
  "recording.getDeviceConfig": [{
36257
36402
  name: "deviceId",
36258
36403
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7824,7 +7824,8 @@ var OpsLogReasonSchema = _enum([
7824
7824
  "quota",
7825
7825
  "manual",
7826
7826
  "operator",
7827
- "maintenance"
7827
+ "maintenance",
7828
+ "orphaned-device"
7828
7829
  ]);
7829
7830
  /** One audit row, shared verbatim by both domains. */
7830
7831
  var OpsLogEntrySchema = object({
@@ -12896,10 +12897,39 @@ var DevicePersistConfigPayloadSchema = object({
12896
12897
  deviceId: number(),
12897
12898
  data: record(string(), unknown())
12898
12899
  });
12900
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12901
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12902
+ var MigrateSwitchOutcomeSchema = _enum([
12903
+ "off",
12904
+ "on",
12905
+ "not-offered",
12906
+ "unreachable"
12907
+ ]);
12908
+ var MigrateSwitchReportSchema = object({
12909
+ deviceId: number(),
12910
+ switchId: CameraSwitchIdSchema,
12911
+ outcome: MigrateSwitchOutcomeSchema,
12912
+ detail: string().optional()
12913
+ });
12914
+ var MigrateDeviceResultSchema = object({
12915
+ sourceId: number(),
12916
+ targetId: number(),
12917
+ switches: array(MigrateSwitchReportSchema).readonly(),
12918
+ /** Switches that could NOT be turned off on the source. Empty is the only
12919
+ * value meaning the replaced hardware is quiet. */
12920
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12921
+ swapped: boolean()
12922
+ });
12899
12923
  method(object({
12900
12924
  addonId: string(),
12901
12925
  stableId: string()
12902
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12926
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12927
+ sourceId: number(),
12928
+ targetId: number()
12929
+ }), MigrateDeviceResultSchema, {
12930
+ kind: "mutation",
12931
+ auth: "admin"
12932
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12903
12933
  deviceId: number(),
12904
12934
  name: string()
12905
12935
  }), _void(), {
@@ -19010,6 +19040,34 @@ var KeyEventsForDeviceSchema = object({
19010
19040
  deviceId: number(),
19011
19041
  events: array(KeyEventSchema).readonly()
19012
19042
  });
19043
+ /** One non-empty bucket of the timeline histogram. */
19044
+ var EventDensityBucketSchema = object({
19045
+ bucketStart: number(),
19046
+ motion: number().int(),
19047
+ object: number().int(),
19048
+ audio: number().int()
19049
+ });
19050
+ /**
19051
+ * One camera's row in a `getEventDensityBatch` answer.
19052
+ *
19053
+ * TWO facts, and the timeline draws them differently:
19054
+ *
19055
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19056
+ * claim: read, and nothing happened in the window.
19057
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19058
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19059
+ * not quiet.
19060
+ *
19061
+ * Fanned out per camera the difference was free — one query rejected while the
19062
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19063
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19064
+ * got eleven cannot tell which one is missing, or that any is.
19065
+ */
19066
+ var EventDensityForDeviceSchema = object({
19067
+ deviceId: number(),
19068
+ read: _enum(["read", "unreadable"]),
19069
+ buckets: array(EventDensityBucketSchema).readonly()
19070
+ });
19013
19071
  object({
19014
19072
  trackId: string(),
19015
19073
  className: string(),
@@ -19320,12 +19378,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19320
19378
  since: number(),
19321
19379
  until: number(),
19322
19380
  bucketMs: number().int().positive()
19323
- }), array(object({
19324
- bucketStart: number(),
19325
- motion: number().int(),
19326
- object: number().int(),
19327
- audio: number().int()
19328
- })).readonly()), method(object({
19381
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19382
+ deviceIds: array(number()).min(1).max(200),
19383
+ since: number(),
19384
+ until: number(),
19385
+ bucketMs: number().int().positive()
19386
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19329
19387
  deviceId: number(),
19330
19388
  cutoffMs: number()
19331
19389
  }), object({
@@ -26635,6 +26693,39 @@ var RecordingDaysSchema = object({
26635
26693
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26636
26694
  days: array(number())
26637
26695
  });
26696
+ /**
26697
+ * One camera's row in a `getAvailabilityBatch` answer.
26698
+ *
26699
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26700
+ * batch collapses the transport, not the work — plus the one thing the singular
26701
+ * method never had to say:
26702
+ *
26703
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26704
+ * no footage in the window", which is a real claim.
26705
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26706
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26707
+ * that emptiness means NOTHING.
26708
+ *
26709
+ * A timeline that renders the second as the first tells an operator there is no
26710
+ * footage when the truth is that nobody looked.
26711
+ */
26712
+ var RecordingAvailabilityForDeviceSchema = object({
26713
+ deviceId: number(),
26714
+ read: _enum(["read", "unreadable"]),
26715
+ ranges: array(RecordingRangeSchema).readonly()
26716
+ });
26717
+ /**
26718
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26719
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26720
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26721
+ * the date-picker's day dots must not spell them the same.
26722
+ */
26723
+ var RecordingDaysForDeviceSchema = object({
26724
+ deviceId: number(),
26725
+ read: _enum(["read", "unreadable"]),
26726
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26727
+ days: array(number()).readonly()
26728
+ });
26638
26729
  var RecordingManifestSchema = object({
26639
26730
  deviceId: number(),
26640
26731
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26869,6 +26960,13 @@ method(object({
26869
26960
  }), RecordingAvailabilitySchema, {
26870
26961
  kind: "query",
26871
26962
  auth: "protected"
26963
+ }), method(object({
26964
+ deviceIds: array(number()).min(1).max(200),
26965
+ fromMs: number(),
26966
+ toMs: number()
26967
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26968
+ kind: "query",
26969
+ auth: "protected"
26872
26970
  }), method(object({
26873
26971
  deviceId: number(),
26874
26972
  fromMs: number(),
@@ -26877,6 +26975,14 @@ method(object({
26877
26975
  }), RecordingDaysSchema, {
26878
26976
  kind: "query",
26879
26977
  auth: "protected"
26978
+ }), method(object({
26979
+ deviceIds: array(number()).min(1).max(200),
26980
+ fromMs: number(),
26981
+ toMs: number(),
26982
+ tzOffsetMinutes: number()
26983
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26984
+ kind: "query",
26985
+ auth: "protected"
26880
26986
  }), method(object({
26881
26987
  deviceId: number(),
26882
26988
  fromMs: number(),
@@ -30340,6 +30446,12 @@ Object.freeze({
30340
30446
  addonId: null,
30341
30447
  access: "view"
30342
30448
  },
30449
+ "deviceManager.migrateDevice": {
30450
+ capName: "device-manager",
30451
+ capScope: "system",
30452
+ addonId: null,
30453
+ access: "create"
30454
+ },
30343
30455
  "deviceManager.persistConfig": {
30344
30456
  capName: "device-manager",
30345
30457
  capScope: "system",
@@ -32074,6 +32186,12 @@ Object.freeze({
32074
32186
  addonId: null,
32075
32187
  access: "view"
32076
32188
  },
32189
+ "pipelineAnalytics.getEventDensityBatch": {
32190
+ capName: "pipeline-analytics",
32191
+ capScope: "device",
32192
+ addonId: null,
32193
+ access: "view"
32194
+ },
32077
32195
  "pipelineAnalytics.getEventMedia": {
32078
32196
  capName: "pipeline-analytics",
32079
32197
  capScope: "device",
@@ -33196,12 +33314,24 @@ Object.freeze({
33196
33314
  addonId: null,
33197
33315
  access: "view"
33198
33316
  },
33317
+ "recording.getAvailabilityBatch": {
33318
+ capName: "recording",
33319
+ capScope: "system",
33320
+ addonId: null,
33321
+ access: "view"
33322
+ },
33199
33323
  "recording.getDaysWithRecordings": {
33200
33324
  capName: "recording",
33201
33325
  capScope: "system",
33202
33326
  addonId: null,
33203
33327
  access: "view"
33204
33328
  },
33329
+ "recording.getDaysWithRecordingsBatch": {
33330
+ capName: "recording",
33331
+ capScope: "system",
33332
+ addonId: null,
33333
+ access: "view"
33334
+ },
33205
33335
  "recording.getDeviceConfig": {
33206
33336
  capName: "recording",
33207
33337
  capScope: "system",
@@ -35806,6 +35936,11 @@ Object.freeze({
35806
35936
  form: "single",
35807
35937
  optional: false
35808
35938
  }],
35939
+ "pipelineAnalytics.getEventDensityBatch": [{
35940
+ name: "deviceIds",
35941
+ form: "array",
35942
+ optional: false
35943
+ }],
35809
35944
  "pipelineAnalytics.getEventMedia": [{
35810
35945
  name: "deviceId",
35811
35946
  form: "single",
@@ -36221,11 +36356,21 @@ Object.freeze({
36221
36356
  form: "single",
36222
36357
  optional: false
36223
36358
  }],
36359
+ "recording.getAvailabilityBatch": [{
36360
+ name: "deviceIds",
36361
+ form: "array",
36362
+ optional: false
36363
+ }],
36224
36364
  "recording.getDaysWithRecordings": [{
36225
36365
  name: "deviceId",
36226
36366
  form: "single",
36227
36367
  optional: false
36228
36368
  }],
36369
+ "recording.getDaysWithRecordingsBatch": [{
36370
+ name: "deviceIds",
36371
+ form: "array",
36372
+ optional: false
36373
+ }],
36229
36374
  "recording.getDeviceConfig": [{
36230
36375
  name: "deviceId",
36231
36376
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.55",
3
+ "version": "1.2.57",
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",