adispatchd 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/api/errors.d.ts +27 -0
- package/dist/api/errors.js +80 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +11 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/logs.d.ts +30 -0
- package/dist/api/logs.js +88 -0
- package/dist/api/logs.js.map +1 -0
- package/dist/api/serialize.d.ts +65 -0
- package/dist/api/serialize.js +135 -0
- package/dist/api/serialize.js.map +1 -0
- package/dist/api/server.d.ts +89 -0
- package/dist/api/server.js +751 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/sse.d.ts +90 -0
- package/dist/api/sse.js +225 -0
- package/dist/api/sse.js.map +1 -0
- package/dist/app.d.ts +173 -0
- package/dist/app.js +488 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/config-validate.d.ts +6 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/config-validate.js.map +1 -0
- package/dist/cli/control.d.ts +31 -0
- package/dist/cli/control.js +186 -0
- package/dist/cli/control.js.map +1 -0
- package/dist/cli/http-client.d.ts +57 -0
- package/dist/cli/http-client.js +174 -0
- package/dist/cli/http-client.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-templates.d.ts +20 -0
- package/dist/cli/init-templates.js +275 -0
- package/dist/cli/init-templates.js.map +1 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +107 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +102 -0
- package/dist/cli/program.js +133 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/service.d.ts +27 -0
- package/dist/cli/service.js +252 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/start.d.ts +22 -0
- package/dist/cli/start.js +133 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/workflow-dry-run.d.ts +13 -0
- package/dist/cli/workflow-dry-run.js +144 -0
- package/dist/cli/workflow-dry-run.js.map +1 -0
- package/dist/config/field-catalog.d.ts +33 -0
- package/dist/config/field-catalog.js +172 -0
- package/dist/config/field-catalog.js.map +1 -0
- package/dist/config/filter-schema.d.ts +15 -0
- package/dist/config/filter-schema.js +110 -0
- package/dist/config/filter-schema.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/issues.d.ts +27 -0
- package/dist/config/issues.js +37 -0
- package/dist/config/issues.js.map +1 -0
- package/dist/config/loader.d.ts +32 -0
- package/dist/config/loader.js +250 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +153 -0
- package/dist/config/manager.js +276 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/model.d.ts +334 -0
- package/dist/config/model.js +2 -0
- package/dist/config/model.js.map +1 -0
- package/dist/config/normalize.d.ts +20 -0
- package/dist/config/normalize.js +200 -0
- package/dist/config/normalize.js.map +1 -0
- package/dist/config/parameters.d.ts +37 -0
- package/dist/config/parameters.js +71 -0
- package/dist/config/parameters.js.map +1 -0
- package/dist/config/public.d.ts +6 -0
- package/dist/config/public.js +18 -0
- package/dist/config/public.js.map +1 -0
- package/dist/config/schemas.d.ts +538 -0
- package/dist/config/schemas.js +321 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/snapshot.d.ts +38 -0
- package/dist/config/snapshot.js +57 -0
- package/dist/config/snapshot.js.map +1 -0
- package/dist/config/validate.d.ts +33 -0
- package/dist/config/validate.js +346 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/clock.d.ts +59 -0
- package/dist/core/clock.js +133 -0
- package/dist/core/clock.js.map +1 -0
- package/dist/core/duration.d.ts +19 -0
- package/dist/core/duration.js +93 -0
- package/dist/core/duration.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +72 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/ids.d.ts +57 -0
- package/dist/core/ids.js +88 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +100 -0
- package/dist/core/logger.js +207 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paths.d.ts +84 -0
- package/dist/core/paths.js +138 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/template.d.ts +136 -0
- package/dist/core/template.js +261 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.d.ts +544 -0
- package/dist/core/types.js +172 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/worker-protocol.d.ts +102 -0
- package/dist/core/worker-protocol.js +75 -0
- package/dist/core/worker-protocol.js.map +1 -0
- package/dist/executors/classify.d.ts +25 -0
- package/dist/executors/classify.js +54 -0
- package/dist/executors/classify.js.map +1 -0
- package/dist/executors/dispatcher.d.ts +35 -0
- package/dist/executors/dispatcher.js +68 -0
- package/dist/executors/dispatcher.js.map +1 -0
- package/dist/executors/env.d.ts +43 -0
- package/dist/executors/env.js +66 -0
- package/dist/executors/env.js.map +1 -0
- package/dist/executors/event-parser.d.ts +41 -0
- package/dist/executors/event-parser.js +106 -0
- package/dist/executors/event-parser.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.js +13 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/log-capture.d.ts +57 -0
- package/dist/executors/log-capture.js +155 -0
- package/dist/executors/log-capture.js.map +1 -0
- package/dist/executors/process-identity.d.ts +124 -0
- package/dist/executors/process-identity.js +283 -0
- package/dist/executors/process-identity.js.map +1 -0
- package/dist/executors/shell-executor.d.ts +35 -0
- package/dist/executors/shell-executor.js +478 -0
- package/dist/executors/shell-executor.js.map +1 -0
- package/dist/executors/stream-redactor.d.ts +40 -0
- package/dist/executors/stream-redactor.js +89 -0
- package/dist/executors/stream-redactor.js.map +1 -0
- package/dist/executors/workspace.d.ts +10 -0
- package/dist/executors/workspace.js +47 -0
- package/dist/executors/workspace.js.map +1 -0
- package/dist/filters/evaluate.d.ts +77 -0
- package/dist/filters/evaluate.js +187 -0
- package/dist/filters/evaluate.js.map +1 -0
- package/dist/filters/fields.d.ts +35 -0
- package/dist/filters/fields.js +75 -0
- package/dist/filters/fields.js.map +1 -0
- package/dist/filters/index.d.ts +11 -0
- package/dist/filters/index.js +12 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/filters/iso-date.d.ts +10 -0
- package/dist/filters/iso-date.js +42 -0
- package/dist/filters/iso-date.js.map +1 -0
- package/dist/filters/predicate.d.ts +24 -0
- package/dist/filters/predicate.js +178 -0
- package/dist/filters/predicate.js.map +1 -0
- package/dist/filters/priority.d.ts +51 -0
- package/dist/filters/priority.js +91 -0
- package/dist/filters/priority.js.map +1 -0
- package/dist/filters/pushdown.d.ts +12 -0
- package/dist/filters/pushdown.js +52 -0
- package/dist/filters/pushdown.js.map +1 -0
- package/dist/filters/shell-check.d.ts +110 -0
- package/dist/filters/shell-check.js +304 -0
- package/dist/filters/shell-check.js.map +1 -0
- package/dist/identities/github-app.d.ts +55 -0
- package/dist/identities/github-app.js +160 -0
- package/dist/identities/github-app.js.map +1 -0
- package/dist/identities/index.d.ts +6 -0
- package/dist/identities/index.js +7 -0
- package/dist/identities/index.js.map +1 -0
- package/dist/identities/jwt.d.ts +24 -0
- package/dist/identities/jwt.js +56 -0
- package/dist/identities/jwt.js.map +1 -0
- package/dist/lifecycle/context.d.ts +106 -0
- package/dist/lifecycle/context.js +170 -0
- package/dist/lifecycle/context.js.map +1 -0
- package/dist/lifecycle/engine.d.ts +185 -0
- package/dist/lifecycle/engine.js +551 -0
- package/dist/lifecycle/engine.js.map +1 -0
- package/dist/lifecycle/errors.d.ts +48 -0
- package/dist/lifecycle/errors.js +71 -0
- package/dist/lifecycle/errors.js.map +1 -0
- package/dist/lifecycle/index.d.ts +9 -0
- package/dist/lifecycle/index.js +9 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/performers.d.ts +66 -0
- package/dist/lifecycle/performers.js +179 -0
- package/dist/lifecycle/performers.js.map +1 -0
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +115 -0
- package/dist/notifications/service.js +364 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/persistence/actions.d.ts +101 -0
- package/dist/persistence/actions.js +196 -0
- package/dist/persistence/actions.js.map +1 -0
- package/dist/persistence/blocks.d.ts +85 -0
- package/dist/persistence/blocks.js +140 -0
- package/dist/persistence/blocks.js.map +1 -0
- package/dist/persistence/cron-firings.d.ts +55 -0
- package/dist/persistence/cron-firings.js +70 -0
- package/dist/persistence/cron-firings.js.map +1 -0
- package/dist/persistence/database.d.ts +60 -0
- package/dist/persistence/database.js +124 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/persistence/errors.d.ts +39 -0
- package/dist/persistence/errors.js +67 -0
- package/dist/persistence/errors.js.map +1 -0
- package/dist/persistence/events.d.ts +72 -0
- package/dist/persistence/events.js +83 -0
- package/dist/persistence/events.js.map +1 -0
- package/dist/persistence/index.d.ts +18 -0
- package/dist/persistence/index.js +19 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/instance-lock.d.ts +26 -0
- package/dist/persistence/instance-lock.js +117 -0
- package/dist/persistence/instance-lock.js.map +1 -0
- package/dist/persistence/lock-keys.d.ts +16 -0
- package/dist/persistence/lock-keys.js +41 -0
- package/dist/persistence/lock-keys.js.map +1 -0
- package/dist/persistence/notifications.d.ts +113 -0
- package/dist/persistence/notifications.js +213 -0
- package/dist/persistence/notifications.js.map +1 -0
- package/dist/persistence/persistence.d.ts +46 -0
- package/dist/persistence/persistence.js +42 -0
- package/dist/persistence/persistence.js.map +1 -0
- package/dist/persistence/retention.d.ts +46 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/retention.js.map +1 -0
- package/dist/persistence/runs.d.ts +215 -0
- package/dist/persistence/runs.js +417 -0
- package/dist/persistence/runs.js.map +1 -0
- package/dist/persistence/schema.d.ts +16 -0
- package/dist/persistence/schema.js +183 -0
- package/dist/persistence/schema.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +49 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/runs/controller.d.ts +307 -0
- package/dist/runs/controller.js +1078 -0
- package/dist/runs/controller.js.map +1 -0
- package/dist/runs/errors.d.ts +20 -0
- package/dist/runs/errors.js +26 -0
- package/dist/runs/errors.js.map +1 -0
- package/dist/runs/event-bus.d.ts +106 -0
- package/dist/runs/event-bus.js +105 -0
- package/dist/runs/event-bus.js.map +1 -0
- package/dist/runs/execution.d.ts +39 -0
- package/dist/runs/execution.js +146 -0
- package/dist/runs/execution.js.map +1 -0
- package/dist/runs/index.d.ts +12 -0
- package/dist/runs/index.js +13 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/retention.d.ts +64 -0
- package/dist/runs/retention.js +232 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/workspace.d.ts +20 -0
- package/dist/runs/workspace.js +51 -0
- package/dist/runs/workspace.js.map +1 -0
- package/dist/scheduler/checks.d.ts +41 -0
- package/dist/scheduler/checks.js +111 -0
- package/dist/scheduler/checks.js.map +1 -0
- package/dist/scheduler/cron.d.ts +32 -0
- package/dist/scheduler/cron.js +83 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/dry-run.d.ts +42 -0
- package/dist/scheduler/dry-run.js +167 -0
- package/dist/scheduler/dry-run.js.map +1 -0
- package/dist/scheduler/errors.d.ts +22 -0
- package/dist/scheduler/errors.js +22 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/format.d.ts +3 -0
- package/dist/scheduler/format.js +73 -0
- package/dist/scheduler/format.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.js +14 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/planner.d.ts +88 -0
- package/dist/scheduler/planner.js +255 -0
- package/dist/scheduler/planner.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +160 -0
- package/dist/scheduler/scheduler.js +680 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/state.d.ts +26 -0
- package/dist/scheduler/state.js +25 -0
- package/dist/scheduler/state.js.map +1 -0
- package/dist/scheduler/types.d.ts +197 -0
- package/dist/scheduler/types.js +3 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service/index.d.ts +6 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/install.d.ts +93 -0
- package/dist/service/install.js +250 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/unit.d.ts +50 -0
- package/dist/service/unit.js +171 -0
- package/dist/service/unit.js.map +1 -0
- package/dist/sources/github/adapter.d.ts +99 -0
- package/dist/sources/github/adapter.js +336 -0
- package/dist/sources/github/adapter.js.map +1 -0
- package/dist/sources/github/fields.d.ts +8 -0
- package/dist/sources/github/fields.js +72 -0
- package/dist/sources/github/fields.js.map +1 -0
- package/dist/sources/github/http.d.ts +100 -0
- package/dist/sources/github/http.js +288 -0
- package/dist/sources/github/http.js.map +1 -0
- package/dist/sources/github/index.d.ts +6 -0
- package/dist/sources/github/index.js +7 -0
- package/dist/sources/github/index.js.map +1 -0
- package/dist/sources/github/pushdown.d.ts +14 -0
- package/dist/sources/github/pushdown.js +47 -0
- package/dist/sources/github/pushdown.js.map +1 -0
- package/dist/sources/github/refs.d.ts +17 -0
- package/dist/sources/github/refs.js +61 -0
- package/dist/sources/github/refs.js.map +1 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.js +7 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +28 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/public/app.css +239 -0
- package/dist/ui/public/favicon.svg +1 -0
- package/dist/ui/public/index.html +27 -0
- package/dist/ui/public/js/client/api.js +68 -0
- package/dist/ui/public/js/client/app.js +146 -0
- package/dist/ui/public/js/client/dom.js +34 -0
- package/dist/ui/public/js/client/page.js +9 -0
- package/dist/ui/public/js/client/pages/actions.js +35 -0
- package/dist/ui/public/js/client/pages/blocked.js +58 -0
- package/dist/ui/public/js/client/pages/config.js +126 -0
- package/dist/ui/public/js/client/pages/dashboard.js +175 -0
- package/dist/ui/public/js/client/pages/run.js +367 -0
- package/dist/ui/public/js/client/pages/runs.js +72 -0
- package/dist/ui/public/js/client/pages/views.js +80 -0
- package/dist/ui/public/js/client/pages/workflows.js +291 -0
- package/dist/ui/public/js/client/types.js +1 -0
- package/dist/ui/public/js/client/ui.js +152 -0
- package/dist/ui/public/js/shared/dom.js +77 -0
- package/dist/ui/public/js/shared/events.js +88 -0
- package/dist/ui/public/js/shared/format.js +213 -0
- package/dist/ui/public/js/shared/logs.js +136 -0
- package/dist/ui/public/js/shared/params.js +90 -0
- package/dist/ui/public/js/shared/route.js +36 -0
- package/dist/ui/public/js/shared/sort.js +70 -0
- package/dist/ui/public/js/shared/sse.js +193 -0
- package/dist/ui/public/js/shared/throttle.js +65 -0
- package/dist/views/index.d.ts +6 -0
- package/dist/views/index.js +7 -0
- package/dist/views/index.js.map +1 -0
- package/dist/views/service.d.ts +131 -0
- package/dist/views/service.js +264 -0
- package/dist/views/service.js.map +1 -0
- package/docs/api.md +239 -0
- package/docs/configuration-reference.md +1785 -0
- package/docs/cron-semantics.md +96 -0
- package/docs/manual-tests/github-fixture/config.yaml +30 -0
- package/docs/manual-tests/github-fixture/helpers.sh +93 -0
- package/docs/manual-tests/github-fixture/identities.yaml +18 -0
- package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
- package/docs/manual-tests/github-fixture/sources.yaml +13 -0
- package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
- package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
- package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
- package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
- package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
- package/docs/manual-tests/github.md +1308 -0
- package/docs/manual-tests/process-supervision.md +64 -0
- package/docs/manual-tests/systemd.md +68 -0
- package/docs/operations.md +271 -0
- package/docs/worker-protocol.md +145 -0
- package/examples/config/config.yaml +35 -0
- package/examples/config/identities.yaml +18 -0
- package/examples/config/sources.yaml +13 -0
- package/examples/config/views/needs-maintainer.yaml +15 -0
- package/examples/config/workflows/coder-project-a.yaml +86 -0
- package/examples/config/workflows/deploy.yaml +15 -0
- package/examples/config/workflows/nightly-maintenance.yaml +13 -0
- package/package.json +75 -4
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run detail (Spec §16): all run fields, paged logs with tail/follow, events,
|
|
3
|
+
* lifecycle actions, cancel / recovery retry.
|
|
4
|
+
*/
|
|
5
|
+
import { api, qs, seg } from '../api.js';
|
|
6
|
+
import { h, replaceChildren, rerender } from '../dom.js';
|
|
7
|
+
import { buildHash } from '../../shared/route.js';
|
|
8
|
+
import { createRefresher } from '../../shared/throttle.js';
|
|
9
|
+
import { LogWindow } from '../../shared/logs.js';
|
|
10
|
+
import { EventList } from '../../shared/events.js';
|
|
11
|
+
import { eventSummary, formatBytes, formatDuration, stateTone } from '../../shared/format.js';
|
|
12
|
+
import { messageRunId } from '../page.js';
|
|
13
|
+
import { act, badge, button, confirmDialog, errorBox, externalLink, jsonBlock, kv, loading, routeLink, runLink, runOutcomeBadges, section, table, text, time } from '../ui.js';
|
|
14
|
+
import { cancelRunButton } from './dashboard.js';
|
|
15
|
+
const TABS = ['overview', 'logs', 'events', 'actions'];
|
|
16
|
+
/** Lifecycle action table (shared with Failed Actions). */
|
|
17
|
+
export function actionButtons(a, after) {
|
|
18
|
+
if (a.status !== 'failed')
|
|
19
|
+
return null;
|
|
20
|
+
const retry = button('Retry', () => void act(retry, () => api.post(`/api/v1/actions/${a.id}/retry`), 'Retry started').then(after), { kind: 'primary', disabled: a.busy === true, title: a.busy ? 'The lifecycle chain of this run is busy' : 'Run the action again (fresh attempt budget)' });
|
|
21
|
+
const skip = button('Skip', () => {
|
|
22
|
+
void (async () => {
|
|
23
|
+
const r = await confirmDialog({
|
|
24
|
+
title: 'Skip lifecycle action?',
|
|
25
|
+
message: h('p', null, `Action #${a.id} (${a.hook} › ${a.name ?? a.type}) is marked as skipped and the hook chain continues with the next action.`),
|
|
26
|
+
confirmLabel: 'Skip action',
|
|
27
|
+
danger: true,
|
|
28
|
+
});
|
|
29
|
+
if (r.ok)
|
|
30
|
+
await act(skip, () => api.post(`/api/v1/actions/${a.id}/skip`), 'Action skipped');
|
|
31
|
+
after();
|
|
32
|
+
})();
|
|
33
|
+
}, { disabled: a.busy === true });
|
|
34
|
+
return h('span', { class: 'actions' }, retry, skip);
|
|
35
|
+
}
|
|
36
|
+
function errorText(e) {
|
|
37
|
+
if (e === null || e === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
if (typeof e === 'string')
|
|
40
|
+
return e;
|
|
41
|
+
if (typeof e === 'object' && typeof e.message === 'string')
|
|
42
|
+
return e.message;
|
|
43
|
+
return JSON.stringify(e);
|
|
44
|
+
}
|
|
45
|
+
export function actionsTable(actions, after, opts = {}) {
|
|
46
|
+
return table([...(opts.showRun ? ['Run'] : []), 'Hook', '#', 'Action', 'State', 'Attempts', 'Last error', 'Updated', ''], actions.map((a) => [
|
|
47
|
+
...(opts.showRun ? [a.run ? h('span', null, runLink(a.runId), h('br'), h('small', { class: 'muted' }, a.run.workflowName ?? a.run.workflowId)) : runLink(a.runId)] : []),
|
|
48
|
+
a.hook,
|
|
49
|
+
String(a.ordinal + 1),
|
|
50
|
+
h('span', null, a.name ?? a.type, a.name ? h('small', { class: 'muted' }, ` (${a.type})`) : null),
|
|
51
|
+
badge(a.status, stateTone(a.status)),
|
|
52
|
+
a.attemptsDetail && a.attemptsDetail.length > 0
|
|
53
|
+
? h('details', null, h('summary', null, String(a.attempts)), h('ol', { class: 'attempts' }, a.attemptsDetail.map((t) => h('li', null, time(t.startedAt), ' ', badge(t.outcome ?? 'running', stateTone(t.outcome ?? 'running')), errorText(t.error) ? h('div', { class: 'error-text' }, errorText(t.error)) : null))))
|
|
54
|
+
: String(a.attempts),
|
|
55
|
+
errorText(a.lastError) ? h('div', { class: 'error-text' }, errorText(a.lastError)) : text(null),
|
|
56
|
+
time(a.updatedAt),
|
|
57
|
+
actionButtons(a, after),
|
|
58
|
+
]), { empty: 'No lifecycle actions.' });
|
|
59
|
+
}
|
|
60
|
+
export const runPage = (ctx, runId) => {
|
|
61
|
+
ctx.setTitle(`Run ${runId}`);
|
|
62
|
+
const tab = TABS.includes(ctx.route.query.tab) ? ctx.route.query.tab : 'overview';
|
|
63
|
+
const header = h('div', { class: 'run-header' }, loading());
|
|
64
|
+
const tabs = h('nav', { class: 'tabs', 'aria-label': 'Run sections' }, TABS.map((t) => h('a', { href: buildHash(['runs', runId], { tab: t }), class: t === tab ? 'active' : null, 'aria-current': t === tab ? 'page' : null }, t[0].toUpperCase() + t.slice(1))));
|
|
65
|
+
const body = h('div', null);
|
|
66
|
+
const el = h('div', { class: 'page' }, h('p', { class: 'crumbs' }, routeLink(['runs'], '← Runs')), header, tabs, body);
|
|
67
|
+
let detail = null;
|
|
68
|
+
// ---------------------------------------------------------------- header
|
|
69
|
+
const renderHeader = (d) => {
|
|
70
|
+
const btns = h('div', { class: 'actions' });
|
|
71
|
+
const cancel = cancelRunButton(d, () => refresher.trigger());
|
|
72
|
+
if (cancel)
|
|
73
|
+
btns.appendChild(cancel);
|
|
74
|
+
if (d.status === 'recovery_pending') {
|
|
75
|
+
const b = button('Retry recovery', () => {
|
|
76
|
+
void (async () => {
|
|
77
|
+
const r = await confirmDialog({
|
|
78
|
+
title: 'Retry recovery?',
|
|
79
|
+
message: h('p', null, 'Recovery checks again whether the worker process is still running. Only tick the box if you have verified that the process is gone.'),
|
|
80
|
+
confirmLabel: 'Retry recovery',
|
|
81
|
+
checkbox: { label: 'I verified the process is gone (assumeGone)' },
|
|
82
|
+
});
|
|
83
|
+
if (r.ok)
|
|
84
|
+
await act(b, () => api.post(`/api/v1/runs/${seg(d.id)}/recovery/retry`, { assumeGone: r.checked }), 'Recovery retry started');
|
|
85
|
+
refresher.trigger();
|
|
86
|
+
})();
|
|
87
|
+
}, { kind: 'primary' });
|
|
88
|
+
btns.appendChild(b);
|
|
89
|
+
}
|
|
90
|
+
rerender(header, h('h1', null, d.workflowName ?? d.workflowId, ' ', h('small', { class: 'muted' }, h('code', null, d.id))), h('div', { class: 'run-status' }, runOutcomeBadges(d), d.statusMessage && d.active ? h('span', { class: 'status-message' }, d.statusMessage) : null), btns);
|
|
91
|
+
};
|
|
92
|
+
// -------------------------------------------------------------- overview
|
|
93
|
+
const renderOverview = (d) => {
|
|
94
|
+
const cmd = d.command ? [d.command.command ?? '', ...(Array.isArray(d.command.args) ? d.command.args.map((a) => String(a)) : [])].join(' ') : null;
|
|
95
|
+
rerender(body, h('div', { class: 'grid' }, section('Run', kv([
|
|
96
|
+
['Name', text(d.workflowName)],
|
|
97
|
+
['Run ID', h('code', null, d.id)],
|
|
98
|
+
['Workflow', routeLink(['workflows', d.workflowId], d.workflowId)],
|
|
99
|
+
['Identity / role', text(d.identity)],
|
|
100
|
+
['Trigger', h('span', null, d.triggerType, d.scheduledFor ? ` (slot ${d.scheduledFor})` : '')],
|
|
101
|
+
['Source item', d.sourceUrl ? externalLink(d.sourceUrl) : text(d.sourceItemKey)],
|
|
102
|
+
['Technical state', badge(d.status, d.active ? 'info' : 'muted')],
|
|
103
|
+
['Business result', d.businessResult ? badge(d.businessResult, d.businessResult === 'success' ? 'ok' : 'warn') : text(null)],
|
|
104
|
+
['Final reason', text(d.finalReason)],
|
|
105
|
+
['Lifecycle', d.lifecycleStatus ? (d.lifecycleFailed ? badge('lifecycle failed', 'error') : badge(d.lifecycleStatus, stateTone(d.lifecycleStatus))) : text(null)],
|
|
106
|
+
['Exit code', text(d.exitCode)],
|
|
107
|
+
])), section('Timing & process', kv([
|
|
108
|
+
['Created', time(d.createdAt)],
|
|
109
|
+
['Started', time(d.startedAt)],
|
|
110
|
+
['Finished', time(d.finishedAt)],
|
|
111
|
+
['Duration', formatDuration(d.durationMs ?? (d.startedAt && d.active ? Date.now() - Date.parse(d.startedAt) : null))],
|
|
112
|
+
['Working directory', d.workingDirectory ? h('code', null, d.workingDirectory) : text(null)],
|
|
113
|
+
['Command', cmd ? h('code', { class: 'wrap' }, cmd) : text(null)],
|
|
114
|
+
['PID', text(d.pid)],
|
|
115
|
+
['Lock', text(d.lockKey)],
|
|
116
|
+
['Config', d.configVersion !== null ? `v${d.configVersion}${d.configHash ? ` (${d.configHash.slice(0, 12)})` : ''}` : text(null)],
|
|
117
|
+
['Logs', `stdout ${formatBytes(d.logs.stdout.size)}${d.logs.stdout.truncated ? ' (truncated)' : ''}, stderr ${formatBytes(d.logs.stderr.size)}${d.logs.stderr.truncated ? ' (truncated)' : ''}`],
|
|
118
|
+
])), d.block
|
|
119
|
+
? h('section', { class: 'card span-all' }, h('h2', null, 'Active retry block'), h('p', null, `Internal retry block #${d.block.id} of this scope (${d.block.failureCount} technical failures): ${d.block.reason} `, routeLink(['blocked'], 'Blocked →')))
|
|
120
|
+
: null, d.error ? h('section', { class: 'card span-all' }, h('h2', null, 'Technical diagnosis'), jsonBlock(d.error)) : null, h('section', { class: 'card span-all' }, h('h2', null, 'Parameters'), jsonBlock(d.parameters && Object.keys(d.parameters).length ? d.parameters : null)), h('section', { class: 'card span-all' }, h('h2', null, 'Result (result file)'), d.resultFileError ? h('div', { class: 'alert alert-warn' }, `Result file unreadable: ${d.resultFileError}`) : null, jsonBlock(d.resultFile ?? d.result)), h('section', { class: 'card span-all' }, h('details', null, h('summary', null, h('strong', null, 'Input JSON (context.json)')), d.inputError ? h('div', { class: 'alert alert-warn' }, d.inputError) : null, jsonBlock(d.input)))));
|
|
121
|
+
};
|
|
122
|
+
// ------------------------------------------------------------------ logs
|
|
123
|
+
let stream = ctx.route.query.stream === 'stderr' ? 'stderr' : 'stdout';
|
|
124
|
+
const win = new LogWindow();
|
|
125
|
+
let follow = true;
|
|
126
|
+
/** Generation of the log window (bumped on stream switch / reset); in-flight responses of older generations are dropped. */
|
|
127
|
+
let logGen = 0;
|
|
128
|
+
/** Generation with a load in flight (-1 = none). */
|
|
129
|
+
let logBusyGen = -1;
|
|
130
|
+
const logPre = h('pre', { class: 'log', tabindex: 0, 'aria-label': 'Log output' });
|
|
131
|
+
const logInfo = h('div', { class: 'log-info muted' });
|
|
132
|
+
const logButtons = h('div', { class: 'actions' });
|
|
133
|
+
let pollTimer = null;
|
|
134
|
+
const logUrl = (query) => `/api/v1/runs/${seg(runId)}/logs${qs({ stream, ...query })}`;
|
|
135
|
+
const atBottom = () => logPre.scrollHeight - logPre.scrollTop - logPre.clientHeight < 30;
|
|
136
|
+
const renderLog = (scrollToEnd) => {
|
|
137
|
+
logPre.textContent = win.text || (win.exists ? '' : '(no log output yet)');
|
|
138
|
+
if (scrollToEnd)
|
|
139
|
+
logPre.scrollTop = logPre.scrollHeight;
|
|
140
|
+
logInfo.textContent = `bytes ${win.start.toLocaleString()}–${win.end.toLocaleString()} of ${win.size.toLocaleString()} (${formatBytes(win.size)})${win.trimmed ? ' · view trimmed to keep the page responsive' : ''}`;
|
|
141
|
+
rerender(logButtons, h('span', { class: 'segmented' }, ['stdout', 'stderr'].map((s) => button(s, () => switchStream(s), { kind: s === stream ? 'primary' : 'plain' }))), button('⇤ Start', () => void loadLog('start'), { disabled: win.start === 0 && !win.trimmed }), button('Load earlier', () => void loadLog('earlier'), { disabled: !win.hasEarlier }), button('Load newer', () => void loadLog('later'), { disabled: !win.hasLater }), button('End ⇥', () => void loadLog('tail')), h('label', { class: 'check' }, (() => {
|
|
142
|
+
const cb = h('input', { type: 'checkbox', checked: follow, on: { change: () => { follow = cb.checked; if (follow)
|
|
143
|
+
void loadLog('tail'); } } });
|
|
144
|
+
return cb;
|
|
145
|
+
})(), ' Follow'));
|
|
146
|
+
};
|
|
147
|
+
const switchStream = (s) => {
|
|
148
|
+
if (s === stream)
|
|
149
|
+
return;
|
|
150
|
+
stream = s;
|
|
151
|
+
void loadLog('tail');
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Load a log range. All requests are bounded pages (LOG_PAGE_BYTES).
|
|
155
|
+
* `tail`/`start` (and a stream switch) start a new generation: responses of
|
|
156
|
+
* older generations are discarded, so bytes of one stream never end up in
|
|
157
|
+
* the window of another or of a newer range.
|
|
158
|
+
*/
|
|
159
|
+
const loadLog = async (mode) => {
|
|
160
|
+
if (mode === 'tail' || mode === 'start')
|
|
161
|
+
logGen++;
|
|
162
|
+
const gen = logGen;
|
|
163
|
+
if (logBusyGen === gen)
|
|
164
|
+
return; // same generation already loading (follow/earlier/later)
|
|
165
|
+
logBusyGen = gen;
|
|
166
|
+
const get = async (query) => {
|
|
167
|
+
const page = await api.get(logUrl(query));
|
|
168
|
+
return gen === logGen ? page : null;
|
|
169
|
+
};
|
|
170
|
+
try {
|
|
171
|
+
if (mode === 'tail') {
|
|
172
|
+
const page = await get(win.tailQuery());
|
|
173
|
+
if (!page)
|
|
174
|
+
return;
|
|
175
|
+
win.reset(page);
|
|
176
|
+
renderLog(true);
|
|
177
|
+
}
|
|
178
|
+
else if (mode === 'start') {
|
|
179
|
+
follow = false;
|
|
180
|
+
const page = await get({ offset: 0, limit: win.pageBytes });
|
|
181
|
+
if (!page)
|
|
182
|
+
return;
|
|
183
|
+
win.reset(page);
|
|
184
|
+
renderLog(false);
|
|
185
|
+
logPre.scrollTop = 0;
|
|
186
|
+
}
|
|
187
|
+
else if (mode === 'earlier') {
|
|
188
|
+
const q = win.earlierQuery();
|
|
189
|
+
if (!q)
|
|
190
|
+
return;
|
|
191
|
+
const before = logPre.scrollHeight;
|
|
192
|
+
const page = await get(q);
|
|
193
|
+
if (!page)
|
|
194
|
+
return;
|
|
195
|
+
if (!win.prepend(page))
|
|
196
|
+
win.reset(page);
|
|
197
|
+
if (win.hasLater)
|
|
198
|
+
follow = false;
|
|
199
|
+
renderLog(false);
|
|
200
|
+
logPre.scrollTop += logPre.scrollHeight - before;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// later / follow: read on from `end` (several pages at most per call)
|
|
204
|
+
const stick = atBottom();
|
|
205
|
+
for (let i = 0; i < 4; i++) {
|
|
206
|
+
const page = await get(win.laterQuery());
|
|
207
|
+
if (!page)
|
|
208
|
+
return;
|
|
209
|
+
if (!win.append(page)) {
|
|
210
|
+
const tail = await get(win.tailQuery());
|
|
211
|
+
if (!tail)
|
|
212
|
+
return;
|
|
213
|
+
win.reset(tail);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (page.eof || page.length === 0)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
renderLog(mode === 'follow' ? stick : false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (gen !== logGen)
|
|
224
|
+
return;
|
|
225
|
+
logInfo.textContent = '';
|
|
226
|
+
logInfo.appendChild(errorBox(err));
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
if (logBusyGen === gen)
|
|
230
|
+
logBusyGen = -1;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const followRefresher = createRefresher(() => (follow && tab === 'logs' ? loadLog('follow') : undefined), 1000);
|
|
234
|
+
const renderLogs = () => {
|
|
235
|
+
replaceChildren(body, h('section', { class: 'card' }, logButtons, logInfo, logPre));
|
|
236
|
+
renderLog(false);
|
|
237
|
+
void loadLog('tail');
|
|
238
|
+
// Safety net: poll while the run is active and following (SSE triggers are the primary signal).
|
|
239
|
+
pollTimer = setInterval(() => {
|
|
240
|
+
if (detail?.active && follow)
|
|
241
|
+
followRefresher.trigger();
|
|
242
|
+
}, 5000);
|
|
243
|
+
};
|
|
244
|
+
// ---------------------------------------------------------------- events
|
|
245
|
+
// Contiguous REST cursor + live events (see shared/events.ts): live events are appended only if they
|
|
246
|
+
// continue the loaded range; otherwise "newer events" are offered and loaded via REST from the cursor.
|
|
247
|
+
let evList = new EventList();
|
|
248
|
+
let evBusy = false;
|
|
249
|
+
const eventsBody = h('tbody');
|
|
250
|
+
const eventsMore = h('div');
|
|
251
|
+
const eventsDropped = h('div');
|
|
252
|
+
const eventRow = (e) => h('tr', null, h('td', null, String(e.sequence)), h('td', null, time(e.timestamp)), h('td', null, badge(e.origin, e.origin === 'worker' ? 'info' : 'muted')), h('td', null, h('code', null, e.type)), h('td', { class: 'event-message' }, eventSummary(e)), h('td', null, Object.keys(e.payload ?? {}).length ? h('details', null, h('summary', null, 'payload'), jsonBlock(e.payload, 50_000)) : null));
|
|
253
|
+
const applyEvents = (change) => {
|
|
254
|
+
if (change) {
|
|
255
|
+
for (const e of change.append)
|
|
256
|
+
eventsBody.appendChild(eventRow(e));
|
|
257
|
+
for (let i = 0; i < change.drop && eventsBody.firstElementChild; i++)
|
|
258
|
+
eventsBody.firstElementChild.remove();
|
|
259
|
+
}
|
|
260
|
+
replaceChildren(eventsDropped, evList.dropped > 0 ? h('p', { class: 'hint' }, `${evList.dropped} older event(s) are not shown (at most ${evList.maxRows} rows are kept).`) : null);
|
|
261
|
+
replaceChildren(eventsMore, evList.pending
|
|
262
|
+
? h('div', { class: 'alert alert-info' }, 'Newer events are available. ', button(evBusy ? 'Loading…' : 'Load newer events', () => void loadMoreEvents(), { kind: 'primary', disabled: evBusy }))
|
|
263
|
+
: null);
|
|
264
|
+
};
|
|
265
|
+
const loadMoreEvents = async () => {
|
|
266
|
+
if (evBusy)
|
|
267
|
+
return;
|
|
268
|
+
evBusy = true;
|
|
269
|
+
const list = evList;
|
|
270
|
+
const q = list.nextQuery(1000);
|
|
271
|
+
applyEvents(null);
|
|
272
|
+
try {
|
|
273
|
+
const res = await api.get(`/api/v1/runs/${seg(runId)}/events${qs(q)}`);
|
|
274
|
+
if (list === evList)
|
|
275
|
+
applyEvents(list.page(res.events, res.hasMore, q.after));
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
replaceChildren(eventsMore, errorBox(err), button('Retry', () => void loadMoreEvents()));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
evBusy = false;
|
|
283
|
+
}
|
|
284
|
+
applyEvents(null);
|
|
285
|
+
};
|
|
286
|
+
const renderEvents = (d) => {
|
|
287
|
+
evList = new EventList();
|
|
288
|
+
eventsBody.replaceChildren();
|
|
289
|
+
applyEvents(evList.page(d.events, d.eventsTruncated, 0));
|
|
290
|
+
rerender(body, h('section', { class: 'card' }, h('p', { class: 'hint' }, 'Structured events (worker @@ADISPATCH_EVENT lines and orchestrator events). Events are informational only. Live updates append here.'), eventsDropped, h('div', { class: 'table-wrap' }, h('table', { class: 'events' }, h('thead', null, h('tr', null, ['#', 'Time', 'Origin', 'Type', 'Message', ''].map((x) => h('th', { scope: 'col' }, x)))), eventsBody)), eventsMore));
|
|
291
|
+
};
|
|
292
|
+
// --------------------------------------------------------------- actions
|
|
293
|
+
const renderActions = (d) => {
|
|
294
|
+
rerender(body, h('section', { class: 'card' }, d.lifecycleFailed ? h('div', { class: 'alert alert-error' }, 'Lifecycle failed: a hook chain stopped at a failed action. Retry or skip it below.') : null, actionsTable(d.actions, () => refresher.trigger())));
|
|
295
|
+
};
|
|
296
|
+
// ------------------------------------------------------------------ load
|
|
297
|
+
let first = true;
|
|
298
|
+
const load = async () => {
|
|
299
|
+
try {
|
|
300
|
+
const d = await api.get(`/api/v1/runs/${seg(runId)}`);
|
|
301
|
+
const wasActive = detail?.active ?? true;
|
|
302
|
+
detail = d;
|
|
303
|
+
renderHeader(d);
|
|
304
|
+
if (tab === 'overview')
|
|
305
|
+
renderOverview(d);
|
|
306
|
+
else if (tab === 'actions')
|
|
307
|
+
renderActions(d);
|
|
308
|
+
else if (tab === 'events') {
|
|
309
|
+
if (first)
|
|
310
|
+
renderEvents(d);
|
|
311
|
+
}
|
|
312
|
+
else if (tab === 'logs') {
|
|
313
|
+
if (first)
|
|
314
|
+
renderLogs();
|
|
315
|
+
else if (wasActive && !d.active)
|
|
316
|
+
followRefresher.trigger(); // final bytes after the run ended
|
|
317
|
+
}
|
|
318
|
+
first = false;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
replaceChildren(header, errorBox(err));
|
|
322
|
+
if (first)
|
|
323
|
+
body.replaceChildren();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const refresher = createRefresher(load, 1000);
|
|
327
|
+
void load();
|
|
328
|
+
return {
|
|
329
|
+
el,
|
|
330
|
+
onLive: (msg) => {
|
|
331
|
+
if (msg.type === 'system.status') {
|
|
332
|
+
refresher.trigger();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (messageRunId(msg) !== runId)
|
|
336
|
+
return;
|
|
337
|
+
if (msg.type === 'run.output') {
|
|
338
|
+
if (tab === 'logs' && msg.payload.stream === stream)
|
|
339
|
+
followRefresher.trigger();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (msg.type === 'run.event' && tab === 'events') {
|
|
343
|
+
applyEvents(evList.live(msg.payload));
|
|
344
|
+
const t = msg.payload.type;
|
|
345
|
+
if (t !== 'run.status' && t !== 'status' && t !== 'lifecycle.chain')
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
refresher.trigger();
|
|
349
|
+
},
|
|
350
|
+
onResync: () => {
|
|
351
|
+
void refresher.now();
|
|
352
|
+
if (tab === 'events') {
|
|
353
|
+
// Events may have been missed while disconnected: continue from the contiguous cursor.
|
|
354
|
+
evList.markPending();
|
|
355
|
+
void loadMoreEvents();
|
|
356
|
+
}
|
|
357
|
+
if (tab === 'logs' && follow)
|
|
358
|
+
void loadLog('tail');
|
|
359
|
+
},
|
|
360
|
+
dispose: () => {
|
|
361
|
+
refresher.dispose();
|
|
362
|
+
followRefresher.dispose();
|
|
363
|
+
if (pollTimer)
|
|
364
|
+
clearInterval(pollTimer);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Runs list (Spec §16): active and historical runs with paging and simple filters. */
|
|
2
|
+
import { api, qs } from '../api.js';
|
|
3
|
+
import { h, replaceChildren } from '../dom.js';
|
|
4
|
+
import { buildHash } from '../../shared/route.js';
|
|
5
|
+
import { createRefresher } from '../../shared/throttle.js';
|
|
6
|
+
import { formatDuration } from '../../shared/format.js';
|
|
7
|
+
import { isStateChange } from '../page.js';
|
|
8
|
+
import { errorBox, externalLink, loading, runLink, runOutcomeBadges, table, text, time } from '../ui.js';
|
|
9
|
+
import { cancelRunButton } from './dashboard.js';
|
|
10
|
+
const PAGE_SIZE = 50;
|
|
11
|
+
export const runsPage = (ctx) => {
|
|
12
|
+
ctx.setTitle('Runs');
|
|
13
|
+
const q = ctx.route.query;
|
|
14
|
+
const scope = q.scope === 'active' || q.scope === 'history' ? q.scope : 'all';
|
|
15
|
+
const workflow = q.workflow ?? '';
|
|
16
|
+
const trigger = q.trigger ?? '';
|
|
17
|
+
const result = q.result === 'success' || q.result === 'failure' ? q.result : '';
|
|
18
|
+
const offset = Math.max(0, Number.parseInt(q.offset ?? '0', 10) || 0);
|
|
19
|
+
const go = (patch) => {
|
|
20
|
+
ctx.navigate(buildHash(['runs'], { scope, workflow, trigger, result, offset: 0, ...patch }));
|
|
21
|
+
};
|
|
22
|
+
const tabs = h('nav', { class: 'tabs', 'aria-label': 'Run scope' }, ['all', 'active', 'history'].map((s) => h('a', { href: buildHash(['runs'], { scope: s, workflow, trigger, result }), class: s === scope ? 'active' : null, 'aria-current': s === scope ? 'page' : null }, s === 'all' ? 'All' : s === 'active' ? 'Active' : 'History')));
|
|
23
|
+
const wfSelect = h('select', { class: 'input', 'aria-label': 'Workflow', on: { change: () => go({ workflow: wfSelect.value }) } }, h('option', { value: '' }, 'All workflows'));
|
|
24
|
+
const trSelect = h('select', { class: 'input', 'aria-label': 'Trigger', on: { change: () => go({ trigger: trSelect.value }) } }, [['', 'All triggers'], ['source', 'source'], ['schedule', 'schedule'], ['manual', 'manual']].map(([v, l]) => h('option', { value: v, selected: v === trigger }, l)));
|
|
25
|
+
const resSelect = h('select', { class: 'input', 'aria-label': 'Business result', on: { change: () => go({ result: resSelect.value }) } }, [['', 'Any result'], ['success', 'result: success'], ['failure', 'result: failure (business)']].map(([v, l]) => h('option', { value: v, selected: v === result }, l)));
|
|
26
|
+
const filters = h('div', { class: 'filters' }, wfSelect, trSelect, resSelect);
|
|
27
|
+
const body = h('div', null, loading());
|
|
28
|
+
const el = h('div', { class: 'page' }, h('h1', null, 'Runs'), tabs, filters, body);
|
|
29
|
+
void api
|
|
30
|
+
.get('/api/v1/workflows')
|
|
31
|
+
.then((res) => {
|
|
32
|
+
for (const wf of res.workflows)
|
|
33
|
+
wfSelect.appendChild(h('option', { value: wf.id, selected: wf.id === workflow }, `${wf.name} (${wf.id})`));
|
|
34
|
+
if (workflow && !res.workflows.some((w) => w.id === workflow))
|
|
35
|
+
wfSelect.appendChild(h('option', { value: workflow, selected: true }, workflow));
|
|
36
|
+
})
|
|
37
|
+
.catch(() => undefined);
|
|
38
|
+
const load = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const res = await api.get(`/api/v1/runs${qs({ active: scope === 'active' ? 'true' : scope === 'history' ? 'false' : null, workflow, trigger, result, limit: PAGE_SIZE, offset })}`);
|
|
41
|
+
const rows = res.runs.map((r) => [
|
|
42
|
+
runLink(r.id),
|
|
43
|
+
h('span', null, r.workflowName ?? r.workflowId, h('br'), h('small', { class: 'muted' }, r.workflowId)),
|
|
44
|
+
r.triggerType,
|
|
45
|
+
r.sourceUrl ? externalLink(r.sourceUrl, r.sourceItemKey ?? undefined) : text(r.sourceItemKey ?? (r.scheduledFor ? `slot ${r.scheduledFor}` : null)),
|
|
46
|
+
runOutcomeBadges(r),
|
|
47
|
+
time(r.startedAt ?? r.createdAt),
|
|
48
|
+
formatDuration(r.durationMs),
|
|
49
|
+
text(r.active ? r.statusMessage : (r.resultMessage ?? r.finalReason)),
|
|
50
|
+
cancelRunButton(r, () => refresher.trigger()),
|
|
51
|
+
]);
|
|
52
|
+
const from = res.total === 0 ? 0 : res.offset + 1;
|
|
53
|
+
const to = res.offset + res.runs.length;
|
|
54
|
+
const pager = h('div', { class: 'pager' }, h('span', null, `${from}–${to} of ${res.total}`), offset > 0 ? h('a', { href: buildHash(['runs'], { scope, workflow, trigger, result, offset: Math.max(0, offset - PAGE_SIZE) }) }, '← Newer') : null, to < res.total ? h('a', { href: buildHash(['runs'], { scope, workflow, trigger, result, offset: offset + PAGE_SIZE }) }, 'Older →') : null);
|
|
55
|
+
replaceChildren(body, table(['Run', 'Workflow', 'Trigger', 'Item', 'Outcome', 'Started', 'Duration', 'Message', ''], rows, { empty: 'No runs match.' }), pager);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
replaceChildren(body, errorBox(err));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const refresher = createRefresher(load, 1500);
|
|
62
|
+
void load();
|
|
63
|
+
return {
|
|
64
|
+
el,
|
|
65
|
+
onLive: (msg) => {
|
|
66
|
+
if (isStateChange(msg) && (msg.type !== 'run.event' || msg.payload.type === 'run.status' || msg.payload.type === 'status'))
|
|
67
|
+
refresher.trigger();
|
|
68
|
+
},
|
|
69
|
+
onResync: () => void refresher.now(),
|
|
70
|
+
dispose: () => refresher.dispose(),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Views (Spec §17): read-only tables from YAML, sorting in the browser only. */
|
|
2
|
+
import { api, seg } from '../api.js';
|
|
3
|
+
import { h, replaceChildren } from '../dom.js';
|
|
4
|
+
import { cellText, nextSort, sortRows } from '../../shared/sort.js';
|
|
5
|
+
import { button, errorBox, externalLink, loading, routeLink, runLink, table, time } from '../ui.js';
|
|
6
|
+
export const viewsListPage = (ctx) => {
|
|
7
|
+
ctx.setTitle('All Views');
|
|
8
|
+
const body = h('div', null, loading());
|
|
9
|
+
const el = h('div', { class: 'page' }, h('h1', null, 'All Views'), body);
|
|
10
|
+
void api
|
|
11
|
+
.get('/api/v1/views')
|
|
12
|
+
.then((res) => replaceChildren(body, table(['View', 'Sources', 'File'], res.views.map((v) => [routeLink(['views', v.id], v.title), v.sources.join(', '), v.file ? h('code', null, v.file) : '—']), { empty: 'No views configured (views/*.yaml).' })))
|
|
13
|
+
.catch((err) => replaceChildren(body, errorBox(err)));
|
|
14
|
+
return { el };
|
|
15
|
+
};
|
|
16
|
+
export const viewPage = (ctx, id) => {
|
|
17
|
+
ctx.setTitle(`View ${id}`);
|
|
18
|
+
const title = h('h1', null, id);
|
|
19
|
+
const meta = h('div', { class: 'muted' });
|
|
20
|
+
const body = h('div', null, loading());
|
|
21
|
+
let data = null;
|
|
22
|
+
let sort = null;
|
|
23
|
+
const refreshBtn = button('Refresh', () => void load(true));
|
|
24
|
+
const el = h('div', { class: 'page' }, h('p', { class: 'crumbs' }, routeLink(['views'], '← All Views')), title, h('div', { class: 'actions' }, refreshBtn, meta), body);
|
|
25
|
+
const cellNode = (c, field) => {
|
|
26
|
+
if (!c.known)
|
|
27
|
+
return h('span', { class: 'unknown', title: 'Field not available for this item' }, '—');
|
|
28
|
+
const t = cellText(c);
|
|
29
|
+
if (field === 'run.last_id' && typeof c.value === 'string')
|
|
30
|
+
return runLink(c.value);
|
|
31
|
+
if (c.link)
|
|
32
|
+
return externalLink(c.link, t || c.link);
|
|
33
|
+
if (/(_at|^run\.last_(started|finished)_at)$/.test(field) && typeof c.value === 'string' && !Number.isNaN(Date.parse(c.value)))
|
|
34
|
+
return time(c.value);
|
|
35
|
+
return document.createTextNode(t);
|
|
36
|
+
};
|
|
37
|
+
const renderTable = () => {
|
|
38
|
+
if (!data)
|
|
39
|
+
return;
|
|
40
|
+
const d = data;
|
|
41
|
+
const rows = sort ? sortRows(d.rows, sort.column, d.columns[sort.column]?.type ?? 'string', sort.direction) : d.rows;
|
|
42
|
+
const headers = d.columns.map((c, i) => {
|
|
43
|
+
const active = sort?.column === i;
|
|
44
|
+
const dir = active ? sort.direction : null;
|
|
45
|
+
const btn = h('button', {
|
|
46
|
+
type: 'button',
|
|
47
|
+
class: 'sort',
|
|
48
|
+
title: 'Sort (in the browser only)',
|
|
49
|
+
on: {
|
|
50
|
+
click: () => {
|
|
51
|
+
sort = nextSort(sort, i);
|
|
52
|
+
renderTable();
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}, c.label, dir ? (dir === 'asc' ? ' ▲' : ' ▼') : '');
|
|
56
|
+
// aria-sort belongs on the column header (<th>), not on the button inside it.
|
|
57
|
+
return { cell: btn, attrs: { 'aria-sort': dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : 'none' } };
|
|
58
|
+
});
|
|
59
|
+
replaceChildren(body, d.errors.length ? h('div', { class: 'alert alert-error' }, d.errors.map((e) => h('div', null, `Source ${e.sourceId}: ${e.message}`))) : null, d.filterUnknown > 0 ? h('div', { class: 'alert alert-info' }, `${d.filterUnknown} item(s) not shown because the filter could not be evaluated (unknown).`) : null, d.truncated ? h('div', { class: 'alert alert-warn' }, 'Result truncated.') : null, table([...headers, ''], rows.map((r) => [...r.cells.map((c, i) => cellNode(c, d.columns[i]?.field ?? '')), r.lastRunId ? runLink(r.lastRunId, 'last run') : '']), { empty: 'No items match this view.' }));
|
|
60
|
+
};
|
|
61
|
+
const load = async (refresh = false) => {
|
|
62
|
+
refreshBtn.disabled = true;
|
|
63
|
+
try {
|
|
64
|
+
data = await api.get(`/api/v1/views/${seg(id)}/items${refresh ? '?refresh=true' : ''}`);
|
|
65
|
+
title.textContent = data.view.title;
|
|
66
|
+
ctx.setTitle(data.view.title);
|
|
67
|
+
meta.textContent = `${data.rows.length} item(s) · sources ${data.view.sources.join(', ')} · fetched ${new Date(data.fetchedAt).toLocaleTimeString()}${data.cached ? ' (cached)' : ''}`;
|
|
68
|
+
renderTable();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
replaceChildren(body, errorBox(err));
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
refreshBtn.disabled = false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
void load();
|
|
78
|
+
// Views are loaded on open/refresh only (source API calls); no live auto-refresh.
|
|
79
|
+
return { el, onResync: () => void load() };
|
|
80
|
+
};
|