hammerkit 2.0.0-next.4 → 2.0.0-next.6
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/dist/cache/explain.js +7 -5
- package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
- package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
- package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
- package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/logo.png +0 -0
- package/dist/executer/docker-task.js +3 -0
- package/dist/executer/execute-work-task.js +3 -1
- package/dist/executer/local-task.js +3 -0
- package/dist/executer/scheduler/enqueue-next.js +6 -1
- package/dist/executer/scheduler/state-key.js +71 -34
- package/dist/executer/watch-loop.js +18 -5
- package/dist/file/get-file-context.js +5 -1
- package/dist/kubernetes/claim-layout.js +10 -1
- package/dist/kubernetes/ensure-persistent-data.js +45 -5
- package/dist/kubernetes/follow-pod-logs.js +124 -27
- package/dist/kubernetes/network-policy.js +4 -4
- package/dist/kubernetes/restore-kubernetes-data.js +3 -1
- package/dist/kubernetes/store-kubernetes-data.js +3 -1
- package/dist/kubernetes/volumes.js +6 -6
- package/dist/optimizer/calculate-checksum.js +46 -3
- package/dist/optimizer/get-work-cache-stats.js +36 -24
- package/dist/planner/work-item-executions.js +27 -0
- package/dist/planner/work-runtime-kubernetes.js +33 -38
- package/dist/remote/remote-work-commands.js +11 -2
- package/dist/server/capacity.js +63 -0
- package/dist/server/dashboard.js +27 -9
- package/dist/server/endpoints.js +10 -1
- package/dist/server/executor.js +10 -0
- package/dist/server/oauth.js +16 -1
- package/dist/server/overview.js +80 -11
- package/dist/server/plan.js +16 -3
- package/dist/server/policy.js +9 -3
- package/dist/server/run-worker.js +37 -4
- package/dist/server/runs.js +74 -6
- package/dist/server/server-config.js +4 -1
- package/dist/server/server.js +45 -10
- package/dist/server/service-runs.js +8 -1
- package/dist/server/timings.js +23 -3
- package/dist/utils/limit.js +30 -0
- package/dist/utils/web-links.js +34 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-CjnKm5-3.css +0 -2
- package/dist/dashboard/assets/index-zPXa4klZ.js +0 -13
- package/dist/dashboard/favicon.svg +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.workerMain = exports.readInput = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
3
|
+
exports.workerMain = exports.readInput = exports.taskCommands = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const cli_1 = require("../cli");
|
|
@@ -10,6 +10,7 @@ const schema_parser_1 = require("../schema/schema-parser");
|
|
|
10
10
|
const reference_parser_1 = require("../schema/reference-parser");
|
|
11
11
|
const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
12
12
|
const work_item_status_1 = require("../planner/work-item-status");
|
|
13
|
+
const work_item_executions_1 = require("../planner/work-item-executions");
|
|
13
14
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
14
15
|
const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
|
|
15
16
|
const hosted_volumes_1 = require("../planner/utils/hosted-volumes");
|
|
@@ -218,6 +219,7 @@ up) {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
}
|
|
222
|
+
let planned = false;
|
|
221
223
|
for (const { name, scope } of scopes) {
|
|
222
224
|
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
|
|
223
225
|
directory: input.directory,
|
|
@@ -234,6 +236,16 @@ up) {
|
|
|
234
236
|
if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
|
|
235
237
|
throw new Error(`no tasks found for ${name}`);
|
|
236
238
|
}
|
|
239
|
+
if (!planned) {
|
|
240
|
+
// from the start of this process, which the executor started for the run
|
|
241
|
+
planned = true;
|
|
242
|
+
emit({
|
|
243
|
+
type: 'phase',
|
|
244
|
+
phase: 'plan',
|
|
245
|
+
startedAt: new Date(performance.timeOrigin).toISOString(),
|
|
246
|
+
finishedAt: new Date().toISOString(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
237
249
|
const execution = input.up
|
|
238
250
|
? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
|
|
239
251
|
: cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
|
|
@@ -392,6 +404,19 @@ function taskCauses(causes, redact) {
|
|
|
392
404
|
: {};
|
|
393
405
|
}
|
|
394
406
|
exports.taskCauses = taskCauses;
|
|
407
|
+
// When the commands of a task that is over ran, as its runtime saw them.
|
|
408
|
+
function taskCommands(task) {
|
|
409
|
+
const executions = (0, work_item_executions_1.readExecutions)(task);
|
|
410
|
+
return executions.length
|
|
411
|
+
? {
|
|
412
|
+
commands: executions.slice(0, runs_1.MAX_COMMANDS).map((e) => ({
|
|
413
|
+
startedAt: e.startedAt.toISOString(),
|
|
414
|
+
finishedAt: e.finishedAt.toISOString(),
|
|
415
|
+
})),
|
|
416
|
+
}
|
|
417
|
+
: {};
|
|
418
|
+
}
|
|
419
|
+
exports.taskCommands = taskCommands;
|
|
395
420
|
// A task event when a task has moved on since the last look, with what its result needs
|
|
396
421
|
// once it is over.
|
|
397
422
|
function reportTasks(emit, workTree, seen,
|
|
@@ -414,17 +439,25 @@ redact) {
|
|
|
414
439
|
state: status,
|
|
415
440
|
...(status === 'skipped' ? {} : { durationMs: state.duration }),
|
|
416
441
|
...taskCauses(state.missCauses, redact),
|
|
442
|
+
...(status === 'succeeded' ? taskCommands(task.data) : {}),
|
|
417
443
|
};
|
|
418
444
|
}
|
|
419
445
|
else if (state.type === 'crash') {
|
|
420
|
-
event = {
|
|
446
|
+
event = {
|
|
447
|
+
type: 'task',
|
|
448
|
+
task: task.name,
|
|
449
|
+
state: 'failed',
|
|
450
|
+
exitCode: state.exitCode,
|
|
451
|
+
...since(),
|
|
452
|
+
...taskCommands(task.data),
|
|
453
|
+
};
|
|
421
454
|
}
|
|
422
455
|
else if (state.type === 'error') {
|
|
423
456
|
const error = redact(state.errorMessage).slice(0, runs_1.MAX_TASK_ERROR);
|
|
424
|
-
event = { type: 'task', task: task.name, state: 'failed', error, ...since() };
|
|
457
|
+
event = { type: 'task', task: task.name, state: 'failed', error, ...since(), ...taskCommands(task.data) };
|
|
425
458
|
}
|
|
426
459
|
else if (state.type === 'canceled') {
|
|
427
|
-
event = { type: 'task', task: task.name, state: 'cancelled' };
|
|
460
|
+
event = { type: 'task', task: task.name, state: 'cancelled', ...taskCommands(task.data) };
|
|
428
461
|
}
|
|
429
462
|
if (event && before?.status !== event.state) {
|
|
430
463
|
seen.set(task.name, { status: event.state, started });
|
package/dist/server/runs.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.unavailableExecutor = exports.RunRegistry = exports.MAX_TASK_ERROR = exports.MAX_CAUSES = exports.SANDBOX_ID_PATTERN = exports.RUN_ID_PATTERN = exports.QuotaError = exports.RunError = exports.isFinal = void 0;
|
|
3
|
+
exports.unavailableExecutor = exports.RunRegistry = exports.MAX_COMMANDS = exports.MAX_TASK_ERROR = exports.MAX_CAUSES = exports.SANDBOX_ID_PATTERN = exports.RUN_ID_PATTERN = exports.QuotaError = exports.RunError = exports.RUN_PHASES = exports.isFinal = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
|
+
const oauth_1 = require("./oauth");
|
|
5
6
|
const run_logs_1 = require("./run-logs");
|
|
6
7
|
const units_1 = require("../utils/units");
|
|
7
8
|
const FINAL_STATES = ['succeeded', 'failed', 'cancelled', 'rejected'];
|
|
@@ -9,6 +10,11 @@ function isFinal(state) {
|
|
|
9
10
|
return FINAL_STATES.includes(state);
|
|
10
11
|
}
|
|
11
12
|
exports.isFinal = isFinal;
|
|
13
|
+
// What an executor reports while it works. `seq` and `time` are added by the registry.
|
|
14
|
+
// The steps of a run before its tasks: `source` gets its files (a checkout of the commit, or
|
|
15
|
+
// those of the sandbox), `namespace` prepares its place on the cluster, `plan` starts the
|
|
16
|
+
// process of the run and reads its build files.
|
|
17
|
+
exports.RUN_PHASES = ['source', 'namespace', 'plan'];
|
|
12
18
|
// The server can not take the run now; `status` is what the caller is told
|
|
13
19
|
class RunError extends Error {
|
|
14
20
|
constructor() {
|
|
@@ -60,6 +66,11 @@ const TAIL_LINE_LENGTH = 1000;
|
|
|
60
66
|
// does not can still not make a result of any size.
|
|
61
67
|
exports.MAX_CAUSES = 20;
|
|
62
68
|
exports.MAX_TASK_ERROR = 2 * 1024;
|
|
69
|
+
// the command times a task result keeps
|
|
70
|
+
exports.MAX_COMMANDS = 50;
|
|
71
|
+
function isTimestamp(value) {
|
|
72
|
+
return typeof value === 'string' && value.length <= 40 && !Number.isNaN(Date.parse(value));
|
|
73
|
+
}
|
|
63
74
|
function bounded(event) {
|
|
64
75
|
const causes = event.causes;
|
|
65
76
|
return {
|
|
@@ -73,12 +84,20 @@ function bounded(event) {
|
|
|
73
84
|
}
|
|
74
85
|
: {}),
|
|
75
86
|
...(event.error !== undefined ? { error: event.error.slice(0, exports.MAX_TASK_ERROR) } : {}),
|
|
87
|
+
...(event.commands
|
|
88
|
+
? {
|
|
89
|
+
commands: event.commands
|
|
90
|
+
.filter((c) => isTimestamp(c?.startedAt) && isTimestamp(c?.finishedAt))
|
|
91
|
+
.slice(0, exports.MAX_COMMANDS)
|
|
92
|
+
.map(({ startedAt, finishedAt }) => ({ startedAt, finishedAt })),
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
76
95
|
};
|
|
77
96
|
}
|
|
78
97
|
// what a task event weighs against the log limits of a run
|
|
79
98
|
function taskEventSize(event) {
|
|
80
99
|
const causes = (event.causes ?? []).reduce((sum, cause) => sum + Buffer.byteLength(cause) + 4, 0);
|
|
81
|
-
return 64 + causes + Buffer.byteLength(event.error ?? '');
|
|
100
|
+
return 64 + causes + Buffer.byteLength(event.error ?? '') + (event.commands?.length ?? 0) * 64;
|
|
82
101
|
}
|
|
83
102
|
// Where runs are kept while the server is up. Everything is scoped to the
|
|
84
103
|
// caller that submitted the run: the same id asked for by someone else is
|
|
@@ -94,6 +113,8 @@ class RunRegistry {
|
|
|
94
113
|
this.waiters = new Set();
|
|
95
114
|
// the moves of service runs that execute beside their services
|
|
96
115
|
this.moving = new Set();
|
|
116
|
+
// the records of evicted runs still being removed from the store, waited for by close
|
|
117
|
+
this.removing = new Set();
|
|
97
118
|
this.maxConcurrent = options.maxConcurrent ?? 4;
|
|
98
119
|
this.keep = options.keep ?? 200;
|
|
99
120
|
this.maxEvents = options.maxEvents ?? 5000;
|
|
@@ -192,12 +213,14 @@ class RunRegistry {
|
|
|
192
213
|
this.checkUpLimits(owner, spec.up.name);
|
|
193
214
|
this.checkHosts(owner, spec.up);
|
|
194
215
|
}
|
|
216
|
+
const name = (0, oauth_1.displayName)(owner);
|
|
195
217
|
const run = {
|
|
196
218
|
...spec,
|
|
197
219
|
id: `run_${(0, crypto_1.randomBytes)(12).toString('base64url')}`,
|
|
198
220
|
owner: {
|
|
199
221
|
iss: owner.iss,
|
|
200
222
|
sub: owner.sub,
|
|
223
|
+
...(name ? { name } : {}),
|
|
201
224
|
...(spec.up?.sleepTtl ? { groups: [...(owner.groups ?? [])], claims: { ...(owner.claims ?? {}) } } : {}),
|
|
202
225
|
},
|
|
203
226
|
state: waiting ? 'awaiting-approval' : 'queued',
|
|
@@ -306,8 +329,10 @@ class RunRegistry {
|
|
|
306
329
|
}
|
|
307
330
|
// A queued run is cancelled at once; a running one when its executor stops. A service run
|
|
308
331
|
// that is up stops its services, and says who stopped it.
|
|
309
|
-
|
|
310
|
-
|
|
332
|
+
// Stops a run for its owner, who decides about it, and, a service run, whoever `mayStop` says.
|
|
333
|
+
cancel(owner, id, canUse, mayStop) {
|
|
334
|
+
const other = this.entries.get(id);
|
|
335
|
+
const entry = this.find(owner, id, canUse) ?? (other?.run.up && mayStop?.(this.copy(other.run)) ? other : undefined);
|
|
311
336
|
if (!entry) {
|
|
312
337
|
return undefined;
|
|
313
338
|
}
|
|
@@ -336,6 +361,24 @@ class RunRegistry {
|
|
|
336
361
|
this.expireLater(entry);
|
|
337
362
|
return { status: 'ok', run: this.copy(entry.run) };
|
|
338
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Wakes a service run that sleeps, for its owner or who `mayOpen` it, as a request to its
|
|
366
|
+
* endpoints would (visit): with the same check of its owner (`mayWake`) and the same places.
|
|
367
|
+
* Answers why it did not wake when it could not.
|
|
368
|
+
*/
|
|
369
|
+
wake(caller, id, mayOpen, mayWake) {
|
|
370
|
+
const entry = this.entries.get(id);
|
|
371
|
+
if (!entry?.run.up || !(this.owns(caller, entry) || mayOpen?.(this.copy(entry.run)))) {
|
|
372
|
+
return { status: 'not-found' };
|
|
373
|
+
}
|
|
374
|
+
if (entry.run.state !== 'asleep') {
|
|
375
|
+
return { status: 'not-asleep', state: entry.run.state };
|
|
376
|
+
}
|
|
377
|
+
const refused = this.visit(id, mayWake);
|
|
378
|
+
return refused
|
|
379
|
+
? { status: 'refused', reason: refused, run: this.copy(entry.run) }
|
|
380
|
+
: { status: 'ok', run: this.copy(entry.run) };
|
|
381
|
+
}
|
|
339
382
|
/**
|
|
340
383
|
* The service run an endpoint host belongs to: the newest that is up, so one that replaces
|
|
341
384
|
* a run of the same name takes its hosts over as soon as it is up; else the newest that
|
|
@@ -575,8 +618,10 @@ class RunRegistry {
|
|
|
575
618
|
[...this.entries.values()].some((entry) => entry.run.state === 'running' || entry.run.state === 'up')) {
|
|
576
619
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
577
620
|
}
|
|
578
|
-
// the runs that were stopped are written down before the server is gone
|
|
621
|
+
// the runs that were stopped are written down before the server is gone, and the records
|
|
622
|
+
// of evicted runs are gone from the store
|
|
579
623
|
await Promise.all([...this.entries.values()].map((entry) => entry.saving));
|
|
624
|
+
await Promise.all([...this.removing]);
|
|
580
625
|
}
|
|
581
626
|
find(owner, id, canUse) {
|
|
582
627
|
const entry = this.entries.get(id);
|
|
@@ -612,6 +657,7 @@ class RunRegistry {
|
|
|
612
657
|
accounts: [...run.accounts],
|
|
613
658
|
...(run.approvals ? { approvals: JSON.parse(JSON.stringify(run.approvals)) } : {}),
|
|
614
659
|
...(run.results ? { results: JSON.parse(JSON.stringify(run.results)) } : {}),
|
|
660
|
+
...(run.phases ? { phases: run.phases.map((phase) => ({ ...phase })) } : {}),
|
|
615
661
|
...(run.moves ? { moves: run.moves.map((move) => ({ ...move })) } : {}),
|
|
616
662
|
...(run.moving ? { moving: { ...run.moving } } : {}),
|
|
617
663
|
};
|
|
@@ -1424,6 +1470,10 @@ class RunRegistry {
|
|
|
1424
1470
|
}
|
|
1425
1471
|
return;
|
|
1426
1472
|
}
|
|
1473
|
+
if (recorded.type === 'phase') {
|
|
1474
|
+
this.keepPhase(entry, recorded);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1427
1477
|
const event = recorded.type === 'task' ? bounded(recorded) : recorded;
|
|
1428
1478
|
if (event.type === 'log') {
|
|
1429
1479
|
this.keepTail(entry, event.task, event.text);
|
|
@@ -1446,6 +1496,19 @@ class RunRegistry {
|
|
|
1446
1496
|
}
|
|
1447
1497
|
this.push(entry, event);
|
|
1448
1498
|
}
|
|
1499
|
+
// once each, from the executor: a phase it reports again (a service run that wakes) is the
|
|
1500
|
+
// newest
|
|
1501
|
+
keepPhase(entry, event) {
|
|
1502
|
+
if (!exports.RUN_PHASES.includes(event.phase) ||
|
|
1503
|
+
!isTimestamp(event.startedAt) ||
|
|
1504
|
+
!isTimestamp(event.finishedAt) ||
|
|
1505
|
+
Date.parse(event.finishedAt) < Date.parse(event.startedAt)) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const phases = (entry.run.phases ?? []).filter((phase) => phase.phase !== event.phase);
|
|
1509
|
+
phases.push({ phase: event.phase, startedAt: event.startedAt, finishedAt: event.finishedAt });
|
|
1510
|
+
entry.run.phases = phases.sort((a, b) => exports.RUN_PHASES.indexOf(a.phase) - exports.RUN_PHASES.indexOf(b.phase));
|
|
1511
|
+
}
|
|
1449
1512
|
keepTail(entry, task, text) {
|
|
1450
1513
|
let tail = entry.tails.get(task);
|
|
1451
1514
|
if (!tail) {
|
|
@@ -1487,6 +1550,7 @@ class RunRegistry {
|
|
|
1487
1550
|
...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}),
|
|
1488
1551
|
...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
|
|
1489
1552
|
...(event.error ? { error: event.error } : {}),
|
|
1553
|
+
...(event.commands?.length ? { commands: event.commands } : {}),
|
|
1490
1554
|
...(event.state === 'cached'
|
|
1491
1555
|
? { cache: { hit: true } }
|
|
1492
1556
|
: event.state === 'succeeded' || event.state === 'failed'
|
|
@@ -1524,7 +1588,11 @@ class RunRegistry {
|
|
|
1524
1588
|
const finished = [...this.entries.values()].filter((entry) => FINAL_STATES.includes(entry.run.state));
|
|
1525
1589
|
for (const entry of finished.slice(0, Math.max(0, finished.length - this.keep))) {
|
|
1526
1590
|
this.entries.delete(entry.run.id);
|
|
1527
|
-
|
|
1591
|
+
const removal = entry.saving
|
|
1592
|
+
.then(() => this.options.store?.remove(entry.run.id))
|
|
1593
|
+
.catch(() => undefined)
|
|
1594
|
+
.finally(() => this.removing.delete(removal));
|
|
1595
|
+
this.removing.add(removal);
|
|
1528
1596
|
}
|
|
1529
1597
|
}
|
|
1530
1598
|
}
|
|
@@ -45,7 +45,10 @@ exports.issuerSchema = zod_1.z
|
|
|
45
45
|
scope: zod_1.z.string().min(1).default('openid'),
|
|
46
46
|
})
|
|
47
47
|
.strict();
|
|
48
|
-
|
|
48
|
+
// `name`: how pages name a subject whose tokens name nobody, such as a service account's
|
|
49
|
+
const subjectSchema = zod_1.z
|
|
50
|
+
.object({ iss: zod_1.z.string().min(1), sub: zod_1.z.string().min(1), name: zod_1.z.string().trim().min(1).max(100).optional() })
|
|
51
|
+
.strict();
|
|
49
52
|
const groupSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), name: zod_1.z.string().min(1) }).strict();
|
|
50
53
|
const claimsSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), match: zod_1.z.record(zod_1.z.string()) }).strict();
|
|
51
54
|
// Who: the person or client (`subject`), everyone in a group of an issuer
|
package/dist/server/server.js
CHANGED
|
@@ -109,7 +109,7 @@ function streamEvents(registry, config, principal, id, req, res, url) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res, url) {
|
|
112
|
-
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs|extend))?)?$/.exec(url.pathname);
|
|
112
|
+
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs|extend|wake))?)?$/.exec(url.pathname);
|
|
113
113
|
if (!match) {
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
@@ -129,7 +129,7 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
129
129
|
}
|
|
130
130
|
const runs = filter.scope === 'mine' ? registry.list(principal) : registry.all();
|
|
131
131
|
(0, http_2.send)(res, 200, {
|
|
132
|
-
runs: (0, overview_1.filterRuns)(runs, viewer, filter).map((run) => listRun(registry, viewer, run)),
|
|
132
|
+
runs: (0, overview_1.filterRuns)(runs, viewer, filter).map((run) => listRun(registry, definitions, viewer, run)),
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
else if (method === 'POST') {
|
|
@@ -148,7 +148,8 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
148
148
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
149
149
|
return true;
|
|
150
150
|
}
|
|
151
|
-
const
|
|
151
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
152
|
+
const run = registry.cancel(principal, id, accountsOf(config, principal), (up) => (0, service_runs_1.mayPublishEndpoints)(grant, up));
|
|
152
153
|
log(`cancel iss=${principal.iss} sub=${principal.sub} run=${id} found=${Boolean(run)}`);
|
|
153
154
|
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
154
155
|
}
|
|
@@ -159,6 +160,28 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
159
160
|
}
|
|
160
161
|
await extendRun(config, registry, log, principal, id, req, res);
|
|
161
162
|
}
|
|
163
|
+
else if (action === 'wake') {
|
|
164
|
+
if (method !== 'POST') {
|
|
165
|
+
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// as a request to one of its endpoints would, with the same check of its owner
|
|
169
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
170
|
+
const result = registry.wake(principal, id, (run) => (0, service_runs_1.mayOpenEndpoints)(grant, run), (run) => refuseWake(config, repos, run));
|
|
171
|
+
log(`wake iss=${principal.iss} sub=${principal.sub} run=${id} result=${result.status}`);
|
|
172
|
+
if (result.status === 'not-found') {
|
|
173
|
+
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
174
|
+
}
|
|
175
|
+
else if (result.status === 'not-asleep') {
|
|
176
|
+
(0, http_2.send)(res, 409, { error: `the run is ${result.state}, only a service run that sleeps wakes` });
|
|
177
|
+
}
|
|
178
|
+
else if (result.status === 'refused') {
|
|
179
|
+
(0, http_2.send)(res, 409, { error: result.reason });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
(0, http_2.send)(res, 200, toRunResponse(result.run));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
162
185
|
else if (method !== 'GET') {
|
|
163
186
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
164
187
|
}
|
|
@@ -205,7 +228,7 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
205
228
|
const may = run && (0, overview_1.runActions)(viewer, run);
|
|
206
229
|
run
|
|
207
230
|
? (0, http_2.send)(res, 200, {
|
|
208
|
-
...toRunResponse(run, (0, overview_1.queuePlace)(registry, viewer, run), !(0, overview_1.owns)(viewer, run)),
|
|
231
|
+
...toRunResponse(run, (0, overview_1.queuePlace)(registry, viewer, run), !(0, overview_1.owns)(viewer, run), viewer.nameOf),
|
|
209
232
|
...(may ? { may } : {}),
|
|
210
233
|
})
|
|
211
234
|
: (0, http_2.send)(res, 404, { error: 'not found' });
|
|
@@ -299,18 +322,21 @@ function runsTheSame(run, next) {
|
|
|
299
322
|
}
|
|
300
323
|
exports.runsTheSame = runsTheSame;
|
|
301
324
|
// A run whole, for its owner and who may read it; who is not its owner is told whose it is.
|
|
302
|
-
function toRunResponse(run, queue, named = false) {
|
|
325
|
+
function toRunResponse(run, queue, named = false, nameOf) {
|
|
303
326
|
const { owner, ...visible } = run;
|
|
327
|
+
const name = owner.name ?? nameOf?.(owner);
|
|
304
328
|
return {
|
|
305
329
|
...visible,
|
|
306
|
-
...(named ? { owner: { iss: owner.iss, sub: owner.sub } } : {}),
|
|
330
|
+
...(named ? { owner: { iss: owner.iss, sub: owner.sub, ...(name ? { name } : {}) } } : {}),
|
|
307
331
|
results: run.results ?? {},
|
|
308
332
|
...(queue ? { queue } : {}),
|
|
309
333
|
};
|
|
310
334
|
}
|
|
311
|
-
function listRun(registry, viewer, run) {
|
|
312
|
-
return (0, overview_1.toListedRun)(viewer, run, (0, overview_1.queuePlace)(registry, viewer, run));
|
|
335
|
+
function listRun(registry, definitions, viewer, run) {
|
|
336
|
+
return (0, overview_1.toListedRun)(viewer, run, (0, overview_1.queuePlace)(registry, viewer, run), entryOf(definitions));
|
|
313
337
|
}
|
|
338
|
+
// where in its repository the build file of a version is
|
|
339
|
+
const entryOf = (definitions) => (definition) => definitions.find(definition)?.entry;
|
|
314
340
|
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res) {
|
|
315
341
|
const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
|
|
316
342
|
if (!parsed.success) {
|
|
@@ -518,8 +544,11 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
518
544
|
: undefined;
|
|
519
545
|
// a run that may sleep keeps what its owner is entitled by, for the check of a wake
|
|
520
546
|
const claimNames = new Set(config.entitlements.flatMap((entitlement) => Object.keys(entitlement.claims?.match ?? {})));
|
|
547
|
+
const name = (0, oauth_1.displayName)(principal);
|
|
521
548
|
const owner = {
|
|
522
549
|
...principal,
|
|
550
|
+
// how the pages name them, which the claims kept no longer tell
|
|
551
|
+
...(name ? { name } : {}),
|
|
523
552
|
claims: Object.fromEntries(Object.entries(principal.claims).filter(([name]) => claimNames.has(name))),
|
|
524
553
|
};
|
|
525
554
|
// A service run of the caller of this name that runs the same build file, services and
|
|
@@ -671,7 +700,7 @@ async function handleApi(api, principal, req, res, url) {
|
|
|
671
700
|
(0, http_2.send)(res, 403, { error: 'you are not entitled to anything on this server' });
|
|
672
701
|
return;
|
|
673
702
|
}
|
|
674
|
-
(0, http_2.send)(res, 200, (0, overview_1.getOverview)(registry.snapshot(), registry.usage(principal), viewer));
|
|
703
|
+
(0, http_2.send)(res, 200, (0, overview_1.getOverview)(registry.snapshot(), registry.usage(principal), viewer, entryOf(definitions)));
|
|
675
704
|
return;
|
|
676
705
|
}
|
|
677
706
|
if (path === '/v1/capacity' || path === '/v1/tasks') {
|
|
@@ -702,7 +731,13 @@ async function handleApi(api, principal, req, res, url) {
|
|
|
702
731
|
return;
|
|
703
732
|
}
|
|
704
733
|
const runs = (0, overview_1.filterRuns)(registry.all(), viewer, filter).map((run) => (0, overview_1.toListedRun)(viewer, run));
|
|
705
|
-
|
|
734
|
+
const oldest = (0, timings_1.getOldestCounted)(runs);
|
|
735
|
+
// where in the repository the build file of the last run is, as its version says
|
|
736
|
+
const tasks = (0, timings_1.getTaskStats)(runs, Date.now()).map((stats) => {
|
|
737
|
+
const entry = stats.last.definition ? api.definitions.find(stats.last.definition)?.entry : undefined;
|
|
738
|
+
return entry ? { ...stats, last: { ...stats.last, entry } } : stats;
|
|
739
|
+
});
|
|
740
|
+
(0, http_2.send)(res, 200, { tasks, ...(oldest ? { oldest } : {}) });
|
|
706
741
|
return;
|
|
707
742
|
}
|
|
708
743
|
if (await (0, approvals_api_1.handleApprovals)(config, registry, log, principal, req, res, url)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.mayOpenEndpoints = exports.getRunRepo = exports.toRunEndpoints = exports.refuseEndpoints = exports.getEndpointHosts = exports.getServiceRunName = void 0;
|
|
3
|
+
exports.mayPublishEndpoints = exports.mayOpenEndpoints = exports.getRunRepo = exports.toRunEndpoints = exports.refuseEndpoints = exports.getEndpointHosts = exports.getServiceRunName = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const plan_1 = require("./plan");
|
|
6
6
|
const policy_1 = require("./policy");
|
|
@@ -79,4 +79,11 @@ function mayOpenEndpoints(grant, run) {
|
|
|
79
79
|
return repo !== null && (0, policy_1.endpointLevel)(grant, repo) !== null;
|
|
80
80
|
}
|
|
81
81
|
exports.mayOpenEndpoints = mayOpenEndpoints;
|
|
82
|
+
// Who may publish endpoints of the run's repository: they could bring up a run of their own
|
|
83
|
+
// there, so they may stop somebody else's (a preview left running), besides its owner.
|
|
84
|
+
function mayPublishEndpoints(grant, run) {
|
|
85
|
+
const repo = getRunRepo(run);
|
|
86
|
+
return repo !== null && (0, policy_1.endpointLevel)(grant, repo) === 'publish';
|
|
87
|
+
}
|
|
88
|
+
exports.mayPublishEndpoints = mayPublishEndpoints;
|
|
82
89
|
//# sourceMappingURL=service-runs.js.map
|
package/dist/server/timings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getTaskStats = exports.roughly = exports.ranMs = exports.waitedMs = void 0;
|
|
3
|
+
exports.getTaskStats = exports.getOldestCounted = exports.roughly = exports.ranMs = exports.waitedMs = void 0;
|
|
4
4
|
const at = (iso) => (iso ? Date.parse(iso) : NaN);
|
|
5
5
|
// How long a run waited in the queue for a place: from when it was queued (after its
|
|
6
6
|
// approval, or when it woke) until it started, or until now while it waits.
|
|
@@ -64,9 +64,27 @@ function quantile(sorted, q) {
|
|
|
64
64
|
}
|
|
65
65
|
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
|
|
66
66
|
}
|
|
67
|
+
// a task that ran to its end or came from the cache; one that runs, was skipped or stopped says
|
|
68
|
+
// nothing about what it takes
|
|
69
|
+
const counts = (timing) => timing.status !== 'running' && timing.status !== 'skipped' && timing.status !== 'cancelled';
|
|
70
|
+
/**
|
|
71
|
+
* When the oldest run that getTaskStats counts was created: the start of what its numbers
|
|
72
|
+
* cover. Undefined when it counts none.
|
|
73
|
+
*/
|
|
74
|
+
function getOldestCounted(runs) {
|
|
75
|
+
let oldest;
|
|
76
|
+
for (const run of runs) {
|
|
77
|
+
if (Object.values(run.timings ?? {}).some(counts) && (!oldest || Date.parse(run.createdAt) < Date.parse(oldest))) {
|
|
78
|
+
oldest = run.createdAt;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return oldest;
|
|
82
|
+
}
|
|
83
|
+
exports.getOldestCounted = getOldestCounted;
|
|
67
84
|
/**
|
|
68
85
|
* Per task and repository, newest first: how often it ran and how, what it took the last
|
|
69
|
-
* time,
|
|
86
|
+
* time, the median and 90th percentile of the times it executed, and those times added up.
|
|
87
|
+
* `runs` is newest first.
|
|
70
88
|
*/
|
|
71
89
|
function getTaskStats(runs, now) {
|
|
72
90
|
const groups = new Map();
|
|
@@ -74,7 +92,7 @@ function getTaskStats(runs, now) {
|
|
|
74
92
|
const repo = run.source.repo ?? 'an upload';
|
|
75
93
|
const waited = waitedMs(run, now);
|
|
76
94
|
for (const [task, timing] of Object.entries(run.timings ?? {})) {
|
|
77
|
-
if (timing
|
|
95
|
+
if (!counts(timing)) {
|
|
78
96
|
continue;
|
|
79
97
|
}
|
|
80
98
|
const key = `${repo}\0${task}`;
|
|
@@ -92,6 +110,7 @@ function getTaskStats(runs, now) {
|
|
|
92
110
|
status: timing.status,
|
|
93
111
|
at: timing.finishedAt ?? run.finishedAt ?? run.createdAt,
|
|
94
112
|
...(timing.durationMs !== undefined ? { durationMs: timing.durationMs } : {}),
|
|
113
|
+
...(run.definition ? { definition: run.definition } : {}),
|
|
95
114
|
},
|
|
96
115
|
},
|
|
97
116
|
durations: [],
|
|
@@ -124,6 +143,7 @@ function getTaskStats(runs, now) {
|
|
|
124
143
|
...stats,
|
|
125
144
|
...(median !== undefined ? { medianMs: median } : {}),
|
|
126
145
|
...(p90 !== undefined ? { p90Ms: p90 } : {}),
|
|
146
|
+
...(durations.length > 0 ? { totalMs: durations.reduce((sum, ms) => sum + ms, 0) } : {}),
|
|
127
147
|
...(wait !== undefined ? { medianWaitMs: wait } : {}),
|
|
128
148
|
};
|
|
129
149
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLimit = void 0;
|
|
4
|
+
function createLimit(concurrency) {
|
|
5
|
+
let running = 0;
|
|
6
|
+
const waiting = [];
|
|
7
|
+
return async (fn) => {
|
|
8
|
+
if (running >= concurrency) {
|
|
9
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
running++;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await fn();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
const next = waiting.shift();
|
|
19
|
+
if (next) {
|
|
20
|
+
// the slot goes to the next one as it is
|
|
21
|
+
next();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
running--;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
exports.createLimit = createLimit;
|
|
30
|
+
//# sourceMappingURL=limit.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.webLinks = void 0;
|
|
4
|
+
const REPO = /^[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._~-]+)+$/;
|
|
5
|
+
const COMMIT = /^[0-9a-f]{7,64}$/;
|
|
6
|
+
// a branch keeps its slashes, as the forges read them; the rest of each part is encoded
|
|
7
|
+
const refPath = (ref) => ref.split('/').map(encodeURIComponent).join('/');
|
|
8
|
+
function webLinks(repo) {
|
|
9
|
+
if (!REPO.test(repo)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const base = `https://${repo}`;
|
|
13
|
+
const host = repo
|
|
14
|
+
.split('/')[0]
|
|
15
|
+
.replace(/:[0-9]+$/, '')
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
const forge = (tree, commits) => ({
|
|
18
|
+
repo: base,
|
|
19
|
+
ref: (ref) => `${base}/${tree}/${refPath(ref)}`,
|
|
20
|
+
commit: (commit) => (COMMIT.test(commit) ? `${base}/${commits}/${commit}` : base),
|
|
21
|
+
});
|
|
22
|
+
if (host === 'github.com' || host.startsWith('github.')) {
|
|
23
|
+
return forge('tree', 'commit');
|
|
24
|
+
}
|
|
25
|
+
if (host.split('.').some((label) => label.includes('gitlab'))) {
|
|
26
|
+
return forge('-/tree', '-/commit');
|
|
27
|
+
}
|
|
28
|
+
if (host === 'bitbucket.org') {
|
|
29
|
+
return forge('src', 'commits');
|
|
30
|
+
}
|
|
31
|
+
return { repo: base };
|
|
32
|
+
}
|
|
33
|
+
exports.webLinks = webLinks;
|
|
34
|
+
//# sourceMappingURL=web-links.js.map
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.3.5 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:block))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-950:oklch(27.9% .077 45.635);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-950:oklch(26.2% .051 172.552);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-950:oklch(29.3% .066 243.157);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;scrollbar-gutter:stable}@media (prefers-color-scheme:dark){html{--lightningcss-light: ;--lightningcss-dark:initial}}body{background-color:var(--color-stone-50);color:var(--color-stone-900);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (prefers-color-scheme:dark){body{background-color:var(--color-stone-950);color:var(--color-stone-100)}}}@layer components{.link{color:var(--color-emerald-700);text-underline-offset:2px}@media (hover:hover){.link:hover{text-decoration-line:underline}}@media (prefers-color-scheme:dark){.link{color:var(--color-emerald-400)}}.log{border-radius:var(--radius-lg);background-color:var(--color-stone-950);padding:calc(var(--spacing) * 3);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);white-space:pre;color:var(--color-stone-200);overflow-x:auto}@media (prefers-color-scheme:dark){.log{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-stone-800)}}}@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.inset-y-0{inset-block:0}.left-0{left:0}.order-last{order:9999}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.my-2{margin-block:calc(var(--spacing) * 2)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mr-1{margin-right:var(--spacing)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.h-2{height:calc(var(--spacing) * 2)}.h-5{height:calc(var(--spacing) * 5)}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing) * 5)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.min-w-0{min-width:0}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[max-content_1fr\]{grid-template-columns:max-content 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-stone-100>:not(:last-child)){border-color:var(--color-stone-100)}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-600\/40{border-color:#dd740066}@supports (color:color-mix(in lab, red, red)){.border-amber-600\/40{border-color:color-mix(in oklab, var(--color-amber-600) 40%, transparent)}}.border-emerald-600{border-color:var(--color-emerald-600)}.border-emerald-600\/40{border-color:#00976766}@supports (color:color-mix(in lab, red, red)){.border-emerald-600\/40{border-color:color-mix(in oklab, var(--color-emerald-600) 40%, transparent)}}.border-emerald-700{border-color:var(--color-emerald-700)}.border-red-300{border-color:var(--color-red-300)}.border-red-600\/40{border-color:#e4001466}@supports (color:color-mix(in lab, red, red)){.border-red-600\/40{border-color:color-mix(in oklab, var(--color-red-600) 40%, transparent)}}.border-sky-600\/40{border-color:#0084cc66}@supports (color:color-mix(in lab, red, red)){.border-sky-600\/40{border-color:color-mix(in oklab, var(--color-sky-600) 40%, transparent)}}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-transparent{border-color:#0000}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-600\/30{background-color:#0097674d}@supports (color:color-mix(in lab, red, red)){.bg-emerald-600\/30{background-color:color-mix(in oklab, var(--color-emerald-600) 30%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-stone-200{background-color:var(--color-stone-200)}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-px{padding-block:1px}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.85em\]{font-size:.85em}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-emerald-700{color:var(--color-emerald-700)}.text-red-300{color:var(--color-red-300)}.text-red-700{color:var(--color-red-700)}.text-sky-700{color:var(--color-sky-700)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-900{color:var(--color-stone-900)}.text-white{color:var(--color-white)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, )}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.hover\:bg-emerald-700:hover{background-color:var(--color-emerald-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-stone-50:hover{background-color:var(--color-stone-50)}.hover\:text-stone-900:hover{color:var(--color-stone-900)}.hover\:underline:hover{text-decoration-line:underline}}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:max-w-xs{max-width:var(--container-xs)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=48rem){.md\:order-none{order:0}.md\:w-auto{width:auto}.md\:flex-1{flex:1}}@media (width>=64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (width>=80rem){.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media (prefers-color-scheme:dark){:where(.dark\:divide-stone-800>:not(:last-child)){border-color:var(--color-stone-800)}.dark\:border-emerald-500{border-color:var(--color-emerald-500)}.dark\:border-red-900{border-color:var(--color-red-900)}.dark\:border-stone-700{border-color:var(--color-stone-700)}.dark\:border-stone-800{border-color:var(--color-stone-800)}.dark\:bg-amber-950{background-color:var(--color-amber-950)}.dark\:bg-emerald-400\/30{background-color:#00d2944d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-emerald-400\/30{background-color:color-mix(in oklab, var(--color-emerald-400) 30%, transparent)}}.dark\:bg-emerald-500{background-color:var(--color-emerald-500)}.dark\:bg-emerald-950{background-color:var(--color-emerald-950)}.dark\:bg-red-950{background-color:var(--color-red-950)}.dark\:bg-sky-950{background-color:var(--color-sky-950)}.dark\:bg-stone-800{background-color:var(--color-stone-800)}.dark\:bg-stone-900{background-color:var(--color-stone-900)}.dark\:text-amber-300{color:var(--color-amber-300)}.dark\:text-amber-400{color:var(--color-amber-400)}.dark\:text-emerald-300{color:var(--color-emerald-300)}.dark\:text-emerald-400{color:var(--color-emerald-400)}.dark\:text-red-300{color:var(--color-red-300)}.dark\:text-red-400{color:var(--color-red-400)}.dark\:text-sky-300{color:var(--color-sky-300)}.dark\:text-stone-300{color:var(--color-stone-300)}.dark\:text-stone-400{color:var(--color-stone-400)}.dark\:text-stone-500{color:var(--color-stone-500)}.dark\:text-white{color:var(--color-white)}@media (hover:hover){.dark\:hover\:bg-red-950:hover{background-color:var(--color-red-950)}.dark\:hover\:bg-stone-800:hover{background-color:var(--color-stone-800)}.dark\:hover\:text-white:hover{color:var(--color-white)}}}}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false}
|