@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1
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/.agents/skills/vidfarm-media/SKILL.md +43 -6
- package/SKILL.director.md +37 -23
- package/SKILL.platform.md +6 -6
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1832 -213
- package/dist/src/cli.js +238 -17
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +116 -73
- package/dist/src/devcli/composition-edit.js +262 -0
- package/dist/src/devcli/timeline-edit.js +283 -0
- package/dist/src/editor-chat.js +29 -6
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +744 -0
- package/dist/src/frontend/template-editor-chat.js +22 -19
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +25 -1
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +503 -187
- package/dist/src/reskin/chat-page.js +739 -299
- package/dist/src/reskin/discover-page.js +1450 -279
- package/dist/src/reskin/document.js +1392 -16
- package/dist/src/reskin/help-page.js +139 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +547 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +1188 -317
- package/dist/src/reskin/login-page.js +124 -87
- package/dist/src/reskin/pricing-page.js +197 -166
- package/dist/src/reskin/settings-page.js +566 -187
- package/dist/src/reskin/theme.js +434 -17
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +79 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +14 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +114 -0
- package/dist/src/services/storage.js +24 -1
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,115 +1,94 @@
|
|
|
1
|
-
// /reskin/job-runs — render & job history / health,
|
|
2
|
-
//
|
|
3
|
-
// zero-arg + SAMPLE-data + page-scoped rk-<page>- CSS + self-contained script
|
|
4
|
-
// conventions.
|
|
1
|
+
// /reskin/job-runs — render & job history / health, MIGRATED to real production
|
|
2
|
+
// data (part of the /reskin/* UX migration; reference: settings-page.ts).
|
|
5
3
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// Like the live page (src/account-pages-legacy.ts → renderJobRunsPage), this
|
|
5
|
+
// renders a SHELL then CLIENT-FETCHES the real history from `input.loadEndpoint`
|
|
6
|
+
// (`/job-runs/history?limit=250`, same-origin, credentials). The endpoint
|
|
7
|
+
// returns `{ entries, storage_driver, next_cursor }`; each entry is an
|
|
8
|
+
// api-call-history row (`id, method, path, routePrefix, tracer, jobId, status,
|
|
9
|
+
// durationMs, request, response, mediaUrls, outputMediaUrls, createdAt`) with an
|
|
10
|
+
// optional resolved `linkedJob` ({ job_id, template_id, operation_name,
|
|
11
|
+
// workflow_name, tracer, status, progress, started_at, completed_at,
|
|
12
|
+
// billing.consumed_usd, ... }). We map each entry into a farmville run row —
|
|
13
|
+
// status tile (✓/●/✕/◔), friendly type + job id, primitive/tracer sub-meta,
|
|
14
|
+
// started + duration, REAL compute cost from `linkedJob.billing.consumed_usd`,
|
|
15
|
+
// and an inline "View logs" details panel. Summary counts + compute total are
|
|
16
|
+
// recomputed from the real data; the status filter runs over the real rows.
|
|
17
|
+
//
|
|
18
|
+
// The renderer takes the SAME input the live route builds
|
|
19
|
+
// (`Parameters<typeof renderJobRunsPage>[0]`) and defaults it, so the existing
|
|
20
|
+
// zero-arg `/reskin/job-runs` route keeps working (it fetches the live endpoint
|
|
21
|
+
// client-side). NOTE: `linkedJob.billing` is present at RUNTIME on the real
|
|
22
|
+
// endpoint but omitted from the declared input type — the client reads it off
|
|
23
|
+
// the parsed JSON, so cost is real, not mocked.
|
|
12
24
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ jobId: "job_dd12", type: "Decompose", emoji: "🧬", status: "running", sub: "Costco haul remix", operation: "decompose", tracer: "decompose-dna", startedAt: "Jul 7, 3:40 PM", duration: "", progress: 0.71, costUsd: 0 },
|
|
23
|
-
{ jobId: "job_77c2", type: "HyperFrames render", emoji: "🎞️", status: "failed", sub: "Q3 launch reel · ffprobe not found", operation: "primitive:hyperframes_render", tracer: "render-final", startedAt: "Jul 7, 2:15 PM", duration: "12s", costUsd: 0.06 },
|
|
24
|
-
{ jobId: "job_9b3e", type: "Text-to-speech", emoji: "🔊", status: "succeeded", sub: "Founder VO · 320 words", operation: "primitive:tts", tracer: "vo-narration", startedAt: "Jul 7, 1:50 PM", duration: "9s", costUsd: 0.03 },
|
|
25
|
-
{ jobId: "job_51aa", type: "Scene annotation", emoji: "🗂️", status: "succeeded", sub: "DTC skincare ad · 8 scenes", operation: "scene_annotation", tracer: "scene-dna", startedAt: "Jul 7, 12:30 PM", duration: "54s", costUsd: 0.71 },
|
|
26
|
-
{ jobId: "job_2f08", type: "Video download", emoji: "⬇️", status: "succeeded", sub: "youtube.com/watch?v=… · URL import", operation: "primitive:video_download", tracer: "url-import", startedAt: "Jul 6, 6:22 PM", duration: "22s", costUsd: 0.08 },
|
|
27
|
-
{ jobId: "job_be61", type: "Swipe customize", emoji: "🃏", status: "running", sub: "Recut: Morning routine → HydraGlow", operation: "swipe_customize", tracer: "swipe-deck", startedAt: "Jul 6, 5:10 PM", duration: "", progress: 0.33, costUsd: 0 },
|
|
28
|
-
{ jobId: "job_04c9", type: "Image generate", emoji: "🖼️", status: "succeeded", sub: "hero-frame-02 · 1080×1920", operation: "primitive:image_generate", tracer: "gen-frames", startedAt: "Jul 6, 3:44 PM", duration: "7s", costUsd: 0.04 },
|
|
29
|
-
{ jobId: "job_7a20", type: "Cast identification", emoji: "🎭", status: "failed", sub: "Interview cut · provider rate limit", operation: "cast_identification", tracer: "cast-isolate", startedAt: "Jul 6, 11:05 AM", duration: "4s", costUsd: 0 },
|
|
30
|
-
{ jobId: "job_3d1c", type: "Speech-to-text", emoji: "📝", status: "succeeded", sub: "podcast-ep12.mp3 · word timestamps", operation: "primitive:stt", tracer: "caption-sync", startedAt: "Jul 5, 8:31 PM", duration: "41s", costUsd: 0.19 },
|
|
31
|
-
{ jobId: "job_e5f7", type: "Product placement", emoji: "📦", status: "queued", sub: "SaaS dashboard promo · waiting for provider", operation: "product_placement", tracer: "placement-scout", startedAt: "Jul 5, 7:58 PM", duration: "", costUsd: 0 }
|
|
32
|
-
]
|
|
33
|
-
};
|
|
34
|
-
const STATUS_META = {
|
|
35
|
-
succeeded: { label: "Succeeded", tile: "rk-tile-green", pill: "rk-pill-green", glyph: "✓" },
|
|
36
|
-
running: { label: "Running", tile: "rk-jobs-tile-run", pill: "rk-pill-gold", glyph: "●" },
|
|
37
|
-
failed: { label: "Failed", tile: "rk-jobs-tile-fail", pill: "rk-pill-red", glyph: "✕" },
|
|
38
|
-
queued: { label: "Queued", tile: "rk-tile-ink", pill: "rk-pill", glyph: "◔" }
|
|
25
|
+
const DEFAULT_INPUT = {
|
|
26
|
+
userId: "",
|
|
27
|
+
currentAccountId: "",
|
|
28
|
+
name: null,
|
|
29
|
+
email: "",
|
|
30
|
+
storageDriver: "s3",
|
|
31
|
+
loadEndpoint: "/job-runs/history?limit=250",
|
|
32
|
+
jobs: [],
|
|
33
|
+
apiCallHistory: []
|
|
39
34
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
36
|
+
function jsonForScript(value) {
|
|
37
|
+
return JSON.stringify(value ?? null)
|
|
38
|
+
.replace(/</g, "\\u003c")
|
|
39
|
+
.replace(/>/g, "\\u003e")
|
|
40
|
+
.replace(/&/g, "\\u0026")
|
|
41
|
+
.replace(/\u2028/g, "\\u2028")
|
|
42
|
+
.replace(/\u2029/g, "\\u2029");
|
|
44
43
|
}
|
|
45
|
-
|
|
44
|
+
/** Server-rendered filter chip (counts start at 0; the client fills them). */
|
|
45
|
+
function statChip(filter, label, tone, active = false) {
|
|
46
46
|
return `<button type="button" class="rk-jobs-chip${tone}${active ? " is-active" : ""}" data-rk-filter="${filter}">
|
|
47
|
-
${rkEscape(label)}<span class="rk-jobs-chip-n"
|
|
47
|
+
${rkEscape(label)}<span class="rk-jobs-chip-n" data-rk-count="${filter}">0</span>
|
|
48
48
|
</button>`;
|
|
49
49
|
}
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</div>
|
|
57
|
-
<div class="rk-jobs-when-dur is-run">${pct}% done</div>`
|
|
58
|
-
: r.status === "queued"
|
|
59
|
-
? `<div class="rk-jobs-when-dur is-wait">In queue</div>`
|
|
60
|
-
: `<div class="rk-jobs-when-dur">${rkEscape(r.duration)}</div>`;
|
|
61
|
-
return `<div class="rk-jobs-row" data-rk-status="${r.status}">
|
|
62
|
-
<span class="rk-tile ${meta.tile} rk-jobs-tile" aria-hidden="true">${meta.glyph}</span>
|
|
63
|
-
<div class="rk-jobs-main">
|
|
64
|
-
<div class="rk-jobs-row-title">
|
|
65
|
-
<span class="rk-jobs-type">${rkEscape(r.type)}</span>
|
|
66
|
-
<span class="rk-mono rk-jobs-id">${rkEscape(r.jobId)}</span>
|
|
67
|
-
<span class="rk-pill ${meta.pill}">${meta.label}</span>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="rk-jobs-sub">
|
|
70
|
-
${rkEscape(r.sub)} · <span class="rk-mono">${rkEscape(r.operation)}</span>${r.tracer ? ` · tracer <span class="rk-mono">${rkEscape(r.tracer)}</span>` : ""}
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="rk-jobs-tail">
|
|
74
|
-
<div class="rk-jobs-when">
|
|
75
|
-
<div class="rk-jobs-when-at">${rkEscape(r.startedAt)}</div>
|
|
76
|
-
${when}
|
|
77
|
-
</div>
|
|
78
|
-
<div class="rk-jobs-cost${r.costUsd ? "" : " is-zero"}">${money(r.costUsd)}</div>
|
|
79
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-jobs-logs">View logs</button>
|
|
80
|
-
</div>
|
|
81
|
-
</div>`;
|
|
82
|
-
}
|
|
83
|
-
export function renderReskinJobRuns() {
|
|
84
|
-
const s = SAMPLE;
|
|
85
|
-
const running = s.runs.filter((r) => r.status === "running").length;
|
|
86
|
-
const queued = s.runs.filter((r) => r.status === "queued").length;
|
|
87
|
-
const succeeded = s.runs.filter((r) => r.status === "succeeded").length;
|
|
88
|
-
const failed = s.runs.filter((r) => r.status === "failed").length;
|
|
89
|
-
const successRate = Math.round((s.succeeded / s.totalRuns) * 100);
|
|
90
|
-
const rows = s.runs.map(runRow).join("");
|
|
50
|
+
export function renderReskinJobRuns(input = DEFAULT_INPUT) {
|
|
51
|
+
const loadEndpoint = input.loadEndpoint || "/job-runs/history?limit=250";
|
|
52
|
+
const accountId = input.currentAccountId || "";
|
|
53
|
+
const storageDriver = input.storageDriver || "s3";
|
|
54
|
+
const seed = Array.isArray(input.apiCallHistory) ? input.apiCallHistory : [];
|
|
55
|
+
const boot = { loadEndpoint, accountId, storageDriver, entries: seed };
|
|
91
56
|
const body = `
|
|
92
57
|
<main class="rk-container rk-page">
|
|
93
58
|
<div class="rk-page-head">
|
|
94
|
-
<span class="rk-badge">Run history & health</span>
|
|
95
59
|
<h1 class="rk-h1">Job runs</h1>
|
|
96
60
|
<p class="rk-sub">Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost.</p>
|
|
97
61
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
62
|
+
${jobRunsPanel(boot)}
|
|
63
|
+
</main>`;
|
|
64
|
+
return reskinDocument({
|
|
65
|
+
title: "vidfarm reskin — Job runs",
|
|
66
|
+
description: "Reskinned vidfarm job runs page — real render & job history, status, timing, and compute cost.",
|
|
67
|
+
activeSlug: "job-runs",
|
|
68
|
+
pageCss: JOBRUNS_PAGE_CSS,
|
|
69
|
+
body,
|
|
70
|
+
script: JOBRUNS_SCRIPT
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// The runs panel (summary + filterable list + boot JSON), WITHOUT the page
|
|
74
|
+
// <main>/head — so it can be embedded standalone (renderReskinJobRuns) OR folded
|
|
75
|
+
// into the Library "Logs" tab (library-page.ts). Hoisted; safe to call above.
|
|
76
|
+
export function jobRunsPanel(boot) {
|
|
77
|
+
return `
|
|
78
|
+
<div class="rk-jobs-summary" data-rk-summary>Loading run history…</div>
|
|
100
79
|
|
|
101
80
|
<div class="rk-card rk-jobs-panel">
|
|
102
81
|
<div class="rk-spread rk-wrap rk-jobs-panel-head">
|
|
103
82
|
<div class="rk-jobs-filters" role="group" aria-label="Filter runs by status">
|
|
104
|
-
${statChip(
|
|
105
|
-
${statChip(
|
|
106
|
-
${statChip(
|
|
107
|
-
${statChip(
|
|
108
|
-
${statChip(
|
|
83
|
+
${statChip("all", "All", "", true)}
|
|
84
|
+
${statChip("succeeded", "Succeeded", " is-green")}
|
|
85
|
+
${statChip("running", "Running", " is-gold")}
|
|
86
|
+
${statChip("failed", "Failed", " is-red")}
|
|
87
|
+
${statChip("queued", "Queued", " is-ink")}
|
|
109
88
|
</div>
|
|
110
89
|
<div class="rk-row">
|
|
111
90
|
<span class="rk-pill rk-pill-gold rk-jobs-live"><span class="rk-jobs-live-dot"></span>Live</span>
|
|
112
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-refresh
|
|
91
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-refresh>↻ Refresh</button>
|
|
113
92
|
</div>
|
|
114
93
|
</div>
|
|
115
94
|
|
|
@@ -123,17 +102,20 @@ export function renderReskinJobRuns() {
|
|
|
123
102
|
</span>
|
|
124
103
|
</div>
|
|
125
104
|
|
|
126
|
-
<div class="rk-jobs-list" data-rk-list
|
|
127
|
-
<div class="rk-jobs-
|
|
105
|
+
<div class="rk-jobs-list" data-rk-list></div>
|
|
106
|
+
<div class="rk-jobs-state" data-rk-state>Loading run history…</div>
|
|
107
|
+
<div class="rk-jobs-empty" data-rk-empty hidden>No runs match this filter.</div>
|
|
128
108
|
|
|
129
109
|
<div class="rk-row rk-jobs-foot">
|
|
130
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm">Load older runs</button>
|
|
131
|
-
<span class="rk-hint"
|
|
110
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-more hidden>Load older runs</button>
|
|
111
|
+
<span class="rk-hint" data-rk-count-foot></span>
|
|
132
112
|
</div>
|
|
133
113
|
</div>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
114
|
+
|
|
115
|
+
<script type="application/json" data-rk-jobs-boot>${jsonForScript(boot)}</script>`;
|
|
116
|
+
}
|
|
117
|
+
export const JOBRUNS_PAGE_CSS = `
|
|
118
|
+
.rk-jobs-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;margin-bottom:22px;line-height:1.7;min-height:24px}
|
|
137
119
|
.rk-jobs-summary b{color:var(--rk-ink);font-weight:800;font-family:var(--rk-font-display);font-size:15px;letter-spacing:-.01em}
|
|
138
120
|
.rk-jobs-summary-note{color:var(--rk-text-faint)}
|
|
139
121
|
.rk-jobs-panel{padding:22px 22px 18px}
|
|
@@ -173,14 +155,15 @@ export function renderReskinJobRuns() {
|
|
|
173
155
|
|
|
174
156
|
/* run rows */
|
|
175
157
|
.rk-jobs-list{display:grid;gap:10px}
|
|
158
|
+
.rk-jobs-item{min-width:0}
|
|
159
|
+
.rk-jobs-item[hidden]{display:none}
|
|
176
160
|
.rk-jobs-row{display:grid;grid-template-columns:44px minmax(0,1fr) 156px 78px auto;gap:16px;align-items:center;
|
|
177
161
|
padding:14px 16px;background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);
|
|
178
162
|
box-shadow:var(--rk-shadow-xs);transition:box-shadow var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
179
163
|
.rk-jobs-row:hover{box-shadow:var(--rk-shadow-md);border-color:var(--rk-border-strong)}
|
|
180
|
-
.rk-jobs-
|
|
164
|
+
.rk-jobs-item[data-rk-status="running"] .rk-jobs-row{border-color:var(--rk-gold-tint2);background:linear-gradient(180deg,var(--rk-gold-tint) 0%,#fff 42%)}
|
|
181
165
|
.rk-jobs-main{min-width:0}
|
|
182
166
|
.rk-jobs-row-title{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:4px}
|
|
183
|
-
.rk-jobs-emoji{font-size:15px;line-height:1}
|
|
184
167
|
.rk-jobs-type{font-family:var(--rk-font-display);font-weight:700;font-size:15px;color:var(--rk-ink);letter-spacing:-.01em}
|
|
185
168
|
.rk-jobs-id{font-size:12px;color:var(--rk-text-faint)}
|
|
186
169
|
.rk-jobs-sub{font-size:13px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -198,8 +181,28 @@ export function renderReskinJobRuns() {
|
|
|
198
181
|
.rk-jobs-cost.is-zero{color:var(--rk-text-faint);font-weight:500}
|
|
199
182
|
.rk-jobs-logs{flex:none}
|
|
200
183
|
|
|
201
|
-
|
|
184
|
+
/* inline "View logs" details panel */
|
|
185
|
+
.rk-jobs-details{margin-top:8px;padding:14px 16px;background:var(--rk-n-50);border:1px solid var(--rk-border);
|
|
186
|
+
border-radius:var(--rk-r-2xl);display:grid;gap:8px;font-size:13px}
|
|
187
|
+
.rk-jobs-details[hidden]{display:none}
|
|
188
|
+
.rk-jobs-detail-row{display:flex;gap:12px;align-items:baseline}
|
|
189
|
+
.rk-jobs-detail-k{flex:none;min-width:96px;color:var(--rk-text-faint);font-weight:800;font-size:10.5px;
|
|
190
|
+
letter-spacing:.06em;text-transform:uppercase;padding-top:1px}
|
|
191
|
+
.rk-jobs-detail-v{color:var(--rk-n-700);word-break:break-word;min-width:0;font-family:var(--rk-font-mono);font-size:12.5px}
|
|
192
|
+
.rk-jobs-detail-media{display:flex;flex-wrap:wrap;gap:8px}
|
|
193
|
+
.rk-jobs-detail-media a{font-family:var(--rk-font-body);font-size:12px;font-weight:600;color:#1d6fb8;
|
|
194
|
+
padding:3px 10px;border-radius:var(--rk-r-full);background:var(--rk-sky-tint)}
|
|
195
|
+
|
|
196
|
+
/* load / empty / error states */
|
|
197
|
+
.rk-jobs-state{padding:44px 16px;text-align:center;color:var(--rk-text-muted);font-size:14px;
|
|
198
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs)}
|
|
199
|
+
.rk-jobs-state[hidden]{display:none}
|
|
200
|
+
.rk-jobs-state.is-error{color:#b91c1c;background:var(--rk-red-tint);border-color:#fecaca;font-weight:500}
|
|
201
|
+
.rk-jobs-empty{padding:36px 16px;text-align:center;color:var(--rk-text-muted);font-size:14px}
|
|
202
|
+
.rk-jobs-empty[hidden]{display:none}
|
|
202
203
|
.rk-jobs-foot{margin-top:16px;justify-content:space-between}
|
|
204
|
+
/* .rk-btn sets display:inline-flex, which beats the UA [hidden] rule — force it */
|
|
205
|
+
[data-rk-more][hidden]{display:none}
|
|
203
206
|
|
|
204
207
|
@media(max-width:780px){
|
|
205
208
|
.rk-jobs-head{display:none}
|
|
@@ -214,36 +217,261 @@ export function renderReskinJobRuns() {
|
|
|
214
217
|
.rk-jobs-foot{flex-direction:column;align-items:flex-start;gap:10px}
|
|
215
218
|
}
|
|
216
219
|
`;
|
|
217
|
-
|
|
220
|
+
export const JOBRUNS_SCRIPT = `
|
|
218
221
|
(function(){
|
|
219
222
|
var root=document;
|
|
220
|
-
var
|
|
221
|
-
var
|
|
223
|
+
var bootEl=root.querySelector('[data-rk-jobs-boot]');
|
|
224
|
+
var boot={};
|
|
225
|
+
try{ boot=JSON.parse(bootEl&&bootEl.textContent?bootEl.textContent:'{}'); }catch(e){ boot={}; }
|
|
226
|
+
var loadEndpoint=boot.loadEndpoint||'/job-runs/history?limit=250';
|
|
227
|
+
var accountId=boot.accountId||'';
|
|
228
|
+
|
|
229
|
+
var listEl=root.querySelector('[data-rk-list]');
|
|
230
|
+
var stateEl=root.querySelector('[data-rk-state]');
|
|
231
|
+
var emptyEl=root.querySelector('[data-rk-empty]');
|
|
232
|
+
var summaryEl=root.querySelector('[data-rk-summary]');
|
|
233
|
+
var footEl=root.querySelector('[data-rk-count-foot]');
|
|
234
|
+
var moreBtn=root.querySelector('[data-rk-more]');
|
|
235
|
+
var refreshBtn=root.querySelector('[data-rk-refresh]');
|
|
236
|
+
|
|
237
|
+
var state={ entries:[], filter:'all', nextCursor:null, loading:false, error:null };
|
|
238
|
+
|
|
239
|
+
function esc(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
240
|
+
function money(n){ n=Number(n)||0; if(!n) return '\\u2014'; return '$'+n.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
|
|
241
|
+
function humanMs(ms){ ms=Number(ms)||0; if(ms<1000) return ms+'ms'; var s=Math.round(ms/1000); if(s<60) return s+'s'; var m=Math.floor(s/60); var r=s%60; return m+'m '+(r<10?'0':'')+r+'s'; }
|
|
242
|
+
function whenText(iso){ var t=Date.parse(iso); if(isNaN(t)) return ''; var d=new Date(t); return d.toLocaleDateString('en-US',{month:'short',day:'numeric'})+', '+d.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'}); }
|
|
243
|
+
function shortId(id){ id=String(id||''); return id.length>18? id.slice(0,16)+'\\u2026' : id; }
|
|
244
|
+
|
|
245
|
+
var TYPE_MAP={timeline_render:'Timeline render',hyperframes_render:'HyperFrames render',still_render:'Still render',render_still:'Still render',clip_scan_lambda:'Clip scan',clip_scan:'Clip scan',decompose:'Decompose',scene_annotation:'Scene annotation',cast_identification:'Cast identification',product_placement:'Product placement',swipe_customize:'Swipe customize',tts:'Text-to-speech',stt:'Speech-to-text',image_generate:'Image generate',image_edit:'Image edit',video_download:'Video download',video_generate:'Video generate',render_slides:'Slide render',captions:'Animated captions',transcribe:'Transcribe',speech:'Narration',regenerate_speech:'Revoice narration',remove_captions:'Caption removal',dedupe:'Media dedupe'};
|
|
246
|
+
function prettify(raw){
|
|
247
|
+
raw=String(raw||'').replace(/^primitive:/,'').trim();
|
|
248
|
+
if(!raw) return 'Job';
|
|
249
|
+
var key=raw.split('/').filter(Boolean).pop()||raw;
|
|
250
|
+
key=key.replace(/:/g,'_');
|
|
251
|
+
if(TYPE_MAP[key]) return TYPE_MAP[key];
|
|
252
|
+
var words=key.split(/[_\\-\\s]+/).filter(Boolean);
|
|
253
|
+
if(!words.length) return 'Job';
|
|
254
|
+
return words.map(function(w,i){ return i===0? (w.charAt(0).toUpperCase()+w.slice(1)) : w.toLowerCase(); }).join(' ');
|
|
255
|
+
}
|
|
256
|
+
function typeLabel(entry){
|
|
257
|
+
var j=entry.linkedJob;
|
|
258
|
+
var raw=(j&&(j.operation_name||j.workflow_name||j.template_id))||'';
|
|
259
|
+
if(!raw) raw=entry.routePrefix||entry.path||'';
|
|
260
|
+
return prettify(raw);
|
|
261
|
+
}
|
|
262
|
+
function operationStr(entry){
|
|
263
|
+
var j=entry.linkedJob;
|
|
264
|
+
return String((j&&(j.template_id||j.operation_name))||entry.routePrefix||entry.path||'\\u2014');
|
|
265
|
+
}
|
|
266
|
+
function runStatus(entry){
|
|
267
|
+
var j=entry.linkedJob;
|
|
268
|
+
if(j&&j.status){
|
|
269
|
+
var s=String(j.status).toLowerCase();
|
|
270
|
+
if(s==='failed'||s==='cancelled'||s==='canceled'||s==='error') return 'failed';
|
|
271
|
+
if(s==='queued'||s==='pending') return 'queued';
|
|
272
|
+
if(s==='running'||s.indexOf('waiting')===0) return 'running';
|
|
273
|
+
if(s==='succeeded'||s==='success'||s==='completed'||s==='done') return 'succeeded';
|
|
274
|
+
return 'running';
|
|
275
|
+
}
|
|
276
|
+
var http=Number(entry.status)||0;
|
|
277
|
+
if(http>=400) return 'failed';
|
|
278
|
+
if(http===202) return 'queued';
|
|
279
|
+
if(http>=200&&http<300) return 'succeeded';
|
|
280
|
+
return 'failed';
|
|
281
|
+
}
|
|
282
|
+
function costOf(entry){ var j=entry.linkedJob; return (j&&j.billing&&Number(j.billing.consumed_usd))||0; }
|
|
283
|
+
function durationText(entry){
|
|
284
|
+
var j=entry.linkedJob;
|
|
285
|
+
if(j&&j.started_at&&j.completed_at){ var ms=Date.parse(j.completed_at)-Date.parse(j.started_at); if(ms>=0) return humanMs(ms); }
|
|
286
|
+
if(entry.durationMs&&entry.durationMs>0) return humanMs(entry.durationMs);
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
var STATUS_META={
|
|
291
|
+
succeeded:{label:'Succeeded',tile:'rk-tile-green',pill:'rk-pill-green',glyph:'\\u2713'},
|
|
292
|
+
running:{label:'Running',tile:'rk-jobs-tile-run',pill:'rk-pill-gold',glyph:'\\u25CF'},
|
|
293
|
+
failed:{label:'Failed',tile:'rk-jobs-tile-fail',pill:'rk-pill-red',glyph:'\\u2715'},
|
|
294
|
+
queued:{label:'Queued',tile:'rk-tile-ink',pill:'rk-pill',glyph:'\\u25D4'}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
function normalize(entry){
|
|
298
|
+
var j=entry.linkedJob||null;
|
|
299
|
+
var status=runStatus(entry);
|
|
300
|
+
var media=(Array.isArray(entry.outputMediaUrls)&&entry.outputMediaUrls.length)?entry.outputMediaUrls:(Array.isArray(entry.mediaUrls)?entry.mediaUrls:[]);
|
|
301
|
+
return {
|
|
302
|
+
status:status,
|
|
303
|
+
type:typeLabel(entry),
|
|
304
|
+
jobId:String(entry.jobId||(j&&j.job_id)||entry.id||''),
|
|
305
|
+
operation:operationStr(entry),
|
|
306
|
+
tracer:String(entry.tracer||(j&&j.tracer)||''),
|
|
307
|
+
when:whenText(entry.createdAt||(j&&j.created_at)),
|
|
308
|
+
duration:durationText(entry),
|
|
309
|
+
progress:(j&&typeof j.progress==='number')?j.progress:0,
|
|
310
|
+
cost:costOf(entry),
|
|
311
|
+
method:String(entry.method||''),
|
|
312
|
+
path:String(entry.path||''),
|
|
313
|
+
httpStatus:Number(entry.status)||0,
|
|
314
|
+
workflow:(j&&j.workflow_name)||'',
|
|
315
|
+
jobStatus:(j&&j.status)||'',
|
|
316
|
+
eventCount:(j&&j.billing&&Number(j.billing.event_count))||0,
|
|
317
|
+
media:media
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function detailRow(k,v){ return '<div class="rk-jobs-detail-row"><span class="rk-jobs-detail-k">'+k+'</span><span class="rk-jobs-detail-v">'+v+'</span></div>'; }
|
|
322
|
+
|
|
323
|
+
function rowHtml(n){
|
|
324
|
+
var meta=STATUS_META[n.status]||STATUS_META.queued;
|
|
325
|
+
var pct=Math.max(0,Math.min(100,Math.round((n.progress||0)*100)));
|
|
326
|
+
var when = n.status==='running'
|
|
327
|
+
? '<div class="rk-jobs-progress" role="progressbar" aria-valuenow="'+pct+'" aria-valuemin="0" aria-valuemax="100"><span class="rk-jobs-progress-bar" style="width:'+pct+'%"></span></div><div class="rk-jobs-when-dur is-run">'+pct+'% done</div>'
|
|
328
|
+
: n.status==='queued'
|
|
329
|
+
? '<div class="rk-jobs-when-dur is-wait">In queue</div>'
|
|
330
|
+
: '<div class="rk-jobs-when-dur">'+esc(n.duration||'\\u2014')+'</div>';
|
|
331
|
+
var sub = '<span class="rk-mono">'+esc(n.operation)+'</span>'+(n.tracer?' \\u00b7 tracer <span class="rk-mono">'+esc(n.tracer)+'</span>':'');
|
|
332
|
+
|
|
333
|
+
var det = detailRow('HTTP', esc(n.method)+' '+esc(n.path)+' \\u2192 '+esc(n.httpStatus||'\\u2014'))
|
|
334
|
+
+ detailRow('Job', esc(n.jobId||'\\u2014'))
|
|
335
|
+
+ detailRow('Operation', esc(n.operation))
|
|
336
|
+
+ (n.workflow?detailRow('Workflow', esc(n.workflow)):'')
|
|
337
|
+
+ (n.tracer?detailRow('Tracer', esc(n.tracer)):'')
|
|
338
|
+
+ detailRow('Job status', esc(n.jobStatus||n.status))
|
|
339
|
+
+ detailRow('Started', esc(n.when||'\\u2014'))
|
|
340
|
+
+ (n.duration?detailRow('Duration', esc(n.duration)):'')
|
|
341
|
+
+ detailRow('Compute', esc(money(n.cost))+(n.eventCount?(' \\u00b7 '+n.eventCount+' event'+(n.eventCount===1?'':'s')):''))
|
|
342
|
+
+ (n.media.length?detailRow('Output', '<span class="rk-jobs-detail-media">'+n.media.map(function(u){return '<a href="'+esc(u)+'" target="_blank" rel="noreferrer">Open \\u2197</a>';}).join('')+'</span>'):'');
|
|
343
|
+
|
|
344
|
+
return '<div class="rk-jobs-item" data-rk-status="'+n.status+'">'
|
|
345
|
+
+ '<div class="rk-jobs-row">'
|
|
346
|
+
+ '<span class="rk-tile '+meta.tile+' rk-jobs-tile" aria-hidden="true">'+meta.glyph+'</span>'
|
|
347
|
+
+ '<div class="rk-jobs-main">'
|
|
348
|
+
+ '<div class="rk-jobs-row-title"><span class="rk-jobs-type">'+esc(n.type)+'</span><span class="rk-mono rk-jobs-id" title="'+esc(n.jobId)+'">'+esc(shortId(n.jobId))+'</span><span class="rk-pill '+meta.pill+'">'+meta.label+'</span></div>'
|
|
349
|
+
+ '<div class="rk-jobs-sub">'+sub+'</div>'
|
|
350
|
+
+ '</div>'
|
|
351
|
+
+ '<div class="rk-jobs-tail">'
|
|
352
|
+
+ '<div class="rk-jobs-when"><div class="rk-jobs-when-at">'+esc(n.when||'\\u2014')+'</div>'+when+'</div>'
|
|
353
|
+
+ '<div class="rk-jobs-cost'+(n.cost?'':' is-zero')+'">'+esc(money(n.cost))+'</div>'
|
|
354
|
+
+ '<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-jobs-logs" data-rk-logs>View logs</button>'
|
|
355
|
+
+ '</div>'
|
|
356
|
+
+ '</div>'
|
|
357
|
+
+ '<div class="rk-jobs-details" data-rk-details hidden>'+det+'</div>'
|
|
358
|
+
+ '</div>';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function computeCounts(){
|
|
362
|
+
var c={all:state.entries.length,succeeded:0,running:0,failed:0,queued:0};
|
|
363
|
+
state.entries.forEach(function(e){ var s=runStatus(e); if(c[s]!=null) c[s]++; });
|
|
364
|
+
return c;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function updateSummary(){
|
|
368
|
+
var c=computeCounts();
|
|
369
|
+
root.querySelectorAll('[data-rk-count]').forEach(function(el){ var k=el.getAttribute('data-rk-count'); el.textContent = (c[k]!=null?c[k]:0); });
|
|
370
|
+
if(!summaryEl) return;
|
|
371
|
+
if(state.loading&&state.entries.length===0){ summaryEl.innerHTML='Loading run history\\u2026'; return; }
|
|
372
|
+
if(state.error&&state.entries.length===0){ summaryEl.innerHTML=esc(state.error); return; }
|
|
373
|
+
var compute=0; state.entries.forEach(function(e){ compute+=costOf(e); });
|
|
374
|
+
var completed=c.succeeded+c.failed;
|
|
375
|
+
var rate=completed?Math.round(c.succeeded/completed*100)+'%':'\\u2014';
|
|
376
|
+
summaryEl.innerHTML='<b>'+c.all+'</b> runs \\u00b7 <b>'+rate+'</b> success \\u00b7 <b>'+c.failed+'</b> failed \\u00b7 <b>'+esc(money(compute))+'</b> compute \\u00b7 <span class="rk-jobs-summary-note">most recent '+c.all+' \\u00b7 BYO AI keys never billed</span>';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function applyFilter(){
|
|
380
|
+
var shown=0;
|
|
381
|
+
listEl.querySelectorAll('.rk-jobs-item').forEach(function(w){
|
|
382
|
+
var m=state.filter==='all'||w.getAttribute('data-rk-status')===state.filter;
|
|
383
|
+
w.hidden=!m; if(m) shown++;
|
|
384
|
+
});
|
|
385
|
+
if(emptyEl) emptyEl.hidden = shown!==0 || state.entries.length===0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function wireRows(){
|
|
389
|
+
listEl.querySelectorAll('[data-rk-logs]').forEach(function(btn){
|
|
390
|
+
btn.addEventListener('click',function(){
|
|
391
|
+
var item=btn.closest('.rk-jobs-item'); if(!item) return;
|
|
392
|
+
var det=item.querySelector('[data-rk-details]'); if(!det) return;
|
|
393
|
+
var willShow=det.hidden; det.hidden=!willShow;
|
|
394
|
+
btn.textContent=willShow?'Hide logs':'View logs';
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function setStateMsg(msg,isError){
|
|
400
|
+
if(!stateEl) return;
|
|
401
|
+
stateEl.textContent=msg;
|
|
402
|
+
stateEl.hidden=false;
|
|
403
|
+
stateEl.classList.toggle('is-error',!!isError);
|
|
404
|
+
if(listEl) listEl.innerHTML='';
|
|
405
|
+
if(emptyEl) emptyEl.hidden=true;
|
|
406
|
+
}
|
|
407
|
+
function hideStateMsg(){ if(stateEl) stateEl.hidden=true; }
|
|
408
|
+
|
|
409
|
+
function render(){
|
|
410
|
+
updateSummary();
|
|
411
|
+
if(footEl) footEl.textContent = state.entries.length? ('Showing '+state.entries.length+' most recent'+(state.nextCursor?' \\u00b7 more available':'')) : '';
|
|
412
|
+
if(moreBtn) moreBtn.hidden = !state.nextCursor;
|
|
413
|
+
|
|
414
|
+
if(state.loading&&state.entries.length===0){ setStateMsg('Loading run history\\u2026',false); return; }
|
|
415
|
+
if(state.error&&state.entries.length===0){ setStateMsg(state.error,true); return; }
|
|
416
|
+
if(state.entries.length===0){ setStateMsg('No runs yet \\u2014 fire a render, clip scan, or decompose and it will show up here.',false); return; }
|
|
417
|
+
|
|
418
|
+
hideStateMsg();
|
|
419
|
+
listEl.innerHTML = state.entries.map(function(e){ return rowHtml(normalize(e)); }).join('');
|
|
420
|
+
wireRows();
|
|
421
|
+
applyFilter();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function load(cursor){
|
|
425
|
+
state.loading=true; state.error=null;
|
|
426
|
+
if(!cursor) render();
|
|
427
|
+
if(refreshBtn){ refreshBtn.disabled=true; }
|
|
428
|
+
var href=loadEndpoint;
|
|
429
|
+
try{
|
|
430
|
+
var url=new URL(loadEndpoint, window.location.href);
|
|
431
|
+
if(accountId) url.searchParams.set('account', accountId);
|
|
432
|
+
if(cursor) url.searchParams.set('cursor', cursor);
|
|
433
|
+
href=url.toString();
|
|
434
|
+
}catch(e){ href=loadEndpoint; }
|
|
435
|
+
fetch(href,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
436
|
+
.then(function(r){
|
|
437
|
+
if(!r.ok){ throw new Error(r.status===401?'Sign in to view your run history.':('Couldn\\'t load run history ('+r.status+').')); }
|
|
438
|
+
return r.json();
|
|
439
|
+
})
|
|
440
|
+
.then(function(j){
|
|
441
|
+
var entries=(j&&Array.isArray(j.entries))?j.entries:[];
|
|
442
|
+
state.entries = cursor? state.entries.concat(entries) : entries;
|
|
443
|
+
state.nextCursor = (j&&j.next_cursor)?j.next_cursor:null;
|
|
444
|
+
state.loading=false; state.error=null;
|
|
445
|
+
if(refreshBtn){ refreshBtn.disabled=false; }
|
|
446
|
+
render();
|
|
447
|
+
})
|
|
448
|
+
.catch(function(err){
|
|
449
|
+
state.loading=false;
|
|
450
|
+
var msg=(err&&err.message)?String(err.message):'';
|
|
451
|
+
// Raw network TypeErrors ("Failed to fetch") aren't user-friendly.
|
|
452
|
+
state.error=(!msg||/failed to fetch|networkerror|load failed/i.test(msg))
|
|
453
|
+
? "Couldn't reach the server \\u2014 check your connection, or sign in to view your run history."
|
|
454
|
+
: msg;
|
|
455
|
+
if(refreshBtn){ refreshBtn.disabled=false; }
|
|
456
|
+
render();
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// filter chips (work over the real, client-rendered rows)
|
|
222
461
|
root.querySelectorAll('[data-rk-filter]').forEach(function(btn){
|
|
223
462
|
btn.addEventListener('click',function(){
|
|
224
|
-
|
|
225
|
-
root.querySelectorAll('[data-rk-filter]').forEach(function(b){b.classList.toggle('is-active',b===btn)});
|
|
226
|
-
|
|
227
|
-
root.querySelectorAll('[data-rk-status]').forEach(function(row){
|
|
228
|
-
var match=f==='all'||row.getAttribute('data-rk-status')===f;
|
|
229
|
-
row.hidden=!match; if(match)shown++;
|
|
230
|
-
});
|
|
231
|
-
if(empty)empty.hidden=shown!==0;
|
|
463
|
+
state.filter=btn.getAttribute('data-rk-filter');
|
|
464
|
+
root.querySelectorAll('[data-rk-filter]').forEach(function(b){ b.classList.toggle('is-active',b===btn); });
|
|
465
|
+
applyFilter();
|
|
232
466
|
});
|
|
233
467
|
});
|
|
234
|
-
|
|
235
|
-
if(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
468
|
+
if(refreshBtn) refreshBtn.addEventListener('click',function(){ load(); });
|
|
469
|
+
if(moreBtn) moreBtn.addEventListener('click',function(){ if(state.nextCursor) load(state.nextCursor); });
|
|
470
|
+
|
|
471
|
+
// boot: render a server seed immediately if present (mirrors the live page),
|
|
472
|
+
// otherwise fetch the real endpoint. Refresh always re-fetches.
|
|
473
|
+
state.entries = Array.isArray(boot.entries)? boot.entries : [];
|
|
474
|
+
if(state.entries.length){ render(); }
|
|
475
|
+
else { state.loading=true; render(); load(); }
|
|
239
476
|
})();`;
|
|
240
|
-
return reskinDocument({
|
|
241
|
-
title: "vidfarm reskin — Job runs",
|
|
242
|
-
description: "Reskinned vidfarm job runs page — render & job history, status, timing, and compute cost.",
|
|
243
|
-
activeSlug: "job-runs",
|
|
244
|
-
pageCss,
|
|
245
|
-
body,
|
|
246
|
-
script
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
477
|
//# sourceMappingURL=job-runs-page.js.map
|