@3minapi/cli 0.0.0-stage → 0.22.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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/bin.js +54 -0
- package/dist/cli.js +241 -0
- package/dist/commands/auth.js +573 -0
- package/dist/commands/common.js +48 -0
- package/dist/commands/dashboard.js +298 -0
- package/dist/commands/delete.js +167 -0
- package/dist/commands/doctor.js +326 -0
- package/dist/commands/guide.js +84 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/init.js +248 -0
- package/dist/commands/logs.js +154 -0
- package/dist/commands/new.js +109 -0
- package/dist/commands/paths.js +64 -0
- package/dist/commands/reload.js +32 -0
- package/dist/commands/results.js +445 -0
- package/dist/commands/start.js +443 -0
- package/dist/commands/stop.js +68 -0
- package/dist/commands/tasks.js +93 -0
- package/dist/commands/worker.js +178 -0
- package/dist/core/api.js +132 -0
- package/dist/core/config.js +188 -0
- package/dist/core/control-db.js +462 -0
- package/dist/core/credentials.js +75 -0
- package/dist/core/domain/constants.js +121 -0
- package/dist/core/domain/contract.js +102 -0
- package/dist/core/domain/errors.js +57 -0
- package/dist/core/domain/index.js +3 -0
- package/dist/core/domain/types.js +1 -0
- package/dist/core/error-report.js +103 -0
- package/dist/core/host/detach.js +49 -0
- package/dist/core/host/live-connection.js +270 -0
- package/dist/core/host/live-receiver.js +264 -0
- package/dist/core/host/module.js +1 -0
- package/dist/core/host/poller.js +113 -0
- package/dist/core/host/pull-step.js +54 -0
- package/dist/core/host/receiver.js +1 -0
- package/dist/core/host/reload.js +34 -0
- package/dist/core/host/runner-lock.js +245 -0
- package/dist/core/host/session.js +84 -0
- package/dist/core/host/usage.js +20 -0
- package/dist/core/log-read.js +59 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/names.js +27 -0
- package/dist/core/network.js +10 -0
- package/dist/core/open-browser.js +26 -0
- package/dist/core/output.js +68 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/project.js +85 -0
- package/dist/core/prompt.js +140 -0
- package/dist/core/redact.js +45 -0
- package/dist/core/secure-fs.js +50 -0
- package/dist/core/text-file.js +12 -0
- package/dist/core/text.js +27 -0
- package/dist/core/time.js +84 -0
- package/dist/core/update-notice.js +142 -0
- package/dist/core/version.js +16 -0
- package/dist/core/warnings.js +10 -0
- package/dist/help.js +144 -0
- package/dist/modules/dashboard/assets.js +411 -0
- package/dist/modules/dashboard/data.js +565 -0
- package/dist/modules/dashboard/guard.js +75 -0
- package/dist/modules/dashboard/lock.js +93 -0
- package/dist/modules/dashboard/server.js +295 -0
- package/dist/modules/dashboard/views/html.js +114 -0
- package/dist/modules/dashboard/views/layout.js +59 -0
- package/dist/modules/dashboard/views/logs.js +122 -0
- package/dist/modules/dashboard/views/overview.js +316 -0
- package/dist/modules/dashboard/views/runs.js +486 -0
- package/dist/modules/dashboard/views/workers.js +157 -0
- package/dist/modules/workers/builtin/host-identity.js +59 -0
- package/dist/modules/workers/builtin/results-worker.js +37 -0
- package/dist/modules/workers/local-server.js +244 -0
- package/dist/modules/workers/module.js +98 -0
- package/dist/modules/workers/reload-result.js +16 -0
- package/dist/modules/workers/repeat-scheduler.js +165 -0
- package/dist/modules/workers/results/missing-paths.js +66 -0
- package/dist/modules/workers/results/query.js +175 -0
- package/dist/modules/workers/results/reader-input.js +134 -0
- package/dist/modules/workers/results/reader.js +430 -0
- package/dist/modules/workers/results/results-child.js +64 -0
- package/dist/modules/workers/results/shape.js +212 -0
- package/dist/modules/workers/results/store.js +336 -0
- package/dist/modules/workers/results/substitute.js +30 -0
- package/dist/modules/workers/runtime/env.js +43 -0
- package/dist/modules/workers/runtime/error-chain.js +109 -0
- package/dist/modules/workers/runtime/executor.js +289 -0
- package/dist/modules/workers/runtime/inspect.js +53 -0
- package/dist/modules/workers/runtime/protocol.js +1 -0
- package/dist/modules/workers/runtime/resolve.js +30 -0
- package/dist/modules/workers/runtime/schema.js +85 -0
- package/dist/modules/workers/runtime/thread.js +117 -0
- package/dist/modules/workers/start.js +265 -0
- package/dist/modules/workers/sync.js +241 -0
- package/dist/modules/workers/task-runner.js +892 -0
- package/dist/modules/workers/template.js +100 -0
- package/dist/modules/workers/worker-set.js +122 -0
- package/dist/modules/workers/workers.js +66 -0
- package/package.json +48 -5
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { BIN } from "../../../core/domain/index.js";
|
|
2
|
+
import { formatBytes } from "../../../core/text.js";
|
|
3
|
+
import { readPayloadSlice } from "../../workers/results/reader.js";
|
|
4
|
+
import { childPath } from "../../workers/results/shape.js";
|
|
5
|
+
import { RUN_RANGES, RUN_STATES, encodeCursor, isRepeat } from "../data.js";
|
|
6
|
+
import { badge, command, copyButton, duration, html, qs, shortTaskId, tag, time, workerName, workerText } from "./html.js";
|
|
7
|
+
import { logTable } from "./logs.js";
|
|
8
|
+
import { rangeTabs } from "./overview.js";
|
|
9
|
+
export const INLINE_RESULT_BYTES = 64 * 1024;
|
|
10
|
+
export const ENTRIES_PER_PAGE = 100;
|
|
11
|
+
const REPLY_LABEL = {
|
|
12
|
+
pending: 'not sent yet',
|
|
13
|
+
result: 'result',
|
|
14
|
+
substitute: 'summary (too large)',
|
|
15
|
+
failed: 'error',
|
|
16
|
+
rejected: 'rejected by server',
|
|
17
|
+
none: 'not sent (local)'
|
|
18
|
+
};
|
|
19
|
+
function replyLabel(reply, repeat) {
|
|
20
|
+
if (repeat && reply === 'none')
|
|
21
|
+
return 'progress only';
|
|
22
|
+
return REPLY_LABEL[reply] ?? reply;
|
|
23
|
+
}
|
|
24
|
+
function runCountText(r) {
|
|
25
|
+
const n = r.runs ?? r.saved_runs ?? 0;
|
|
26
|
+
const max = r.repeat ? (r.repeat.max_runs ?? '∞') : null;
|
|
27
|
+
return max === null ? `↻ ${n} runs` : `↻ ${n}/${max} runs`;
|
|
28
|
+
}
|
|
29
|
+
export function runsPage(page, filter, workers, ctx) {
|
|
30
|
+
const { runs, next, paged, firstNo } = page;
|
|
31
|
+
const keep = { worker: filter.worker, state: filter.state };
|
|
32
|
+
const here = { ...keep, range: filter.range };
|
|
33
|
+
return html `
|
|
34
|
+
<div class="head">
|
|
35
|
+
<h1>Runs</h1>
|
|
36
|
+
${rangeTabs('/runs', filter.range, keep, RUN_RANGES)}
|
|
37
|
+
</div>
|
|
38
|
+
<form class="filters" method="get" action="/runs" data-autosubmit>
|
|
39
|
+
<input type="hidden" name="range" value="${filter.range}" />
|
|
40
|
+
<label
|
|
41
|
+
>Worker
|
|
42
|
+
<select name="worker">
|
|
43
|
+
<option value="">All</option>
|
|
44
|
+
${workers.map((w) => html `<option value="${w}" ${w === filter.worker ? html ` selected` : ''}>
|
|
45
|
+
${workerText(w)}
|
|
46
|
+
</option>`)}
|
|
47
|
+
</select>
|
|
48
|
+
</label>
|
|
49
|
+
<label
|
|
50
|
+
>Status
|
|
51
|
+
<select name="state">
|
|
52
|
+
<option value="">All</option>
|
|
53
|
+
${RUN_STATES.map((s) => html `<option value="${s}" ${s === filter.state ? html ` selected` : ''}>${s}</option>`)}
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
<noscript><button type="submit">Apply</button></noscript>
|
|
57
|
+
</form>
|
|
58
|
+
${runs.length === 0
|
|
59
|
+
? html `<p class="muted">No runs match.</p>`
|
|
60
|
+
: html `<div class="scroll">
|
|
61
|
+
<table class="table runs">
|
|
62
|
+
<thead>
|
|
63
|
+
<tr>
|
|
64
|
+
<th class="num">No</th>
|
|
65
|
+
<th>Task</th>
|
|
66
|
+
<th>Worker</th>
|
|
67
|
+
<th>Status</th>
|
|
68
|
+
<th class="num">Duration</th>
|
|
69
|
+
<th class="num">Size</th>
|
|
70
|
+
<th>Reply</th>
|
|
71
|
+
<th>Time</th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
${runs.map((r, i) => html `<tr>
|
|
76
|
+
<td class="num muted">${firstNo + i}</td>
|
|
77
|
+
<td class="task">
|
|
78
|
+
<a href="/runs/${encodeURIComponent(r.task_id)}" title="${r.task_id}"
|
|
79
|
+
><code>${shortTaskId(r.task_id)}</code></a
|
|
80
|
+
>${isRepeat(r)
|
|
81
|
+
? html ` ${tag(runCountText(r), 'A repeating task — every run is saved; open it to see the runs')}`
|
|
82
|
+
: ''}${r.in === 'results'
|
|
83
|
+
? html ` ${tag('result only', `No run history on host ${ctx.host} (pruned with tasks prune, or never recorded here); the saved result remains`)}`
|
|
84
|
+
: ''}
|
|
85
|
+
</td>
|
|
86
|
+
<td>${workerName(r.worker)}</td>
|
|
87
|
+
<td>${badge(r.state)}</td>
|
|
88
|
+
<td class="num">${duration(r.duration_ms)}</td>
|
|
89
|
+
<td class="num">${r.bytes === null ? '—' : formatBytes(r.bytes)}</td>
|
|
90
|
+
<td>
|
|
91
|
+
${r.reply === null
|
|
92
|
+
? html `<span class="muted">—</span>`
|
|
93
|
+
: replyLabel(r.reply, isRepeat(r))}
|
|
94
|
+
</td>
|
|
95
|
+
<td>
|
|
96
|
+
${time(r.at)}${r.last_run_at !== null
|
|
97
|
+
? html `<br /><span class="muted">last run ${time(r.last_run_at)}</span>`
|
|
98
|
+
: ''}
|
|
99
|
+
</td>
|
|
100
|
+
</tr>`)}
|
|
101
|
+
</tbody>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
${paged || next
|
|
105
|
+
? html `<nav class="pager pager-runs" aria-label="Pages">
|
|
106
|
+
<span>${paged ? html `<a href="/runs${qs(here)}">← Newest</a>` : ''}</span>
|
|
107
|
+
<span
|
|
108
|
+
>${next
|
|
109
|
+
? html `<a
|
|
110
|
+
href="/runs${qs({
|
|
111
|
+
...here,
|
|
112
|
+
after: encodeCursor(next),
|
|
113
|
+
no: firstNo + runs.length
|
|
114
|
+
})}"
|
|
115
|
+
>Older →</a
|
|
116
|
+
>`
|
|
117
|
+
: html `<span class="muted">No older runs</span>`}</span
|
|
118
|
+
>
|
|
119
|
+
</nav>`
|
|
120
|
+
: ''}`}
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
export function runPage(d, view, log, ctx) {
|
|
124
|
+
const r = d.run;
|
|
125
|
+
const base = `/runs/${encodeURIComponent(r.task_id)}`;
|
|
126
|
+
return html `
|
|
127
|
+
<p class="crumbs"><a href="/runs">Runs</a> / <code>${r.task_id}</code></p>
|
|
128
|
+
<div class="head"><h1>${workerName(r.worker)} ${badge(r.state)}</h1></div>
|
|
129
|
+
<div class="grid">
|
|
130
|
+
<section class="panel">
|
|
131
|
+
<h2>Run</h2>
|
|
132
|
+
<dl class="kv">
|
|
133
|
+
<dt>Task</dt>
|
|
134
|
+
<dd><code>${r.task_id}</code> ${copyButton(r.task_id)}</dd>
|
|
135
|
+
<dt>Received</dt>
|
|
136
|
+
<dd>${received(r, ctx)}</dd>
|
|
137
|
+
<dt>Started</dt>
|
|
138
|
+
<dd>${time(r.started_at)}</dd>
|
|
139
|
+
<dt>Finished</dt>
|
|
140
|
+
<dd>${time(r.finished_at)}</dd>
|
|
141
|
+
<dt>Duration</dt>
|
|
142
|
+
<dd>${duration(r.duration_ms)}</dd>
|
|
143
|
+
${r.delivery_attempt !== null
|
|
144
|
+
? html `<dt>Delivery</dt>
|
|
145
|
+
<dd>attempt ${r.delivery_attempt} · ran ${r.run_count} time(s)</dd>`
|
|
146
|
+
: ''}
|
|
147
|
+
${lastError(r)}
|
|
148
|
+
</dl>
|
|
149
|
+
</section>
|
|
150
|
+
<section class="panel">
|
|
151
|
+
<h2>Saved result</h2>
|
|
152
|
+
${d.result
|
|
153
|
+
? html `${savedKv(d.result, false)}
|
|
154
|
+
<p>${command(`${BIN} worker results show ${r.task_id}`, ctx)}</p>`
|
|
155
|
+
: html `<p class="muted">
|
|
156
|
+
${r.state === 'running'
|
|
157
|
+
? 'Not finished yet.'
|
|
158
|
+
: 'No result saved for this task (cancelled, failed before running, a built-in reader task, or pruned from data.db).'}
|
|
159
|
+
</p>`}
|
|
160
|
+
</section>
|
|
161
|
+
</div>
|
|
162
|
+
${d.result ? resultViewer(base, d.result.payload, d.result.bytes, d.result.status, view) : ''}
|
|
163
|
+
${logPanel('Log for this task', log)}
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
export function repeatPage(run, page, paged, ctx) {
|
|
167
|
+
const base = `/runs/${encodeURIComponent(run.task_id)}`;
|
|
168
|
+
const { runs, runsNext, total } = page;
|
|
169
|
+
const newest = runs[0]?.run_no;
|
|
170
|
+
const oldest = runs.at(-1)?.run_no;
|
|
171
|
+
return html `
|
|
172
|
+
<p class="crumbs"><a href="/runs">Runs</a> / <code>${run.task_id}</code></p>
|
|
173
|
+
<div class="head"><h1>${workerName(run.worker)} ${badge(run.state)}</h1></div>
|
|
174
|
+
<section class="panel">
|
|
175
|
+
<h2>Repeat</h2>
|
|
176
|
+
<dl class="kv">
|
|
177
|
+
<dt>Task</dt>
|
|
178
|
+
<dd><code>${run.task_id}</code> ${copyButton(run.task_id)}</dd>
|
|
179
|
+
${run.repeat
|
|
180
|
+
? html `<dt>Every</dt>
|
|
181
|
+
<dd>${run.repeat.interval_seconds}s</dd>
|
|
182
|
+
<dt>Runs</dt>
|
|
183
|
+
<dd>
|
|
184
|
+
${run.runs ?? 0}/${run.repeat.max_runs ?? '∞'}${run.failed
|
|
185
|
+
? ` · ${run.failed} failed`
|
|
186
|
+
: ''}
|
|
187
|
+
</dd>`
|
|
188
|
+
: html `<dt>Runs</dt>
|
|
189
|
+
<dd>${total} saved</dd>`}
|
|
190
|
+
<dt>Received</dt>
|
|
191
|
+
<dd>${received(run, ctx)}</dd>
|
|
192
|
+
${run.repeat
|
|
193
|
+
? html `<dt>Last run</dt>
|
|
194
|
+
<dd>${time(run.last_run_at)}</dd>
|
|
195
|
+
${run.state === 'running'
|
|
196
|
+
? html `<dt>Next run</dt>
|
|
197
|
+
<dd>${time(run.next_at)}</dd>`
|
|
198
|
+
: ''}`
|
|
199
|
+
: ''}
|
|
200
|
+
${lastError(run)}
|
|
201
|
+
</dl>
|
|
202
|
+
<p>${command(`${BIN} worker results show ${run.task_id} --run <n>`, ctx)}</p>
|
|
203
|
+
</section>
|
|
204
|
+
<section class="panel">
|
|
205
|
+
<h2>
|
|
206
|
+
Runs${newest !== undefined
|
|
207
|
+
? html ` <span class="muted"
|
|
208
|
+
>— run ${oldest}–${newest} of ${total} saved, newest first</span
|
|
209
|
+
>`
|
|
210
|
+
: ''}
|
|
211
|
+
</h2>
|
|
212
|
+
${runs.length === 0
|
|
213
|
+
? html `<p class="muted">No run saved yet.</p>`
|
|
214
|
+
: html `<div class="scroll">
|
|
215
|
+
<table class="table runs-of-task">
|
|
216
|
+
<thead>
|
|
217
|
+
<tr>
|
|
218
|
+
<th class="num">Run</th>
|
|
219
|
+
<th>Status</th>
|
|
220
|
+
<th>Saved</th>
|
|
221
|
+
<th class="num">Size</th>
|
|
222
|
+
<th>Preview</th>
|
|
223
|
+
</tr>
|
|
224
|
+
</thead>
|
|
225
|
+
<tbody>
|
|
226
|
+
${runs.map((x) => {
|
|
227
|
+
const href = `${base}/${x.run_no}`;
|
|
228
|
+
return html `<tr>
|
|
229
|
+
<td class="num"><a href="${href}">${x.run_no}</a></td>
|
|
230
|
+
<td>${badge(x.status)}</td>
|
|
231
|
+
<td>${time(Date.parse(x.created_at))}</td>
|
|
232
|
+
<td class="num">${formatBytes(x.bytes)}</td>
|
|
233
|
+
<td class="preview">
|
|
234
|
+
<a href="${href}" tabindex="-1"
|
|
235
|
+
>${x.large
|
|
236
|
+
? html `<span class="muted">large result — open to read it in parts</span>`
|
|
237
|
+
: html `<code${x.status === 'failed' ? html ` class="failed"` : ''}
|
|
238
|
+
>${x.preview}</code
|
|
239
|
+
>`}</a
|
|
240
|
+
>
|
|
241
|
+
</td>
|
|
242
|
+
</tr>`;
|
|
243
|
+
})}
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>`}
|
|
247
|
+
${paged || runsNext !== null
|
|
248
|
+
? html `<nav class="pager pager-runs" aria-label="Pages">
|
|
249
|
+
<span>${paged ? html `<a href="${base}">← Newest</a>` : ''}</span>
|
|
250
|
+
<span
|
|
251
|
+
>${runsNext !== null
|
|
252
|
+
? html `<a href="${base}${qs({ runs_before: runsNext })}">Older →</a>`
|
|
253
|
+
: html `<span class="muted">No older runs</span>`}</span
|
|
254
|
+
>
|
|
255
|
+
</nav>`
|
|
256
|
+
: ''}
|
|
257
|
+
</section>
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
export function runOfTaskPage(run, runNo, d, view, log, ctx) {
|
|
261
|
+
const task = `/runs/${encodeURIComponent(run.task_id)}`;
|
|
262
|
+
const base = `${task}/${runNo}`;
|
|
263
|
+
const nav = html `<nav class="pager run-nav" aria-label="Runs">
|
|
264
|
+
<span>${d.prev !== null ? html `<a href="${task}/${d.prev}">← run ${d.prev}</a>` : ''}</span>
|
|
265
|
+
<span><a href="${task}">All runs</a></span>
|
|
266
|
+
<span>${d.next !== null ? html `<a href="${task}/${d.next}">run ${d.next} →</a>` : ''}</span>
|
|
267
|
+
</nav>`;
|
|
268
|
+
return html `
|
|
269
|
+
<p class="crumbs">
|
|
270
|
+
<a href="/runs">Runs</a> /
|
|
271
|
+
<a href="${task}" title="${run.task_id}"><code>${shortTaskId(run.task_id)}</code></a> / run
|
|
272
|
+
${runNo}
|
|
273
|
+
</p>
|
|
274
|
+
<div class="head">
|
|
275
|
+
<h1>${workerName(run.worker)} · run ${runNo} ${d.result ? badge(d.result.status) : ''}</h1>
|
|
276
|
+
</div>
|
|
277
|
+
${nav}
|
|
278
|
+
${d.result
|
|
279
|
+
? html `<section class="panel">
|
|
280
|
+
<h2>Run ${runNo}</h2>
|
|
281
|
+
${savedKv(d.result, true)}
|
|
282
|
+
<p>${command(`${BIN} worker results show ${run.task_id} --run ${runNo}`, ctx)}</p>
|
|
283
|
+
</section>
|
|
284
|
+
${resultViewer(base, d.result.payload, d.result.bytes, d.result.status, view)}
|
|
285
|
+
${log ? logPanel(`Log for run ${runNo}`, log) : ''}`
|
|
286
|
+
: html `<section class="panel">
|
|
287
|
+
<p class="error">No saved run ${runNo} for this task.</p>
|
|
288
|
+
<p class="muted">
|
|
289
|
+
It has not run yet, it stopped before saving a result, or it was pruned from data.db.
|
|
290
|
+
${d.prev !== null || d.next !== null ? 'The nearest saved runs are linked above.' : ''}
|
|
291
|
+
</p>
|
|
292
|
+
</section>`}
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
function received(r, ctx) {
|
|
296
|
+
return r.in === 'results'
|
|
297
|
+
? html `<span class="muted">no run history on host ${ctx.host}</span>`
|
|
298
|
+
: time(r.at);
|
|
299
|
+
}
|
|
300
|
+
function lastError(r) {
|
|
301
|
+
return r.last_error
|
|
302
|
+
? html `<dt>Error</dt>
|
|
303
|
+
<dd class="error">${r.last_error}</dd>`
|
|
304
|
+
: '';
|
|
305
|
+
}
|
|
306
|
+
function savedKv(result, repeat) {
|
|
307
|
+
return html `<dl class="kv">
|
|
308
|
+
<dt>Status</dt>
|
|
309
|
+
<dd>${result.status}</dd>
|
|
310
|
+
<dt>Saved</dt>
|
|
311
|
+
<dd>${time(Date.parse(result.created_at))}</dd>
|
|
312
|
+
<dt>Size</dt>
|
|
313
|
+
<dd>${formatBytes(result.bytes)}</dd>
|
|
314
|
+
<dt>Reply</dt>
|
|
315
|
+
<dd>${replyLabel(result.reply, repeat)}</dd>
|
|
316
|
+
<dt>Received via</dt>
|
|
317
|
+
<dd>${result.source}</dd>
|
|
318
|
+
${result.shape_hash
|
|
319
|
+
? html `<dt>Shape</dt>
|
|
320
|
+
<dd><code>${result.shape_hash}</code></dd>`
|
|
321
|
+
: ''}
|
|
322
|
+
</dl>`;
|
|
323
|
+
}
|
|
324
|
+
function logPanel(title, log) {
|
|
325
|
+
return html `<section class="panel">
|
|
326
|
+
<h2>${title}</h2>
|
|
327
|
+
${log.error
|
|
328
|
+
? html `<p class="error">Could not read the log: ${log.error}</p>`
|
|
329
|
+
: log.lines.length > 0
|
|
330
|
+
? logTable(log.lines, { channel: true, task: false })
|
|
331
|
+
: log.deleted
|
|
332
|
+
? html `<p class="muted">
|
|
333
|
+
No log files from this run's dates remain — most likely deleted after
|
|
334
|
+
<code>logRetentionDays</code> (default 30) or when the logs passed
|
|
335
|
+
<code>logMaxBytes</code>.
|
|
336
|
+
</p>`
|
|
337
|
+
: html `<p class="muted">
|
|
338
|
+
No log lines for this run (older parts of that day's log may have been deleted).
|
|
339
|
+
</p>`}
|
|
340
|
+
</section>`;
|
|
341
|
+
}
|
|
342
|
+
function resultViewer(base, payload, bytes, status, view) {
|
|
343
|
+
const title = status === 'failed' ? 'Error' : 'Result';
|
|
344
|
+
if (bytes <= INLINE_RESULT_BYTES && view.path === '$' && view.offset === 0) {
|
|
345
|
+
let pretty;
|
|
346
|
+
let broken = null;
|
|
347
|
+
try {
|
|
348
|
+
pretty = JSON.stringify(JSON.parse(payload), null, 2);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
pretty = payload;
|
|
352
|
+
broken = err.message;
|
|
353
|
+
}
|
|
354
|
+
return html `<section class="panel">
|
|
355
|
+
<h2>${title}</h2>
|
|
356
|
+
${broken
|
|
357
|
+
? html `<p class="error">
|
|
358
|
+
The saved result is not valid JSON (it may be corrupted): ${broken}
|
|
359
|
+
</p>`
|
|
360
|
+
: ''}
|
|
361
|
+
<pre class="code json">${pretty}</pre>
|
|
362
|
+
</section>`;
|
|
363
|
+
}
|
|
364
|
+
let read = readPayloadSlice(payload, { ...view, limit: ENTRIES_PER_PAGE, summarize });
|
|
365
|
+
if (read.ok && read.result.kind === 'text')
|
|
366
|
+
read = readPayloadSlice(payload, view);
|
|
367
|
+
const crumbs = pathCrumbs(view.path, base);
|
|
368
|
+
if (!read.ok) {
|
|
369
|
+
return html `<section class="panel">
|
|
370
|
+
<h2>${title}</h2>
|
|
371
|
+
${crumbs}
|
|
372
|
+
<p class="error">${read.message}</p>
|
|
373
|
+
</section>`;
|
|
374
|
+
}
|
|
375
|
+
const res = read.result;
|
|
376
|
+
const pager = html `<p class="pager">
|
|
377
|
+
${res.offset > 0
|
|
378
|
+
? html `<a
|
|
379
|
+
href="${base}${qs({
|
|
380
|
+
path: view.path,
|
|
381
|
+
offset: Math.max(0, res.offset - (res.kind === 'text' ? res.count : ENTRIES_PER_PAGE))
|
|
382
|
+
})}"
|
|
383
|
+
>← Previous</a
|
|
384
|
+
>`
|
|
385
|
+
: ''}
|
|
386
|
+
<span class="muted">${res.kind} · ${res.offset}–${res.offset + res.count} of ${res.total}</span>
|
|
387
|
+
${res.next !== null
|
|
388
|
+
? html `<a href="${base}${qs({ path: view.path, offset: res.next })}">Next →</a>`
|
|
389
|
+
: ''}
|
|
390
|
+
</p>`;
|
|
391
|
+
let body;
|
|
392
|
+
if (res.kind === 'array' || res.kind === 'object') {
|
|
393
|
+
const entries = res.kind === 'array'
|
|
394
|
+
? res.items.map((v, i) => [
|
|
395
|
+
String(res.offset + i),
|
|
396
|
+
`${view.path}[${res.offset + i}]`,
|
|
397
|
+
v
|
|
398
|
+
])
|
|
399
|
+
: Object.entries(res.items).map(([k, v]) => [
|
|
400
|
+
k,
|
|
401
|
+
childPath(view.path, k),
|
|
402
|
+
v
|
|
403
|
+
]);
|
|
404
|
+
body = html `<div class="scroll">
|
|
405
|
+
<table class="table entries">
|
|
406
|
+
<tbody>
|
|
407
|
+
${entries.map(([key, p, v]) => html `<tr>
|
|
408
|
+
<th scope="row">
|
|
409
|
+
${v instanceof Folded || (v !== null && typeof v === 'object')
|
|
410
|
+
? html `<a href="${base}${qs({ path: p })}">${key}</a>`
|
|
411
|
+
: key}
|
|
412
|
+
</th>
|
|
413
|
+
<td><pre class="code">${preview(v)}</pre></td>
|
|
414
|
+
</tr>`)}
|
|
415
|
+
</tbody>
|
|
416
|
+
</table>
|
|
417
|
+
</div>`;
|
|
418
|
+
}
|
|
419
|
+
else if (res.kind === 'text') {
|
|
420
|
+
body = html `<pre class="code">${res.text ?? ''}</pre>`;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
body = html `<pre class="code">${JSON.stringify(res.value)}</pre>`;
|
|
424
|
+
}
|
|
425
|
+
return html `<section class="panel">
|
|
426
|
+
<h2>${title} <span class="muted">${formatBytes(bytes)} — shown in parts</span></h2>
|
|
427
|
+
${crumbs}${pager}${body}${pager}
|
|
428
|
+
</section>`;
|
|
429
|
+
}
|
|
430
|
+
class Folded {
|
|
431
|
+
text;
|
|
432
|
+
constructor(text) {
|
|
433
|
+
this.text = text;
|
|
434
|
+
}
|
|
435
|
+
toJSON() {
|
|
436
|
+
return this.text;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const FOLD_CHARS = 600;
|
|
440
|
+
const TEXT_PREVIEW_CHARS = 2000;
|
|
441
|
+
function summarize(v) {
|
|
442
|
+
if (Array.isArray(v)) {
|
|
443
|
+
return JSON.stringify(v).length <= FOLD_CHARS ? v : new Folded(`[ ${v.length} items ]`);
|
|
444
|
+
}
|
|
445
|
+
if (v !== null && typeof v === 'object') {
|
|
446
|
+
return JSON.stringify(v).length <= FOLD_CHARS
|
|
447
|
+
? v
|
|
448
|
+
: new Folded(`{ ${Object.keys(v).length} keys }`);
|
|
449
|
+
}
|
|
450
|
+
if (typeof v === 'string' && v.length > TEXT_PREVIEW_CHARS) {
|
|
451
|
+
return new Folded(`${JSON.stringify(v.slice(0, TEXT_PREVIEW_CHARS))}… (${v.length} characters)`);
|
|
452
|
+
}
|
|
453
|
+
return v;
|
|
454
|
+
}
|
|
455
|
+
function preview(v) {
|
|
456
|
+
if (v instanceof Folded)
|
|
457
|
+
return v.text;
|
|
458
|
+
return v !== null && typeof v === 'object' ? JSON.stringify(v, null, 2) : JSON.stringify(v);
|
|
459
|
+
}
|
|
460
|
+
function pathCrumbs(path, base) {
|
|
461
|
+
const cuts = ['$'];
|
|
462
|
+
let i = 1;
|
|
463
|
+
while (i < path.length) {
|
|
464
|
+
if (path[i] === '[')
|
|
465
|
+
i = path.indexOf(']', i) + 1;
|
|
466
|
+
else if (path[i] === '.' && path[i + 1] === '"') {
|
|
467
|
+
let j = i + 2;
|
|
468
|
+
while (j < path.length && path[j] !== '"')
|
|
469
|
+
j += path[j] === '\\' ? 2 : 1;
|
|
470
|
+
i = j + 1;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
i++;
|
|
474
|
+
while (i < path.length && path[i] !== '.' && path[i] !== '[')
|
|
475
|
+
i++;
|
|
476
|
+
}
|
|
477
|
+
if (i <= 0)
|
|
478
|
+
break;
|
|
479
|
+
cuts.push(path.slice(0, i));
|
|
480
|
+
}
|
|
481
|
+
return html `<p class="crumbs path">
|
|
482
|
+
${cuts.map((c, n) => n === cuts.length - 1
|
|
483
|
+
? html `<code>${c}</code>`
|
|
484
|
+
: html `<a href="${base}${qs({ path: c })}"><code>${c}</code></a> › `)}
|
|
485
|
+
</p>`;
|
|
486
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { BIN } from "../../../core/domain/index.js";
|
|
3
|
+
import { formatBytes } from "../../../core/text.js";
|
|
4
|
+
import { formatInterval } from "../../../core/time.js";
|
|
5
|
+
import { BUILTIN_WORKER_LABEL, badge, command, copyButton, hostPath, html, tag, time } from "./html.js";
|
|
6
|
+
export function vscodeLink(file) {
|
|
7
|
+
return `vscode://file${pathToFileURL(file).pathname}`;
|
|
8
|
+
}
|
|
9
|
+
export function workersPage(v, ctx) {
|
|
10
|
+
const sync = v.lastSync;
|
|
11
|
+
return html `
|
|
12
|
+
<div class="head">
|
|
13
|
+
<h1>Workers</h1>
|
|
14
|
+
${v.dir ? html `<p class="path">${hostPath(v.dir, ctx)}</p>` : ''}
|
|
15
|
+
</div>
|
|
16
|
+
<p class="note">
|
|
17
|
+
${sync
|
|
18
|
+
? html `Description, timeout and input schema are as of the last sync (${time(sync.at)}).
|
|
19
|
+
Files are not loaded here — loading a worker runs its top-level code.`
|
|
20
|
+
: html `No sync record in the recent logs — description, timeout and input schema appear
|
|
21
|
+
after <code>${BIN} start</code> or <code>${BIN} reload</code>.`}
|
|
22
|
+
After editing a file, apply it to a running start with ${command(`${BIN} reload`, ctx)}
|
|
23
|
+
</p>
|
|
24
|
+
${v.error
|
|
25
|
+
? html `<p class="error">Cannot read the workers folder: ${v.error}</p>`
|
|
26
|
+
: v.dirMissing
|
|
27
|
+
? html `<p class="muted">
|
|
28
|
+
${v.dir ? 'The workers folder does not exist.' : 'This host has no project.'} Run
|
|
29
|
+
${command(`${BIN} init`, ctx)} (creates ~/3minapi) or
|
|
30
|
+
${command(`${BIN} init <folder>`, ctx)}.
|
|
31
|
+
</p>`
|
|
32
|
+
: v.files.length === 0
|
|
33
|
+
? html `<p class="muted">
|
|
34
|
+
No worker files. Create one with ${command(`${BIN} worker new <name>`, ctx)}
|
|
35
|
+
</p>`
|
|
36
|
+
: html `<div class="scroll">
|
|
37
|
+
<table class="table">
|
|
38
|
+
<thead>
|
|
39
|
+
<tr>
|
|
40
|
+
<th class="num">No</th>
|
|
41
|
+
<th>Worker</th>
|
|
42
|
+
<th>Description</th>
|
|
43
|
+
<th class="num">Timeout</th>
|
|
44
|
+
<th>Input schema</th>
|
|
45
|
+
<th>Last sync</th>
|
|
46
|
+
<th>File</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
${v.files.map((w, i) => {
|
|
51
|
+
const meta = sync?.local?.get(w.name) ?? null;
|
|
52
|
+
const result = sync?.results.get(w.name);
|
|
53
|
+
const rejected = sync?.rejected.get(w.name);
|
|
54
|
+
const file = w.files[0];
|
|
55
|
+
return html `<tr>
|
|
56
|
+
<td class="num muted">${i + 1}</td>
|
|
57
|
+
<td>
|
|
58
|
+
<a href="/workers/${encodeURIComponent(w.name)}"><code>${w.name}</code></a>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
${meta?.description ?? html `<span class="muted">—</span>`}${meta?.message
|
|
62
|
+
? html `<div class="error">${meta.message}</div>`
|
|
63
|
+
: ''}
|
|
64
|
+
</td>
|
|
65
|
+
<td class="num">
|
|
66
|
+
${meta?.timeout_ms
|
|
67
|
+
? formatInterval(meta.timeout_ms)
|
|
68
|
+
: html `<span class="muted">default</span>`}
|
|
69
|
+
</td>
|
|
70
|
+
<td>
|
|
71
|
+
${meta
|
|
72
|
+
? meta.input_schema
|
|
73
|
+
? 'yes'
|
|
74
|
+
: 'no'
|
|
75
|
+
: html `<span class="muted">—</span>`}
|
|
76
|
+
</td>
|
|
77
|
+
<td>
|
|
78
|
+
${syncBadge(result, rejected) ??
|
|
79
|
+
(sync
|
|
80
|
+
? html `<span class="muted">not sent</span>`
|
|
81
|
+
: html `<span class="muted">—</span>`)}${sync &&
|
|
82
|
+
file &&
|
|
83
|
+
file.modifiedAt > sync.at
|
|
84
|
+
? html ` ${tag('changed', 'The file changed after the last sync')}`
|
|
85
|
+
: ''}
|
|
86
|
+
</td>
|
|
87
|
+
<td class="files">
|
|
88
|
+
${w.files.map((f) => html `<div>
|
|
89
|
+
${formatBytes(f.bytes)}${ctx.remote
|
|
90
|
+
? html ` · <span class="muted">on host ${ctx.host}</span>`
|
|
91
|
+
: html ` ·
|
|
92
|
+
<a href="${vscodeLink(f.file)}">Open in VS Code</a>
|
|
93
|
+
${copyButton(f.file, 'Copy path')}`}
|
|
94
|
+
</div>`)}${w.files.length > 1
|
|
95
|
+
? html `<div class="error">
|
|
96
|
+
More than one file with this name — keep only one.
|
|
97
|
+
</div>`
|
|
98
|
+
: ''}
|
|
99
|
+
</td>
|
|
100
|
+
</tr>`;
|
|
101
|
+
})}
|
|
102
|
+
</tbody>
|
|
103
|
+
</table>
|
|
104
|
+
</div>`}
|
|
105
|
+
${v.builtin ? builtinPanel(v.builtin, v.lastSync, ctx) : ''}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
function builtinPanel(name, sync, ctx) {
|
|
109
|
+
const result = sync?.results.get(name);
|
|
110
|
+
const rejected = sync?.rejected.get(name);
|
|
111
|
+
return html `<section class="panel">
|
|
112
|
+
<h2>Built-in ${syncBadge(result, rejected) ?? ''}</h2>
|
|
113
|
+
<dl class="kv">
|
|
114
|
+
<dt>${BUILTIN_WORKER_LABEL}</dt>
|
|
115
|
+
<dd>
|
|
116
|
+
Part of 3minapi, not a file — it cannot be edited. Remote AIs use it to read the results
|
|
117
|
+
saved on host ${ctx.host} (describe, query, read).
|
|
118
|
+
<div class="muted">
|
|
119
|
+
Registered as <code>${name}</code> — the name you see in remote AIs and in the start
|
|
120
|
+
output.
|
|
121
|
+
</div>
|
|
122
|
+
</dd>
|
|
123
|
+
</dl>
|
|
124
|
+
</section>`;
|
|
125
|
+
}
|
|
126
|
+
function syncBadge(result, rejected) {
|
|
127
|
+
if (rejected)
|
|
128
|
+
return badge(rejected, 'fail');
|
|
129
|
+
if (result)
|
|
130
|
+
return badge(result, 'ok');
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
export function workerCodePage(name, files, ctx) {
|
|
134
|
+
return html `
|
|
135
|
+
<p class="crumbs"><a href="/workers">Workers</a> / <code>${name}</code></p>
|
|
136
|
+
<div class="head">
|
|
137
|
+
<h1><code>${name}</code></h1>
|
|
138
|
+
</div>
|
|
139
|
+
<p class="note">
|
|
140
|
+
Read-only view of the file as text. Edit it in your editor, then run
|
|
141
|
+
${command(`${BIN} reload`, ctx)}
|
|
142
|
+
</p>
|
|
143
|
+
${files.map((f) => html `<section class="panel">
|
|
144
|
+
<h2 class="path"><code>${f.file}</code></h2>
|
|
145
|
+
<p>
|
|
146
|
+
${ctx.remote
|
|
147
|
+
? html `<span class="muted">on host ${ctx.host}</span>`
|
|
148
|
+
: html `<a href="${vscodeLink(f.file)}">Open in VS Code</a> ${copyButton(f.file, 'Copy path')}`}
|
|
149
|
+
<span class="muted">${formatBytes(f.bytes)}</span>
|
|
150
|
+
</p>
|
|
151
|
+
<pre class="code source">${f.text}</pre>
|
|
152
|
+
${f.truncated
|
|
153
|
+
? html `<p class="muted">Showing the first part only — the file is larger.</p>`
|
|
154
|
+
: ''}
|
|
155
|
+
</section>`)}
|
|
156
|
+
`;
|
|
157
|
+
}
|