@norskvideo/ctl-sdk 0.1.53 → 0.1.54

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.
@@ -225,7 +225,7 @@ export declare const ManifestSchema: z.ZodObject<{
225
225
  name: z.ZodString;
226
226
  url: z.ZodString;
227
227
  }, z.core.$strip>>>;
228
- defaultDashboards: z.ZodDefault<z.ZodArray<z.ZodObject<{
228
+ defaultDashboards: z.ZodOptional<z.ZodArray<z.ZodObject<{
229
229
  name: z.ZodString;
230
230
  displayName: z.ZodOptional<z.ZodString>;
231
231
  url: z.ZodString;
@@ -210,7 +210,7 @@ export const ManifestSchema = z.object({
210
210
  defaultProductTemplates: z.array(DefaultProductTemplateSchema).default([]).meta({
211
211
  description: "Product templates the runner auto-fetches at product-registration time. Each entry is GET'd against the product's base URL and stored under its `name` via the runner's product-template store. Lets a product offer 'zero-config' launch paths — operators add the product and immediately have a launchable product template without visiting configure.",
212
212
  }),
213
- defaultDashboards: z.array(DefaultDashboardSchema).default([]).meta({
213
+ defaultDashboards: z.array(DefaultDashboardSchema).optional().meta({
214
214
  description: "Fleet dashboards the product brings: norsk-mgr fetches each bundle from the product at registration and on every reload, registers it as the product's, and removes it with the product. norsk-ctl serving alone ignores them.",
215
215
  }),
216
216
  export: ExportSchema.optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-sdk",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,7 +1,7 @@
1
1
  import { type Result } from "@norskvideo/ctl-foundation";
2
2
  import { type ProductPlacement } from "./docker-runner.js";
3
3
  import { type LicenseStager } from "./license-registration.js";
4
- import type { Manifest, ProductRoleDeclaration } from "./manifest-schema.js";
4
+ import type { DefaultDashboard, Manifest, ProductRoleDeclaration } from "./manifest-schema.js";
5
5
  import { type ProductReach } from "./product-reach.js";
6
6
  import type { ProductTemplateSource } from "./product-template-record.js";
7
7
  import type { ProductLicense, ProductRegistration, ProductSpec, RoleDefault } from "./product-types.js";
@@ -99,8 +99,22 @@ export interface ProductContainerOps {
99
99
  /** The real docker-runner wiring. Exported so a host can override one op
100
100
  * (typically `pull`, onto its own docker adapter) and keep the rest. */
101
101
  export declare const defaultProductContainerOps: ProductContainerOps;
102
+ /** A fleet dashboard a product brings, handed to the host to register or
103
+ * replace in place (norsk-ctl task 123 G). */
104
+ export type ImportDashboardBytesFn = (input: {
105
+ productName: string;
106
+ entry: DefaultDashboard;
107
+ bytes: Uint8Array;
108
+ }) => Promise<void>;
102
109
  export interface ProductServiceOptions {
103
110
  store: ProductStore;
111
+ /** Optional: called once per `manifest.defaultDashboards` entry at add and
112
+ * at every reload, with the bundle fetched from the product. Omit on hosts
113
+ * that host no dashboards (norsk-ctl serving alone): they are then skipped. */
114
+ importDashboardBytes?: ImportDashboardBytesFn;
115
+ /** Optional: told when a product is removed, so the host removes the
116
+ * dashboards it brought. */
117
+ removeProductDashboards?: (productName: string) => Promise<void>;
104
118
  /** Optional: called once per `manifest.defaultProductTemplates` entry at
105
119
  * registration time. Omit on hosts that don't store product templates
106
120
  * (defaultProductTemplates is then silently skipped). */
@@ -211,6 +225,8 @@ export declare class ProductService {
211
225
  private readonly store;
212
226
  private readonly importProductTemplateBytes?;
213
227
  private readonly refreshProductTemplateBytes?;
228
+ private readonly importDashboardBytes?;
229
+ private readonly removeProductDashboards?;
214
230
  private readonly stageLicense?;
215
231
  private readonly isDevUrlAlive;
216
232
  private readonly containerOps;
@@ -255,6 +271,9 @@ export declare class ProductService {
255
271
  private changed;
256
272
  add(spec: ProductSpec, opts?: AddProductOpts): Promise<AddProductResult>;
257
273
  private addSerialised;
274
+ /** Each dashboard the manifest declares, fetched from the product and handed
275
+ * to the host; the warnings for those that could not be. */
276
+ private bringDashboards;
258
277
  remove(name: string): Promise<void>;
259
278
  private removeSerialised;
260
279
  /** Bring a registration up to date with what its image now serves. For a
@@ -55,6 +55,8 @@ export class ProductService {
55
55
  store;
56
56
  importProductTemplateBytes;
57
57
  refreshProductTemplateBytes;
58
+ importDashboardBytes;
59
+ removeProductDashboards;
58
60
  stageLicense;
59
61
  isDevUrlAlive;
60
62
  containerOps;
@@ -67,6 +69,8 @@ export class ProductService {
67
69
  this.store = opts.store;
68
70
  this.importProductTemplateBytes = opts.importProductTemplateBytes;
69
71
  this.refreshProductTemplateBytes = opts.refreshProductTemplateBytes;
72
+ this.importDashboardBytes = opts.importDashboardBytes;
73
+ this.removeProductDashboards = opts.removeProductDashboards;
70
74
  this.stageLicense = opts.stageLicense;
71
75
  this.isDevUrlAlive = opts.isDevUrlAlive ?? isDevUrlAlive;
72
76
  this.containerOps = opts.containerOps ?? defaultProductContainerOps;
@@ -374,10 +378,29 @@ export class ProductService {
374
378
  }
375
379
  }
376
380
  }
381
+ warnings.push(...(await this.bringDashboards(manifest, baseUrl)));
377
382
  for (const w of warnings)
378
383
  logger.warn(`[product:${manifest.name}] ${w}`);
379
384
  return { registration, warnings };
380
385
  }
386
+ /** Each dashboard the manifest declares, fetched from the product and handed
387
+ * to the host; the warnings for those that could not be. */
388
+ async bringDashboards(manifest, baseUrl) {
389
+ if (!this.importDashboardBytes)
390
+ return [];
391
+ const warnings = [];
392
+ for (const entry of manifest.defaultDashboards ?? []) {
393
+ try {
394
+ const bytes = await fetchProductTemplateBytes(baseUrl, entry.url, this.probeHeadersFor?.(manifest.name) ?? {});
395
+ await this.importDashboardBytes({ productName: manifest.name, entry, bytes });
396
+ logger.info(`Product '${manifest.name}': dashboard '${entry.name}' from ${entry.url}`);
397
+ }
398
+ catch (e) {
399
+ warnings.push(`dashboard '${entry.name}' from ${entry.url} failed: ${e instanceof Error ? e.message : String(e)}`);
400
+ }
401
+ }
402
+ return warnings;
403
+ }
381
404
  async remove(name) {
382
405
  return this.changed(await this.mutations.run(() => this.removeSerialised(name)));
383
406
  }
@@ -395,6 +418,11 @@ export class ProductService {
395
418
  // so a re-added product of the same name is stamped anew.
396
419
  this.forgetIdentity?.(name);
397
420
  this.onRemoved?.(name);
421
+ // The product's dashboards go with it; a failure leaves them registered,
422
+ // for the operator to remove, rather than the product half-removed.
423
+ await this.removeProductDashboards?.(name).catch((e) => {
424
+ logger.warn(`[product:${name}] its dashboards were not removed: ${e instanceof Error ? e.message : String(e)}`);
425
+ });
398
426
  logger.info(`Product '${name}' removed`);
399
427
  }
400
428
  /** Bring a registration up to date with what its image now serves. For a
@@ -478,6 +506,7 @@ export class ProductService {
478
506
  }
479
507
  }
480
508
  }
509
+ warnings.push(...(await this.bringDashboards(manifest, baseUrl)));
481
510
  for (const w of warnings)
482
511
  logger.warn(`[product:${name}] ${w}`);
483
512
  return { registration, warnings, containerRestarted, productTemplates };