@camstack/addon-ai 0.4.105 → 0.4.106

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
@@ -5506,7 +5506,7 @@ function number(params) {
5506
5506
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
5507
5507
  }
5508
5508
  //#endregion
5509
- //#region ../types/dist/sleep-CnLyvg3w.mjs
5509
+ //#region ../types/dist/sleep-DBKu2-U5.mjs
5510
5510
  /**
5511
5511
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5512
5512
  * window to float samples (D455).
@@ -7578,7 +7578,23 @@ var EncodeProfileSchema = object({
7578
7578
  "zerolatency",
7579
7579
  "film",
7580
7580
  "animation"
7581
- ]).optional()
7581
+ ]).optional(),
7582
+ /**
7583
+ * ONE slice per access unit.
7584
+ *
7585
+ * `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
7586
+ * the encoder as five NAL slices that share one RTP timestamp and carry one
7587
+ * marker bit. A libwebrtc depacketiser sees five frame-starts and one
7588
+ * frame-end per frame: the first pictures render and the video then freezes
7589
+ * for good while the audio, on its own plane, plays on. Measured on this hub
7590
+ * 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
7591
+ *
7592
+ * A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
7593
+ * `outputArgs` on purpose: an opaque array is part of the sharing key, so two
7594
+ * consumers meaning the same thing spelled differently would stop sharing one
7595
+ * child. Absent means "whatever the encoder does" — today's behaviour.
7596
+ */
7597
+ singleSlicePerFrame: boolean().optional()
7582
7598
  }),
7583
7599
  audio: union([literal("passthrough"), object({
7584
7600
  codec: _enum([
@@ -7628,7 +7644,7 @@ var BASE_LIVE_EGRESS_PROFILE = {
7628
7644
  audio: "passthrough"
7629
7645
  };
7630
7646
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7631
- ({ ...BASE_LIVE_EGRESS_PROFILE });
7647
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7632
7648
  /**
7633
7649
  * Deep wiring healthcheck — snapshot of active reachability probes across
7634
7650
  * every declared capability + widget of every installed plugin, on every
@@ -20059,13 +20075,109 @@ var SummariesQueryInput = object({
20059
20075
  deviceIds: array(number$1()).min(1),
20060
20076
  since: number$1().optional(),
20061
20077
  until: number$1().optional(),
20062
- limit: number$1().int().min(1).max(200).default(50)
20078
+ limit: number$1().int().min(1).max(200).default(50),
20079
+ /** The same member-wise filter {@link listRecentSummaries} takes — one
20080
+ * meaning of "dog" on every summary surface. */
20081
+ classes: array(string().min(1).max(60)).max(40).optional()
20063
20082
  });
20064
- var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
20065
20083
  var SummaryDetailSchema = object({
20066
20084
  summary: SummarySchema,
20067
- /** Member track ids, in absorption order. */
20068
- trackIds: array(string()).readonly()
20085
+ /** Members in absorption order, oldest join first. */
20086
+ members: array(object({
20087
+ trackId: string(),
20088
+ /**
20089
+ * The track's class at the time it JOINED, or `null` when it was unresolved
20090
+ * and the track no longer exists to fill it.
20091
+ *
20092
+ * The join, not the latest opinion: class voting can re-decide a track's
20093
+ * class afterwards, and a group's composition is the history of who came
20094
+ * together. A consumer counts these to badge a thumbnail — "two people and a
20095
+ * dog" — rather than being handed a second, denormalised tally that would
20096
+ * drift from the members it claims to describe.
20097
+ */
20098
+ className: string().nullable(),
20099
+ /** `primary` is the one member that carries the group's identity. */
20100
+ role: _enum([
20101
+ "primary",
20102
+ "sibling",
20103
+ "child"
20104
+ ]).nullable(),
20105
+ joinedAt: number$1(),
20106
+ /** Tier 1 — the sub-class (`dog`, `van`), resolved from the member's track
20107
+ * at read time. Absent when nothing resolved, never an empty string. */
20108
+ label: string().optional(),
20109
+ /** Tier 2 — the instance: a person's name, a plate. Absent when the pipeline
20110
+ * recognised nobody, and absent for a member whose track has been pruned —
20111
+ * a thumbnail that names the wrong person is worse than one naming nobody. */
20112
+ subLabel: string().optional()
20113
+ })).readonly()
20114
+ });
20115
+ /**
20116
+ * What a LIST of groups carries: the envelope plus what it is MADE OF.
20117
+ *
20118
+ * Coarse on purpose — the operator's rule is "only the rough data on the list,
20119
+ * we extend when we notice we need more". Enough to draw the card ("two people
20120
+ * and a dog") and no more. The MEMBERS, their names and the per-member links
20121
+ * are on {@link getSummary}: shipping every member of every group so a client
20122
+ * can count them turns a page of two hundred groups into a thousand records to
20123
+ * draw two hundred badges.
20124
+ */
20125
+ var SummaryListItemSchema = object({
20126
+ summary: SummarySchema,
20127
+ /** Class counts, most numerous first; `className: null` is a member whose
20128
+ * class never resolved, counted so the counts add up to `memberCount`. */
20129
+ classes: array(object({
20130
+ className: string().nullable(),
20131
+ count: number$1().int().positive()
20132
+ })).readonly()
20133
+ });
20134
+ var SummariesPageSchema = object({ summaries: array(SummaryListItemSchema).readonly() });
20135
+ /**
20136
+ * The paged feed, deliberately the SAME shape as {@link RecentTracksQueryInput}.
20137
+ *
20138
+ * The reel on the cameras page and the Events gallery both ride a cursor, so a
20139
+ * summaries feed that could only answer "the last N for these cameras"
20140
+ * (`listSummaries`) would make groups available in the timeline and nowhere
20141
+ * else. Two feeds with different ordering contracts would be two ideas of what
20142
+ * "newest" means, on one screen.
20143
+ */
20144
+ var RecentSummariesQueryInput = object({
20145
+ /** Devices to merge. An empty array yields an empty page. */
20146
+ deviceIds: array(number$1()),
20147
+ /** Window lower bound on `lastActiveAt` (inclusive). */
20148
+ since: number$1().optional(),
20149
+ /** Window upper bound on `lastActiveAt` (inclusive). */
20150
+ until: number$1().optional(),
20151
+ limit: number$1().int().min(1).max(500).default(100),
20152
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
20153
+ cursor: string().optional(),
20154
+ /**
20155
+ * Restrict to these classes — THE SAME FIELD `listRecentTracks` takes, and
20156
+ * deliberately the same name: the events filter tree emits one list of ticked
20157
+ * taxonomy nodes, and a summaries feed that called it something else would
20158
+ * make the operator's one filter component produce two shapes.
20159
+ *
20160
+ * A question about the MEMBERS: a group matches when ANY of them does. "Show
20161
+ * me the arrivals with a dog" has to return the group of four people who had
20162
+ * one with them; a filter on the group's own fields could only ever answer
20163
+ * about its primary.
20164
+ *
20165
+ * Matched against all THREE tiers of a member — the class (`animal`), the
20166
+ * sub-class (`dog`) and the instance (a name, a plate) — because which tier a
20167
+ * ticked node lives on is the taxonomy's business, not the caller's. `dog` is
20168
+ * an event kind in the tree and a tier-1 label on an `animal` track; a filter
20169
+ * that read only the class would answer "animal" and never "dog".
20170
+ *
20171
+ * ABSENT or EMPTY means no filter — absent is not a selection of none.
20172
+ */
20173
+ classes: array(string().min(1).max(60)).max(40).optional()
20174
+ });
20175
+ var RecentSummariesPageSchema = object({
20176
+ /** Ordered by (`lastActiveAt` DESC, id DESC). Same light item as
20177
+ * {@link listSummaries} — one list shape, so a card renders the same
20178
+ * whichever feed drew it. */
20179
+ summaries: array(SummaryListItemSchema).readonly(),
20180
+ nextCursor: string().nullable()
20069
20181
  });
20070
20182
  var RecentTracksPageSchema = object({
20071
20183
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -20479,7 +20591,7 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
20479
20591
  classes: array(string()).optional(),
20480
20592
  /** See {@link ExcludeSourcesDoc}. */
20481
20593
  excludeSources: array(TrackSourceSchema).optional()
20482
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
20594
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(RecentSummariesQueryInput, RecentSummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
20483
20595
  kind: "mutation",
20484
20596
  auth: "admin"
20485
20597
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number$1() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -30085,7 +30197,29 @@ var LoggingSettingsPatchSchema = object({
30085
30197
  * disarmed the channels it did not mention would make the Levels page and
30086
30198
  * the Diagnostics page fight over the same value.
30087
30199
  */
30088
- channels: array(LogChannelWindowPatchSchema).readonly().optional()
30200
+ channels: array(LogChannelWindowPatchSchema).readonly().optional(),
30201
+ /**
30202
+ * How many days the analytics OPS LOG is kept — the audit trail of what
30203
+ * retention actually deleted.
30204
+ *
30205
+ * It lives HERE, on the logging document, and not on the analytics addon's
30206
+ * settings form, on the operator's instruction: every other log-ish window in
30207
+ * this system is configured through this one document, and a retention audit
30208
+ * configured somewhere else is a second place to look when the question is
30209
+ * "what happened to my data".
30210
+ *
30211
+ * That placement has a cost worth stating: this document ships in the
30212
+ * framework closure, so this knob needs a published `@camstack/server` while
30213
+ * the four windows beside it (D485) reach a node on `camstack deploy`.
30214
+ *
30215
+ * `retention-model.ts` argued this one must NOT follow a device window,
30216
+ * because the audit trail is the history of the SWEEP and not of the camera.
30217
+ * That argument stands and is untouched — what changes is that 30 days stops
30218
+ * being a constant nobody can reach. `0` keeps it forever. The floor is 1 day
30219
+ * rather than 0-as-delete: an audit log truncated to nothing is precisely the
30220
+ * state in which the next 9.5 GiB leak cannot be explained.
30221
+ */
30222
+ opsLogRetentionDays: number$1().int().min(0).max(3650).optional()
30089
30223
  });
30090
30224
  /**
30091
30225
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -30158,6 +30292,14 @@ var LoggingSettingsStateSchema = object({
30158
30292
  channels: array(LogChannelDescriptorSchema).readonly(),
30159
30293
  /** The channels ARMED right now, each with its deadline. */
30160
30294
  activeChannels: array(LogChannelWindowStateSchema).readonly(),
30295
+ /**
30296
+ * The ops-log window in days, as it is in force. `0` = kept forever.
30297
+ *
30298
+ * Reported rather than left to the caller's memory of what it wrote: this is
30299
+ * the value the analytics sweep will actually use, and an operator asking how
30300
+ * long their audit trail lives should not have to infer it from a default.
30301
+ */
30302
+ opsLogRetentionDays: number$1().int().min(0),
30161
30303
  persisted: boolean()
30162
30304
  });
30163
30305
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -34317,6 +34459,12 @@ Object.freeze({
34317
34459
  addonId: null,
34318
34460
  access: "view"
34319
34461
  },
34462
+ "pipelineAnalytics.listRecentSummaries": {
34463
+ capName: "pipeline-analytics",
34464
+ capScope: "device",
34465
+ addonId: null,
34466
+ access: "view"
34467
+ },
34320
34468
  "pipelineAnalytics.listRecentTracks": {
34321
34469
  capName: "pipeline-analytics",
34322
34470
  capScope: "device",
@@ -38144,6 +38292,11 @@ Object.freeze({
38144
38292
  form: "single",
38145
38293
  optional: true
38146
38294
  }],
38295
+ "pipelineAnalytics.listRecentSummaries": [{
38296
+ name: "deviceIds",
38297
+ form: "array",
38298
+ optional: false
38299
+ }],
38147
38300
  "pipelineAnalytics.listRecentTracks": [{
38148
38301
  name: "deviceIds",
38149
38302
  form: "array",
package/dist/addon.mjs CHANGED
@@ -5533,7 +5533,7 @@ function number(params) {
5533
5533
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
5534
5534
  }
5535
5535
  //#endregion
5536
- //#region ../types/dist/sleep-CnLyvg3w.mjs
5536
+ //#region ../types/dist/sleep-DBKu2-U5.mjs
5537
5537
  /**
5538
5538
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5539
5539
  * window to float samples (D455).
@@ -7605,7 +7605,23 @@ var EncodeProfileSchema = object({
7605
7605
  "zerolatency",
7606
7606
  "film",
7607
7607
  "animation"
7608
- ]).optional()
7608
+ ]).optional(),
7609
+ /**
7610
+ * ONE slice per access unit.
7611
+ *
7612
+ * `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
7613
+ * the encoder as five NAL slices that share one RTP timestamp and carry one
7614
+ * marker bit. A libwebrtc depacketiser sees five frame-starts and one
7615
+ * frame-end per frame: the first pictures render and the video then freezes
7616
+ * for good while the audio, on its own plane, plays on. Measured on this hub
7617
+ * 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
7618
+ *
7619
+ * A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
7620
+ * `outputArgs` on purpose: an opaque array is part of the sharing key, so two
7621
+ * consumers meaning the same thing spelled differently would stop sharing one
7622
+ * child. Absent means "whatever the encoder does" — today's behaviour.
7623
+ */
7624
+ singleSlicePerFrame: boolean().optional()
7609
7625
  }),
7610
7626
  audio: union([literal("passthrough"), object({
7611
7627
  codec: _enum([
@@ -7655,7 +7671,7 @@ var BASE_LIVE_EGRESS_PROFILE = {
7655
7671
  audio: "passthrough"
7656
7672
  };
7657
7673
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7658
- ({ ...BASE_LIVE_EGRESS_PROFILE });
7674
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7659
7675
  /**
7660
7676
  * Deep wiring healthcheck — snapshot of active reachability probes across
7661
7677
  * every declared capability + widget of every installed plugin, on every
@@ -20086,13 +20102,109 @@ var SummariesQueryInput = object({
20086
20102
  deviceIds: array(number$1()).min(1),
20087
20103
  since: number$1().optional(),
20088
20104
  until: number$1().optional(),
20089
- limit: number$1().int().min(1).max(200).default(50)
20105
+ limit: number$1().int().min(1).max(200).default(50),
20106
+ /** The same member-wise filter {@link listRecentSummaries} takes — one
20107
+ * meaning of "dog" on every summary surface. */
20108
+ classes: array(string().min(1).max(60)).max(40).optional()
20090
20109
  });
20091
- var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
20092
20110
  var SummaryDetailSchema = object({
20093
20111
  summary: SummarySchema,
20094
- /** Member track ids, in absorption order. */
20095
- trackIds: array(string()).readonly()
20112
+ /** Members in absorption order, oldest join first. */
20113
+ members: array(object({
20114
+ trackId: string(),
20115
+ /**
20116
+ * The track's class at the time it JOINED, or `null` when it was unresolved
20117
+ * and the track no longer exists to fill it.
20118
+ *
20119
+ * The join, not the latest opinion: class voting can re-decide a track's
20120
+ * class afterwards, and a group's composition is the history of who came
20121
+ * together. A consumer counts these to badge a thumbnail — "two people and a
20122
+ * dog" — rather than being handed a second, denormalised tally that would
20123
+ * drift from the members it claims to describe.
20124
+ */
20125
+ className: string().nullable(),
20126
+ /** `primary` is the one member that carries the group's identity. */
20127
+ role: _enum([
20128
+ "primary",
20129
+ "sibling",
20130
+ "child"
20131
+ ]).nullable(),
20132
+ joinedAt: number$1(),
20133
+ /** Tier 1 — the sub-class (`dog`, `van`), resolved from the member's track
20134
+ * at read time. Absent when nothing resolved, never an empty string. */
20135
+ label: string().optional(),
20136
+ /** Tier 2 — the instance: a person's name, a plate. Absent when the pipeline
20137
+ * recognised nobody, and absent for a member whose track has been pruned —
20138
+ * a thumbnail that names the wrong person is worse than one naming nobody. */
20139
+ subLabel: string().optional()
20140
+ })).readonly()
20141
+ });
20142
+ /**
20143
+ * What a LIST of groups carries: the envelope plus what it is MADE OF.
20144
+ *
20145
+ * Coarse on purpose — the operator's rule is "only the rough data on the list,
20146
+ * we extend when we notice we need more". Enough to draw the card ("two people
20147
+ * and a dog") and no more. The MEMBERS, their names and the per-member links
20148
+ * are on {@link getSummary}: shipping every member of every group so a client
20149
+ * can count them turns a page of two hundred groups into a thousand records to
20150
+ * draw two hundred badges.
20151
+ */
20152
+ var SummaryListItemSchema = object({
20153
+ summary: SummarySchema,
20154
+ /** Class counts, most numerous first; `className: null` is a member whose
20155
+ * class never resolved, counted so the counts add up to `memberCount`. */
20156
+ classes: array(object({
20157
+ className: string().nullable(),
20158
+ count: number$1().int().positive()
20159
+ })).readonly()
20160
+ });
20161
+ var SummariesPageSchema = object({ summaries: array(SummaryListItemSchema).readonly() });
20162
+ /**
20163
+ * The paged feed, deliberately the SAME shape as {@link RecentTracksQueryInput}.
20164
+ *
20165
+ * The reel on the cameras page and the Events gallery both ride a cursor, so a
20166
+ * summaries feed that could only answer "the last N for these cameras"
20167
+ * (`listSummaries`) would make groups available in the timeline and nowhere
20168
+ * else. Two feeds with different ordering contracts would be two ideas of what
20169
+ * "newest" means, on one screen.
20170
+ */
20171
+ var RecentSummariesQueryInput = object({
20172
+ /** Devices to merge. An empty array yields an empty page. */
20173
+ deviceIds: array(number$1()),
20174
+ /** Window lower bound on `lastActiveAt` (inclusive). */
20175
+ since: number$1().optional(),
20176
+ /** Window upper bound on `lastActiveAt` (inclusive). */
20177
+ until: number$1().optional(),
20178
+ limit: number$1().int().min(1).max(500).default(100),
20179
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
20180
+ cursor: string().optional(),
20181
+ /**
20182
+ * Restrict to these classes — THE SAME FIELD `listRecentTracks` takes, and
20183
+ * deliberately the same name: the events filter tree emits one list of ticked
20184
+ * taxonomy nodes, and a summaries feed that called it something else would
20185
+ * make the operator's one filter component produce two shapes.
20186
+ *
20187
+ * A question about the MEMBERS: a group matches when ANY of them does. "Show
20188
+ * me the arrivals with a dog" has to return the group of four people who had
20189
+ * one with them; a filter on the group's own fields could only ever answer
20190
+ * about its primary.
20191
+ *
20192
+ * Matched against all THREE tiers of a member — the class (`animal`), the
20193
+ * sub-class (`dog`) and the instance (a name, a plate) — because which tier a
20194
+ * ticked node lives on is the taxonomy's business, not the caller's. `dog` is
20195
+ * an event kind in the tree and a tier-1 label on an `animal` track; a filter
20196
+ * that read only the class would answer "animal" and never "dog".
20197
+ *
20198
+ * ABSENT or EMPTY means no filter — absent is not a selection of none.
20199
+ */
20200
+ classes: array(string().min(1).max(60)).max(40).optional()
20201
+ });
20202
+ var RecentSummariesPageSchema = object({
20203
+ /** Ordered by (`lastActiveAt` DESC, id DESC). Same light item as
20204
+ * {@link listSummaries} — one list shape, so a card renders the same
20205
+ * whichever feed drew it. */
20206
+ summaries: array(SummaryListItemSchema).readonly(),
20207
+ nextCursor: string().nullable()
20096
20208
  });
20097
20209
  var RecentTracksPageSchema = object({
20098
20210
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -20506,7 +20618,7 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
20506
20618
  classes: array(string()).optional(),
20507
20619
  /** See {@link ExcludeSourcesDoc}. */
20508
20620
  excludeSources: array(TrackSourceSchema).optional()
20509
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
20621
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(RecentSummariesQueryInput, RecentSummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number$1() }), _void(), {
20510
20622
  kind: "mutation",
20511
20623
  auth: "admin"
20512
20624
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number$1() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -30112,7 +30224,29 @@ var LoggingSettingsPatchSchema = object({
30112
30224
  * disarmed the channels it did not mention would make the Levels page and
30113
30225
  * the Diagnostics page fight over the same value.
30114
30226
  */
30115
- channels: array(LogChannelWindowPatchSchema).readonly().optional()
30227
+ channels: array(LogChannelWindowPatchSchema).readonly().optional(),
30228
+ /**
30229
+ * How many days the analytics OPS LOG is kept — the audit trail of what
30230
+ * retention actually deleted.
30231
+ *
30232
+ * It lives HERE, on the logging document, and not on the analytics addon's
30233
+ * settings form, on the operator's instruction: every other log-ish window in
30234
+ * this system is configured through this one document, and a retention audit
30235
+ * configured somewhere else is a second place to look when the question is
30236
+ * "what happened to my data".
30237
+ *
30238
+ * That placement has a cost worth stating: this document ships in the
30239
+ * framework closure, so this knob needs a published `@camstack/server` while
30240
+ * the four windows beside it (D485) reach a node on `camstack deploy`.
30241
+ *
30242
+ * `retention-model.ts` argued this one must NOT follow a device window,
30243
+ * because the audit trail is the history of the SWEEP and not of the camera.
30244
+ * That argument stands and is untouched — what changes is that 30 days stops
30245
+ * being a constant nobody can reach. `0` keeps it forever. The floor is 1 day
30246
+ * rather than 0-as-delete: an audit log truncated to nothing is precisely the
30247
+ * state in which the next 9.5 GiB leak cannot be explained.
30248
+ */
30249
+ opsLogRetentionDays: number$1().int().min(0).max(3650).optional()
30116
30250
  });
30117
30251
  /**
30118
30252
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -30185,6 +30319,14 @@ var LoggingSettingsStateSchema = object({
30185
30319
  channels: array(LogChannelDescriptorSchema).readonly(),
30186
30320
  /** The channels ARMED right now, each with its deadline. */
30187
30321
  activeChannels: array(LogChannelWindowStateSchema).readonly(),
30322
+ /**
30323
+ * The ops-log window in days, as it is in force. `0` = kept forever.
30324
+ *
30325
+ * Reported rather than left to the caller's memory of what it wrote: this is
30326
+ * the value the analytics sweep will actually use, and an operator asking how
30327
+ * long their audit trail lives should not have to infer it from a default.
30328
+ */
30329
+ opsLogRetentionDays: number$1().int().min(0),
30188
30330
  persisted: boolean()
30189
30331
  });
30190
30332
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -34344,6 +34486,12 @@ Object.freeze({
34344
34486
  addonId: null,
34345
34487
  access: "view"
34346
34488
  },
34489
+ "pipelineAnalytics.listRecentSummaries": {
34490
+ capName: "pipeline-analytics",
34491
+ capScope: "device",
34492
+ addonId: null,
34493
+ access: "view"
34494
+ },
34347
34495
  "pipelineAnalytics.listRecentTracks": {
34348
34496
  capName: "pipeline-analytics",
34349
34497
  capScope: "device",
@@ -38171,6 +38319,11 @@ Object.freeze({
38171
38319
  form: "single",
38172
38320
  optional: true
38173
38321
  }],
38322
+ "pipelineAnalytics.listRecentSummaries": [{
38323
+ name: "deviceIds",
38324
+ form: "array",
38325
+ optional: false
38326
+ }],
38174
38327
  "pipelineAnalytics.listRecentTracks": [{
38175
38328
  name: "deviceIds",
38176
38329
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.105",
3
+ "version": "0.4.106",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",