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.
Files changed (46) hide show
  1. package/dist/cache/explain.js +7 -5
  2. package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
  3. package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
  4. package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
  5. package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
  6. package/dist/dashboard/index.html +3 -3
  7. package/dist/dashboard/logo.png +0 -0
  8. package/dist/executer/docker-task.js +3 -0
  9. package/dist/executer/execute-work-task.js +3 -1
  10. package/dist/executer/local-task.js +3 -0
  11. package/dist/executer/scheduler/enqueue-next.js +6 -1
  12. package/dist/executer/scheduler/state-key.js +71 -34
  13. package/dist/executer/watch-loop.js +18 -5
  14. package/dist/file/get-file-context.js +5 -1
  15. package/dist/kubernetes/claim-layout.js +10 -1
  16. package/dist/kubernetes/ensure-persistent-data.js +45 -5
  17. package/dist/kubernetes/follow-pod-logs.js +124 -27
  18. package/dist/kubernetes/network-policy.js +4 -4
  19. package/dist/kubernetes/restore-kubernetes-data.js +3 -1
  20. package/dist/kubernetes/store-kubernetes-data.js +3 -1
  21. package/dist/kubernetes/volumes.js +6 -6
  22. package/dist/optimizer/calculate-checksum.js +46 -3
  23. package/dist/optimizer/get-work-cache-stats.js +36 -24
  24. package/dist/planner/work-item-executions.js +27 -0
  25. package/dist/planner/work-runtime-kubernetes.js +33 -38
  26. package/dist/remote/remote-work-commands.js +11 -2
  27. package/dist/server/capacity.js +63 -0
  28. package/dist/server/dashboard.js +27 -9
  29. package/dist/server/endpoints.js +10 -1
  30. package/dist/server/executor.js +10 -0
  31. package/dist/server/oauth.js +16 -1
  32. package/dist/server/overview.js +80 -11
  33. package/dist/server/plan.js +16 -3
  34. package/dist/server/policy.js +9 -3
  35. package/dist/server/run-worker.js +37 -4
  36. package/dist/server/runs.js +74 -6
  37. package/dist/server/server-config.js +4 -1
  38. package/dist/server/server.js +45 -10
  39. package/dist/server/service-runs.js +8 -1
  40. package/dist/server/timings.js +23 -3
  41. package/dist/utils/limit.js +30 -0
  42. package/dist/utils/web-links.js +34 -0
  43. package/package.json +1 -1
  44. package/dist/dashboard/assets/index-CjnKm5-3.css +0 -2
  45. package/dist/dashboard/assets/index-zPXa4klZ.js +0 -13
  46. 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 = { type: 'task', task: task.name, state: 'failed', exitCode: state.exitCode, ...since() };
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 });
@@ -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
- cancel(owner, id, canUse) {
310
- const entry = this.find(owner, id, canUse);
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
- void entry.saving.then(() => this.options.store?.remove(entry.run.id)).catch(() => undefined);
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
- const subjectSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), sub: zod_1.z.string().min(1) }).strict();
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
@@ -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 run = registry.cancel(principal, id, accountsOf(config, principal));
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
- (0, http_2.send)(res, 200, { tasks: (0, timings_1.getTaskStats)(runs, Date.now()) });
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
@@ -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, and the median and 90th percentile of the times it executed. `runs` is newest first.
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.status === 'running' || timing.status === 'skipped' || timing.status === 'cancelled') {
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,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "2.0.0-next.4",
4
+ "version": "2.0.0-next.6",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",
@@ -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}