@norskvideo/ctl-sdk 0.1.14 → 0.1.16
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/components/ProductIframe.d.ts +9 -0
- package/components/ProductIframe.js +11 -1
- package/components/ProductTemplateBuildForm.d.ts +10 -1
- package/components/ProductTemplateBuildForm.js +38 -3
- package/components/ShotBar.js +1 -1
- package/iframe-protocol.d.ts +6 -0
- package/manifest-schema.d.ts +50 -1
- package/manifest-schema.js +4 -0
- package/package.json +1 -1
- package/product-template-parsing.js +6 -0
- package/product-template-record.d.ts +13 -0
|
@@ -21,6 +21,15 @@ export declare const ProductIframe: import("react").ForwardRefExoticComponent<{
|
|
|
21
21
|
* don't emit it leave the field's default behaviour unchanged.
|
|
22
22
|
*/
|
|
23
23
|
onSuggestName?: (name: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Fires when the iframe emits a `requirements` postMessage — the product's
|
|
26
|
+
* suggested placement requirements for the template being configured, which
|
|
27
|
+
* it may recompute as the operator changes the config. The raw payload is
|
|
28
|
+
* forwarded (kept schema-agnostic here); the parent validates + pre-fills its
|
|
29
|
+
* requirements editor. Products that don't emit it leave the editor at its
|
|
30
|
+
* static/blank default.
|
|
31
|
+
*/
|
|
32
|
+
onRequirements?: (requirements: unknown) => void;
|
|
24
33
|
/**
|
|
25
34
|
* Optional product-template/launch context forwarded verbatim in the `init`
|
|
26
35
|
* payload (as `launchContext`). Used by the instance-launch config screen
|
|
@@ -6,7 +6,7 @@ function resolveSrc(productName, configScreenUrl) {
|
|
|
6
6
|
const path = configScreenUrl.startsWith("/") ? configScreenUrl : `/${configScreenUrl}`;
|
|
7
7
|
return `/products/${encodeURIComponent(productName)}${path}`;
|
|
8
8
|
}
|
|
9
|
-
export const ProductIframe = forwardRef(function ProductIframe({ productName, configScreenUrl, onValidityChange, onSuggestName, launchContext }, ref) {
|
|
9
|
+
export const ProductIframe = forwardRef(function ProductIframe({ productName, configScreenUrl, onValidityChange, onSuggestName, onRequirements, launchContext }, ref) {
|
|
10
10
|
const iframeRef = useRef(null);
|
|
11
11
|
const [height, setHeight] = useState(420);
|
|
12
12
|
const [valid, setValid] = useState(true);
|
|
@@ -22,6 +22,10 @@ export const ProductIframe = forwardRef(function ProductIframe({ productName, co
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
onSuggestNameRef.current = onSuggestName;
|
|
24
24
|
}, [onSuggestName]);
|
|
25
|
+
const onRequirementsRef = useRef(onRequirements);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
onRequirementsRef.current = onRequirements;
|
|
28
|
+
}, [onRequirements]);
|
|
25
29
|
const src = resolveSrc(productName, configScreenUrl);
|
|
26
30
|
useEffect(() => {
|
|
27
31
|
const onMessage = (event) => {
|
|
@@ -53,6 +57,12 @@ export const ProductIframe = forwardRef(function ProductIframe({ productName, co
|
|
|
53
57
|
onSuggestNameRef.current?.(payload.name);
|
|
54
58
|
break;
|
|
55
59
|
}
|
|
60
|
+
case "requirements": {
|
|
61
|
+
// Forward the raw suggestion; the parent validates against the schema.
|
|
62
|
+
if ("requirements" in payload)
|
|
63
|
+
onRequirementsRef.current?.(payload.requirements);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
56
66
|
case "submit-result": {
|
|
57
67
|
const resolve = submitResolverRef.current;
|
|
58
68
|
submitResolverRef.current = null;
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
export type ProductTemplateBuildRequest = {
|
|
15
15
|
productName: string;
|
|
16
16
|
productTemplateName: string;
|
|
17
|
+
/** Config-screen values, forwarded verbatim to the product's build
|
|
18
|
+
* endpoint. May additionally carry `$advancedDefaults` — create-time
|
|
19
|
+
* custom defaults from the Custom defaults disclosure, in the
|
|
20
|
+
* ProductTemplateAdvanced per-knob `{ default }` shape — which a
|
|
21
|
+
* dev-kit-generated product splits off before its strict config parse
|
|
22
|
+
* and bakes into the manifest's `advanced` block. */
|
|
17
23
|
formValues: Record<string, unknown>;
|
|
18
24
|
};
|
|
19
25
|
export type ProductTemplateBuildOutcome<TProductTemplate> = {
|
|
@@ -23,10 +29,13 @@ export type ProductTemplateBuildOutcome<TProductTemplate> = {
|
|
|
23
29
|
ok: false;
|
|
24
30
|
error: string;
|
|
25
31
|
};
|
|
26
|
-
export declare function ProductTemplateBuildForm<TProductTemplate>({ productName, configScreenUrl, onClose, onSubmit, onSaved, onSavedAndLaunch, }: {
|
|
32
|
+
export declare function ProductTemplateBuildForm<TProductTemplate>({ productName, configScreenUrl, onClose, onSubmit, onSaved, onSavedAndLaunch, onRequirements, }: {
|
|
27
33
|
productName: string;
|
|
28
34
|
configScreenUrl: string | null;
|
|
29
35
|
onClose: () => void;
|
|
36
|
+
/** Forwarded from the config-screen iframe: the product's suggested placement
|
|
37
|
+
* requirements (raw payload; the consumer validates). Optional. */
|
|
38
|
+
onRequirements?: (requirements: unknown) => void;
|
|
30
39
|
/**
|
|
31
40
|
* Persists the product template. Implementation owns the wire protocol —
|
|
32
41
|
* runner stores on disk; manager submits to manager-daemon. Returns the
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { X } from "lucide-react";
|
|
2
|
+
import { ChevronDown, ChevronRight, X } from "lucide-react";
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
4
|
import toast from "react-hot-toast";
|
|
5
5
|
import { validateInstanceId } from "../validate.js";
|
|
6
6
|
import { ProductIframe } from "./ProductIframe.js";
|
|
7
7
|
import { useUIPrimitives } from "./ui-primitives.js";
|
|
8
|
-
|
|
8
|
+
const emptyAdvancedDefaults = {
|
|
9
|
+
networkMode: "",
|
|
10
|
+
hostPorts: "",
|
|
11
|
+
containerUser: "",
|
|
12
|
+
shmSize: "",
|
|
13
|
+
openFileLimit: "",
|
|
14
|
+
};
|
|
15
|
+
/** Assemble the disclosure's draft into the ProductTemplateAdvanced per-knob
|
|
16
|
+
* `{ default }` shape. Only set fields attach; undefined when untouched, so
|
|
17
|
+
* the build request stays byte-identical for authors who ignore it. */
|
|
18
|
+
function assembleAdvancedDefaults(d) {
|
|
19
|
+
const out = {};
|
|
20
|
+
if (d.networkMode)
|
|
21
|
+
out.networkMode = { default: d.networkMode };
|
|
22
|
+
const ports = d.hostPorts
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((t) => t.trim())
|
|
25
|
+
.filter((t) => t.length > 0);
|
|
26
|
+
if (ports.length > 0)
|
|
27
|
+
out.hostPorts = { default: ports };
|
|
28
|
+
if (d.containerUser.trim())
|
|
29
|
+
out.containerUser = { default: d.containerUser.trim() };
|
|
30
|
+
if (d.shmSize.trim())
|
|
31
|
+
out.shmSize = { default: d.shmSize.trim() };
|
|
32
|
+
const limit = Number(d.openFileLimit);
|
|
33
|
+
if (d.openFileLimit.trim() && Number.isInteger(limit))
|
|
34
|
+
out.openFileLimit = { default: limit };
|
|
35
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
36
|
+
}
|
|
37
|
+
export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose, onSubmit, onSaved, onSavedAndLaunch, onRequirements, }) {
|
|
9
38
|
const { Button, Input, Label } = useUIPrimitives();
|
|
10
39
|
const [productTemplateName, setProductTemplateName] = useState("");
|
|
11
40
|
// The iframe can suggest a name derived from its current choices (#317).
|
|
@@ -19,6 +48,9 @@ export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose
|
|
|
19
48
|
// products that don't care about validation aren't required to emit).
|
|
20
49
|
const [iframeValid, setIframeValid] = useState(true);
|
|
21
50
|
const iframeRef = useRef(null);
|
|
51
|
+
const [defaultsOpen, setDefaultsOpen] = useState(false);
|
|
52
|
+
const [advancedDraft, setAdvancedDraft] = useState(emptyAdvancedDefaults);
|
|
53
|
+
const patchDraft = (patch) => setAdvancedDraft((d) => ({ ...d, ...patch }));
|
|
22
54
|
const handleSuggestName = (name) => {
|
|
23
55
|
setSuggestedName(name);
|
|
24
56
|
if (!nameTouched)
|
|
@@ -46,6 +78,9 @@ export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose
|
|
|
46
78
|
}
|
|
47
79
|
formValues = result.formValues;
|
|
48
80
|
}
|
|
81
|
+
const advanced = assembleAdvancedDefaults(advancedDraft);
|
|
82
|
+
if (advanced)
|
|
83
|
+
formValues = { ...formValues, $advancedDefaults: advanced };
|
|
49
84
|
const outcome = await onSubmit({ productName, productTemplateName, formValues });
|
|
50
85
|
if (!outcome.ok) {
|
|
51
86
|
toast.error(`Save product template failed: ${outcome.error}`);
|
|
@@ -83,5 +118,5 @@ export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose
|
|
|
83
118
|
return (_jsxs("div", { className: "mb-6 rounded-lg border border-zinc-700 bg-zinc-900 p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { children: [_jsxs("h4", { className: "text-sm font-semibold text-zinc-100", children: ["Create ", productName, " product template"] }), _jsx("p", { className: "text-xs text-zinc-500 mt-0.5", children: "Saves a reusable product template. Launch instances from it later." })] }), _jsx("button", { type: "button", onClick: onClose, className: "text-zinc-500 hover:text-zinc-300", children: _jsx(X, { size: 14 }) })] }), _jsxs("form", { onSubmit: handleSave, className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { children: "Product template name *" }), _jsx(Input, { value: productTemplateName, onChange: (e) => {
|
|
84
119
|
setNameTouched(true);
|
|
85
120
|
setProductTemplateName(e.target.value);
|
|
86
|
-
}, placeholder: suggestedName || `e.g. ${productName}-prod`, required: true }), productTemplateNameError ? (_jsx("p", { className: "text-xs text-red-400 mt-1", children: productTemplateNameError })) : (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Lowercase letters, numbers, and hyphens only. Used to launch instances later." }))] }), configScreenUrl ? (_jsx(ProductIframe, { ref: iframeRef, productName: productName, configScreenUrl: configScreenUrl, onValidityChange: (v) => setIframeValid(v), onSuggestName: handleSuggestName })) : (_jsxs("p", { className: "text-xs text-zinc-500", children: ["Product manifest does not declare ", _jsx("code", { children: "ui.configScreenUrl" }), " \u2014 nothing to configure."] })), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "primary", disabled: !canSubmit, onClick: handleSaveAndLaunch, children: submitting ? "Working…" : "Save & Launch…" }), _jsx(Button, { type: "submit", variant: "default", disabled: !canSubmit, children: "Save" }), _jsx(Button, { type: "button", variant: "ghost", onClick: onClose, children: "Cancel" })] })] })] }));
|
|
121
|
+
}, placeholder: suggestedName || `e.g. ${productName}-prod`, required: true }), productTemplateNameError ? (_jsx("p", { className: "text-xs text-red-400 mt-1", children: productTemplateNameError })) : (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Lowercase letters, numbers, and hyphens only. Used to launch instances later." }))] }), configScreenUrl ? (_jsx(ProductIframe, { ref: iframeRef, productName: productName, configScreenUrl: configScreenUrl, onValidityChange: (v) => setIframeValid(v), onSuggestName: handleSuggestName, ...(onRequirements ? { onRequirements } : {}) })) : (_jsxs("p", { className: "text-xs text-zinc-500", children: ["Product manifest does not declare ", _jsx("code", { children: "ui.configScreenUrl" }), " \u2014 nothing to configure."] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setDefaultsOpen((v) => !v), className: "flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-500 hover:text-zinc-300", children: [defaultsOpen ? _jsx(ChevronDown, { size: 12 }) : _jsx(ChevronRight, { size: 12 }), "Custom defaults"] }), defaultsOpen && (_jsxs("div", { className: "mt-2", children: [_jsx("p", { className: "text-xs text-zinc-500 mb-2", children: "Launch-form defaults baked into this template's manifest. Operators can still override them per instance." }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Network mode" }), _jsxs("select", { "aria-label": "Network mode", value: advancedDraft.networkMode, onChange: (e) => patchDraft({ networkMode: e.target.value }), className: "block rounded border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm text-zinc-100", children: [_jsx("option", { value: "", children: "product default" }), _jsx("option", { value: "docker", children: "docker" }), _jsx("option", { value: "hybrid", children: "hybrid" })] })] }), _jsxs("div", { className: "min-w-[14rem] flex-1", children: [_jsx(Label, { children: "Host ports" }), _jsx(Input, { "aria-label": "Host ports", placeholder: "e.g. 1935, 5001/udp, 9000:80@svc", value: advancedDraft.hostPorts, onChange: (e) => patchDraft({ hostPorts: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Container user" }), _jsx(Input, { "aria-label": "Container user", placeholder: "uid:gid", value: advancedDraft.containerUser, onChange: (e) => patchDraft({ containerUser: e.target.value }), className: "w-28" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Shared memory size" }), _jsx(Input, { "aria-label": "Shared memory size", placeholder: "e.g. 8gb", value: advancedDraft.shmSize, onChange: (e) => patchDraft({ shmSize: e.target.value }), className: "w-24" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Open file limit" }), _jsx(Input, { "aria-label": "Open file limit", type: "number", min: 1024, max: 1048576, placeholder: "runner default", value: advancedDraft.openFileLimit, onChange: (e) => patchDraft({ openFileLimit: e.target.value }), className: "w-32" })] })] })] }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "primary", disabled: !canSubmit, onClick: handleSaveAndLaunch, children: submitting ? "Working…" : "Save & Launch…" }), _jsx(Button, { type: "submit", variant: "default", disabled: !canSubmit, children: "Save" }), _jsx(Button, { type: "button", variant: "ghost", onClick: onClose, children: "Cancel" })] })] })] }));
|
|
87
122
|
}
|
package/components/ShotBar.js
CHANGED
|
@@ -28,7 +28,7 @@ export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transitio
|
|
|
28
28
|
// preview take handler or a preview monitor renderer). Otherwise it's a dead
|
|
29
29
|
// pane — a direct-take product has no PVW feed — so give On Air the full row.
|
|
30
30
|
const showPreview = Boolean(onPreview || renderPreviewMonitor);
|
|
31
|
-
return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className:
|
|
31
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: "grid gap-3", style: { gridTemplateColumns: showPreview ? "minmax(0, 1fr) minmax(0, 1fr)" : "minmax(0, 1fr)" }, children: [showPreview ? (_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor ? () => renderPreviewMonitor(selected ?? null) : undefined })) : null, _jsx(Monitor, { label: "Live", tone: "pgm", shot: findLabel(shots, pgmShotId), render: renderProgramMonitor })] }), _jsx("div", { className: "flex gap-3 overflow-x-auto pb-1", children: shots.map((shot) => (_jsx(ShotCard, { shot: shot, selected: shot.id === selectedId, onAir: shot.id === pgmShotId, config: shot.status === "ready" ? shot.config : undefined, onSelect: () => shot.status === "ready" && setSelectedId(shot.id) }, shot.id))) }), selected ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/60 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [selected.label, previewing ? (_jsx("span", { className: "rounded bg-emerald-600/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400", children: "Previewing" })) : null] }), _jsxs("div", { className: "flex gap-2", children: [previewing ? (onCancelPreview ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onCancelPreview, children: "Cancel" })) : null) : onPreview ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: !takeableGraphic, onClick: () => onPreview(commands, selected), children: "Preview" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !takeableGraphic, onClick: () => onTake(commands, selected), children: "Take" })] })] }), !takeableGraphic ? (_jsx("p", { className: "text-xs text-amber-500/90", children: "This shot's graphic page is a relative path but no engine-reachable graphic base is configured, so it can't be loaded into the overlay. Set an absolute page URL, or configure the overlay base." })) : null, editableGraphics && selected.graphicUrl !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1 text-xs text-zinc-400", children: [_jsx("label", { htmlFor: "shotbar-graphic-url", children: "Graphic page" }), _jsx(Input, { id: "shotbar-graphic-url", type: "text", value: editedUrls[selected.id] ?? selected.graphicUrl, placeholder: selected.graphicUrl, onChange: (e) => setEditedUrls((prev) => ({ ...prev, [selected.id]: e.target.value })) })] })) : null] })) : (_jsx("div", { className: "rounded-lg border border-dashed border-zinc-800 p-4 text-center text-sm text-zinc-500", children: "No shot selected \u2014 pick an available shot above." }))] }));
|
|
32
32
|
}
|
|
33
33
|
function findLabel(shots, id) {
|
|
34
34
|
if (!id)
|
package/iframe-protocol.d.ts
CHANGED
package/manifest-schema.d.ts
CHANGED
|
@@ -91,8 +91,8 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
91
91
|
}, z.core.$strip>>>;
|
|
92
92
|
}, z.core.$strip>>;
|
|
93
93
|
targets: z.ZodArray<z.ZodEnum<{
|
|
94
|
-
"norsk-ctl": "norsk-ctl";
|
|
95
94
|
"docker-compose": "docker-compose";
|
|
95
|
+
"norsk-ctl": "norsk-ctl";
|
|
96
96
|
}>>;
|
|
97
97
|
components: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
98
98
|
name: z.ZodString;
|
|
@@ -102,6 +102,55 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
102
102
|
runtime: z.ZodDefault<z.ZodObject<{
|
|
103
103
|
sharedWorkingDirectory: z.ZodDefault<z.ZodBoolean>;
|
|
104
104
|
}, z.core.$strip>>;
|
|
105
|
+
requirements: z.ZodOptional<z.ZodObject<{
|
|
106
|
+
capacity: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
mode: z.ZodEnum<{
|
|
108
|
+
default: "default";
|
|
109
|
+
fixed: "fixed";
|
|
110
|
+
required: "required";
|
|
111
|
+
}>;
|
|
112
|
+
value: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
}, z.core.$strip>>;
|
|
114
|
+
cores: z.ZodOptional<z.ZodObject<{
|
|
115
|
+
mode: z.ZodEnum<{
|
|
116
|
+
default: "default";
|
|
117
|
+
fixed: "fixed";
|
|
118
|
+
required: "required";
|
|
119
|
+
}>;
|
|
120
|
+
value: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
}, z.core.$strip>>;
|
|
122
|
+
gpu: z.ZodOptional<z.ZodObject<{
|
|
123
|
+
mode: z.ZodEnum<{
|
|
124
|
+
default: "default";
|
|
125
|
+
fixed: "fixed";
|
|
126
|
+
required: "required";
|
|
127
|
+
}>;
|
|
128
|
+
value: z.ZodOptional<z.ZodObject<{
|
|
129
|
+
capacity: z.ZodOptional<z.ZodNumber>;
|
|
130
|
+
model: z.ZodOptional<z.ZodString>;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
}, z.core.$strip>>;
|
|
133
|
+
capabilities: z.ZodOptional<z.ZodObject<{
|
|
134
|
+
mode: z.ZodEnum<{
|
|
135
|
+
default: "default";
|
|
136
|
+
fixed: "fixed";
|
|
137
|
+
required: "required";
|
|
138
|
+
}>;
|
|
139
|
+
value: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
140
|
+
name: z.ZodString;
|
|
141
|
+
count: z.ZodOptional<z.ZodNumber>;
|
|
142
|
+
attributeMatches: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
143
|
+
}, z.core.$strip>>>;
|
|
144
|
+
}, z.core.$strip>>;
|
|
145
|
+
hostPorts: z.ZodOptional<z.ZodObject<{
|
|
146
|
+
mode: z.ZodEnum<{
|
|
147
|
+
default: "default";
|
|
148
|
+
fixed: "fixed";
|
|
149
|
+
required: "required";
|
|
150
|
+
}>;
|
|
151
|
+
value: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
152
|
+
}, z.core.$strip>>;
|
|
153
|
+
}, z.core.$strip>>;
|
|
105
154
|
defaultProductTemplates: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
106
155
|
name: z.ZodString;
|
|
107
156
|
url: z.ZodString;
|
package/manifest-schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProductTemplateRequirementsSchema } from "@norskvideo/ctl-product-template-schema";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
const TargetSchema = z.enum(["norsk-ctl", "docker-compose"]);
|
|
3
4
|
const SidebarEntrySchema = z.object({
|
|
@@ -137,6 +138,9 @@ export const ManifestSchema = z.object({
|
|
|
137
138
|
targets: z.array(TargetSchema),
|
|
138
139
|
components: z.array(ComponentDescSchema).default([]),
|
|
139
140
|
runtime: RuntimeHintsSchema,
|
|
141
|
+
requirements: ProductTemplateRequirementsSchema.optional().meta({
|
|
142
|
+
description: "Optional default placement requirements the product suggests (requirements-authoring.md). Same fixed/default/required shape as a product template's manifest — norsk-mgr pre-fills the template + instance requirements editors from it, and the operator can edit. Omit if the product expresses no requirements (the editors default to blank). Products may also refine these live via the config-screen iframe.",
|
|
143
|
+
}),
|
|
140
144
|
defaultProductTemplates: z.array(DefaultProductTemplateSchema).default([]).meta({
|
|
141
145
|
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.",
|
|
142
146
|
}),
|
package/package.json
CHANGED
|
@@ -47,6 +47,12 @@ export function parseProductTemplatesFile(raw, path) {
|
|
|
47
47
|
};
|
|
48
48
|
if (typeof entry.sha256 === "string")
|
|
49
49
|
rec.sha256 = entry.sha256;
|
|
50
|
+
if (isRecord(entry.launchDefaults))
|
|
51
|
+
rec.launchDefaults = entry.launchDefaults;
|
|
52
|
+
if (Array.isArray(entry.suggestedSecurityGroups) &&
|
|
53
|
+
entry.suggestedSecurityGroups.every((l) => typeof l === "string")) {
|
|
54
|
+
rec.suggestedSecurityGroups = entry.suggestedSecurityGroups;
|
|
55
|
+
}
|
|
50
56
|
if (isRecord(entry.source)) {
|
|
51
57
|
if (entry.source.kind === "import" && typeof entry.source.fromFile === "string") {
|
|
52
58
|
rec.source = { kind: "import", fromFile: entry.source.fromFile };
|
|
@@ -42,4 +42,17 @@ export interface ProductTemplateRecord {
|
|
|
42
42
|
/** Lowercase hex SHA-256 of the source tar bytes. Optional for
|
|
43
43
|
* historic records that predate the field. */
|
|
44
44
|
sha256?: string;
|
|
45
|
+
/** Template-author launch defaults — the fusion tier between the
|
|
46
|
+
* manifest's advanced defaults and operator intent
|
|
47
|
+
* (placement-requirements.md §7). Shape is norsk-ctl's
|
|
48
|
+
* ProductTemplateLaunchConfig; validated by the store's writer and
|
|
49
|
+
* authoritatively by the worker at launch. Rides launches as
|
|
50
|
+
* template_launch_defaults_json. */
|
|
51
|
+
launchDefaults?: Record<string, unknown>;
|
|
52
|
+
/** Template-author security-group suggestions, by discovery-tag LABEL
|
|
53
|
+
* (portable across deployments; ids are not). The launch UI pre-ticks
|
|
54
|
+
* discovered groups whose label matches — a suggestion the operator
|
|
55
|
+
* can untick, never a pin, and never a wire field of its own: the
|
|
56
|
+
* confirmed selection rides the job as extraSecurityGroups ids. */
|
|
57
|
+
suggestedSecurityGroups?: string[];
|
|
45
58
|
}
|