@norskvideo/ctl-dev-kit 0.2.106 → 0.2.108
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/conventions/checks.sdk-app.yml +9 -2
- package/create-demo/create-demo.ts +191 -0
- package/create-demo/template/core/CLAUDE.md.tmpl +158 -0
- package/create-demo/template/core/README.md.tmpl +74 -0
- package/create-demo/template/core/ctl/install.sh +27 -0
- package/create-demo/template/core/ctl/lib.sh +146 -0
- package/create-demo/template/core/ctl/mode.sh +19 -0
- package/create-demo/template/core/ctl/push.sh +16 -0
- package/create-demo/template/core/ctl/site.example.env +30 -0
- package/create-demo/template/core/ctl/studio.example.env +12 -0
- package/create-demo/template/core/gitignore +18 -0
- package/create-demo/template/core/scripts/build.sh +30 -0
- package/create-demo/template/core/scripts/test-feed.sh +44 -0
- package/create-demo/template/dashboard/dashboard/index.html +13 -0
- package/create-demo/template/dashboard/dashboard/package.json +22 -0
- package/create-demo/template/dashboard/dashboard/src/App.tsx +61 -0
- package/create-demo/template/dashboard/dashboard/src/api/types.ts +38 -0
- package/create-demo/template/dashboard/dashboard/src/components/ImagePreview.tsx +23 -0
- package/create-demo/template/dashboard/dashboard/src/components/StatusPill.tsx +65 -0
- package/create-demo/template/dashboard/dashboard/src/hooks/useAlerts.ts +98 -0
- package/create-demo/template/dashboard/dashboard/src/hooks/useDiscovery.ts +88 -0
- package/create-demo/template/dashboard/dashboard/src/hooks/useEnv.ts +53 -0
- package/create-demo/template/dashboard/dashboard/src/hooks/useLiveComponent.ts +249 -0
- package/create-demo/template/dashboard/dashboard/src/lib/workflow.ts +11 -0
- package/create-demo/template/dashboard/dashboard/src/main.tsx +12 -0
- package/create-demo/template/dashboard/dashboard/src/styles.css +84 -0
- package/create-demo/template/dashboard/dashboard/src/vite-env.d.ts +1 -0
- package/create-demo/template/dashboard/dashboard/tsconfig.json +18 -0
- package/create-demo/template/dashboard/dashboard/vite.config.ts +35 -0
- package/create-product/cli.ts +39 -1
- package/create-product/features/components.ts +10 -1
- package/package.json +1 -1
|
@@ -246,11 +246,18 @@ jobs:
|
|
|
246
246
|
# The image name is CI's own rule: PUBLISH_IMAGE when set, else the build
|
|
247
247
|
# script's IMAGE_TAG default, tag stripped -- a licence matches the repo
|
|
248
248
|
# part alone, never the tag.
|
|
249
|
+
#
|
|
250
|
+
# PRODUCT_NAME is read as smoke.yml reads it: an sdk-app keeps it in
|
|
251
|
+
# src/product/version.ts (norsk-relay) or shared/src/version.ts
|
|
252
|
+
# (norsk-conversion); the first that exists is read.
|
|
249
253
|
- name: Check this repo records its own licence
|
|
250
254
|
run: |
|
|
251
255
|
set -euo pipefail
|
|
252
|
-
|
|
253
|
-
[ -
|
|
256
|
+
vf=""
|
|
257
|
+
for f in shared/src/version.ts src/product/version.ts; do [ -f "$f" ] && { vf="$f"; break; }; done
|
|
258
|
+
[ -n "$vf" ] || { echo "::error::no shared/src/version.ts or src/product/version.ts"; exit 1; }
|
|
259
|
+
product="$(sed -nE 's/.*PRODUCT_NAME[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$vf" | head -1)"
|
|
260
|
+
[ -n "$product" ] || { echo "::error::could not read PRODUCT_NAME from $vf"; exit 1; }
|
|
254
261
|
pub="$(sed -nE 's/^[[:space:]]*PUBLISH_IMAGE:[[:space:]]*"?([^" ]*)"?[[:space:]]*$/\1/p' .github/workflows/build-image.yml | head -1)"
|
|
255
262
|
def="$(sed -nE 's/.*IMAGE_TAG:-([^}]+)\}.*/\1/p' deployment/build-image.sh | head -1)"
|
|
256
263
|
ref="${pub:-$def}"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// `ctl-dev-kit create-demo` — renders a new customer demo repo: a Studio
|
|
2
|
+
// working directory, the scripts that install it into norsk-ctl, and the docs
|
|
3
|
+
// that make every demo look the same. Unlike create-product, the output is
|
|
4
|
+
// copied once and then owned by the customer: there is no drift gate, no CI and
|
|
5
|
+
// no canonical bytes to stay in step with.
|
|
6
|
+
//
|
|
7
|
+
// The files live under template/ as real files, with `__DEMO__` where the demo's
|
|
8
|
+
// name goes, so they can be read and edited as what they are:
|
|
9
|
+
// template/core/ every demo
|
|
10
|
+
// template/<feature>/ added by --with <feature>
|
|
11
|
+
// Markdown is kept as `<name>.md.tmpl` and emitted as `<name>.md`: as a .md the
|
|
12
|
+
// repo's formatter would rewrite it (`__DEMO__` becomes bold, and the feature
|
|
13
|
+
// markers split its tables). It may carry `<!-- if:<feature> -->` ...
|
|
14
|
+
// `<!-- end:<feature> -->` blocks, kept when that feature is on and dropped
|
|
15
|
+
// when it is off. `gitignore` is emitted as `.gitignore`: npm drops a
|
|
16
|
+
// .gitignore from a published package.
|
|
17
|
+
//
|
|
18
|
+
// The components feature is not a template: it is create-product's components/
|
|
19
|
+
// workspace (its build chain and example node), without the product glue.
|
|
20
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join, relative } from "node:path";
|
|
22
|
+
import { type GeneratedFile, shapeContext } from "../create-product/create-product.ts";
|
|
23
|
+
import { componentsWorkspaceFiles, EXAMPLE_IDENTIFIER } from "../create-product/features/components.ts";
|
|
24
|
+
|
|
25
|
+
// - dashboard an operator dashboard, built into the workflow's dashboards/
|
|
26
|
+
// and served by Studio at /dashboard/<name>/
|
|
27
|
+
// - components this demo's own Studio nodes, built into the working dir's
|
|
28
|
+
// components/, where norsk-ctl finds and mounts them
|
|
29
|
+
export type DemoFeature = "dashboard" | "components";
|
|
30
|
+
|
|
31
|
+
const ALL_FEATURES: readonly DemoFeature[] = ["dashboard", "components"];
|
|
32
|
+
|
|
33
|
+
export interface CreateDemoOptions {
|
|
34
|
+
/** The demo's name: its workflow, its norsk-ctl instance and its library scope. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Whose `.ai/<user>/tasks/` the seeded task file goes in. */
|
|
37
|
+
user: string;
|
|
38
|
+
withFeatures?: DemoFeature[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TOKEN = "__DEMO__";
|
|
42
|
+
const TEMPLATE_DIR = join(import.meta.dir, "template");
|
|
43
|
+
|
|
44
|
+
function resolveFeatures(names: DemoFeature[] | undefined): Set<DemoFeature> {
|
|
45
|
+
for (const name of names ?? []) {
|
|
46
|
+
if (!ALL_FEATURES.includes(name)) {
|
|
47
|
+
throw new Error(`unknown feature ${JSON.stringify(name)} in --with — features: ${ALL_FEATURES.join(", ")}.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return new Set(names ?? []);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function* walk(dir: string): Generator<string> {
|
|
54
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
55
|
+
const full = join(dir, entry);
|
|
56
|
+
if (statSync(full).isDirectory()) yield* walk(full);
|
|
57
|
+
else yield full;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function featureBlocks(text: string, features: ReadonlySet<DemoFeature>): string {
|
|
62
|
+
return text.replace(/^<!-- if:(\w+) -->\n([\s\S]*?)^<!-- end:\1 -->\n/gm, (_, feature: DemoFeature, body: string) =>
|
|
63
|
+
features.has(feature) ? body : "",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function templateFiles(layer: string, name: string, features: ReadonlySet<DemoFeature>): GeneratedFile[] {
|
|
68
|
+
const root = join(TEMPLATE_DIR, layer);
|
|
69
|
+
return [...walk(root)].map((full) => {
|
|
70
|
+
const rel = relative(root, full);
|
|
71
|
+
const markdown = rel.endsWith(".md.tmpl");
|
|
72
|
+
const emitted = rel === "gitignore" ? ".gitignore" : markdown ? rel.slice(0, -".tmpl".length) : rel;
|
|
73
|
+
const raw = readFileSync(full, "utf8");
|
|
74
|
+
const path = emitted.replaceAll(TOKEN, name);
|
|
75
|
+
const content = (markdown ? featureBlocks(raw, features) : raw).replaceAll(TOKEN, name);
|
|
76
|
+
return { path, content, executable: /^(ctl|scripts)\/.*\.sh$/.test(path) };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function workflowYaml(name: string, features: ReadonlySet<DemoFeature>): string {
|
|
81
|
+
const example = features.has("components")
|
|
82
|
+
? `
|
|
83
|
+
# The placeholder from components/: it carries no media, publishes a heartbeat
|
|
84
|
+
# and serves /status. Delete it, and components/src/library/example/, once a
|
|
85
|
+
# real component exists.
|
|
86
|
+
- type: ${EXAMPLE_IDENTIFIER}
|
|
87
|
+
config:
|
|
88
|
+
id: example
|
|
89
|
+
displayName: Example
|
|
90
|
+
heartbeatMs: 1000
|
|
91
|
+
subscriptions: []
|
|
92
|
+
`
|
|
93
|
+
: "";
|
|
94
|
+
const exampleLayout = features.has("components") ? " - { id: example, x: 480, y: 300 }\n" : "";
|
|
95
|
+
return `# ${name}: the starter workflow.
|
|
96
|
+
#
|
|
97
|
+
# srt_in ─► preview
|
|
98
|
+
#
|
|
99
|
+
# srt_in is an SRT listener for a camera or an encoder to send to
|
|
100
|
+
# (scripts/test-feed.sh sends a test picture). preview is a still of its
|
|
101
|
+
# picture, which Studio's editor and the dashboard show. Replace this with the
|
|
102
|
+
# demo's real graph, keeping the comments and the __layout block in step.
|
|
103
|
+
#
|
|
104
|
+
# Site settings (ports, passphrases, destinations) are not set here: they come
|
|
105
|
+
# from ctl/studio.env as Studio environment overrides, <ID>_<FIELD>.
|
|
106
|
+
components:
|
|
107
|
+
- type: input.srt-listener
|
|
108
|
+
config:
|
|
109
|
+
id: srt_in
|
|
110
|
+
displayName: SRT In
|
|
111
|
+
# Published on the host by HOST_PORTS in ctl/site.env; change both together.
|
|
112
|
+
port: 5001
|
|
113
|
+
host: 0.0.0.0
|
|
114
|
+
sourceNames: permissive
|
|
115
|
+
streamIds: [camera]
|
|
116
|
+
subscriptions: []
|
|
117
|
+
- type: output.preview
|
|
118
|
+
config:
|
|
119
|
+
id: preview
|
|
120
|
+
displayName: Preview
|
|
121
|
+
# A JPEG, refreshed: it needs no media ports, so it works in norsk-ctl's
|
|
122
|
+
# docker network mode. moq mode gives live video the same way.
|
|
123
|
+
previewMode: image
|
|
124
|
+
showPreview: true
|
|
125
|
+
subscriptions:
|
|
126
|
+
- source: srt_in
|
|
127
|
+
streams: { type: take-first-stream, filter: [{ media: video }] }
|
|
128
|
+
${example}__layout:
|
|
129
|
+
- { id: srt_in, x: 0, y: 0 }
|
|
130
|
+
- { id: preview, x: 480, y: 0 }
|
|
131
|
+
${exampleLayout}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function taskFile(name: string, features: ReadonlySet<DemoFeature>): string {
|
|
135
|
+
const withs = features.size > 0 ? ` --with ${[...features].join(",")}` : "";
|
|
136
|
+
return `# ${name} demo
|
|
137
|
+
|
|
138
|
+
**Started:** generated by \`ctl-dev-kit create-demo --name ${name}${withs}\`.
|
|
139
|
+
|
|
140
|
+
## STATUS (current)
|
|
141
|
+
|
|
142
|
+
Scaffold only: the starter workflow (SRT in, image preview) installs and runs.
|
|
143
|
+
Nothing demo-specific is built yet.
|
|
144
|
+
|
|
145
|
+
### Open / not done
|
|
146
|
+
- TODO: what the customer asked to see.
|
|
147
|
+
|
|
148
|
+
## Brief
|
|
149
|
+
|
|
150
|
+
TODO: who the customer is, what the demo must show, when and where it is shown,
|
|
151
|
+
and what they take away afterwards.
|
|
152
|
+
|
|
153
|
+
## Decisions
|
|
154
|
+
|
|
155
|
+
| Decision | Status | Outcome |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
|
|
158
|
+
## Log
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function planDemo(opts: CreateDemoOptions): GeneratedFile[] {
|
|
163
|
+
// An instance id, a workflow file name and an npm scope all at once.
|
|
164
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(opts.name)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`invalid demo name ${JSON.stringify(opts.name)} — a lowercase letter, then lowercase letters, digits and hyphens; 1-40 chars (it names the norsk-ctl instance, the workflow and the components' npm scope).`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const features = resolveFeatures(opts.withFeatures);
|
|
170
|
+
const { name } = opts;
|
|
171
|
+
const files = [
|
|
172
|
+
...templateFiles("core", name, features),
|
|
173
|
+
{ path: `workdir/studio-save-files/${name}/${name}.yaml`, content: workflowYaml(name, features) },
|
|
174
|
+
{ path: `.ai/${opts.user}/tasks/${name}-demo.md`, content: taskFile(name, features) },
|
|
175
|
+
];
|
|
176
|
+
if (features.has("dashboard")) files.push(...templateFiles("dashboard", name, features));
|
|
177
|
+
if (features.has("components")) {
|
|
178
|
+
files.push(...componentsWorkspaceFiles(shapeContext({ dir: "", name, shape: "backend-turnkey" })));
|
|
179
|
+
}
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createDemo(opts: CreateDemoOptions & { dir: string }): { files: string[] } {
|
|
184
|
+
const plan = planDemo(opts);
|
|
185
|
+
for (const file of plan) {
|
|
186
|
+
const path = join(opts.dir, file.path);
|
|
187
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
188
|
+
writeFileSync(path, file.content, { mode: file.executable ? 0o755 : 0o644 });
|
|
189
|
+
}
|
|
190
|
+
return { files: plan.map((f) => f.path) };
|
|
191
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# __DEMO__: a Norsk Studio demo
|
|
2
|
+
|
|
3
|
+
This repo is a customer demo: a Studio workflow, plus what it needs around it,
|
|
4
|
+
built by us, shown to the customer, then handed over for them to develop. It is
|
|
5
|
+
not a product: there is no CI, no drift gate and no test tiers, and after
|
|
6
|
+
handover the customer owns every file in it. Keep it small enough that someone
|
|
7
|
+
new to Norsk can read the whole thing.
|
|
8
|
+
|
|
9
|
+
It was generated by `ctl-dev-kit create-demo`, so its layout matches every other
|
|
10
|
+
demo. Keep to it: anyone in the team should be able to pick this up and find
|
|
11
|
+
things where they expect.
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
| Path | What it is |
|
|
16
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `workdir/` | The Studio working directory, staged as-is on the box and mounted at `/data` |
|
|
18
|
+
| `workdir/studio-save-files/__DEMO__/__DEMO__.yaml` | The workflow. Hand-edited and commented; the `__layout` block places nodes in the editor |
|
|
19
|
+
| `ctl/` | Installing into norsk-ctl: `install.sh`, `push.sh`, `mode.sh`, the shared `lib.sh` and the site's settings |
|
|
20
|
+
| `scripts/build.sh` | Builds what the demo adds to Studio into `workdir/` |
|
|
21
|
+
| `scripts/test-feed.sh` | `up`/`down` a test picture into the SRT input |
|
|
22
|
+
<!-- if:dashboard -->
|
|
23
|
+
| `dashboard/` | The operator dashboard, a vite app Studio serves at `/dashboard/__DEMO__/` |
|
|
24
|
+
<!-- end:dashboard -->
|
|
25
|
+
<!-- if:components -->
|
|
26
|
+
| `components/` | This demo's own Studio nodes, the `@__DEMO__/library` library |
|
|
27
|
+
<!-- end:components -->
|
|
28
|
+
| `README.md` | The customer's handover document. Keep it true as the demo changes |
|
|
29
|
+
| `.ai/<user>/tasks/` | Task files: what was asked, what was decided, what is left |
|
|
30
|
+
|
|
31
|
+
## How it runs
|
|
32
|
+
|
|
33
|
+
norsk-ctl is the only run path, here and on the customer's box. ctl provides
|
|
34
|
+
TLS, ports and Studio's config, so this repo has no compose file, proxy or certs.
|
|
35
|
+
The demo is one instance, `__DEMO__`, launched from the `studio-dev` product
|
|
36
|
+
template with `workdir/` as its working directory.
|
|
37
|
+
|
|
38
|
+
1. `cp ctl/site.example.env ctl/site.env` and `cp ctl/studio.example.env ctl/studio.env`, then fill them in.
|
|
39
|
+
`site.env` says where the demo runs (here, or `DEMO_HOST=user@box`); `studio.env` holds the workflow's
|
|
40
|
+
site settings.
|
|
41
|
+
2. `ctl/install.sh` the first time. It builds, stages `workdir/` at `WORKDIR` owned by the norsk-ctl
|
|
42
|
+
user, and launches the instance.
|
|
43
|
+
3. `ctl/push.sh` after every change. It builds, syncs, relaunches and waits until Studio is up.
|
|
44
|
+
4. `ctl/mode.sh editor` to change the workflow in Studio's editor; `ctl/mode.sh document` to put it back
|
|
45
|
+
to running on start.
|
|
46
|
+
5. `scripts/test-feed.sh up` for a picture without a camera.
|
|
47
|
+
|
|
48
|
+
The scripts print the Studio URL. The same scripts serve a local norsk-ctl and
|
|
49
|
+
the customer's box, so what is shown is what was tested.
|
|
50
|
+
|
|
51
|
+
A customer who wants the demo out of norsk-ctl uses
|
|
52
|
+
`norsk-ctl instance export __DEMO__ --to <dir>`, which writes a compose bundle
|
|
53
|
+
with its own `HANDOVER.md`. It refuses a different compose project name (`-p`),
|
|
54
|
+
and it has no proxy: the deployer supplies `NORSK_PUBLIC_BASE_URL` and a
|
|
55
|
+
reverse proxy for `/instance/__DEMO__/studio`.
|
|
56
|
+
|
|
57
|
+
## Working rules
|
|
58
|
+
|
|
59
|
+
- **The YAML is the source.** Edit `__DEMO__.yaml` by hand, keep its comments,
|
|
60
|
+
and keep `__layout` in step when nodes are added. Saving from Studio's editor
|
|
61
|
+
strips comments and `__layout`; if a change was made there, copy the shape
|
|
62
|
+
back by hand rather than committing the editor's file.
|
|
63
|
+
- **Site settings are not in the YAML.** Ports, passphrases and destinations go
|
|
64
|
+
in `ctl/studio.env` as `<COMPONENT_ID>_<FIELD>=value`, for fields the
|
|
65
|
+
component declares overridable. A `${VAR}` in a YAML value fails validation.
|
|
66
|
+
- **Component ids are an interface.** The dashboard, `studio.env` and the README
|
|
67
|
+
all name them. Rename one everywhere or not at all.
|
|
68
|
+
- **Ports:** let norsk-ctl allocate them. Pin one in `HOST_PORTS` only when a
|
|
69
|
+
sender or a firewall outside the box must agree on it. On a shared box, never
|
|
70
|
+
stop another instance's containers.
|
|
71
|
+
- **Push, then check.** After `ctl/push.sh`, open the URL it prints and look:
|
|
72
|
+
a relaunch that came up is not a workflow that works.
|
|
73
|
+
- **The README is the handover.** When something changes what the customer
|
|
74
|
+
sees or does, change the README in the same commit.
|
|
75
|
+
|
|
76
|
+
## Gotchas
|
|
77
|
+
|
|
78
|
+
- Under norsk-ctl the working dir must be owned by the norsk-ctl user, and on a
|
|
79
|
+
path it can reach: `/srv/__DEMO__`, not a home directory.
|
|
80
|
+
- `STUDIO_DOCUMENT` does nothing under norsk-ctl, which writes Studio's config
|
|
81
|
+
itself. The document comes from `NODE_CONFIG` in the overrides, which is what
|
|
82
|
+
`ctl/mode.sh` switches.
|
|
83
|
+
- `NODE_CONFIG` replaces the library list norsk-ctl writes, so the overrides
|
|
84
|
+
name every library. `ctl/lib.sh` does this; keep it if you edit it.
|
|
85
|
+
- A fixed document hides the editor's Stop button. Use `ctl/mode.sh editor`.
|
|
86
|
+
- Studio's `/api/files` refuses to overwrite: a write is DELETE then POST.
|
|
87
|
+
- Put a `processor.smartSourceSwitch` with a fallback after inputs, or a source
|
|
88
|
+
dropping resets HLS. Silence-fill audio that comes and goes, so nothing
|
|
89
|
+
renegotiates.
|
|
90
|
+
- A WHEP return carries one audio stream; WHIP clients should DELETE the
|
|
91
|
+
session on pagehide.
|
|
92
|
+
- WebRTC media (WHEP, WHIP) to a viewer needs `NETWORK_MODE=hybrid`. MoQ and
|
|
93
|
+
image previews work in docker mode.
|
|
94
|
+
- The microphone and MoQ/WebTransport need HTTPS. norsk-ctl serves HTTPS.
|
|
95
|
+
- Studio's COOP header stops a Studio-served page painting in a
|
|
96
|
+
`processor.browserOverlay`: use `--disable-features=CrossOriginOpenerPolicy`
|
|
97
|
+
in `customCefArgs`, or a `file:///data/...` URL.
|
|
98
|
+
- Headless browser tests need Debian's chromium: Playwright's own build has no
|
|
99
|
+
H.264.
|
|
100
|
+
|
|
101
|
+
<!-- if:dashboard -->
|
|
102
|
+
|
|
103
|
+
## Dashboard
|
|
104
|
+
|
|
105
|
+
`dashboard/` is a vite + React app with one page per HTML entry. `scripts/build.sh`
|
|
106
|
+
builds it into `workdir/studio-save-files/__DEMO__/dashboards/`, and Studio serves
|
|
107
|
+
that at `/dashboard/__DEMO__/` beside the workflow. A top-level
|
|
108
|
+
`workdir/dashboards/` is not served.
|
|
109
|
+
|
|
110
|
+
- `src/lib/workflow.ts` is the one place the dashboard names component ids.
|
|
111
|
+
Keep it in step with the YAML.
|
|
112
|
+
- Each page fetches `./env` for `apiBasePath` and `wsBasePath`, so it works
|
|
113
|
+
under any URL prefix. Never hard-code `/live` or a host.
|
|
114
|
+
- The hooks are the dashboard's whole Studio API:
|
|
115
|
+
- `useEnv`: the env.
|
|
116
|
+
- `useDiscovery`: the running components, polled until the workflow is up.
|
|
117
|
+
- `useLiveComponent`: a component's state over its websocket, with
|
|
118
|
+
reconnect, and `sendCommand`.
|
|
119
|
+
- `useAlerts`: a component's alerts, polled.
|
|
120
|
+
- The starter preview is `output.preview` in image mode: a JPEG URL in the
|
|
121
|
+
component's state. For live video, switch the preview to `moq` mode and use a
|
|
122
|
+
MoQ player; livemode and eurovision have one (`moq-player.tsx`).
|
|
123
|
+
- `bun run dev` serves the pages with Studio's routes proxied to `STUDIO_URL`,
|
|
124
|
+
which must be a Studio reachable without the sign-in proxy in front.
|
|
125
|
+
<!-- end:dashboard -->
|
|
126
|
+
<!-- if:components -->
|
|
127
|
+
|
|
128
|
+
## Custom components
|
|
129
|
+
|
|
130
|
+
`components/` is this demo's own Studio library, `@__DEMO__/library`, with one
|
|
131
|
+
placeholder node, `output.example`. `scripts/build.sh` builds it and copies
|
|
132
|
+
`lib/library/` to `workdir/components/@__DEMO__/library/`. norsk-ctl finds
|
|
133
|
+
packages under the working dir's `components/` and mounts them, and the
|
|
134
|
+
overrides name the library, so Studio loads it.
|
|
135
|
+
|
|
136
|
+
- A component is a directory under `src/library/` with `info.ts` (design time:
|
|
137
|
+
config form, subscriptions), `runtime.ts` (the media nodes and routes) and
|
|
138
|
+
`types.source.yaml` (the schema; `_gen/types.ts` is generated from it). Adding
|
|
139
|
+
one is adding a directory: `index.ts` registers every one it finds.
|
|
140
|
+
- The build order is load-bearing: codegen, then tsc to CommonJS, then
|
|
141
|
+
`copy-yamls.ts`, which writes `types.yaml` beside each runtime and a browser
|
|
142
|
+
bundle of `info.ts`. That bundle must not import react or `@norskvideo/*` as
|
|
143
|
+
bare specifiers; `info-client-externals.test.ts` checks it.
|
|
144
|
+
- Publish a component's state from its constructor: Studio refuses websocket
|
|
145
|
+
connections while a state is undefined.
|
|
146
|
+
- Keep the pure logic in sibling modules, testable with `bun test` and no Studio.
|
|
147
|
+
- Delete `example/` (and its node in the YAML) once a real component exists.
|
|
148
|
+
<!-- end:components -->
|
|
149
|
+
|
|
150
|
+
## Handover
|
|
151
|
+
|
|
152
|
+
Before the customer takes it:
|
|
153
|
+
|
|
154
|
+
- The README is complete: install, URLs, the workflow block by block, how to
|
|
155
|
+
change it, what it does not do, versions, and how to take it out of norsk-ctl.
|
|
156
|
+
- `ctl/site.example.env` and `ctl/studio.example.env` describe every setting.
|
|
157
|
+
- The task file says what was shown and what is left.
|
|
158
|
+
- Nothing in the repo is a site's secret.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# __DEMO__
|
|
2
|
+
|
|
3
|
+
<!-- The customer's handover document. Replace every TODO before handover; the
|
|
4
|
+
CLAUDE.md "Handover" list says what this needs to cover. -->
|
|
5
|
+
|
|
6
|
+
TODO: one paragraph on what this demo does and who it is for.
|
|
7
|
+
|
|
8
|
+
## What is in it
|
|
9
|
+
|
|
10
|
+
| Path | What it is |
|
|
11
|
+
| ---------- | ------------------------------------------------------------------------------------------ |
|
|
12
|
+
| `workdir/` | The Norsk Studio working directory: the workflow, and what Studio serves and loads with it |
|
|
13
|
+
| `ctl/` | The scripts that install it into norsk-ctl and update it |
|
|
14
|
+
| `scripts/` | Building it, and a test feed |
|
|
15
|
+
<!-- if:dashboard -->
|
|
16
|
+
| `dashboard/` | The operator dashboard |
|
|
17
|
+
<!-- end:dashboard -->
|
|
18
|
+
<!-- if:components -->
|
|
19
|
+
| `components/` | This demo's own Studio components |
|
|
20
|
+
<!-- end:components -->
|
|
21
|
+
|
|
22
|
+
## Installing
|
|
23
|
+
|
|
24
|
+
It runs under norsk-ctl, on this machine or another one over ssh.
|
|
25
|
+
|
|
26
|
+
1. Install norsk-ctl on the machine, and have the norsk-studio licence there.
|
|
27
|
+
2. Copy `ctl/site.example.env` to `ctl/site.env` and fill it in: where it runs, where the working
|
|
28
|
+
dir goes, the licence path, the ports to publish.
|
|
29
|
+
3. Copy `ctl/studio.example.env` to `ctl/studio.env` and fill in the site's settings.
|
|
30
|
+
4. Run `ctl/install.sh`. It prints the Studio URL when it is up.
|
|
31
|
+
|
|
32
|
+
Building needs bun on the machine you run the scripts from.
|
|
33
|
+
|
|
34
|
+
## Using it
|
|
35
|
+
|
|
36
|
+
TODO: the URLs, and what the operator does on each page.
|
|
37
|
+
|
|
38
|
+
- `scripts/test-feed.sh up` sends a test picture into the SRT input; `down` stops it.
|
|
39
|
+
|
|
40
|
+
## The workflow
|
|
41
|
+
|
|
42
|
+
TODO: each block in `workdir/studio-save-files/__DEMO__/__DEMO__.yaml`, in signal
|
|
43
|
+
order: what it is for and the settings that matter.
|
|
44
|
+
|
|
45
|
+
## Changing it
|
|
46
|
+
|
|
47
|
+
- Edit the workflow YAML, then `ctl/push.sh`. It rebuilds, copies the working dir over and relaunches.
|
|
48
|
+
- To change it in Studio's editor, `ctl/mode.sh editor`; `ctl/mode.sh document` puts it back to
|
|
49
|
+
running on start. Saving from the editor drops the YAML's comments, so copy changes back by hand.
|
|
50
|
+
- Site settings (ports, passphrases, destinations) live in `ctl/studio.env`, as
|
|
51
|
+
`<COMPONENT_ID>_<FIELD>=value`.
|
|
52
|
+
<!-- if:dashboard -->
|
|
53
|
+
- The dashboard is in `dashboard/`; `src/lib/workflow.ts` names the components it shows.
|
|
54
|
+
<!-- end:dashboard -->
|
|
55
|
+
<!-- if:components -->
|
|
56
|
+
- Custom components are in `components/src/library/`, one directory each.
|
|
57
|
+
<!-- end:components -->
|
|
58
|
+
|
|
59
|
+
## Limits
|
|
60
|
+
|
|
61
|
+
TODO: what this demo does not do, and what a production build would add.
|
|
62
|
+
|
|
63
|
+
There is no access control beyond norsk-ctl's own sign-in.
|
|
64
|
+
|
|
65
|
+
## Versions
|
|
66
|
+
|
|
67
|
+
TODO: the norsk-ctl, Studio and Norsk versions this was shown on
|
|
68
|
+
(`norsk-ctl instance describe __DEMO__`).
|
|
69
|
+
|
|
70
|
+
## Taking it out of norsk-ctl
|
|
71
|
+
|
|
72
|
+
`norsk-ctl instance export __DEMO__ --to <dir>` writes a docker compose bundle that runs
|
|
73
|
+
without norsk-ctl, with a `HANDOVER.md` of its own. Run it under its own project name, and put
|
|
74
|
+
a reverse proxy in front of Studio: the bundle has none.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# First install of the demo into norsk-ctl: here, or on DEMO_HOST over ssh
|
|
3
|
+
# (see ctl/site.env). Builds, stages workdir/ at WORKDIR, and launches it as
|
|
4
|
+
# instance __DEMO__ from the studio-dev template. ctl/push.sh updates it after.
|
|
5
|
+
#
|
|
6
|
+
# ctl/install.sh [--no-build]
|
|
7
|
+
source "$(dirname "$0")/lib.sh"
|
|
8
|
+
load_site
|
|
9
|
+
|
|
10
|
+
if ctl instance exists "$DEMO" >/dev/null 2>&1; then
|
|
11
|
+
die "instance $DEMO already exists: ctl/push.sh updates it"
|
|
12
|
+
fi
|
|
13
|
+
if [[ "${1:-}" != --no-build ]]; then "$ROOT/scripts/build.sh"; fi
|
|
14
|
+
|
|
15
|
+
ensure_template
|
|
16
|
+
sync_workdir
|
|
17
|
+
write_overrides "$(current_mode)"
|
|
18
|
+
|
|
19
|
+
ports=()
|
|
20
|
+
for port in $HOST_PORTS; do ports+=(--host-ports "$port"); done
|
|
21
|
+
ctl instance launch-template "$DEMO" --template studio-dev \
|
|
22
|
+
--param "INSTANCE_NAME=$DEMO" --param GLOBAL_MOQ_PREVIEW_WEBTRANSPORTPORT=auto \
|
|
23
|
+
--working-directory "$WORKDIR" --overrides "$WORKDIR/ctl-overrides.yml" \
|
|
24
|
+
--network-mode "$NETWORK_MODE" "${ports[@]}" >/dev/null
|
|
25
|
+
|
|
26
|
+
wait_ready
|
|
27
|
+
print_urls
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared by install.sh, push.sh and mode.sh: the site's settings, how to reach
|
|
3
|
+
# the machine the demo runs on, and the overrides file norsk-ctl launches
|
|
4
|
+
# Studio with. Sourced, not run.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
DEMO=__DEMO__
|
|
8
|
+
CTL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
ROOT="$(dirname "$CTL_DIR")"
|
|
10
|
+
|
|
11
|
+
die() {
|
|
12
|
+
echo "error: $*" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# ctl/site.env, with the defaults for anything it leaves unset.
|
|
17
|
+
load_site() {
|
|
18
|
+
[[ -f "$CTL_DIR/site.env" ]] || die "no ctl/site.env: copy ctl/site.example.env to it and fill it in"
|
|
19
|
+
set -a
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
source "$CTL_DIR/site.env"
|
|
22
|
+
set +a
|
|
23
|
+
DEMO_HOST="${DEMO_HOST:-}"
|
|
24
|
+
WORKDIR="${WORKDIR:-/srv/$DEMO}"
|
|
25
|
+
NETWORK_MODE="${NETWORK_MODE:-docker}"
|
|
26
|
+
RUN_AS="${RUN_AS-norsk}"
|
|
27
|
+
NORSK_CTL="${NORSK_CTL:-norsk-ctl}"
|
|
28
|
+
HOST_PORTS="${HOST_PORTS:-}"
|
|
29
|
+
LICENSE_FILE="${LICENSE_FILE:-}"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
quote() {
|
|
33
|
+
local out=() arg
|
|
34
|
+
for arg in "$@"; do out+=("$(printf '%q' "$arg")"); done
|
|
35
|
+
echo "${out[*]}"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Run a shell command line on the target: over ssh when DEMO_HOST is set, here
|
|
39
|
+
# otherwise. stdin passes through either way.
|
|
40
|
+
on_target() {
|
|
41
|
+
if [[ -n "$DEMO_HOST" ]]; then ssh "$DEMO_HOST" "$*"; else bash -c "$*"; fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# "sudo " when the working dir belongs to another user (RUN_AS), else nothing.
|
|
45
|
+
target_sudo() {
|
|
46
|
+
if [[ -n "$RUN_AS" ]]; then echo "sudo "; fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# norsk-ctl on the target, as the user its daemon runs as.
|
|
50
|
+
ctl() {
|
|
51
|
+
local cmd
|
|
52
|
+
cmd="$NORSK_CTL $(quote "$@")"
|
|
53
|
+
if [[ -n "$RUN_AS" ]]; then cmd="sudo -u $(quote "$RUN_AS") -H $cmd"; fi
|
|
54
|
+
on_target "$cmd"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
current_mode() {
|
|
58
|
+
cat "$CTL_DIR/.mode" 2>/dev/null || echo document
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
yaml_quote() {
|
|
62
|
+
local s="${1//\'/\'\'}"
|
|
63
|
+
printf "'%s'" "$s"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# The compose override norsk-ctl merges over the studio service, for a mode:
|
|
67
|
+
# document Studio runs acme.yaml on start (the demo as shown)
|
|
68
|
+
# editor Studio opens on the editor with nothing running (for changing it)
|
|
69
|
+
# NODE_CONFIG names every library, the custom components included, because a
|
|
70
|
+
# norsk-ctl older than the library fix lets this list replace the one it
|
|
71
|
+
# writes. ctl/studio.env lines are Studio environment overrides, <ID>_<FIELD>.
|
|
72
|
+
overrides_yaml() {
|
|
73
|
+
local mode="$1" libs server line
|
|
74
|
+
[[ "$mode" == document || "$mode" == editor ]] || die "unknown mode '$mode': document or editor"
|
|
75
|
+
libs='"@norskvideo/norsk-studio-built-ins","@norskvideo/norsk-studio-beta"'
|
|
76
|
+
if [[ -d "$ROOT/components" ]]; then libs="$libs,\"@$DEMO/library\""; fi
|
|
77
|
+
server="\"library\":[$libs]"
|
|
78
|
+
if [[ "$mode" == document ]]; then server="\"document\":\"$DEMO.yaml\",$server"; fi
|
|
79
|
+
echo "services:"
|
|
80
|
+
echo " studio:"
|
|
81
|
+
echo " environment:"
|
|
82
|
+
echo " NODE_CONFIG: $(yaml_quote "{\"server\":{$server}}")"
|
|
83
|
+
if [[ -f "$CTL_DIR/studio.env" ]]; then
|
|
84
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
85
|
+
[[ -z "${line// /}" || "$line" == \#* ]] && continue
|
|
86
|
+
echo " ${line%%=*}: $(yaml_quote "${line#*=}")"
|
|
87
|
+
done <"$CTL_DIR/studio.env"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
write_overrides() {
|
|
92
|
+
overrides_yaml "$1" | on_target "$(target_sudo)tee $(quote "$WORKDIR/ctl-overrides.yml") >/dev/null"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Copy workdir/ to WORKDIR on the target, owned by RUN_AS. Nothing already
|
|
96
|
+
# there is deleted (Studio keeps its own files in it), except inside the two
|
|
97
|
+
# build outputs, where a stale file would still be served or loaded.
|
|
98
|
+
sync_workdir() {
|
|
99
|
+
local dest="$WORKDIR" rsync=(rsync) opts=(-a) sub
|
|
100
|
+
on_target "$(target_sudo)mkdir -p $(quote "$WORKDIR")"
|
|
101
|
+
if [[ -n "$RUN_AS" ]]; then
|
|
102
|
+
opts+=("--chown=$RUN_AS:$RUN_AS")
|
|
103
|
+
if [[ -n "$DEMO_HOST" ]]; then opts+=(--rsync-path="sudo rsync"); else rsync=(sudo rsync); fi
|
|
104
|
+
fi
|
|
105
|
+
if [[ -n "$DEMO_HOST" ]]; then dest="$DEMO_HOST:$WORKDIR"; fi
|
|
106
|
+
"${rsync[@]}" "${opts[@]}" --exclude logs --exclude .norsk --exclude moq-certs --exclude ctl-overrides.yml \
|
|
107
|
+
"$ROOT/workdir/" "$dest/"
|
|
108
|
+
for sub in "studio-save-files/$DEMO/dashboards" components; do
|
|
109
|
+
if [[ -d "$ROOT/workdir/$sub" ]]; then
|
|
110
|
+
"${rsync[@]}" "${opts[@]}" --delete "$ROOT/workdir/$sub/" "$dest/$sub/"
|
|
111
|
+
fi
|
|
112
|
+
done
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# The studio-dev product template, added from the licence when it is missing.
|
|
116
|
+
ensure_template() {
|
|
117
|
+
if ctl template list 2>/dev/null | grep -q "name: studio-dev"; then return; fi
|
|
118
|
+
[[ -n "$LICENSE_FILE" ]] || die "norsk-ctl has no studio-dev template: set LICENSE_FILE in ctl/site.env"
|
|
119
|
+
ctl product add --license-file "$LICENSE_FILE" --product norsk-studio >/dev/null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe_field() {
|
|
123
|
+
ctl instance describe "$DEMO" -o json 2>/dev/null | bun -e '
|
|
124
|
+
const text = await Bun.stdin.text();
|
|
125
|
+
let v = JSON.parse(text.slice(text.indexOf("{")));
|
|
126
|
+
for (const key of process.argv[1].split(".")) v = v?.[key];
|
|
127
|
+
console.log(v ?? "");
|
|
128
|
+
' "$1"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
wait_ready() {
|
|
132
|
+
local i
|
|
133
|
+
for ((i = 0; i < 90; i++)); do
|
|
134
|
+
if [[ "$(describe_field runtimeScreen.ready 2>/dev/null)" == true ]]; then return 0; fi
|
|
135
|
+
sleep 2
|
|
136
|
+
done
|
|
137
|
+
die "$DEMO did not come up within 3 minutes: see norsk-ctl instance describe $DEMO"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
print_urls() {
|
|
141
|
+
local studio
|
|
142
|
+
studio="$(describe_field runtimeScreen.href)"
|
|
143
|
+
echo "Studio: $studio"
|
|
144
|
+
if [[ -d "$ROOT/dashboard" ]]; then echo "Dashboard: ${studio}dashboard/$DEMO/"; fi
|
|
145
|
+
echo "Mode: $(current_mode)"
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Switch the installed demo between its two modes, and relaunch it:
|
|
3
|
+
# document Studio runs the workflow on start: the demo as it is shown.
|
|
4
|
+
# The editor's Stop button is hidden in this mode.
|
|
5
|
+
# editor Studio opens on the editor with nothing running, to change it.
|
|
6
|
+
#
|
|
7
|
+
# ctl/mode.sh document|editor
|
|
8
|
+
source "$(dirname "$0")/lib.sh"
|
|
9
|
+
load_site
|
|
10
|
+
|
|
11
|
+
mode="${1:-}"
|
|
12
|
+
overrides_yaml "$mode" >/dev/null
|
|
13
|
+
ctl instance exists "$DEMO" >/dev/null 2>&1 || die "no instance $DEMO yet: run ctl/install.sh first"
|
|
14
|
+
|
|
15
|
+
echo "$mode" >"$CTL_DIR/.mode"
|
|
16
|
+
write_overrides "$mode"
|
|
17
|
+
ctl instance relaunch "$DEMO" >/dev/null
|
|
18
|
+
wait_ready
|
|
19
|
+
print_urls
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Push a change to the installed demo: build, sync workdir/ to WORKDIR, rewrite
|
|
3
|
+
# the overrides from ctl/studio.env, relaunch, and wait until Studio is up.
|
|
4
|
+
#
|
|
5
|
+
# ctl/push.sh [--no-build]
|
|
6
|
+
source "$(dirname "$0")/lib.sh"
|
|
7
|
+
load_site
|
|
8
|
+
|
|
9
|
+
ctl instance exists "$DEMO" >/dev/null 2>&1 || die "no instance $DEMO yet: run ctl/install.sh first"
|
|
10
|
+
if [[ "${1:-}" != --no-build ]]; then "$ROOT/scripts/build.sh"; fi
|
|
11
|
+
|
|
12
|
+
sync_workdir
|
|
13
|
+
write_overrides "$(current_mode)"
|
|
14
|
+
ctl instance relaunch "$DEMO" >/dev/null
|
|
15
|
+
wait_ready
|
|
16
|
+
print_urls
|