@3minapi/cli 0.0.0-stage → 0.22.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/LICENSE +21 -0
- package/README.md +88 -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,316 @@
|
|
|
1
|
+
import { BIN } from "../../../core/domain/index.js";
|
|
2
|
+
import { stopHint } from "../../../core/host/session.js";
|
|
3
|
+
import { formatBytes, formatCount } from "../../../core/text.js";
|
|
4
|
+
import { daysToMs, formatInterval, formatLocal, relative } from "../../../core/time.js";
|
|
5
|
+
import { RANGES } from "../data.js";
|
|
6
|
+
import { badge, command, html, qs, time, workerName } from "./html.js";
|
|
7
|
+
const RANGE_LABEL = {
|
|
8
|
+
all: 'All',
|
|
9
|
+
today: 'Today',
|
|
10
|
+
'7d': '7 days',
|
|
11
|
+
'30d': '30 days'
|
|
12
|
+
};
|
|
13
|
+
export function overviewPage(o, now, ctx) {
|
|
14
|
+
return html `
|
|
15
|
+
<div class="head">
|
|
16
|
+
<h1>Overview</h1>
|
|
17
|
+
${rangeTabs('/', o.range)}
|
|
18
|
+
</div>
|
|
19
|
+
${o.hasControlDb
|
|
20
|
+
? ''
|
|
21
|
+
: html `<p class="note">
|
|
22
|
+
No task history on host ${ctx.host} yet — run <code>${BIN} start</code> to receive tasks.
|
|
23
|
+
</p>`}
|
|
24
|
+
<section class="cards" aria-label="Totals">
|
|
25
|
+
${card('Runs', o.totals.total, `/runs${qs({ range: o.range })}`)}
|
|
26
|
+
${card('Completed', o.totals.completed, `/runs${qs({ range: o.range, state: 'completed' })}`)}
|
|
27
|
+
${card('Failed', o.totals.failed, `/runs${qs({ range: o.range, state: 'failed' })}`, o.totals.failed > 0)}
|
|
28
|
+
${card('Cancelled', o.totals.cancelled, `/runs${qs({ range: o.range, state: 'cancelled' })}`)}
|
|
29
|
+
${card('Running', o.totals.running, `/runs${qs({ range: o.range, state: 'running' })}`)}
|
|
30
|
+
</section>
|
|
31
|
+
|
|
32
|
+
<section class="panel">
|
|
33
|
+
<h2>Trend <span class="muted">${o.trend.unit === 'hour' ? 'by hour' : 'by day'}</span></h2>
|
|
34
|
+
${trendChart(o.trend.buckets, o.trend.unit)}
|
|
35
|
+
<p class="legend">
|
|
36
|
+
<span class="key key-ok"></span>completed <span class="key key-fail"></span>failed
|
|
37
|
+
<span class="key key-other"></span>other
|
|
38
|
+
</p>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<div class="grid">
|
|
42
|
+
<section class="panel">
|
|
43
|
+
<h2>By worker</h2>
|
|
44
|
+
${o.byWorker.length === 0
|
|
45
|
+
? html `<p class="muted">No runs in this period.</p>`
|
|
46
|
+
: html `<table class="table">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th>Worker</th>
|
|
50
|
+
<th class="num">Runs</th>
|
|
51
|
+
<th class="num">Failed</th>
|
|
52
|
+
<th class="bar-col"></th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
${o.byWorker.map((w) => html `<tr>
|
|
57
|
+
<td>
|
|
58
|
+
<a href="/runs${qs({ range: o.range, worker: w.worker })}"
|
|
59
|
+
>${workerName(w.worker)}</a
|
|
60
|
+
>
|
|
61
|
+
</td>
|
|
62
|
+
<td class="num">${formatCount(w.total)}</td>
|
|
63
|
+
<td class="num">
|
|
64
|
+
${w.failed > 0
|
|
65
|
+
? html `<a
|
|
66
|
+
href="/runs${qs({
|
|
67
|
+
range: o.range,
|
|
68
|
+
worker: w.worker,
|
|
69
|
+
state: 'failed'
|
|
70
|
+
})}"
|
|
71
|
+
>${formatCount(w.failed)}</a
|
|
72
|
+
>`
|
|
73
|
+
: '0'}
|
|
74
|
+
</td>
|
|
75
|
+
<td class="bar-col">${miniBar(w, o.byWorker[0].total)}</td>
|
|
76
|
+
</tr>`)}
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>`}
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section class="panel">
|
|
82
|
+
<h2>Recent failures</h2>
|
|
83
|
+
${o.recentFailures.length === 0
|
|
84
|
+
? html `<p class="muted">No failures in this period.</p>`
|
|
85
|
+
: html `<ul class="list">
|
|
86
|
+
${o.recentFailures.map((t) => html `<li>
|
|
87
|
+
<div class="row">
|
|
88
|
+
<a href="/runs/${encodeURIComponent(t.task_id)}"><code>${t.task_id}</code></a>
|
|
89
|
+
${badge('failed')}
|
|
90
|
+
</div>
|
|
91
|
+
<div class="row muted">
|
|
92
|
+
${workerName(t.worker)} · ${time(t.finished_at ?? t.received_at)}
|
|
93
|
+
</div>
|
|
94
|
+
${t.last_error ? html `<div class="error">${t.last_error}</div>` : ''}
|
|
95
|
+
</li>`)}
|
|
96
|
+
</ul>`}
|
|
97
|
+
</section>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<section class="now" aria-labelledby="now-title">
|
|
101
|
+
<h2 id="now-title" class="section-title">
|
|
102
|
+
Host ${ctx.host} <span class="muted">— now, not limited to the period above</span>
|
|
103
|
+
</h2>
|
|
104
|
+
<div class="grid">${runnerPanel(o, now, ctx)} ${diskPanel(o, now, ctx)}</div>
|
|
105
|
+
</section>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
export function rangeTabs(base, current, extra = {}, ranges = RANGES) {
|
|
109
|
+
return html `<nav class="tabs" aria-label="Period">
|
|
110
|
+
${ranges.map((r) => html `<a
|
|
111
|
+
href="${base}${qs({ ...extra, range: r })}"
|
|
112
|
+
${r === current ? html ` aria-current="page"` : ''}
|
|
113
|
+
>${RANGE_LABEL[r]}</a
|
|
114
|
+
>`)}
|
|
115
|
+
</nav>`;
|
|
116
|
+
}
|
|
117
|
+
function card(label, value, href, alert = false) {
|
|
118
|
+
return html `<a class="card${alert ? ' card-alert' : ''}" href="${href}"
|
|
119
|
+
><span class="card-label">${label}</span
|
|
120
|
+
><span class="card-value">${formatCount(value)}</span></a
|
|
121
|
+
>`;
|
|
122
|
+
}
|
|
123
|
+
const COUNT_CHAR_WIDTH = 6.5;
|
|
124
|
+
function trendChart(buckets, unit) {
|
|
125
|
+
const width = 720;
|
|
126
|
+
const height = 160;
|
|
127
|
+
const pad = { top: 18, bottom: 22, left: 4, right: 4 };
|
|
128
|
+
const max = Math.max(1, ...buckets.map((b) => b.total));
|
|
129
|
+
const innerW = width - pad.left - pad.right;
|
|
130
|
+
const innerH = height - pad.top - pad.bottom;
|
|
131
|
+
const step = innerW / Math.max(1, buckets.length);
|
|
132
|
+
const barW = Math.max(2, Math.min(28, step * 0.7));
|
|
133
|
+
const y = (n) => (n / max) * innerH;
|
|
134
|
+
const labelEvery = Math.ceil(buckets.length / 8);
|
|
135
|
+
const label = (b) => (unit === 'hour' ? `${b.slice(11)}h` : b.slice(5));
|
|
136
|
+
return html `<svg
|
|
137
|
+
class="chart"
|
|
138
|
+
viewBox="0 0 ${width} ${height}"
|
|
139
|
+
role="img"
|
|
140
|
+
aria-label="Runs per ${unit}"
|
|
141
|
+
>
|
|
142
|
+
<line
|
|
143
|
+
class="axis"
|
|
144
|
+
x1="${pad.left}"
|
|
145
|
+
y1="${pad.top + innerH}"
|
|
146
|
+
x2="${width - pad.right}"
|
|
147
|
+
y2="${pad.top + innerH}"
|
|
148
|
+
/>
|
|
149
|
+
${buckets.map((b, i) => {
|
|
150
|
+
const x = pad.left + i * step + (step - barW) / 2;
|
|
151
|
+
const other = b.total - b.completed - b.failed;
|
|
152
|
+
let base = pad.top + innerH;
|
|
153
|
+
const seg = (n, cls) => {
|
|
154
|
+
if (n === 0)
|
|
155
|
+
return '';
|
|
156
|
+
const h = Math.max(1, y(n));
|
|
157
|
+
base -= h;
|
|
158
|
+
return html `<rect
|
|
159
|
+
class="${cls}"
|
|
160
|
+
x="${x.toFixed(1)}"
|
|
161
|
+
y="${base.toFixed(1)}"
|
|
162
|
+
width="${barW.toFixed(1)}"
|
|
163
|
+
height="${h.toFixed(1)}"
|
|
164
|
+
/>`;
|
|
165
|
+
};
|
|
166
|
+
const count = formatCount(b.total);
|
|
167
|
+
const fits = count.length * COUNT_CHAR_WIDTH <= step;
|
|
168
|
+
return html `<g
|
|
169
|
+
><title>${b.bucket}: ${b.total} runs, ${b.completed} completed, ${b.failed} failed</title>
|
|
170
|
+
${seg(b.completed, 'bar-ok')}${seg(other, 'bar-other')}${seg(b.failed, 'bar-fail')}
|
|
171
|
+
${b.total > 0 && fits
|
|
172
|
+
? html `<text
|
|
173
|
+
class="count"
|
|
174
|
+
x="${(x + barW / 2).toFixed(1)}"
|
|
175
|
+
y="${(base - 4).toFixed(1)}"
|
|
176
|
+
text-anchor="middle"
|
|
177
|
+
>${count}</text
|
|
178
|
+
>`
|
|
179
|
+
: ''}
|
|
180
|
+
${i % labelEvery === 0
|
|
181
|
+
? html `<text
|
|
182
|
+
class="tick"
|
|
183
|
+
x="${(x + barW / 2).toFixed(1)}"
|
|
184
|
+
y="${height - 6}"
|
|
185
|
+
text-anchor="middle"
|
|
186
|
+
>${label(b.bucket)}</text
|
|
187
|
+
>`
|
|
188
|
+
: ''}</g
|
|
189
|
+
>`;
|
|
190
|
+
})}
|
|
191
|
+
</svg>`;
|
|
192
|
+
}
|
|
193
|
+
function miniBar(c, max) {
|
|
194
|
+
const w = 120;
|
|
195
|
+
const scale = (n) => (max === 0 ? 0 : (n / max) * w);
|
|
196
|
+
const ok = scale(c.completed);
|
|
197
|
+
const fail = scale(c.failed);
|
|
198
|
+
const other = scale(c.total - c.completed - c.failed);
|
|
199
|
+
return html `<svg class="mini" viewBox="0 0 ${w} 10" aria-hidden="true">
|
|
200
|
+
<rect class="bar-ok" x="0" y="1" width="${ok.toFixed(1)}" height="8" />
|
|
201
|
+
<rect class="bar-other" x="${ok.toFixed(1)}" y="1" width="${other.toFixed(1)}" height="8" />
|
|
202
|
+
<rect
|
|
203
|
+
class="bar-fail"
|
|
204
|
+
x="${(ok + other).toFixed(1)}"
|
|
205
|
+
y="1"
|
|
206
|
+
width="${fail.toFixed(1)}"
|
|
207
|
+
height="8"
|
|
208
|
+
/>
|
|
209
|
+
</svg>`;
|
|
210
|
+
}
|
|
211
|
+
function runnerPanel(o, now, ctx) {
|
|
212
|
+
const r = o.runner;
|
|
213
|
+
const rows = [];
|
|
214
|
+
if (r.state === 'running') {
|
|
215
|
+
const info = r.info;
|
|
216
|
+
rows.push([
|
|
217
|
+
'State',
|
|
218
|
+
html `${badge('running')} pid ${info.pid}${info.detached ? ' · detached' : ''}`
|
|
219
|
+
]);
|
|
220
|
+
rows.push([
|
|
221
|
+
'Started',
|
|
222
|
+
html `${time(info.startedAt)} <span class="muted">(${relative(info.startedAt, now)})</span>`
|
|
223
|
+
]);
|
|
224
|
+
rows.push(info.intervalMs === null
|
|
225
|
+
? ['Receive mode', 'live (pulls when 3Min API signals new tasks)']
|
|
226
|
+
: ['Poll interval', `${formatInterval(info.intervalMs)} (or the server minimum, if larger)`]);
|
|
227
|
+
if (info.port !== null)
|
|
228
|
+
rows.push([
|
|
229
|
+
'Local server',
|
|
230
|
+
html `127.0.0.1:${info.port}${info.public ? ' (public)' : ''}${ctx.remote
|
|
231
|
+
? html ` <span class="muted">— reachable only on host ${ctx.host}</span>`
|
|
232
|
+
: ''}`
|
|
233
|
+
]);
|
|
234
|
+
if (info.workersDir)
|
|
235
|
+
rows.push(['Workers folder', html `<code>${info.workersDir}</code>`]);
|
|
236
|
+
}
|
|
237
|
+
else if (r.state === 'starting') {
|
|
238
|
+
rows.push(['State', html `${badge('running')} starting`]);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
rows.push([
|
|
242
|
+
'State',
|
|
243
|
+
html `${badge('stopped')} <span class="muted">not running</span> ${command(`${BIN} start`, ctx)}`
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
if (o.host.name)
|
|
247
|
+
rows.push(['Host', o.host.name]);
|
|
248
|
+
if (o.lastResponse) {
|
|
249
|
+
const q = o.lastResponse.quota;
|
|
250
|
+
rows.push([
|
|
251
|
+
'Last server response',
|
|
252
|
+
html `${time(o.lastResponse.at)}
|
|
253
|
+
<span class="muted">(${relative(o.lastResponse.at, now)})</span>`
|
|
254
|
+
]);
|
|
255
|
+
rows.push([
|
|
256
|
+
'Quota',
|
|
257
|
+
html `${formatCount(q.used)} / ${formatCount(q.limit)} calls · resets
|
|
258
|
+
${time(Date.parse(q.reset_at))}`
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
if (o.stop) {
|
|
262
|
+
rows.push([
|
|
263
|
+
'Last stop',
|
|
264
|
+
html `${badge('failed')} <code>${o.stop.reason}</code> ${time(o.stop.stoppedAt)}
|
|
265
|
+
<div class="muted">${stopHint(o.stop.reason)}</div>`
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
if (o.end) {
|
|
269
|
+
rows.push([
|
|
270
|
+
'Last end',
|
|
271
|
+
html `<code>${o.end.reason}</code> ${time(o.end.endedAt)}${o.end.deletedWorkers.length > 0
|
|
272
|
+
? html `<div class="muted">Deleted on the server: ${o.end.deletedWorkers.join(', ')}</div>`
|
|
273
|
+
: ''}`
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
return html `<section class="panel">
|
|
277
|
+
<h2>Runner</h2>
|
|
278
|
+
<dl class="kv">
|
|
279
|
+
${rows.map(([k, v]) => html `<dt>${k}</dt>
|
|
280
|
+
<dd>${v}</dd>`)}
|
|
281
|
+
</dl>
|
|
282
|
+
</section>`;
|
|
283
|
+
}
|
|
284
|
+
function diskPanel(o, now, ctx) {
|
|
285
|
+
const { controlDb, dataDb, logs } = o.disk;
|
|
286
|
+
const before = formatLocal(now - daysToMs(30)).slice(0, 10);
|
|
287
|
+
return html `<section class="panel">
|
|
288
|
+
<h2>Disk</h2>
|
|
289
|
+
<dl class="kv">
|
|
290
|
+
<dt>Task history</dt>
|
|
291
|
+
<dd>
|
|
292
|
+
${controlDb
|
|
293
|
+
? html `${formatCount(controlDb.tasks)} task(s) · ${formatBytes(controlDb.bytes)}
|
|
294
|
+
<div class="path"><code>${controlDb.file}</code></div>
|
|
295
|
+
${command(`${BIN} tasks prune --before ${before}`, ctx)}`
|
|
296
|
+
: html `<span class="muted">none</span>`}
|
|
297
|
+
</dd>
|
|
298
|
+
<dt>Saved results</dt>
|
|
299
|
+
<dd>
|
|
300
|
+
${dataDb
|
|
301
|
+
? html `${formatCount(dataDb.results)} result(s)
|
|
302
|
+
<span class="muted">(a repeating task saves each run)</span> ·
|
|
303
|
+
${formatBytes(dataDb.bytes)}
|
|
304
|
+
<div class="path"><code>${dataDb.file}</code></div>
|
|
305
|
+
${command(`${BIN} worker results prune --before ${before}`, ctx)}`
|
|
306
|
+
: html `<span class="muted">none</span>`}
|
|
307
|
+
</dd>
|
|
308
|
+
<dt>Logs</dt>
|
|
309
|
+
<dd>
|
|
310
|
+
${formatCount(logs.files)} file(s) · ${formatBytes(logs.bytes)}
|
|
311
|
+
<div class="path"><code>${logs.dir}</code></div>
|
|
312
|
+
<span class="muted">Deleted automatically (logRetentionDays, logMaxBytes)</span>
|
|
313
|
+
</dd>
|
|
314
|
+
</dl>
|
|
315
|
+
</section>`;
|
|
316
|
+
}
|