@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,16 @@
|
|
|
1
|
+
import { MIN_NODE_VERSION } from "./domain/constants.js";
|
|
2
|
+
export function parseVersion(version) {
|
|
3
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
4
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
5
|
+
}
|
|
6
|
+
export function satisfiesMinNode(current, min = MIN_NODE_VERSION) {
|
|
7
|
+
const a = parseVersion(current);
|
|
8
|
+
const b = parseVersion(min);
|
|
9
|
+
if (!a || !b)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if (a[i] !== b[i])
|
|
13
|
+
return a[i] > b[i];
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const SQLITE_EXPERIMENTAL = 'SQLite is an experimental feature';
|
|
2
|
+
export function suppressSqliteExperimentalWarning() {
|
|
3
|
+
const original = process.emitWarning;
|
|
4
|
+
process.emitWarning = function (warning, ...rest) {
|
|
5
|
+
const message = typeof warning === 'string' ? warning : warning?.message;
|
|
6
|
+
if (typeof message === 'string' && message.startsWith(SQLITE_EXPERIMENTAL))
|
|
7
|
+
return;
|
|
8
|
+
return original.call(process, warning, ...rest);
|
|
9
|
+
};
|
|
10
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BIN, DEFAULT_API_URL } from "./core/domain/index.js";
|
|
2
|
+
export const GLOBAL_OPTIONS = {
|
|
3
|
+
help: { type: 'boolean', short: 'h', description: 'Show help' },
|
|
4
|
+
version: { type: 'boolean', description: 'Show the CLI version' },
|
|
5
|
+
json: { type: 'boolean', description: 'Print the result as JSON on stdout' },
|
|
6
|
+
color: { type: 'boolean', description: 'Force color on or off (--no-color)' },
|
|
7
|
+
'api-url': {
|
|
8
|
+
type: 'string',
|
|
9
|
+
valueName: 'url',
|
|
10
|
+
description: `API base URL (default ${DEFAULT_API_URL})`
|
|
11
|
+
},
|
|
12
|
+
config: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
valueName: 'file',
|
|
15
|
+
description: "Project config file (default: this host's project — '3minapi init')"
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export function rootHelp(commands, version) {
|
|
19
|
+
const flat = (c) => (c.subcommands ? c.subcommands.flatMap(flat) : [c]);
|
|
20
|
+
const rows = commands.flatMap(flat);
|
|
21
|
+
const width = Math.max(...rows.map((c) => c.name.length)) + 2;
|
|
22
|
+
const lines = [
|
|
23
|
+
`${BIN} ${version} — run this machine as a 3Min API host for your own worker files`,
|
|
24
|
+
'',
|
|
25
|
+
`Usage: ${BIN} <command> [options]`,
|
|
26
|
+
'',
|
|
27
|
+
'Commands:',
|
|
28
|
+
...rows.map((c) => ` ${c.name.padEnd(width)}${c.summary}${c.pendingIssue ? ' (not yet available)' : ''}`),
|
|
29
|
+
'',
|
|
30
|
+
'Global options:',
|
|
31
|
+
...formatOptions(GLOBAL_OPTIONS),
|
|
32
|
+
'',
|
|
33
|
+
`Run '${BIN} help <command>' for details on a command.`
|
|
34
|
+
];
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
export function commandHelp(cmd) {
|
|
38
|
+
const lines = [`Usage: ${BIN} ${cmd.name}${cmd.usage ? ' ' + cmd.usage : ''}`, '', cmd.summary];
|
|
39
|
+
if (cmd.description)
|
|
40
|
+
lines.push('', cmd.description);
|
|
41
|
+
if (cmd.pendingIssue)
|
|
42
|
+
lines.push('', `Not yet available — tracked in ${cmd.pendingIssue}.`);
|
|
43
|
+
if (Object.keys(cmd.options).length > 0)
|
|
44
|
+
lines.push('', 'Options:', ...formatOptions(cmd.options));
|
|
45
|
+
lines.push('', 'Global options:', ...formatOptions(GLOBAL_OPTIONS));
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
export function groupHelp(group) {
|
|
49
|
+
const subs = group.subcommands ?? [];
|
|
50
|
+
const width = Math.max(...subs.map((c) => c.name.length)) + 2;
|
|
51
|
+
const lines = [
|
|
52
|
+
`Usage: ${BIN} ${group.name} <command> [options]`,
|
|
53
|
+
'',
|
|
54
|
+
group.summary,
|
|
55
|
+
...(group.description ? ['', group.description] : []),
|
|
56
|
+
'',
|
|
57
|
+
'Commands:',
|
|
58
|
+
...subs.map((c) => ` ${c.name.padEnd(width)}${c.summary}`),
|
|
59
|
+
'',
|
|
60
|
+
`Run '${BIN} help ${group.name} <command>' for details on a command.`
|
|
61
|
+
];
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
function formatOptions(options) {
|
|
65
|
+
const rows = Object.entries(options).map(([name, spec]) => {
|
|
66
|
+
const flag = spec.type === 'boolean' && name === 'color'
|
|
67
|
+
? '--[no-]color'
|
|
68
|
+
: spec.negated
|
|
69
|
+
? `--no-${name}`
|
|
70
|
+
: `--${name}`;
|
|
71
|
+
const value = spec.type === 'string' ? ` <${spec.valueName ?? 'value'}>` : '';
|
|
72
|
+
const short = spec.short ? `-${spec.short}, ` : ' ';
|
|
73
|
+
return [`${short}${flag}${value}`, spec.description];
|
|
74
|
+
});
|
|
75
|
+
const width = Math.max(...rows.map(([left]) => left.length)) + 2;
|
|
76
|
+
return rows.map(([left, desc]) => ` ${left.padEnd(width)}${desc}`);
|
|
77
|
+
}
|
|
78
|
+
function optionsJson(options) {
|
|
79
|
+
return Object.entries(options).map(([name, spec]) => ({
|
|
80
|
+
name: spec.negated ? `no-${name}` : name,
|
|
81
|
+
type: spec.type,
|
|
82
|
+
...(spec.short ? { short: spec.short } : {}),
|
|
83
|
+
...(spec.type === 'string' ? { valueName: spec.valueName ?? 'value' } : {}),
|
|
84
|
+
...(spec.negated ? { negated: true } : {}),
|
|
85
|
+
...(spec.multiple ? { multiple: true } : {}),
|
|
86
|
+
description: spec.description
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
function commandJson(cmd) {
|
|
90
|
+
return {
|
|
91
|
+
name: cmd.name,
|
|
92
|
+
summary: cmd.summary,
|
|
93
|
+
usage: `${BIN} ${cmd.name}${cmd.usage ? ' ' + cmd.usage : ''}`,
|
|
94
|
+
...(cmd.description ? { description: cmd.description } : {}),
|
|
95
|
+
args: cmd.args,
|
|
96
|
+
options: optionsJson(cmd.options),
|
|
97
|
+
...(cmd.pendingIssue ? { available: false } : {})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function rootHelpJson(commands, version) {
|
|
101
|
+
const flat = (c) => (c.subcommands ? c.subcommands.flatMap(flat) : [c]);
|
|
102
|
+
return {
|
|
103
|
+
name: BIN,
|
|
104
|
+
version,
|
|
105
|
+
commands: commands.flatMap(flat).map(commandJson),
|
|
106
|
+
globalOptions: optionsJson(GLOBAL_OPTIONS)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function commandHelpJson(cmd) {
|
|
110
|
+
return { ...commandJson(cmd), globalOptions: optionsJson(GLOBAL_OPTIONS) };
|
|
111
|
+
}
|
|
112
|
+
export function groupHelpJson(group) {
|
|
113
|
+
return {
|
|
114
|
+
name: group.name,
|
|
115
|
+
summary: group.summary,
|
|
116
|
+
...(group.description ? { description: group.description } : {}),
|
|
117
|
+
commands: (group.subcommands ?? []).map((c) => c.subcommands ? groupHelpJson(c) : commandJson(c))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function suggest(input, names) {
|
|
121
|
+
let best;
|
|
122
|
+
let bestDistance = 3;
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const d = distance(input, name);
|
|
125
|
+
if (d < bestDistance) {
|
|
126
|
+
best = name;
|
|
127
|
+
bestDistance = d;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
function distance(a, b) {
|
|
133
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
134
|
+
for (let i = 1; i <= a.length; i++) {
|
|
135
|
+
let prev = row[0];
|
|
136
|
+
row[0] = i;
|
|
137
|
+
for (let j = 1; j <= b.length; j++) {
|
|
138
|
+
const tmp = row[j];
|
|
139
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
140
|
+
prev = tmp;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return row[b.length];
|
|
144
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
export const APP_CSS = `
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #ffffff;
|
|
4
|
+
--fg: #111111;
|
|
5
|
+
--muted: #6b6b6b;
|
|
6
|
+
--line: #e3e3e3;
|
|
7
|
+
--soft: #f4f4f4;
|
|
8
|
+
--other: #b5b5b5;
|
|
9
|
+
--ok: #1a7f37;
|
|
10
|
+
--ok-bg: #dafbe1;
|
|
11
|
+
--warn: #9a6700;
|
|
12
|
+
--warn-bg: #fff8c5;
|
|
13
|
+
--fail: #cf222e;
|
|
14
|
+
--fail-bg: #ffebe9;
|
|
15
|
+
/* Chart — separate from text color. Success uses the text color, others gray, only failure red */
|
|
16
|
+
--chart-ok: var(--fg);
|
|
17
|
+
--chart-other: var(--other);
|
|
18
|
+
--chart-fail: var(--fail);
|
|
19
|
+
/* Font sizes — small buttons, badges, tags / labels, secondary text / code, logs / chart numbers */
|
|
20
|
+
--fs-xs: 11.5px;
|
|
21
|
+
--fs-sm: 12px;
|
|
22
|
+
--fs-code: 12.5px;
|
|
23
|
+
--fs-chart: 10px;
|
|
24
|
+
--radius: 6px;
|
|
25
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
26
|
+
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
27
|
+
color-scheme: light dark;
|
|
28
|
+
}
|
|
29
|
+
@media (prefers-color-scheme: dark) {
|
|
30
|
+
:root {
|
|
31
|
+
--bg: #0f0f0f;
|
|
32
|
+
--fg: #ededed;
|
|
33
|
+
--muted: #9a9a9a;
|
|
34
|
+
--line: #2a2a2a;
|
|
35
|
+
--soft: #1a1a1a;
|
|
36
|
+
--other: #555555;
|
|
37
|
+
--ok: #3fb950;
|
|
38
|
+
--ok-bg: rgba(63, 185, 80, 0.15);
|
|
39
|
+
--warn: #d29922;
|
|
40
|
+
--warn-bg: rgba(210, 153, 34, 0.15);
|
|
41
|
+
--fail: #f85149;
|
|
42
|
+
--fail-bg: rgba(248, 81, 73, 0.15);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
* { box-sizing: border-box; }
|
|
46
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
47
|
+
body {
|
|
48
|
+
margin: 0;
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
color: var(--fg);
|
|
51
|
+
font: 14px/1.5 var(--sans);
|
|
52
|
+
}
|
|
53
|
+
a { color: inherit; text-underline-offset: 2px; }
|
|
54
|
+
code, pre, .mono { font-family: var(--mono); font-size: var(--fs-code); }
|
|
55
|
+
h1 { font-size: 20px; margin: 0; letter-spacing: -0.01em; }
|
|
56
|
+
h2 { font-size: 13px; margin: 0 0 12px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
57
|
+
h2 .muted { text-transform: none; letter-spacing: 0; font-weight: normal; }
|
|
58
|
+
h2 .badge { text-transform: none; letter-spacing: 0; font-weight: normal; margin-left: 6px; vertical-align: 1px; }
|
|
59
|
+
.muted { color: var(--muted); }
|
|
60
|
+
|
|
61
|
+
.top {
|
|
62
|
+
position: sticky; top: 0; z-index: 1;
|
|
63
|
+
display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
|
|
64
|
+
padding: 10px 24px;
|
|
65
|
+
background: var(--bg);
|
|
66
|
+
border-bottom: 1px solid var(--line);
|
|
67
|
+
}
|
|
68
|
+
.brand { font-weight: 700; text-decoration: none; }
|
|
69
|
+
.brand span { font-weight: 400; color: var(--muted); }
|
|
70
|
+
.top nav { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
71
|
+
.top nav a, .tabs a {
|
|
72
|
+
padding: 4px 10px; border-radius: var(--radius); text-decoration: none; color: var(--muted);
|
|
73
|
+
}
|
|
74
|
+
.top nav a:hover, .tabs a:hover { color: var(--fg); background: var(--soft); }
|
|
75
|
+
.top nav a[aria-current], .tabs a[aria-current] { color: var(--bg); background: var(--fg); }
|
|
76
|
+
.auto { margin-left: auto; color: var(--muted); font-size: 13px; display: flex; gap: 6px; align-items: center; }
|
|
77
|
+
|
|
78
|
+
main { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
79
|
+
.foot {
|
|
80
|
+
max-width: 1200px; margin: 0 auto; padding: 16px 24px 32px;
|
|
81
|
+
display: flex; gap: 16px; flex-wrap: wrap; justify-content: space-between;
|
|
82
|
+
color: var(--muted); font-size: var(--fs-sm); border-top: 1px solid var(--line);
|
|
83
|
+
}
|
|
84
|
+
.head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
85
|
+
.tabs { display: flex; gap: 4px; }
|
|
86
|
+
.note { color: var(--muted); margin: 0 0 16px; }
|
|
87
|
+
.crumbs { color: var(--muted); margin: 0 0 8px; overflow-wrap: anywhere; }
|
|
88
|
+
.path code { overflow-wrap: anywhere; }
|
|
89
|
+
h2.path { text-transform: none; letter-spacing: 0; font-weight: 500; }
|
|
90
|
+
|
|
91
|
+
.cards { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
92
|
+
.card {
|
|
93
|
+
display: flex; flex-direction: column; gap: 4px; padding: 14px 16px;
|
|
94
|
+
border: 1px solid var(--line); border-radius: var(--radius); text-decoration: none;
|
|
95
|
+
}
|
|
96
|
+
.card:hover { border-color: var(--fg); }
|
|
97
|
+
.card-label { color: var(--muted); font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
98
|
+
.card-value { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
99
|
+
.card-alert .card-value { color: var(--fail); }
|
|
100
|
+
|
|
101
|
+
.panel { border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; margin-bottom: 16px; min-width: 0; }
|
|
102
|
+
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
|
103
|
+
.grid > .panel { margin-bottom: 16px; }
|
|
104
|
+
.now { border-top: 1px solid var(--fg); margin-top: 32px; padding-top: 20px; }
|
|
105
|
+
.section-title { margin-bottom: 16px; }
|
|
106
|
+
|
|
107
|
+
.chart { width: 100%; height: auto; display: block; }
|
|
108
|
+
.chart .axis { stroke: var(--line); }
|
|
109
|
+
.chart .tick { fill: var(--muted); font: var(--fs-chart) var(--sans); }
|
|
110
|
+
.chart .count { fill: var(--fg); font: 600 var(--fs-chart) var(--sans); font-variant-numeric: tabular-nums; }
|
|
111
|
+
.bar-ok { fill: var(--chart-ok); }
|
|
112
|
+
.bar-other { fill: var(--chart-other); }
|
|
113
|
+
.bar-fail { fill: var(--chart-fail); }
|
|
114
|
+
.mini { width: 120px; height: 10px; display: block; }
|
|
115
|
+
.legend { color: var(--muted); font-size: var(--fs-sm); margin: 8px 0 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
116
|
+
.key { display: inline-block; width: 10px; height: 10px; margin-left: 8px; }
|
|
117
|
+
.key-ok { background: var(--chart-ok); }
|
|
118
|
+
.key-other { background: var(--chart-other); }
|
|
119
|
+
.key-fail { background: var(--chart-fail); }
|
|
120
|
+
|
|
121
|
+
.scroll { overflow-x: auto; }
|
|
122
|
+
.scroll + .panel { margin-top: 16px; }
|
|
123
|
+
.table { width: 100%; border-collapse: collapse; }
|
|
124
|
+
.table th, .table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
125
|
+
.table thead th { color: var(--muted); font-weight: 500; font-size: var(--fs-sm); white-space: nowrap; }
|
|
126
|
+
.table tbody tr:hover { background: var(--soft); }
|
|
127
|
+
.num { text-align: right !important; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
128
|
+
.bar-col { width: 130px; }
|
|
129
|
+
.runs td { white-space: nowrap; }
|
|
130
|
+
.runs-of-task td { white-space: nowrap; }
|
|
131
|
+
.runs-of-task td.preview { white-space: nowrap; max-width: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
132
|
+
.runs-of-task td.preview a { color: inherit; text-decoration: none; }
|
|
133
|
+
.runs-of-task td.preview code.failed { color: var(--fail); }
|
|
134
|
+
.run-nav { justify-content: space-between; margin: 0 0 12px; }
|
|
135
|
+
.entries th { width: 1%; white-space: nowrap; font-family: var(--mono); font-weight: 500; }
|
|
136
|
+
.entries pre { margin: 0; }
|
|
137
|
+
.files div { white-space: nowrap; }
|
|
138
|
+
|
|
139
|
+
.badge {
|
|
140
|
+
display: inline-block; padding: 0 7px; border-radius: 999px; font-size: var(--fs-xs); line-height: 18px;
|
|
141
|
+
border: 1px solid var(--fg); white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
/* Badges are colored by tone only — the state → tone mapping lives in views/html.ts stateTone */
|
|
144
|
+
.badge-ok { background: var(--ok-bg); color: var(--ok); border-color: var(--ok); }
|
|
145
|
+
.badge-fail { background: var(--fail-bg); color: var(--fail); border-color: var(--fail); font-weight: 600; }
|
|
146
|
+
.badge-muted { border-style: dashed; color: var(--muted); border-color: var(--muted); }
|
|
147
|
+
.badge-progress { border-style: dotted; }
|
|
148
|
+
.tag { font-size: var(--fs-xs); color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 0 5px; white-space: nowrap; }
|
|
149
|
+
|
|
150
|
+
.list { list-style: none; margin: 0; padding: 0; }
|
|
151
|
+
.list li { padding: 8px 0; border-bottom: 1px solid var(--line); }
|
|
152
|
+
.list li:last-child { border-bottom: 0; }
|
|
153
|
+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
154
|
+
.error { font-family: var(--mono); font-size: var(--fs-code); white-space: pre-wrap; overflow-wrap: anywhere; border-left: 2px solid var(--fail); padding-left: 8px; margin-top: 4px; }
|
|
155
|
+
|
|
156
|
+
.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
|
|
157
|
+
.kv dt { color: var(--muted); }
|
|
158
|
+
.kv dd { margin: 0; overflow-wrap: anywhere; }
|
|
159
|
+
|
|
160
|
+
.filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
161
|
+
.filters label { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-sm); color: var(--muted); }
|
|
162
|
+
select, button {
|
|
163
|
+
font: inherit; font-size: 13px; color: var(--fg); background: var(--bg);
|
|
164
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 4px 8px;
|
|
165
|
+
}
|
|
166
|
+
select:focus-visible, button:focus-visible, a:focus-visible, input:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
|
|
167
|
+
/* Small buttons — Copy, Show all, etc. Secondary actions: muted, darker on hover */
|
|
168
|
+
.btn-sm { font-size: var(--fs-xs); padding: 1px 7px; cursor: pointer; color: var(--muted); }
|
|
169
|
+
.btn-sm:hover { color: var(--fg); border-color: var(--fg); }
|
|
170
|
+
.btn-sm.done { color: var(--bg); background: var(--fg); border-color: var(--fg); }
|
|
171
|
+
/* Temporary field for the copy fallback — CSP blocks style attributes, so it sits off-screen */
|
|
172
|
+
.copy-buf { position: fixed; top: 0; left: -9999px; width: 1px; height: 1px; opacity: 0; }
|
|
173
|
+
.cmd { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
174
|
+
.cmd code { background: var(--soft); padding: 1px 6px; border-radius: 4px; }
|
|
175
|
+
|
|
176
|
+
pre.code {
|
|
177
|
+
margin: 0; padding: 12px; background: var(--soft); border-radius: var(--radius);
|
|
178
|
+
overflow: auto; max-height: 70vh; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
179
|
+
}
|
|
180
|
+
pre.source { white-space: pre; tab-size: 4; }
|
|
181
|
+
.pager { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
|
182
|
+
.pager-runs { justify-content: space-between; margin-top: 12px; }
|
|
183
|
+
|
|
184
|
+
.log td { font-family: var(--mono); font-size: var(--fs-code); }
|
|
185
|
+
.log td:first-child, .log td.task, .runs td.task { white-space: nowrap; }
|
|
186
|
+
.log .attrs { color: var(--muted); overflow-wrap: anywhere; }
|
|
187
|
+
.log .msg { overflow-wrap: anywhere; }
|
|
188
|
+
.log .clamp {
|
|
189
|
+
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3;
|
|
190
|
+
overflow: hidden; overflow-wrap: anywhere;
|
|
191
|
+
}
|
|
192
|
+
.log .clamp.open { display: block; -webkit-line-clamp: unset; line-clamp: none; overflow: visible; }
|
|
193
|
+
.log .more { margin-top: 4px; }
|
|
194
|
+
.lvl-error .level { color: var(--fail); font-weight: 700; }
|
|
195
|
+
.lvl-warn .level { color: var(--warn); font-weight: 700; }
|
|
196
|
+
.lvl-debug { color: var(--muted); }
|
|
197
|
+
|
|
198
|
+
.empty { text-align: center; padding: 64px 16px; }
|
|
199
|
+
/* Auto-refresh failure notice — tells that the view is frozen */
|
|
200
|
+
.stale {
|
|
201
|
+
width: calc(100% - 48px); max-width: 1152px; margin: 16px auto 0; padding: 8px 12px; border-radius: var(--radius);
|
|
202
|
+
background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn); font-size: var(--fs-sm);
|
|
203
|
+
}
|
|
204
|
+
.stale[hidden] { display: none; }
|
|
205
|
+
|
|
206
|
+
@media (max-width: 800px) {
|
|
207
|
+
.top { padding: 10px 16px; gap: 10px; }
|
|
208
|
+
.auto { margin-left: 0; }
|
|
209
|
+
main { padding: 16px; }
|
|
210
|
+
.foot { padding: 16px; }
|
|
211
|
+
.stale { width: calc(100% - 32px); }
|
|
212
|
+
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
213
|
+
.grid { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
214
|
+
.kv { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
|
|
215
|
+
.kv dt { margin-top: 8px; }
|
|
216
|
+
.bar-col { display: none; }
|
|
217
|
+
/* In Logs the message is the content — as a table on narrow screens only the Message column shrinks and wraps per word.
|
|
218
|
+
Stack rows instead: time, level, task on one line, the message full width below */
|
|
219
|
+
.log thead { display: none; }
|
|
220
|
+
.log, .log tbody { display: block; }
|
|
221
|
+
.log tr { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; padding: 8px 0; border-bottom: 1px solid var(--line); }
|
|
222
|
+
.log td { display: block; padding: 0; border-bottom: 0; }
|
|
223
|
+
.log td:empty { display: none; }
|
|
224
|
+
.log td.msg-cell { flex-basis: 100%; }
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
export const APP_JS = `
|
|
228
|
+
(function () {
|
|
229
|
+
'use strict';
|
|
230
|
+
var KEY = 'tm-dashboard-auto-refresh';
|
|
231
|
+
// Logs find — filters fetched lines in the browser only. The value survives auto refresh replacing <main>
|
|
232
|
+
var findText = '';
|
|
233
|
+
|
|
234
|
+
function applyFind(root) {
|
|
235
|
+
var q = findText.trim().toLowerCase();
|
|
236
|
+
var rows = root.querySelectorAll('table.log tbody tr');
|
|
237
|
+
var shown = 0;
|
|
238
|
+
rows.forEach(function (row) {
|
|
239
|
+
var cell = row.querySelector('.msg-cell .clamp') || row;
|
|
240
|
+
var hit = !q || cell.textContent.toLowerCase().indexOf(q) !== -1;
|
|
241
|
+
row.hidden = !hit;
|
|
242
|
+
if (hit) shown++;
|
|
243
|
+
});
|
|
244
|
+
var count = root.querySelector('#log-find-count');
|
|
245
|
+
if (count) {
|
|
246
|
+
count.hidden = !q;
|
|
247
|
+
count.textContent = shown + ' of ' + rows.length + ' lines match';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function bind(root) {
|
|
252
|
+
var find = root.querySelector('#log-find');
|
|
253
|
+
if (find) {
|
|
254
|
+
find.value = findText;
|
|
255
|
+
find.addEventListener('input', function () {
|
|
256
|
+
findText = find.value;
|
|
257
|
+
applyFind(root);
|
|
258
|
+
});
|
|
259
|
+
// Keep Enter from submitting the form and reloading — find is never sent to the server
|
|
260
|
+
find.addEventListener('keydown', function (e) {
|
|
261
|
+
if (e.key === 'Enter') e.preventDefault();
|
|
262
|
+
});
|
|
263
|
+
applyFind(root);
|
|
264
|
+
}
|
|
265
|
+
root.querySelectorAll('form[data-autosubmit] select').forEach(function (el) {
|
|
266
|
+
el.addEventListener('change', function () { el.form.submit(); });
|
|
267
|
+
});
|
|
268
|
+
// Long log messages — enable the expand button only on cells overflowing 3 lines
|
|
269
|
+
root.querySelectorAll('.log .clamp').forEach(function (box) {
|
|
270
|
+
var more = box.nextElementSibling;
|
|
271
|
+
if (!more || !more.classList.contains('more')) return;
|
|
272
|
+
if (box.scrollHeight > box.clientHeight + 1) more.hidden = false;
|
|
273
|
+
more.addEventListener('click', function () {
|
|
274
|
+
var open = box.classList.toggle('open');
|
|
275
|
+
more.textContent = open ? 'Show less' : 'Show all';
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
root.querySelectorAll('button[data-copy]').forEach(function (btn) {
|
|
279
|
+
// Read the original label once — reading on click would lock in "Copied" on a second click while it shows
|
|
280
|
+
var label = btn.textContent;
|
|
281
|
+
var timer = null;
|
|
282
|
+
var show = function (msg, ms) {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
btn.textContent = msg;
|
|
285
|
+
btn.classList.add('done');
|
|
286
|
+
timer = setTimeout(function () { btn.textContent = label; btn.classList.remove('done'); }, ms);
|
|
287
|
+
};
|
|
288
|
+
btn.addEventListener('click', function () {
|
|
289
|
+
var text = btn.getAttribute('data-copy');
|
|
290
|
+
// navigator.clipboard needs a secure context (HTTPS, localhost) — absent when opened via a LAN address (http://192.168.x.x)
|
|
291
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
292
|
+
navigator.clipboard.writeText(text).then(
|
|
293
|
+
function () { show('Copied', 1200); },
|
|
294
|
+
function () { fallbackCopy(btn, text, show); }
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
fallbackCopy(btn, text, show);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Clipboard API missing or denied — select a temporary field and execCommand('copy'). If that fails too, select the text
|
|
304
|
+
// before the button for the user to copy. Auto refresh keeps the selection (it skips while text is selected)
|
|
305
|
+
function fallbackCopy(btn, text, show) {
|
|
306
|
+
var active = document.activeElement;
|
|
307
|
+
var box = document.createElement('textarea');
|
|
308
|
+
box.className = 'copy-buf';
|
|
309
|
+
box.value = text;
|
|
310
|
+
box.setAttribute('readonly', '');
|
|
311
|
+
document.body.appendChild(box);
|
|
312
|
+
box.select();
|
|
313
|
+
box.setSelectionRange(0, text.length);
|
|
314
|
+
var ok = false;
|
|
315
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
316
|
+
document.body.removeChild(box);
|
|
317
|
+
if (active && active.focus) active.focus();
|
|
318
|
+
if (ok) { show('Copied', 1200); return; }
|
|
319
|
+
var code = btn.previousElementSibling;
|
|
320
|
+
var sel = window.getSelection && window.getSelection();
|
|
321
|
+
if (sel && code && code.tagName === 'CODE') {
|
|
322
|
+
var range = document.createRange();
|
|
323
|
+
range.selectNodeContents(code);
|
|
324
|
+
sel.removeAllRanges();
|
|
325
|
+
sel.addRange(range);
|
|
326
|
+
show('Selected — copy it', 2500);
|
|
327
|
+
} else {
|
|
328
|
+
show('Copy failed', 2500);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Viewed remotely (another device), the terminal is the host's — say so explicitly
|
|
333
|
+
function onHost() {
|
|
334
|
+
var host = document.body.getAttribute('data-host');
|
|
335
|
+
if (!document.body.hasAttribute('data-remote')) return '';
|
|
336
|
+
return host ? ' on host ' + host : ' on the computer running the dashboard';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Auto refresh failed — don't hide that the view is frozen at the last success (people judge failure counts from it)
|
|
340
|
+
var lastOk = new Date();
|
|
341
|
+
function stale(text) {
|
|
342
|
+
var box = document.getElementById('stale');
|
|
343
|
+
if (!box) return;
|
|
344
|
+
// The UI is English only — fixed HH:MM:SS instead of the browser locale format (e.g. 4:27 PM)
|
|
345
|
+
box.textContent = text + ' Showing data from ' + lastOk.toTimeString().slice(0, 8) + '.';
|
|
346
|
+
box.hidden = false;
|
|
347
|
+
}
|
|
348
|
+
function fresh() {
|
|
349
|
+
lastOk = new Date();
|
|
350
|
+
var box = document.getElementById('stale');
|
|
351
|
+
if (box) box.hidden = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function autoOn() {
|
|
355
|
+
try { return localStorage.getItem(KEY) !== 'off'; } catch (e) { return true; }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
359
|
+
bind(document);
|
|
360
|
+
var main = document.getElementById('main');
|
|
361
|
+
var toggle = document.getElementById('auto-refresh');
|
|
362
|
+
if (!main || !main.hasAttribute('data-refresh')) return;
|
|
363
|
+
if (toggle) {
|
|
364
|
+
toggle.checked = autoOn();
|
|
365
|
+
toggle.addEventListener('change', function () {
|
|
366
|
+
try { localStorage.setItem(KEY, toggle.checked ? 'on' : 'off'); } catch (e) {}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
var ms = Number(main.getAttribute('data-refresh')) || 5000;
|
|
370
|
+
var busy = false;
|
|
371
|
+
var timer = setInterval(function () {
|
|
372
|
+
if (busy || document.hidden || (toggle && !toggle.checked)) return;
|
|
373
|
+
var sel = window.getSelection && window.getSelection();
|
|
374
|
+
if (sel && sel.type === 'Range') return;
|
|
375
|
+
var active = document.activeElement;
|
|
376
|
+
if (active && (active.tagName === 'SELECT' || active.id === 'log-find')) return;
|
|
377
|
+
// Don't refresh while a message is expanded — it would collapse and shift
|
|
378
|
+
if (main.querySelector('.clamp.open')) return;
|
|
379
|
+
busy = true;
|
|
380
|
+
fetch(location.href, { credentials: 'same-origin', cache: 'no-store' })
|
|
381
|
+
.then(function (res) {
|
|
382
|
+
if (res.status === 401) {
|
|
383
|
+
// This link is dead — retries only get 401. Stop (in LAN mode each one would log a rejection)
|
|
384
|
+
clearInterval(timer);
|
|
385
|
+
stale('This dashboard run has ended — open the new link printed by 3minapi dashboard (or 3minapi dashboard url' + onHost() + ').');
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (!res.ok) {
|
|
389
|
+
stale('Could not refresh (HTTP ' + res.status + ') — reload the page to see the error; details are in the terminal' + onHost() + '.');
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
return res.text();
|
|
393
|
+
})
|
|
394
|
+
.then(function (text) {
|
|
395
|
+
if (!text) return;
|
|
396
|
+
var doc = new DOMParser().parseFromString(text, 'text/html');
|
|
397
|
+
var next = doc.getElementById('main');
|
|
398
|
+
if (!next) return;
|
|
399
|
+
// Same-origin document escaped by the server — move its nodes over (no re-parse from a string)
|
|
400
|
+
main.replaceChildren.apply(main, Array.from(next.childNodes));
|
|
401
|
+
bind(main);
|
|
402
|
+
fresh();
|
|
403
|
+
})
|
|
404
|
+
.catch(function () {
|
|
405
|
+
stale('The dashboard is not running (stopped with Ctrl+C or 3minapi dashboard stop?) — start it again' + onHost() + ' to see current data.');
|
|
406
|
+
})
|
|
407
|
+
.then(function () { busy = false; });
|
|
408
|
+
}, ms);
|
|
409
|
+
});
|
|
410
|
+
})();
|
|
411
|
+
`;
|