@mevdragon/vidfarm-devcli 0.1.0
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/.env.example +41 -0
- package/AWS_REMOTION_HANDOFF.md +311 -0
- package/PLATFORM_SPEC.md +898 -0
- package/README.md +151 -0
- package/dist/src/app.js +224 -0
- package/dist/src/cli.js +187 -0
- package/dist/src/config.js +52 -0
- package/dist/src/context.js +198 -0
- package/dist/src/db.js +588 -0
- package/dist/src/dev-app.js +859 -0
- package/dist/src/domain.js +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/lib/crypto.js +30 -0
- package/dist/src/lib/ids.js +4 -0
- package/dist/src/lib/images.js +18 -0
- package/dist/src/lib/json.js +14 -0
- package/dist/src/lib/time.js +6 -0
- package/dist/src/registry.js +10 -0
- package/dist/src/runtime.js +19 -0
- package/dist/src/services/auth.js +80 -0
- package/dist/src/services/billing.js +16 -0
- package/dist/src/services/jobs.js +97 -0
- package/dist/src/services/providers.js +529 -0
- package/dist/src/services/remotion.js +158 -0
- package/dist/src/services/storage.js +93 -0
- package/dist/src/services/webhooks.js +61 -0
- package/dist/src/template-sdk.js +3 -0
- package/dist/src/worker.js +122 -0
- package/dist/templates/template_0000/demo-template.js +196 -0
- package/dist/templates/template_0000/remotion/Root.js +66 -0
- package/dist/templates/template_0000/remotion/index.js +3 -0
- package/package.json +59 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
function escapeJsonForHtml(value) {
|
|
2
|
+
return JSON.stringify(value)
|
|
3
|
+
.replace(/</g, "\\u003c")
|
|
4
|
+
.replace(/>/g, "\\u003e")
|
|
5
|
+
.replace(/&/g, "\\u0026");
|
|
6
|
+
}
|
|
7
|
+
export function renderDevApp(input) {
|
|
8
|
+
const boot = escapeJsonForHtml({
|
|
9
|
+
templates: input.templates,
|
|
10
|
+
environment: input.environment
|
|
11
|
+
});
|
|
12
|
+
return `<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<title>Vidfarm Developer Console</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #f3efe5;
|
|
21
|
+
--paper: rgba(255, 252, 245, 0.82);
|
|
22
|
+
--ink: #171411;
|
|
23
|
+
--muted: #63584b;
|
|
24
|
+
--line: rgba(23, 20, 17, 0.12);
|
|
25
|
+
--accent: #d34c2d;
|
|
26
|
+
--accent-soft: rgba(211, 76, 45, 0.12);
|
|
27
|
+
--teal: #0f8b8d;
|
|
28
|
+
--gold: #b48811;
|
|
29
|
+
--shadow: 0 30px 80px rgba(40, 27, 18, 0.12);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
|
|
34
|
+
body {
|
|
35
|
+
margin: 0;
|
|
36
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
37
|
+
color: var(--ink);
|
|
38
|
+
background:
|
|
39
|
+
radial-gradient(circle at top left, rgba(211, 76, 45, 0.18), transparent 28%),
|
|
40
|
+
radial-gradient(circle at 85% 20%, rgba(15, 139, 141, 0.16), transparent 24%),
|
|
41
|
+
linear-gradient(180deg, #f7f2e6 0%, #efe5d4 100%);
|
|
42
|
+
min-height: 100vh;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
body::before {
|
|
46
|
+
content: "";
|
|
47
|
+
position: fixed;
|
|
48
|
+
inset: 0;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
background-image:
|
|
51
|
+
linear-gradient(rgba(23, 20, 17, 0.035) 1px, transparent 1px),
|
|
52
|
+
linear-gradient(90deg, rgba(23, 20, 17, 0.035) 1px, transparent 1px);
|
|
53
|
+
background-size: 32px 32px;
|
|
54
|
+
mask-image: linear-gradient(180deg, rgba(0,0,0,0.65), rgba(0,0,0,0.1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.shell {
|
|
58
|
+
max-width: 1480px;
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
padding: 28px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.hero {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-template-columns: 1.5fr 1fr;
|
|
66
|
+
gap: 24px;
|
|
67
|
+
align-items: stretch;
|
|
68
|
+
margin-bottom: 24px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.panel {
|
|
72
|
+
background: var(--paper);
|
|
73
|
+
border: 1px solid var(--line);
|
|
74
|
+
backdrop-filter: blur(18px);
|
|
75
|
+
box-shadow: var(--shadow);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.hero-main {
|
|
79
|
+
padding: 28px;
|
|
80
|
+
min-height: 260px;
|
|
81
|
+
position: relative;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hero-main::after {
|
|
86
|
+
content: "";
|
|
87
|
+
position: absolute;
|
|
88
|
+
right: -30px;
|
|
89
|
+
top: -20px;
|
|
90
|
+
width: 220px;
|
|
91
|
+
height: 220px;
|
|
92
|
+
background: conic-gradient(from 40deg, rgba(211, 76, 45, 0.35), rgba(180, 136, 17, 0.2), rgba(15, 139, 141, 0.2), rgba(211, 76, 45, 0.35));
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
filter: blur(18px);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.eyebrow {
|
|
98
|
+
letter-spacing: 0.24em;
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
color: var(--muted);
|
|
102
|
+
margin-bottom: 18px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
h1, h2, h3 {
|
|
106
|
+
margin: 0;
|
|
107
|
+
font-weight: 400;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
h1 {
|
|
111
|
+
font-size: clamp(2.8rem, 6vw, 5.4rem);
|
|
112
|
+
line-height: 0.92;
|
|
113
|
+
max-width: 9ch;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.hero-copy {
|
|
117
|
+
max-width: 60ch;
|
|
118
|
+
font-size: 18px;
|
|
119
|
+
line-height: 1.6;
|
|
120
|
+
color: var(--muted);
|
|
121
|
+
margin-top: 18px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.hero-side {
|
|
125
|
+
display: grid;
|
|
126
|
+
gap: 16px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.stat-card {
|
|
130
|
+
padding: 18px 20px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.stat-value {
|
|
134
|
+
font-size: 2rem;
|
|
135
|
+
color: var(--accent);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.stat-label {
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
letter-spacing: 0.2em;
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
color: var(--muted);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.grid {
|
|
146
|
+
display: grid;
|
|
147
|
+
grid-template-columns: 340px 1fr;
|
|
148
|
+
gap: 24px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.stack {
|
|
152
|
+
display: grid;
|
|
153
|
+
gap: 18px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.card {
|
|
157
|
+
padding: 20px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.card h2 {
|
|
161
|
+
font-size: 1.15rem;
|
|
162
|
+
margin-bottom: 12px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.label {
|
|
166
|
+
display: block;
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
letter-spacing: 0.18em;
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
color: var(--muted);
|
|
171
|
+
margin-bottom: 8px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
input, select, textarea, button {
|
|
175
|
+
width: 100%;
|
|
176
|
+
border: 1px solid var(--line);
|
|
177
|
+
background: rgba(255, 255, 255, 0.7);
|
|
178
|
+
color: var(--ink);
|
|
179
|
+
padding: 12px 14px;
|
|
180
|
+
font: inherit;
|
|
181
|
+
outline: none;
|
|
182
|
+
border-radius: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
textarea {
|
|
186
|
+
min-height: 140px;
|
|
187
|
+
resize: vertical;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
input:focus, select:focus, textarea:focus {
|
|
191
|
+
border-color: var(--accent);
|
|
192
|
+
box-shadow: 0 0 0 4px var(--accent-soft);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
button {
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
background: var(--ink);
|
|
198
|
+
color: #f7f2e6;
|
|
199
|
+
transition: transform 140ms ease, background 140ms ease;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
button:hover {
|
|
203
|
+
transform: translateY(-1px);
|
|
204
|
+
background: var(--accent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.secondary {
|
|
208
|
+
background: transparent;
|
|
209
|
+
color: var(--ink);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.inline {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
215
|
+
gap: 12px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.inline-3 {
|
|
219
|
+
display: grid;
|
|
220
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
221
|
+
gap: 12px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.workspace {
|
|
225
|
+
display: grid;
|
|
226
|
+
gap: 24px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ribbon {
|
|
230
|
+
display: flex;
|
|
231
|
+
flex-wrap: wrap;
|
|
232
|
+
gap: 10px;
|
|
233
|
+
margin-bottom: 16px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.pill {
|
|
237
|
+
display: inline-flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 8px;
|
|
240
|
+
padding: 8px 12px;
|
|
241
|
+
background: rgba(255,255,255,0.72);
|
|
242
|
+
border: 1px solid var(--line);
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
letter-spacing: 0.12em;
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.template-grid, .job-grid {
|
|
249
|
+
display: grid;
|
|
250
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
251
|
+
gap: 16px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.template-card, .job-card {
|
|
255
|
+
border: 1px solid var(--line);
|
|
256
|
+
padding: 18px;
|
|
257
|
+
background: rgba(255,255,255,0.56);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.template-card.active {
|
|
261
|
+
outline: 3px solid var(--accent-soft);
|
|
262
|
+
border-color: rgba(211, 76, 45, 0.4);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.template-title {
|
|
266
|
+
font-size: 1.3rem;
|
|
267
|
+
margin-bottom: 6px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.template-desc, .muted {
|
|
271
|
+
color: var(--muted);
|
|
272
|
+
line-height: 1.5;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.operation-list {
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-wrap: wrap;
|
|
278
|
+
gap: 8px;
|
|
279
|
+
margin-top: 16px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.op {
|
|
283
|
+
padding: 6px 10px;
|
|
284
|
+
border: 1px solid var(--line);
|
|
285
|
+
font-size: 12px;
|
|
286
|
+
background: rgba(211, 76, 45, 0.08);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pre {
|
|
290
|
+
margin: 0;
|
|
291
|
+
white-space: pre-wrap;
|
|
292
|
+
word-break: break-word;
|
|
293
|
+
background: #16120f;
|
|
294
|
+
color: #eee3ce;
|
|
295
|
+
padding: 16px;
|
|
296
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
297
|
+
overflow: auto;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.timeline {
|
|
301
|
+
border-left: 1px solid var(--line);
|
|
302
|
+
margin-left: 6px;
|
|
303
|
+
padding-left: 18px;
|
|
304
|
+
display: grid;
|
|
305
|
+
gap: 16px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.event {
|
|
309
|
+
position: relative;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.event::before {
|
|
313
|
+
content: "";
|
|
314
|
+
position: absolute;
|
|
315
|
+
left: -24px;
|
|
316
|
+
top: 6px;
|
|
317
|
+
width: 10px;
|
|
318
|
+
height: 10px;
|
|
319
|
+
border-radius: 50%;
|
|
320
|
+
background: var(--accent);
|
|
321
|
+
box-shadow: 0 0 0 5px rgba(211, 76, 45, 0.14);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.event-head {
|
|
325
|
+
display: flex;
|
|
326
|
+
justify-content: space-between;
|
|
327
|
+
gap: 12px;
|
|
328
|
+
font-size: 13px;
|
|
329
|
+
color: var(--muted);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.event-msg {
|
|
333
|
+
margin-top: 6px;
|
|
334
|
+
font-size: 16px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.status {
|
|
338
|
+
display: inline-block;
|
|
339
|
+
padding: 4px 10px;
|
|
340
|
+
border: 1px solid var(--line);
|
|
341
|
+
font-size: 11px;
|
|
342
|
+
text-transform: uppercase;
|
|
343
|
+
letter-spacing: 0.18em;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.status.succeeded { color: var(--teal); }
|
|
347
|
+
.status.failed { color: var(--accent); }
|
|
348
|
+
.status.running { color: var(--gold); }
|
|
349
|
+
|
|
350
|
+
.footer-note {
|
|
351
|
+
margin-top: 18px;
|
|
352
|
+
color: var(--muted);
|
|
353
|
+
font-size: 14px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@media (max-width: 1100px) {
|
|
357
|
+
.hero, .grid {
|
|
358
|
+
grid-template-columns: 1fr;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@media (max-width: 720px) {
|
|
363
|
+
.shell { padding: 16px; }
|
|
364
|
+
.inline, .inline-3 {
|
|
365
|
+
grid-template-columns: 1fr;
|
|
366
|
+
}
|
|
367
|
+
h1 {
|
|
368
|
+
font-size: 2.7rem;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
</style>
|
|
372
|
+
</head>
|
|
373
|
+
<body>
|
|
374
|
+
<div class="shell">
|
|
375
|
+
<section class="hero">
|
|
376
|
+
<div class="panel hero-main">
|
|
377
|
+
<div class="eyebrow">Vidfarm Developer Console</div>
|
|
378
|
+
<h1>Build, queue, inspect, render.</h1>
|
|
379
|
+
<p class="hero-copy">
|
|
380
|
+
This is the in-house cockpit for template developers: OTP login, provider key setup, template config,
|
|
381
|
+
async job launches, and live execution traces against the local Vidfarm runtime.
|
|
382
|
+
</p>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="hero-side">
|
|
385
|
+
<div class="panel stat-card">
|
|
386
|
+
<div class="stat-label">Environment</div>
|
|
387
|
+
<div class="stat-value" id="envLabel">${input.environment}</div>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="panel stat-card">
|
|
390
|
+
<div class="stat-label">Templates Loaded</div>
|
|
391
|
+
<div class="stat-value" id="templateCount">${input.templates.length}</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="panel stat-card">
|
|
394
|
+
<div class="stat-label">Mode</div>
|
|
395
|
+
<div class="stat-value">Async First</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</section>
|
|
399
|
+
|
|
400
|
+
<section class="grid">
|
|
401
|
+
<aside class="stack">
|
|
402
|
+
<div class="panel card">
|
|
403
|
+
<div class="eyebrow">Access</div>
|
|
404
|
+
<h2>Developer Login</h2>
|
|
405
|
+
<label class="label" for="email">Email</label>
|
|
406
|
+
<input id="email" placeholder="dev@example.com" />
|
|
407
|
+
<div class="inline" style="margin-top:12px;">
|
|
408
|
+
<button id="requestOtp">Request OTP</button>
|
|
409
|
+
<button id="clearSession" class="secondary">Clear Session</button>
|
|
410
|
+
</div>
|
|
411
|
+
<label class="label" for="otp" style="margin-top:14px;">6 Digit OTP</label>
|
|
412
|
+
<input id="otp" placeholder="123456" />
|
|
413
|
+
<label class="label" for="name" style="margin-top:14px;">Display Name</label>
|
|
414
|
+
<input id="name" placeholder="Template Dev" />
|
|
415
|
+
<button id="verifyOtp" style="margin-top:12px;">Verify And Issue API Key</button>
|
|
416
|
+
<div class="footer-note" id="sessionSummary">No active session.</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="panel card">
|
|
420
|
+
<div class="eyebrow">Providers</div>
|
|
421
|
+
<h2>Customer AI Keys</h2>
|
|
422
|
+
<div class="inline">
|
|
423
|
+
<div>
|
|
424
|
+
<label class="label" for="provider">Provider</label>
|
|
425
|
+
<select id="provider">
|
|
426
|
+
<option value="openai">openai</option>
|
|
427
|
+
<option value="openrouter">openrouter</option>
|
|
428
|
+
<option value="gemini">gemini</option>
|
|
429
|
+
<option value="perplexity">perplexity</option>
|
|
430
|
+
</select>
|
|
431
|
+
</div>
|
|
432
|
+
<div>
|
|
433
|
+
<label class="label" for="weight">Weight</label>
|
|
434
|
+
<input id="weight" type="number" value="1" min="1" max="100" />
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
<label class="label" for="providerLabel" style="margin-top:14px;">Label</label>
|
|
438
|
+
<input id="providerLabel" placeholder="Primary key" />
|
|
439
|
+
<label class="label" for="providerSecret" style="margin-top:14px;">Secret</label>
|
|
440
|
+
<input id="providerSecret" placeholder="sk-..." />
|
|
441
|
+
<button id="saveProviderKey" style="margin-top:12px;">Store Provider Key</button>
|
|
442
|
+
<button id="refreshProviderKeys" class="secondary" style="margin-top:10px;">Refresh Key Inventory</button>
|
|
443
|
+
<pre id="providerKeysView" style="margin-top:14px;">[]</pre>
|
|
444
|
+
</div>
|
|
445
|
+
</aside>
|
|
446
|
+
|
|
447
|
+
<main class="workspace">
|
|
448
|
+
<div class="panel card">
|
|
449
|
+
<div class="eyebrow">Registry</div>
|
|
450
|
+
<h2>Templates</h2>
|
|
451
|
+
<div class="template-grid" id="templates"></div>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div class="panel card">
|
|
455
|
+
<div class="eyebrow">Template Control</div>
|
|
456
|
+
<h2 id="selectedTemplateTitle">Select a template</h2>
|
|
457
|
+
<div class="ribbon">
|
|
458
|
+
<span class="pill">Selected <strong id="selectedTemplateId">None</strong></span>
|
|
459
|
+
<span class="pill">Operation <strong id="selectedOperationName">None</strong></span>
|
|
460
|
+
<span class="pill">Tracer <strong id="selectedTracer">Pending</strong></span>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div class="inline-3">
|
|
464
|
+
<div>
|
|
465
|
+
<label class="label" for="operationSelect">Operation</label>
|
|
466
|
+
<select id="operationSelect"></select>
|
|
467
|
+
</div>
|
|
468
|
+
<div>
|
|
469
|
+
<label class="label" for="tracerInput">Tracer</label>
|
|
470
|
+
<input id="tracerInput" placeholder="dev-run-001" />
|
|
471
|
+
</div>
|
|
472
|
+
<div>
|
|
473
|
+
<label class="label" for="webhookInput">Webhook URL</label>
|
|
474
|
+
<input id="webhookInput" placeholder="Optional https://..." />
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<label class="label" for="configEditor" style="margin-top:18px;">Template Config JSON</label>
|
|
479
|
+
<textarea id="configEditor"></textarea>
|
|
480
|
+
<div class="inline" style="margin-top:12px;">
|
|
481
|
+
<button id="saveConfig">Save Config</button>
|
|
482
|
+
<button id="loadTemplate" class="secondary">Reload Template Metadata</button>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<label class="label" for="payloadEditor" style="margin-top:18px;">Operation Payload JSON</label>
|
|
486
|
+
<textarea id="payloadEditor"></textarea>
|
|
487
|
+
<div class="inline" style="margin-top:12px;">
|
|
488
|
+
<button id="launchJob">Launch Async Job</button>
|
|
489
|
+
<button id="loadJobs" class="secondary">Refresh Jobs</button>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div class="panel card">
|
|
494
|
+
<div class="eyebrow">Async Jobs</div>
|
|
495
|
+
<h2>Job Board</h2>
|
|
496
|
+
<div class="job-grid" id="jobs"></div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<div class="panel card">
|
|
500
|
+
<div class="eyebrow">Execution Trace</div>
|
|
501
|
+
<h2>Live Timeline</h2>
|
|
502
|
+
<div class="timeline" id="timeline"></div>
|
|
503
|
+
<div class="footer-note" id="jobSummary">No job selected.</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="panel card">
|
|
507
|
+
<div class="eyebrow">Response View</div>
|
|
508
|
+
<h2>Inspector</h2>
|
|
509
|
+
<pre id="inspector">{}</pre>
|
|
510
|
+
</div>
|
|
511
|
+
</main>
|
|
512
|
+
</section>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<script>
|
|
516
|
+
const BOOT = ${boot};
|
|
517
|
+
const state = {
|
|
518
|
+
baseUrl: window.location.origin,
|
|
519
|
+
session: loadSession(),
|
|
520
|
+
templates: BOOT.templates,
|
|
521
|
+
selectedTemplateId: BOOT.templates[0]?.id ?? null,
|
|
522
|
+
selectedOperation: null,
|
|
523
|
+
selectedJobId: null,
|
|
524
|
+
jobs: []
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const els = {
|
|
528
|
+
email: document.getElementById("email"),
|
|
529
|
+
otp: document.getElementById("otp"),
|
|
530
|
+
name: document.getElementById("name"),
|
|
531
|
+
requestOtp: document.getElementById("requestOtp"),
|
|
532
|
+
verifyOtp: document.getElementById("verifyOtp"),
|
|
533
|
+
clearSession: document.getElementById("clearSession"),
|
|
534
|
+
sessionSummary: document.getElementById("sessionSummary"),
|
|
535
|
+
provider: document.getElementById("provider"),
|
|
536
|
+
weight: document.getElementById("weight"),
|
|
537
|
+
providerLabel: document.getElementById("providerLabel"),
|
|
538
|
+
providerSecret: document.getElementById("providerSecret"),
|
|
539
|
+
saveProviderKey: document.getElementById("saveProviderKey"),
|
|
540
|
+
refreshProviderKeys: document.getElementById("refreshProviderKeys"),
|
|
541
|
+
providerKeysView: document.getElementById("providerKeysView"),
|
|
542
|
+
templates: document.getElementById("templates"),
|
|
543
|
+
operationSelect: document.getElementById("operationSelect"),
|
|
544
|
+
tracerInput: document.getElementById("tracerInput"),
|
|
545
|
+
webhookInput: document.getElementById("webhookInput"),
|
|
546
|
+
selectedTemplateTitle: document.getElementById("selectedTemplateTitle"),
|
|
547
|
+
selectedTemplateId: document.getElementById("selectedTemplateId"),
|
|
548
|
+
selectedOperationName: document.getElementById("selectedOperationName"),
|
|
549
|
+
selectedTracer: document.getElementById("selectedTracer"),
|
|
550
|
+
configEditor: document.getElementById("configEditor"),
|
|
551
|
+
payloadEditor: document.getElementById("payloadEditor"),
|
|
552
|
+
saveConfig: document.getElementById("saveConfig"),
|
|
553
|
+
loadTemplate: document.getElementById("loadTemplate"),
|
|
554
|
+
launchJob: document.getElementById("launchJob"),
|
|
555
|
+
loadJobs: document.getElementById("loadJobs"),
|
|
556
|
+
jobs: document.getElementById("jobs"),
|
|
557
|
+
timeline: document.getElementById("timeline"),
|
|
558
|
+
inspector: document.getElementById("inspector"),
|
|
559
|
+
jobSummary: document.getElementById("jobSummary")
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
function loadSession() {
|
|
563
|
+
try {
|
|
564
|
+
return JSON.parse(localStorage.getItem("vidfarm-dev-session") || "null");
|
|
565
|
+
} catch {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function saveSession(session) {
|
|
571
|
+
state.session = session;
|
|
572
|
+
if (session) {
|
|
573
|
+
localStorage.setItem("vidfarm-dev-session", JSON.stringify(session));
|
|
574
|
+
} else {
|
|
575
|
+
localStorage.removeItem("vidfarm-dev-session");
|
|
576
|
+
}
|
|
577
|
+
renderSession();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function headers() {
|
|
581
|
+
if (!state.session) {
|
|
582
|
+
throw new Error("No active session. Verify OTP first.");
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
"content-type": "application/json",
|
|
586
|
+
"vidfarm-user-id": state.session.customer.id,
|
|
587
|
+
"vidfarm-api-key": state.session.apiKey
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function request(path, options = {}) {
|
|
592
|
+
const response = await fetch(state.baseUrl + path, options);
|
|
593
|
+
const data = await response.json().catch(() => ({}));
|
|
594
|
+
if (!response.ok) {
|
|
595
|
+
throw new Error(data.error || JSON.stringify(data));
|
|
596
|
+
}
|
|
597
|
+
els.inspector.textContent = JSON.stringify(data, null, 2);
|
|
598
|
+
return data;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function renderSession() {
|
|
602
|
+
if (!state.session) {
|
|
603
|
+
els.sessionSummary.textContent = "No active session.";
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
els.sessionSummary.textContent = "Active: " + state.session.customer.email + " • " + state.session.customer.id;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getSelectedTemplate() {
|
|
610
|
+
return state.templates.find((template) => template.id === state.selectedTemplateId) || null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getTemplateDefaults(templateId, operation) {
|
|
614
|
+
if (templateId === "demo-template") {
|
|
615
|
+
const config = {
|
|
616
|
+
defaultProvider: "openai",
|
|
617
|
+
textModel: "gpt-4.1-mini",
|
|
618
|
+
imageModel: "gpt-image-1",
|
|
619
|
+
renderCompositionId: "demo-template"
|
|
620
|
+
};
|
|
621
|
+
const payloads = {
|
|
622
|
+
generate: {
|
|
623
|
+
slides: [
|
|
624
|
+
["a cinematic founder working late in a warm studio", "Build the pipeline before you scale the content"],
|
|
625
|
+
["a crisp top-down desk with storyboard pages and coffee", "Use layouts that leave clean space for exact text"],
|
|
626
|
+
["a polished vertical product montage with dramatic lighting", "Render to 9:16 and ship straight to TikTok"]
|
|
627
|
+
],
|
|
628
|
+
secondsPerSlide: 4
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
return {
|
|
632
|
+
config,
|
|
633
|
+
payload: payloads[operation] || {}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return { config: {}, payload: {} };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderTemplates() {
|
|
640
|
+
els.templates.innerHTML = "";
|
|
641
|
+
state.templates.forEach((template) => {
|
|
642
|
+
const card = document.createElement("button");
|
|
643
|
+
card.className = "template-card" + (template.id === state.selectedTemplateId ? " active" : "");
|
|
644
|
+
card.innerHTML = \`
|
|
645
|
+
<div class="template-title">\${template.id}</div>
|
|
646
|
+
<div class="template-desc">\${template.description || ""}</div>
|
|
647
|
+
<div class="operation-list">
|
|
648
|
+
\${(template.operations || []).map((op) => \`<span class="op">\${op.name}</span>\`).join("")}
|
|
649
|
+
</div>
|
|
650
|
+
\`;
|
|
651
|
+
card.addEventListener("click", () => {
|
|
652
|
+
state.selectedTemplateId = template.id;
|
|
653
|
+
state.selectedOperation = template.operations?.[0]?.name || null;
|
|
654
|
+
hydrateTemplateEditors();
|
|
655
|
+
renderTemplates();
|
|
656
|
+
});
|
|
657
|
+
els.templates.appendChild(card);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function hydrateTemplateEditors() {
|
|
662
|
+
const template = getSelectedTemplate();
|
|
663
|
+
if (!template) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
els.selectedTemplateTitle.textContent = template.description || template.id;
|
|
667
|
+
els.selectedTemplateId.textContent = template.id;
|
|
668
|
+
els.operationSelect.innerHTML = "";
|
|
669
|
+
(template.operations || []).forEach((op) => {
|
|
670
|
+
const option = document.createElement("option");
|
|
671
|
+
option.value = op.name;
|
|
672
|
+
option.textContent = op.name;
|
|
673
|
+
els.operationSelect.appendChild(option);
|
|
674
|
+
});
|
|
675
|
+
if (!state.selectedOperation) {
|
|
676
|
+
state.selectedOperation = template.operations?.[0]?.name || null;
|
|
677
|
+
}
|
|
678
|
+
els.operationSelect.value = state.selectedOperation || "";
|
|
679
|
+
const defaults = getTemplateDefaults(template.id, state.selectedOperation);
|
|
680
|
+
els.configEditor.value = JSON.stringify(defaults.config, null, 2);
|
|
681
|
+
els.payloadEditor.value = JSON.stringify(defaults.payload, null, 2);
|
|
682
|
+
els.tracerInput.value = "dev-" + Math.random().toString(36).slice(2, 9);
|
|
683
|
+
renderOperationBanner();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function renderOperationBanner() {
|
|
687
|
+
els.selectedOperationName.textContent = state.selectedOperation || "None";
|
|
688
|
+
els.selectedTracer.textContent = els.tracerInput.value || "Pending";
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function renderJobs() {
|
|
692
|
+
els.jobs.innerHTML = "";
|
|
693
|
+
state.jobs.forEach((job) => {
|
|
694
|
+
const card = document.createElement("button");
|
|
695
|
+
card.className = "job-card";
|
|
696
|
+
card.innerHTML = \`
|
|
697
|
+
<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;">
|
|
698
|
+
<strong>\${job.operationName || job.operation_name}</strong>
|
|
699
|
+
<span class="status \${job.status}">\${job.status}</span>
|
|
700
|
+
</div>
|
|
701
|
+
<div class="muted" style="margin-top:8px;">\${job.id || job.job_id}</div>
|
|
702
|
+
<div class="muted" style="margin-top:8px;">Tracer: \${job.tracer}</div>
|
|
703
|
+
\`;
|
|
704
|
+
card.addEventListener("click", async () => {
|
|
705
|
+
state.selectedJobId = job.id || job.job_id;
|
|
706
|
+
await refreshJobDetail();
|
|
707
|
+
});
|
|
708
|
+
els.jobs.appendChild(card);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function refreshProviderKeys() {
|
|
713
|
+
const data = await request("/me/provider-keys", { headers: headers() });
|
|
714
|
+
els.providerKeysView.textContent = JSON.stringify(data.provider_keys, null, 2);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function refreshJobs() {
|
|
718
|
+
const data = await request("/me/jobs", { headers: headers() });
|
|
719
|
+
state.jobs = data.jobs || [];
|
|
720
|
+
renderJobs();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function refreshJobDetail() {
|
|
724
|
+
if (!state.selectedJobId || !state.selectedTemplateId) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const [job, logs] = await Promise.all([
|
|
728
|
+
request("/templates/" + state.selectedTemplateId + "/jobs/" + state.selectedJobId, { headers: headers() }),
|
|
729
|
+
request("/templates/" + state.selectedTemplateId + "/jobs/" + state.selectedJobId + "/logs", { headers: headers() })
|
|
730
|
+
]);
|
|
731
|
+
renderTimeline(logs.logs || []);
|
|
732
|
+
els.jobSummary.textContent = "Job " + job.job_id + " is " + job.status + " with progress " + job.progress;
|
|
733
|
+
if (job.status === "queued" || job.status === "running") {
|
|
734
|
+
setTimeout(refreshJobDetail, 1500);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function renderTimeline(logs) {
|
|
739
|
+
els.timeline.innerHTML = "";
|
|
740
|
+
if (!logs.length) {
|
|
741
|
+
els.timeline.innerHTML = "<div class='muted'>No events yet.</div>";
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
logs.forEach((log) => {
|
|
745
|
+
const el = document.createElement("div");
|
|
746
|
+
el.className = "event";
|
|
747
|
+
el.innerHTML = \`
|
|
748
|
+
<div class="event-head">
|
|
749
|
+
<span>\${log.level.toUpperCase()}</span>
|
|
750
|
+
<span>\${new Date(log.createdAt).toLocaleTimeString()}</span>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="event-msg">\${log.message}</div>
|
|
753
|
+
<div class="muted" style="margin-top:6px;">progress: \${log.progress ?? "n/a"}</div>
|
|
754
|
+
\`;
|
|
755
|
+
els.timeline.appendChild(el);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
els.requestOtp.addEventListener("click", async () => {
|
|
760
|
+
const email = els.email.value.trim();
|
|
761
|
+
if (!email) return;
|
|
762
|
+
await request("/auth/request-otp", {
|
|
763
|
+
method: "POST",
|
|
764
|
+
headers: { "content-type": "application/json" },
|
|
765
|
+
body: JSON.stringify({ email })
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
els.verifyOtp.addEventListener("click", async () => {
|
|
770
|
+
const data = await request("/auth/verify-otp", {
|
|
771
|
+
method: "POST",
|
|
772
|
+
headers: { "content-type": "application/json" },
|
|
773
|
+
body: JSON.stringify({
|
|
774
|
+
email: els.email.value.trim(),
|
|
775
|
+
code: els.otp.value.trim(),
|
|
776
|
+
name: els.name.value.trim() || undefined
|
|
777
|
+
})
|
|
778
|
+
});
|
|
779
|
+
saveSession(data);
|
|
780
|
+
await refreshProviderKeys();
|
|
781
|
+
await refreshJobs();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
els.clearSession.addEventListener("click", () => {
|
|
785
|
+
saveSession(null);
|
|
786
|
+
els.providerKeysView.textContent = "[]";
|
|
787
|
+
els.jobs.innerHTML = "";
|
|
788
|
+
els.timeline.innerHTML = "";
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
els.saveProviderKey.addEventListener("click", async () => {
|
|
792
|
+
await request("/me/provider-keys", {
|
|
793
|
+
method: "POST",
|
|
794
|
+
headers: headers(),
|
|
795
|
+
body: JSON.stringify({
|
|
796
|
+
provider: els.provider.value,
|
|
797
|
+
label: els.providerLabel.value.trim() || undefined,
|
|
798
|
+
secret: els.providerSecret.value.trim(),
|
|
799
|
+
weight: Number(els.weight.value || 1)
|
|
800
|
+
})
|
|
801
|
+
});
|
|
802
|
+
els.providerSecret.value = "";
|
|
803
|
+
await refreshProviderKeys();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
els.refreshProviderKeys.addEventListener("click", refreshProviderKeys);
|
|
807
|
+
|
|
808
|
+
els.operationSelect.addEventListener("change", () => {
|
|
809
|
+
state.selectedOperation = els.operationSelect.value;
|
|
810
|
+
const defaults = getTemplateDefaults(state.selectedTemplateId, state.selectedOperation);
|
|
811
|
+
els.payloadEditor.value = JSON.stringify(defaults.payload, null, 2);
|
|
812
|
+
renderOperationBanner();
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
els.tracerInput.addEventListener("input", renderOperationBanner);
|
|
816
|
+
|
|
817
|
+
els.saveConfig.addEventListener("click", async () => {
|
|
818
|
+
if (!state.selectedTemplateId) return;
|
|
819
|
+
await request("/templates/" + state.selectedTemplateId + "/config", {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: headers(),
|
|
822
|
+
body: JSON.stringify({ config: JSON.parse(els.configEditor.value) })
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
els.launchJob.addEventListener("click", async () => {
|
|
827
|
+
if (!state.selectedTemplateId || !state.selectedOperation) return;
|
|
828
|
+
const data = await request("/templates/" + state.selectedTemplateId + "/operations/" + state.selectedOperation, {
|
|
829
|
+
method: "POST",
|
|
830
|
+
headers: headers(),
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
tracer: els.tracerInput.value.trim() || ("dev-" + Date.now()),
|
|
833
|
+
payload: JSON.parse(els.payloadEditor.value),
|
|
834
|
+
webhook_url: els.webhookInput.value.trim() || undefined
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
state.selectedJobId = data.job_id;
|
|
838
|
+
await refreshJobs();
|
|
839
|
+
await refreshJobDetail();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
els.loadJobs.addEventListener("click", refreshJobs);
|
|
843
|
+
els.loadTemplate.addEventListener("click", hydrateTemplateEditors);
|
|
844
|
+
|
|
845
|
+
renderSession();
|
|
846
|
+
renderTemplates();
|
|
847
|
+
hydrateTemplateEditors();
|
|
848
|
+
if (state.session) {
|
|
849
|
+
refreshProviderKeys().catch((error) => {
|
|
850
|
+
els.inspector.textContent = JSON.stringify({ error: error.message }, null, 2);
|
|
851
|
+
});
|
|
852
|
+
refreshJobs().catch((error) => {
|
|
853
|
+
els.inspector.textContent = JSON.stringify({ error: error.message }, null, 2);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
</script>
|
|
857
|
+
</body>
|
|
858
|
+
</html>`;
|
|
859
|
+
}
|