@camstack/addon-waste-collections 0.1.1 → 0.1.3

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.
@@ -18084,14 +18084,14 @@ var LlmProfileSchema = object({
18084
18084
  /** ConfigUISchema tree passed through untyped on the wire (the
18085
18085
  * notification-output `ConfigSchemaPassthrough` precedent at
18086
18086
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
18087
- var ConfigSchemaPassthrough$1 = unknown();
18087
+ var ConfigSchemaPassthrough$3 = unknown();
18088
18088
  var LlmProfileKindDescriptorSchema = object({
18089
18089
  kind: LlmProfileKindSchema,
18090
18090
  label: string(),
18091
18091
  icon: string(),
18092
18092
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
18093
18093
  addonId: string(),
18094
- configSchema: ConfigSchemaPassthrough$1
18094
+ configSchema: ConfigSchemaPassthrough$3
18095
18095
  });
18096
18096
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
18097
18097
  var LlmDefaultSchema = object({
@@ -19212,7 +19212,7 @@ var TargetKindCapsSchema = object({
19212
19212
  * the union is large and not meant for runtime validation here; the exported
19213
19213
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
19214
19214
  */
19215
- var ConfigSchemaPassthrough = unknown();
19215
+ var ConfigSchemaPassthrough$2 = unknown();
19216
19216
  var TargetKindSchema = object({
19217
19217
  kind: string(),
19218
19218
  label: string(),
@@ -19249,7 +19249,7 @@ var TargetKindSchema = object({
19249
19249
  * its raster path, which is the safe default for an unknown image.
19250
19250
  */
19251
19251
  iconMediaType: string().optional(),
19252
- configSchema: ConfigSchemaPassthrough,
19252
+ configSchema: ConfigSchemaPassthrough$2,
19253
19253
  /**
19254
19254
  * The config keys that decide WHERE this kind's secrets are sent (server,
19255
19255
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -22548,6 +22548,61 @@ var OauthIntegrationDescriptorSchema = object({
22548
22548
  });
22549
22549
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
22550
22550
  /**
22551
+ * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
22552
+ *
22553
+ * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
22554
+ * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
22555
+ * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
22556
+ * immutable after create.
22557
+ */
22558
+ var ConfigSchemaPassthrough$1 = unknown();
22559
+ var OidcConnectionSchema = object({
22560
+ id: string().min(1),
22561
+ displayName: string(),
22562
+ icon: string(),
22563
+ issuerUrl: string(),
22564
+ clientId: string(),
22565
+ /** Client id + secret present — never the secret value. */
22566
+ configured: boolean(),
22567
+ /**
22568
+ * Values for the connection form schema. `clientSecret` is a keychain
22569
+ * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
22570
+ */
22571
+ formValues: record(string(), unknown()).optional()
22572
+ });
22573
+ /**
22574
+ * Patch one connection. Omitted or empty `clientSecret` keeps the stored
22575
+ * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
22576
+ */
22577
+ var OidcConnectionUpdateInputSchema = object({
22578
+ id: string().min(1),
22579
+ displayName: string().min(1),
22580
+ icon: string().optional(),
22581
+ issuerUrl: string().optional(),
22582
+ clientId: string().optional(),
22583
+ clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
22584
+ redirectUri: string().optional(),
22585
+ scopes: string().optional(),
22586
+ usernameClaim: _enum([
22587
+ "preferred_username",
22588
+ "email",
22589
+ "sub"
22590
+ ]).optional()
22591
+ });
22592
+ method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
22593
+ id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
22594
+ displayName: string().min(1)
22595
+ }), object({ id: string() }), {
22596
+ kind: "mutation",
22597
+ auth: "admin"
22598
+ }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
22599
+ kind: "mutation",
22600
+ auth: "admin"
22601
+ }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
22602
+ kind: "mutation",
22603
+ auth: "admin"
22604
+ });
22605
+ /**
22551
22606
  * What started a camera's onboard motion (D672). `camera` is the camera's own
22552
22607
  * firmware; `sensor` is a device the operator ATTACHED to it. Absent on the
22553
22608
  * wire means the camera: only the camera-sensors bridge ever says `sensor`.
@@ -31572,6 +31627,7 @@ var calendarSourceCapability = {
31572
31627
  scope: "system",
31573
31628
  mode: "collection",
31574
31629
  internal: true,
31630
+ providerKind: "calendar",
31575
31631
  methods: {
31576
31632
  listCalendars: method(_void(), array(CalendarInfoSchema).readonly(), { auth: "admin" }),
31577
31633
  listEvents: method(CalendarListEventsInputSchema, array(CalendarEventSchema).readonly(), { auth: "admin" })
@@ -35053,6 +35109,7 @@ var OsdNamedTemplateSchema = object({
35053
35109
  });
35054
35110
  /** Where a slot's value comes from. */
35055
35111
  var OsdSourceSchema = discriminatedUnion("kind", [
35112
+ object({ kind: literal("none") }),
35056
35113
  object({
35057
35114
  kind: literal("static"),
35058
35115
  text: string().max(64)
@@ -39376,6 +39433,12 @@ var TrainingProviderDefaultsSchema = object({
39376
39433
  /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
39377
39434
  failedDatasetGraceHours: number().nonnegative()
39378
39435
  });
39436
+ /**
39437
+ * Entry form schema for a provider type — same seam as notification
39438
+ * `TargetKind.configSchema` (`z.unknown()` at the wire; callers tighten to
39439
+ * {@link ConfigUISchema}). Drives `ConfigFormBuilder` on Training → Providers.
39440
+ */
39441
+ var ConfigSchemaPassthrough = unknown();
39379
39442
  /** One creatable provider type, for the Add-entry dialog. */
39380
39443
  var TrainingProviderTypeSchema = object({
39381
39444
  typeId: string(),
@@ -39383,7 +39446,13 @@ var TrainingProviderTypeSchema = object({
39383
39446
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
39384
39447
  iconAsset: string(),
39385
39448
  /** Owning `training-provider` addon — collection selector for createEntry. */
39386
- addonId: string()
39449
+ addonId: string(),
39450
+ /**
39451
+ * Fields for one entry of this type (credentials + defaults). Absent on a
39452
+ * type that only needs a name. Password fields are keychain-backed on the
39453
+ * provider addon (`secretStatePaths`), not D634 plaintext.
39454
+ */
39455
+ configSchema: ConfigSchemaPassthrough.optional()
39387
39456
  });
39388
39457
  /** One configured account / workspace under a provider type. */
39389
39458
  var TrainingProviderEntrySchema = object({
@@ -39413,7 +39482,30 @@ var TrainingProviderDescriptorSchema = object({
39413
39482
  dataResidencyNote: string(),
39414
39483
  /** Credentials are present — never the values. */
39415
39484
  configured: boolean(),
39416
- defaults: TrainingProviderDefaultsSchema
39485
+ defaults: TrainingProviderDefaultsSchema,
39486
+ /**
39487
+ * Values for {@link TrainingProviderTypeSchema}'s `configSchema` — password
39488
+ * fields are keychain {@link SecretFieldView} or `null` / `''` (create),
39489
+ * never plaintext. Same hydrate contract as settings forms (D789).
39490
+ */
39491
+ formValues: record(string(), unknown()).optional()
39492
+ });
39493
+ /**
39494
+ * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
39495
+ * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
39496
+ * `{ $secretClear: true }` clears it.
39497
+ */
39498
+ var TrainingProviderUpdateEntryInputSchema = object({
39499
+ entryId: string().min(1),
39500
+ name: string().min(1),
39501
+ tokenId: string().optional(),
39502
+ tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
39503
+ environment: string().optional(),
39504
+ gpuType: string().optional(),
39505
+ region: string().optional(),
39506
+ costCapUsd: number().nonnegative().optional(),
39507
+ maxRuntimeSec: number().int().positive().optional(),
39508
+ failedDatasetGraceHours: number().nonnegative().optional()
39417
39509
  });
39418
39510
  var TrainingCredentialCheckSchema = object({
39419
39511
  ok: boolean(),
@@ -39483,6 +39575,9 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
39483
39575
  }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
39484
39576
  kind: "mutation",
39485
39577
  auth: "admin"
39578
+ }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
39579
+ kind: "mutation",
39580
+ auth: "admin"
39486
39581
  }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
39487
39582
  kind: "mutation",
39488
39583
  auth: "admin",
@@ -43858,6 +43953,36 @@ Object.freeze({
43858
43953
  addonId: null,
43859
43954
  access: "view"
43860
43955
  },
43956
+ "oidcConnections.connectionFormSchema": {
43957
+ capName: "oidc-connections",
43958
+ capScope: "system",
43959
+ addonId: null,
43960
+ access: "view"
43961
+ },
43962
+ "oidcConnections.createConnection": {
43963
+ capName: "oidc-connections",
43964
+ capScope: "system",
43965
+ addonId: null,
43966
+ access: "create"
43967
+ },
43968
+ "oidcConnections.deleteConnection": {
43969
+ capName: "oidc-connections",
43970
+ capScope: "system",
43971
+ addonId: null,
43972
+ access: "delete"
43973
+ },
43974
+ "oidcConnections.listConnections": {
43975
+ capName: "oidc-connections",
43976
+ capScope: "system",
43977
+ addonId: null,
43978
+ access: "view"
43979
+ },
43980
+ "oidcConnections.updateConnection": {
43981
+ capName: "oidc-connections",
43982
+ capScope: "system",
43983
+ addonId: null,
43984
+ access: "create"
43985
+ },
43861
43986
  "osd.getStatus": {
43862
43987
  capName: "osd",
43863
43988
  capScope: "device",
@@ -47044,6 +47169,12 @@ Object.freeze({
47044
47169
  addonId: null,
47045
47170
  access: "create"
47046
47171
  },
47172
+ "trainingProvider.updateEntry": {
47173
+ capName: "training-provider",
47174
+ capScope: "system",
47175
+ addonId: null,
47176
+ access: "create"
47177
+ },
47047
47178
  "trainingProvider.uploadDataset": {
47048
47179
  capName: "training-provider",
47049
47180
  capScope: "system",
@@ -51589,6 +51720,7 @@ Object.freeze({
51589
51720
  });
51590
51721
  Object.freeze({
51591
51722
  "broker": "broker",
51723
+ "calendar-source": "calendar",
51592
51724
  "device-export": "device-export",
51593
51725
  "llm": "ai",
51594
51726
  "mesh-network": "mesh",
@@ -52628,18 +52760,6 @@ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
52628
52760
  var MB = 1024 * 1024;
52629
52761
  1024 * MB, 3072 * MB;
52630
52762
  //#endregion
52631
- //#region src/custom-actions.ts
52632
- var wasteCollectionsActions = defineCustomActions({ fetchNow: customAction(object({
52633
- /** When set, refresh only this configured source id; otherwise all enabled. */
52634
- sourceId: string().min(1).optional() }).optional(), object({
52635
- ok: literal(true),
52636
- refreshed: array(object({
52637
- sourceId: string(),
52638
- entryCount: number().int(),
52639
- error: string().optional()
52640
- }))
52641
- }), { kind: "mutation" }) });
52642
- //#endregion
52643
52763
  //#region src/waste-types.ts
52644
52764
  var WasteSourceConfigSchema = object({
52645
52765
  id: string().min(1),
@@ -52680,6 +52800,54 @@ var WasteFetchResultSchema = discriminatedUnion("ok", [object({
52680
52800
  error: string()
52681
52801
  })]);
52682
52802
  //#endregion
52803
+ //#region src/custom-actions.ts
52804
+ var SourceIdSchema = string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug");
52805
+ var WasteSourceRowSchema = WasteSourceConfigSchema.extend({ formValues: record(string(), unknown()).optional() });
52806
+ var wasteCollectionsActions = defineCustomActions({
52807
+ fetchNow: customAction(object({
52808
+ /** When set, refresh only this configured source id; otherwise all enabled. */
52809
+ sourceId: string().min(1).optional() }).optional(), object({
52810
+ ok: literal(true),
52811
+ refreshed: array(object({
52812
+ sourceId: string(),
52813
+ entryCount: number().int(),
52814
+ error: string().optional()
52815
+ }))
52816
+ }), {
52817
+ kind: "mutation",
52818
+ auth: "admin"
52819
+ }),
52820
+ listSources: customAction(object({}), array(WasteSourceRowSchema).readonly(), {
52821
+ kind: "mutation",
52822
+ auth: "admin"
52823
+ }),
52824
+ sourceFormSchema: customAction(object({}), unknown(), {
52825
+ kind: "mutation",
52826
+ auth: "admin"
52827
+ }),
52828
+ createSource: customAction(object({
52829
+ id: SourceIdSchema,
52830
+ label: string().min(1)
52831
+ }), object({ id: string() }), {
52832
+ kind: "mutation",
52833
+ auth: "admin"
52834
+ }),
52835
+ updateSource: customAction(object({
52836
+ id: string().min(1),
52837
+ label: string().min(1),
52838
+ module: string().min(1).optional(),
52839
+ argsJson: string().optional(),
52840
+ enabled: boolean().optional()
52841
+ }), object({ ok: literal(true) }), {
52842
+ kind: "mutation",
52843
+ auth: "admin"
52844
+ }),
52845
+ deleteSource: customAction(object({ id: string().min(1) }), object({ ok: literal(true) }), {
52846
+ kind: "mutation",
52847
+ auth: "admin"
52848
+ })
52849
+ });
52850
+ //#endregion
52683
52851
  //#region src/fetch-source.ts
52684
52852
  /**
52685
52853
  * Spawn the embedded Python bridge once per source fetch.
@@ -52824,11 +52992,56 @@ function createWasteCalendarSource(deps) {
52824
52992
  };
52825
52993
  }
52826
52994
  //#endregion
52995
+ //#region src/waste-source-schema.ts
52996
+ var WASTE_SOURCE_FORM_SCHEMA = { sections: [{
52997
+ id: "source",
52998
+ title: "Source",
52999
+ columns: 2,
53000
+ fields: [
53001
+ {
53002
+ type: "text",
53003
+ key: "label",
53004
+ label: "Label",
53005
+ description: "Shown under System → Calendars.",
53006
+ default: "New source"
53007
+ },
53008
+ {
53009
+ type: "text",
53010
+ key: "module",
53011
+ label: "Source module",
53012
+ description: "HACS provider name, e.g. `ics`, `junker_app`, `abfallnavi_de`.",
53013
+ default: "ics"
53014
+ },
53015
+ {
53016
+ type: "textarea",
53017
+ key: "argsJson",
53018
+ label: "Args (JSON object)",
53019
+ description: "Constructor kwargs for the HACS source, e.g. `{\"url\":\"https://…/calendar.ics\"}`.",
53020
+ rows: 4,
53021
+ default: "{}",
53022
+ span: 2
53023
+ },
53024
+ {
53025
+ type: "boolean",
53026
+ key: "enabled",
53027
+ label: "Enabled",
53028
+ default: true
53029
+ }
53030
+ ]
53031
+ }] };
53032
+ var WASTE_SOURCE_CREATE_VALUES = {
53033
+ label: "New source",
53034
+ module: "ics",
53035
+ argsJson: "{}",
53036
+ enabled: true
53037
+ };
53038
+ //#endregion
52827
53039
  //#region src/waste-collections.addon.ts
52828
53040
  /**
52829
53041
  * Waste Collections — hub-only calendar-source over the vendored HACS
52830
53042
  * `waste_collection_schedule` Python core (full source catalog).
52831
53043
  */
53044
+ var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
52832
53045
  var DAY_MS = 1440 * 60 * 1e3;
52833
53046
  var WasteCollectionsAddon = class extends BaseAddon {
52834
53047
  pythonPath = null;
@@ -52876,7 +53089,14 @@ var WasteCollectionsAddon = class extends BaseAddon {
52876
53089
  provider
52877
53090
  }],
52878
53091
  customActions: wasteCollectionsActions,
52879
- actionHandlers: { fetchNow: async (input) => this.fetchNow(input) }
53092
+ actionHandlers: {
53093
+ fetchNow: async (input) => this.fetchNow(input),
53094
+ listSources: async () => this.listSources(),
53095
+ sourceFormSchema: async () => WASTE_SOURCE_FORM_SCHEMA,
53096
+ createSource: async (input) => this.createSource(input),
53097
+ updateSource: async (input) => this.updateSource(input),
53098
+ deleteSource: async (input) => this.deleteSource(input)
53099
+ }
52880
53100
  };
52881
53101
  }
52882
53102
  async onConfigChanged() {
@@ -52885,6 +53105,49 @@ var WasteCollectionsAddon = class extends BaseAddon {
52885
53105
  this.persistCache();
52886
53106
  this.refreshAll("config-changed");
52887
53107
  }
53108
+ listSources() {
53109
+ return this.config.sources.map((s) => ({
53110
+ ...s,
53111
+ formValues: {
53112
+ label: s.label,
53113
+ module: s.module,
53114
+ argsJson: s.argsJson,
53115
+ enabled: s.enabled
53116
+ }
53117
+ }));
53118
+ }
53119
+ async createSource(input) {
53120
+ if (!SOURCE_ID_PATTERN.test(input.id)) throw new Error("id must be a lowercase slug (e.g. ics-home, junker)");
53121
+ if (this.config.sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' already exists`);
53122
+ const row = WasteSourceConfigSchema.parse({
53123
+ ...WASTE_SOURCE_CREATE_VALUES,
53124
+ id: input.id,
53125
+ label: input.label
53126
+ });
53127
+ await this.updateGlobalSettings({ sources: [...this.config.sources, row] });
53128
+ return { id: input.id };
53129
+ }
53130
+ async updateSource(input) {
53131
+ const idx = this.config.sources.findIndex((s) => s.id === input.id);
53132
+ if (idx < 0) throw new Error(`source '${input.id}' not found`);
53133
+ const prev = this.config.sources[idx];
53134
+ const next = WasteSourceConfigSchema.parse({
53135
+ id: prev.id,
53136
+ label: input.label,
53137
+ module: input.module ?? prev.module,
53138
+ argsJson: input.argsJson ?? prev.argsJson,
53139
+ enabled: input.enabled ?? prev.enabled
53140
+ });
53141
+ parseSourceArgs(next);
53142
+ const sources = this.config.sources.map((s, i) => i === idx ? next : s);
53143
+ await this.updateGlobalSettings({ sources });
53144
+ return { ok: true };
53145
+ }
53146
+ async deleteSource(input) {
53147
+ if (!this.config.sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' not found`);
53148
+ await this.updateGlobalSettings({ sources: this.config.sources.filter((s) => s.id !== input.id) });
53149
+ return { ok: true };
53150
+ }
52888
53151
  async fetchNow(input) {
52889
53152
  const targets = this.config.sources.filter((s) => {
52890
53153
  if (!s.enabled) return false;
@@ -52987,76 +53250,29 @@ var WasteCollectionsAddon = class extends BaseAddon {
52987
53250
  }
52988
53251
  globalSettingsSchema() {
52989
53252
  return this.schema({ sections: [{
52990
- id: "sources",
52991
- title: "Waste sources",
52992
- description: "Each row becomes one calendar under System → Calendars. `module` must be a HACS source name from the bundled catalog (978 providers). `args` are the provider kwargs — see the HACS docs for that module. Use Fetch now after editing.",
52993
- columns: 1,
52994
- fields: [
52995
- {
52996
- type: "editable-array",
52997
- key: "sources",
52998
- label: "Configured sources",
52999
- itemIdKey: "id",
53000
- itemFields: [
53001
- {
53002
- type: "text",
53003
- key: "id",
53004
- label: "Id",
53005
- description: "Stable id (calendar local id)."
53006
- },
53007
- {
53008
- type: "text",
53009
- key: "label",
53010
- label: "Label"
53011
- },
53012
- {
53013
- type: "text",
53014
- key: "module",
53015
- label: "Source module",
53016
- description: "e.g. `ics`, `junker_app`, `abfallnavi_de`."
53017
- },
53018
- {
53019
- type: "textarea",
53020
- key: "argsJson",
53021
- label: "Args (JSON object)",
53022
- description: "Constructor kwargs for the HACS source, e.g. `{\"url\":\"https://…/calendar.ics\"}`.",
53023
- rows: 4
53024
- },
53025
- {
53026
- type: "boolean",
53027
- key: "enabled",
53028
- label: "Enabled"
53029
- }
53030
- ],
53031
- defaultItem: {
53032
- id: "",
53033
- label: "New source",
53034
- module: "ics",
53035
- argsJson: "{}",
53036
- enabled: true
53037
- }
53038
- },
53039
- {
53040
- type: "number",
53041
- key: "fetchHour",
53042
- label: "Daily fetch hour (local)",
53043
- min: 0,
53044
- max: 23,
53045
- step: 1,
53046
- description: "Informational — the addon refreshes about once per day from boot; use Fetch now to force."
53047
- },
53048
- {
53049
- type: "addon-action-button",
53050
- key: "_fetchNow",
53051
- label: "Refresh",
53052
- buttonLabel: "Fetch now",
53053
- description: "Refresh every enabled source immediately.",
53054
- addonId: "waste-collections",
53055
- action: "fetchNow",
53056
- variant: "primary",
53057
- paramsStatic: {}
53058
- }
53059
- ]
53253
+ id: "refresh",
53254
+ title: "Refresh",
53255
+ description: "Sources are managed in the table above (Add / Edit). Each source becomes one calendar under System → Calendars.",
53256
+ columns: 2,
53257
+ fields: [{
53258
+ type: "number",
53259
+ key: "fetchHour",
53260
+ label: "Daily fetch hour (local)",
53261
+ min: 0,
53262
+ max: 23,
53263
+ step: 1,
53264
+ description: "Informational — the addon refreshes about once per day from boot; use Fetch now to force."
53265
+ }, {
53266
+ type: "addon-action-button",
53267
+ key: "_fetchNow",
53268
+ label: "Refresh",
53269
+ buttonLabel: "Fetch now",
53270
+ description: "Refresh every enabled source immediately.",
53271
+ addonId: "waste-collections",
53272
+ action: "fetchNow",
53273
+ variant: "primary",
53274
+ paramsStatic: {}
53275
+ }]
53060
53276
  }] });
53061
53277
  }
53062
53278
  };
@@ -18080,14 +18080,14 @@ var LlmProfileSchema = object({
18080
18080
  /** ConfigUISchema tree passed through untyped on the wire (the
18081
18081
  * notification-output `ConfigSchemaPassthrough` precedent at
18082
18082
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
18083
- var ConfigSchemaPassthrough$1 = unknown();
18083
+ var ConfigSchemaPassthrough$3 = unknown();
18084
18084
  var LlmProfileKindDescriptorSchema = object({
18085
18085
  kind: LlmProfileKindSchema,
18086
18086
  label: string(),
18087
18087
  icon: string(),
18088
18088
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
18089
18089
  addonId: string(),
18090
- configSchema: ConfigSchemaPassthrough$1
18090
+ configSchema: ConfigSchemaPassthrough$3
18091
18091
  });
18092
18092
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
18093
18093
  var LlmDefaultSchema = object({
@@ -19208,7 +19208,7 @@ var TargetKindCapsSchema = object({
19208
19208
  * the union is large and not meant for runtime validation here; the exported
19209
19209
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
19210
19210
  */
19211
- var ConfigSchemaPassthrough = unknown();
19211
+ var ConfigSchemaPassthrough$2 = unknown();
19212
19212
  var TargetKindSchema = object({
19213
19213
  kind: string(),
19214
19214
  label: string(),
@@ -19245,7 +19245,7 @@ var TargetKindSchema = object({
19245
19245
  * its raster path, which is the safe default for an unknown image.
19246
19246
  */
19247
19247
  iconMediaType: string().optional(),
19248
- configSchema: ConfigSchemaPassthrough,
19248
+ configSchema: ConfigSchemaPassthrough$2,
19249
19249
  /**
19250
19250
  * The config keys that decide WHERE this kind's secrets are sent (server,
19251
19251
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -22544,6 +22544,61 @@ var OauthIntegrationDescriptorSchema = object({
22544
22544
  });
22545
22545
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
22546
22546
  /**
22547
+ * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
22548
+ *
22549
+ * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
22550
+ * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
22551
+ * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
22552
+ * immutable after create.
22553
+ */
22554
+ var ConfigSchemaPassthrough$1 = unknown();
22555
+ var OidcConnectionSchema = object({
22556
+ id: string().min(1),
22557
+ displayName: string(),
22558
+ icon: string(),
22559
+ issuerUrl: string(),
22560
+ clientId: string(),
22561
+ /** Client id + secret present — never the secret value. */
22562
+ configured: boolean(),
22563
+ /**
22564
+ * Values for the connection form schema. `clientSecret` is a keychain
22565
+ * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
22566
+ */
22567
+ formValues: record(string(), unknown()).optional()
22568
+ });
22569
+ /**
22570
+ * Patch one connection. Omitted or empty `clientSecret` keeps the stored
22571
+ * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
22572
+ */
22573
+ var OidcConnectionUpdateInputSchema = object({
22574
+ id: string().min(1),
22575
+ displayName: string().min(1),
22576
+ icon: string().optional(),
22577
+ issuerUrl: string().optional(),
22578
+ clientId: string().optional(),
22579
+ clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
22580
+ redirectUri: string().optional(),
22581
+ scopes: string().optional(),
22582
+ usernameClaim: _enum([
22583
+ "preferred_username",
22584
+ "email",
22585
+ "sub"
22586
+ ]).optional()
22587
+ });
22588
+ method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
22589
+ id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
22590
+ displayName: string().min(1)
22591
+ }), object({ id: string() }), {
22592
+ kind: "mutation",
22593
+ auth: "admin"
22594
+ }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
22595
+ kind: "mutation",
22596
+ auth: "admin"
22597
+ }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
22598
+ kind: "mutation",
22599
+ auth: "admin"
22600
+ });
22601
+ /**
22547
22602
  * What started a camera's onboard motion (D672). `camera` is the camera's own
22548
22603
  * firmware; `sensor` is a device the operator ATTACHED to it. Absent on the
22549
22604
  * wire means the camera: only the camera-sensors bridge ever says `sensor`.
@@ -31568,6 +31623,7 @@ var calendarSourceCapability = {
31568
31623
  scope: "system",
31569
31624
  mode: "collection",
31570
31625
  internal: true,
31626
+ providerKind: "calendar",
31571
31627
  methods: {
31572
31628
  listCalendars: method(_void(), array(CalendarInfoSchema).readonly(), { auth: "admin" }),
31573
31629
  listEvents: method(CalendarListEventsInputSchema, array(CalendarEventSchema).readonly(), { auth: "admin" })
@@ -35049,6 +35105,7 @@ var OsdNamedTemplateSchema = object({
35049
35105
  });
35050
35106
  /** Where a slot's value comes from. */
35051
35107
  var OsdSourceSchema = discriminatedUnion("kind", [
35108
+ object({ kind: literal("none") }),
35052
35109
  object({
35053
35110
  kind: literal("static"),
35054
35111
  text: string().max(64)
@@ -39372,6 +39429,12 @@ var TrainingProviderDefaultsSchema = object({
39372
39429
  /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
39373
39430
  failedDatasetGraceHours: number().nonnegative()
39374
39431
  });
39432
+ /**
39433
+ * Entry form schema for a provider type — same seam as notification
39434
+ * `TargetKind.configSchema` (`z.unknown()` at the wire; callers tighten to
39435
+ * {@link ConfigUISchema}). Drives `ConfigFormBuilder` on Training → Providers.
39436
+ */
39437
+ var ConfigSchemaPassthrough = unknown();
39375
39438
  /** One creatable provider type, for the Add-entry dialog. */
39376
39439
  var TrainingProviderTypeSchema = object({
39377
39440
  typeId: string(),
@@ -39379,7 +39442,13 @@ var TrainingProviderTypeSchema = object({
39379
39442
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
39380
39443
  iconAsset: string(),
39381
39444
  /** Owning `training-provider` addon — collection selector for createEntry. */
39382
- addonId: string()
39445
+ addonId: string(),
39446
+ /**
39447
+ * Fields for one entry of this type (credentials + defaults). Absent on a
39448
+ * type that only needs a name. Password fields are keychain-backed on the
39449
+ * provider addon (`secretStatePaths`), not D634 plaintext.
39450
+ */
39451
+ configSchema: ConfigSchemaPassthrough.optional()
39383
39452
  });
39384
39453
  /** One configured account / workspace under a provider type. */
39385
39454
  var TrainingProviderEntrySchema = object({
@@ -39409,7 +39478,30 @@ var TrainingProviderDescriptorSchema = object({
39409
39478
  dataResidencyNote: string(),
39410
39479
  /** Credentials are present — never the values. */
39411
39480
  configured: boolean(),
39412
- defaults: TrainingProviderDefaultsSchema
39481
+ defaults: TrainingProviderDefaultsSchema,
39482
+ /**
39483
+ * Values for {@link TrainingProviderTypeSchema}'s `configSchema` — password
39484
+ * fields are keychain {@link SecretFieldView} or `null` / `''` (create),
39485
+ * never plaintext. Same hydrate contract as settings forms (D789).
39486
+ */
39487
+ formValues: record(string(), unknown()).optional()
39488
+ });
39489
+ /**
39490
+ * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
39491
+ * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
39492
+ * `{ $secretClear: true }` clears it.
39493
+ */
39494
+ var TrainingProviderUpdateEntryInputSchema = object({
39495
+ entryId: string().min(1),
39496
+ name: string().min(1),
39497
+ tokenId: string().optional(),
39498
+ tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
39499
+ environment: string().optional(),
39500
+ gpuType: string().optional(),
39501
+ region: string().optional(),
39502
+ costCapUsd: number().nonnegative().optional(),
39503
+ maxRuntimeSec: number().int().positive().optional(),
39504
+ failedDatasetGraceHours: number().nonnegative().optional()
39413
39505
  });
39414
39506
  var TrainingCredentialCheckSchema = object({
39415
39507
  ok: boolean(),
@@ -39479,6 +39571,9 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
39479
39571
  }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
39480
39572
  kind: "mutation",
39481
39573
  auth: "admin"
39574
+ }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
39575
+ kind: "mutation",
39576
+ auth: "admin"
39482
39577
  }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
39483
39578
  kind: "mutation",
39484
39579
  auth: "admin",
@@ -43854,6 +43949,36 @@ Object.freeze({
43854
43949
  addonId: null,
43855
43950
  access: "view"
43856
43951
  },
43952
+ "oidcConnections.connectionFormSchema": {
43953
+ capName: "oidc-connections",
43954
+ capScope: "system",
43955
+ addonId: null,
43956
+ access: "view"
43957
+ },
43958
+ "oidcConnections.createConnection": {
43959
+ capName: "oidc-connections",
43960
+ capScope: "system",
43961
+ addonId: null,
43962
+ access: "create"
43963
+ },
43964
+ "oidcConnections.deleteConnection": {
43965
+ capName: "oidc-connections",
43966
+ capScope: "system",
43967
+ addonId: null,
43968
+ access: "delete"
43969
+ },
43970
+ "oidcConnections.listConnections": {
43971
+ capName: "oidc-connections",
43972
+ capScope: "system",
43973
+ addonId: null,
43974
+ access: "view"
43975
+ },
43976
+ "oidcConnections.updateConnection": {
43977
+ capName: "oidc-connections",
43978
+ capScope: "system",
43979
+ addonId: null,
43980
+ access: "create"
43981
+ },
43857
43982
  "osd.getStatus": {
43858
43983
  capName: "osd",
43859
43984
  capScope: "device",
@@ -47040,6 +47165,12 @@ Object.freeze({
47040
47165
  addonId: null,
47041
47166
  access: "create"
47042
47167
  },
47168
+ "trainingProvider.updateEntry": {
47169
+ capName: "training-provider",
47170
+ capScope: "system",
47171
+ addonId: null,
47172
+ access: "create"
47173
+ },
47043
47174
  "trainingProvider.uploadDataset": {
47044
47175
  capName: "training-provider",
47045
47176
  capScope: "system",
@@ -51585,6 +51716,7 @@ Object.freeze({
51585
51716
  });
51586
51717
  Object.freeze({
51587
51718
  "broker": "broker",
51719
+ "calendar-source": "calendar",
51588
51720
  "device-export": "device-export",
51589
51721
  "llm": "ai",
51590
51722
  "mesh-network": "mesh",
@@ -52624,18 +52756,6 @@ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
52624
52756
  var MB = 1024 * 1024;
52625
52757
  1024 * MB, 3072 * MB;
52626
52758
  //#endregion
52627
- //#region src/custom-actions.ts
52628
- var wasteCollectionsActions = defineCustomActions({ fetchNow: customAction(object({
52629
- /** When set, refresh only this configured source id; otherwise all enabled. */
52630
- sourceId: string().min(1).optional() }).optional(), object({
52631
- ok: literal(true),
52632
- refreshed: array(object({
52633
- sourceId: string(),
52634
- entryCount: number().int(),
52635
- error: string().optional()
52636
- }))
52637
- }), { kind: "mutation" }) });
52638
- //#endregion
52639
52759
  //#region src/waste-types.ts
52640
52760
  var WasteSourceConfigSchema = object({
52641
52761
  id: string().min(1),
@@ -52676,6 +52796,54 @@ var WasteFetchResultSchema = discriminatedUnion("ok", [object({
52676
52796
  error: string()
52677
52797
  })]);
52678
52798
  //#endregion
52799
+ //#region src/custom-actions.ts
52800
+ var SourceIdSchema = string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug");
52801
+ var WasteSourceRowSchema = WasteSourceConfigSchema.extend({ formValues: record(string(), unknown()).optional() });
52802
+ var wasteCollectionsActions = defineCustomActions({
52803
+ fetchNow: customAction(object({
52804
+ /** When set, refresh only this configured source id; otherwise all enabled. */
52805
+ sourceId: string().min(1).optional() }).optional(), object({
52806
+ ok: literal(true),
52807
+ refreshed: array(object({
52808
+ sourceId: string(),
52809
+ entryCount: number().int(),
52810
+ error: string().optional()
52811
+ }))
52812
+ }), {
52813
+ kind: "mutation",
52814
+ auth: "admin"
52815
+ }),
52816
+ listSources: customAction(object({}), array(WasteSourceRowSchema).readonly(), {
52817
+ kind: "mutation",
52818
+ auth: "admin"
52819
+ }),
52820
+ sourceFormSchema: customAction(object({}), unknown(), {
52821
+ kind: "mutation",
52822
+ auth: "admin"
52823
+ }),
52824
+ createSource: customAction(object({
52825
+ id: SourceIdSchema,
52826
+ label: string().min(1)
52827
+ }), object({ id: string() }), {
52828
+ kind: "mutation",
52829
+ auth: "admin"
52830
+ }),
52831
+ updateSource: customAction(object({
52832
+ id: string().min(1),
52833
+ label: string().min(1),
52834
+ module: string().min(1).optional(),
52835
+ argsJson: string().optional(),
52836
+ enabled: boolean().optional()
52837
+ }), object({ ok: literal(true) }), {
52838
+ kind: "mutation",
52839
+ auth: "admin"
52840
+ }),
52841
+ deleteSource: customAction(object({ id: string().min(1) }), object({ ok: literal(true) }), {
52842
+ kind: "mutation",
52843
+ auth: "admin"
52844
+ })
52845
+ });
52846
+ //#endregion
52679
52847
  //#region src/fetch-source.ts
52680
52848
  /**
52681
52849
  * Spawn the embedded Python bridge once per source fetch.
@@ -52820,11 +52988,56 @@ function createWasteCalendarSource(deps) {
52820
52988
  };
52821
52989
  }
52822
52990
  //#endregion
52991
+ //#region src/waste-source-schema.ts
52992
+ var WASTE_SOURCE_FORM_SCHEMA = { sections: [{
52993
+ id: "source",
52994
+ title: "Source",
52995
+ columns: 2,
52996
+ fields: [
52997
+ {
52998
+ type: "text",
52999
+ key: "label",
53000
+ label: "Label",
53001
+ description: "Shown under System → Calendars.",
53002
+ default: "New source"
53003
+ },
53004
+ {
53005
+ type: "text",
53006
+ key: "module",
53007
+ label: "Source module",
53008
+ description: "HACS provider name, e.g. `ics`, `junker_app`, `abfallnavi_de`.",
53009
+ default: "ics"
53010
+ },
53011
+ {
53012
+ type: "textarea",
53013
+ key: "argsJson",
53014
+ label: "Args (JSON object)",
53015
+ description: "Constructor kwargs for the HACS source, e.g. `{\"url\":\"https://…/calendar.ics\"}`.",
53016
+ rows: 4,
53017
+ default: "{}",
53018
+ span: 2
53019
+ },
53020
+ {
53021
+ type: "boolean",
53022
+ key: "enabled",
53023
+ label: "Enabled",
53024
+ default: true
53025
+ }
53026
+ ]
53027
+ }] };
53028
+ var WASTE_SOURCE_CREATE_VALUES = {
53029
+ label: "New source",
53030
+ module: "ics",
53031
+ argsJson: "{}",
53032
+ enabled: true
53033
+ };
53034
+ //#endregion
52823
53035
  //#region src/waste-collections.addon.ts
52824
53036
  /**
52825
53037
  * Waste Collections — hub-only calendar-source over the vendored HACS
52826
53038
  * `waste_collection_schedule` Python core (full source catalog).
52827
53039
  */
53040
+ var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
52828
53041
  var DAY_MS = 1440 * 60 * 1e3;
52829
53042
  var WasteCollectionsAddon = class extends BaseAddon {
52830
53043
  pythonPath = null;
@@ -52872,7 +53085,14 @@ var WasteCollectionsAddon = class extends BaseAddon {
52872
53085
  provider
52873
53086
  }],
52874
53087
  customActions: wasteCollectionsActions,
52875
- actionHandlers: { fetchNow: async (input) => this.fetchNow(input) }
53088
+ actionHandlers: {
53089
+ fetchNow: async (input) => this.fetchNow(input),
53090
+ listSources: async () => this.listSources(),
53091
+ sourceFormSchema: async () => WASTE_SOURCE_FORM_SCHEMA,
53092
+ createSource: async (input) => this.createSource(input),
53093
+ updateSource: async (input) => this.updateSource(input),
53094
+ deleteSource: async (input) => this.deleteSource(input)
53095
+ }
52876
53096
  };
52877
53097
  }
52878
53098
  async onConfigChanged() {
@@ -52881,6 +53101,49 @@ var WasteCollectionsAddon = class extends BaseAddon {
52881
53101
  this.persistCache();
52882
53102
  this.refreshAll("config-changed");
52883
53103
  }
53104
+ listSources() {
53105
+ return this.config.sources.map((s) => ({
53106
+ ...s,
53107
+ formValues: {
53108
+ label: s.label,
53109
+ module: s.module,
53110
+ argsJson: s.argsJson,
53111
+ enabled: s.enabled
53112
+ }
53113
+ }));
53114
+ }
53115
+ async createSource(input) {
53116
+ if (!SOURCE_ID_PATTERN.test(input.id)) throw new Error("id must be a lowercase slug (e.g. ics-home, junker)");
53117
+ if (this.config.sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' already exists`);
53118
+ const row = WasteSourceConfigSchema.parse({
53119
+ ...WASTE_SOURCE_CREATE_VALUES,
53120
+ id: input.id,
53121
+ label: input.label
53122
+ });
53123
+ await this.updateGlobalSettings({ sources: [...this.config.sources, row] });
53124
+ return { id: input.id };
53125
+ }
53126
+ async updateSource(input) {
53127
+ const idx = this.config.sources.findIndex((s) => s.id === input.id);
53128
+ if (idx < 0) throw new Error(`source '${input.id}' not found`);
53129
+ const prev = this.config.sources[idx];
53130
+ const next = WasteSourceConfigSchema.parse({
53131
+ id: prev.id,
53132
+ label: input.label,
53133
+ module: input.module ?? prev.module,
53134
+ argsJson: input.argsJson ?? prev.argsJson,
53135
+ enabled: input.enabled ?? prev.enabled
53136
+ });
53137
+ parseSourceArgs(next);
53138
+ const sources = this.config.sources.map((s, i) => i === idx ? next : s);
53139
+ await this.updateGlobalSettings({ sources });
53140
+ return { ok: true };
53141
+ }
53142
+ async deleteSource(input) {
53143
+ if (!this.config.sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' not found`);
53144
+ await this.updateGlobalSettings({ sources: this.config.sources.filter((s) => s.id !== input.id) });
53145
+ return { ok: true };
53146
+ }
52884
53147
  async fetchNow(input) {
52885
53148
  const targets = this.config.sources.filter((s) => {
52886
53149
  if (!s.enabled) return false;
@@ -52983,76 +53246,29 @@ var WasteCollectionsAddon = class extends BaseAddon {
52983
53246
  }
52984
53247
  globalSettingsSchema() {
52985
53248
  return this.schema({ sections: [{
52986
- id: "sources",
52987
- title: "Waste sources",
52988
- description: "Each row becomes one calendar under System → Calendars. `module` must be a HACS source name from the bundled catalog (978 providers). `args` are the provider kwargs — see the HACS docs for that module. Use Fetch now after editing.",
52989
- columns: 1,
52990
- fields: [
52991
- {
52992
- type: "editable-array",
52993
- key: "sources",
52994
- label: "Configured sources",
52995
- itemIdKey: "id",
52996
- itemFields: [
52997
- {
52998
- type: "text",
52999
- key: "id",
53000
- label: "Id",
53001
- description: "Stable id (calendar local id)."
53002
- },
53003
- {
53004
- type: "text",
53005
- key: "label",
53006
- label: "Label"
53007
- },
53008
- {
53009
- type: "text",
53010
- key: "module",
53011
- label: "Source module",
53012
- description: "e.g. `ics`, `junker_app`, `abfallnavi_de`."
53013
- },
53014
- {
53015
- type: "textarea",
53016
- key: "argsJson",
53017
- label: "Args (JSON object)",
53018
- description: "Constructor kwargs for the HACS source, e.g. `{\"url\":\"https://…/calendar.ics\"}`.",
53019
- rows: 4
53020
- },
53021
- {
53022
- type: "boolean",
53023
- key: "enabled",
53024
- label: "Enabled"
53025
- }
53026
- ],
53027
- defaultItem: {
53028
- id: "",
53029
- label: "New source",
53030
- module: "ics",
53031
- argsJson: "{}",
53032
- enabled: true
53033
- }
53034
- },
53035
- {
53036
- type: "number",
53037
- key: "fetchHour",
53038
- label: "Daily fetch hour (local)",
53039
- min: 0,
53040
- max: 23,
53041
- step: 1,
53042
- description: "Informational — the addon refreshes about once per day from boot; use Fetch now to force."
53043
- },
53044
- {
53045
- type: "addon-action-button",
53046
- key: "_fetchNow",
53047
- label: "Refresh",
53048
- buttonLabel: "Fetch now",
53049
- description: "Refresh every enabled source immediately.",
53050
- addonId: "waste-collections",
53051
- action: "fetchNow",
53052
- variant: "primary",
53053
- paramsStatic: {}
53054
- }
53055
- ]
53249
+ id: "refresh",
53250
+ title: "Refresh",
53251
+ description: "Sources are managed in the table above (Add / Edit). Each source becomes one calendar under System → Calendars.",
53252
+ columns: 2,
53253
+ fields: [{
53254
+ type: "number",
53255
+ key: "fetchHour",
53256
+ label: "Daily fetch hour (local)",
53257
+ min: 0,
53258
+ max: 23,
53259
+ step: 1,
53260
+ description: "Informational — the addon refreshes about once per day from boot; use Fetch now to force."
53261
+ }, {
53262
+ type: "addon-action-button",
53263
+ key: "_fetchNow",
53264
+ label: "Refresh",
53265
+ buttonLabel: "Fetch now",
53266
+ description: "Refresh every enabled source immediately.",
53267
+ addonId: "waste-collections",
53268
+ action: "fetchNow",
53269
+ variant: "primary",
53270
+ paramsStatic: {}
53271
+ }]
53056
53272
  }] });
53057
53273
  }
53058
53274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-waste-collections",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Waste collection schedules via the vendored HACS waste_collection_schedule Python core — full source catalog as calendar-source.",
5
5
  "keywords": [
6
6
  "camstack",