@atolis-hq/wake 0.2.61 → 0.2.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters/http/ui-assets.js +22 -3
- package/dist/src/adapters/http/ui-data.js +17 -0
- package/dist/src/adapters/http/ui-server.js +41 -1
- package/dist/src/core/projection-updater.js +11 -1
- package/dist/src/domain/event-types.js +7 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
52
52
|
nav button:hover { color: #fff; }
|
|
53
53
|
nav button.active { color: var(--accent-light); border-bottom-color: var(--accent); }
|
|
54
54
|
main { padding: 1rem; }
|
|
55
|
-
.columns { display: grid; grid-template-columns: repeat(
|
|
55
|
+
.columns { display: grid; grid-template-columns: repeat(6, minmax(180px, 1fr)); gap: 0.6rem; overflow-x: auto; }
|
|
56
56
|
.col { background: #1a1d23; border-radius: 10px; padding: 0.5rem; min-height: 200px; }
|
|
57
57
|
.col h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: #9aa2ad; margin: 0.2rem 0.4rem 0.5rem; }
|
|
58
58
|
.card { background: #22262e; border: 1px solid #2c313a; border-radius: 8px; padding: 0.5rem; margin-bottom: 0.5rem; cursor: pointer; font-size: 0.8rem; transition: border-color 0.12s ease; }
|
|
@@ -154,7 +154,7 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
154
154
|
</div>
|
|
155
155
|
<script>
|
|
156
156
|
const API = '/api/v1';
|
|
157
|
-
const CONDITIONS = ['ready', 'active', 'needs-human', 'error', 'finished'];
|
|
157
|
+
const CONDITIONS = ['ready', 'scheduled', 'active', 'needs-human', 'error', 'finished'];
|
|
158
158
|
let currentView = 'board';
|
|
159
159
|
let analyticsWindow = '7d';
|
|
160
160
|
let analyticsMetric = 'runs-over-time';
|
|
@@ -312,7 +312,10 @@ async function renderBoard(context) {
|
|
|
312
312
|
...(item.labels && item.labels.length > 0
|
|
313
313
|
? [el('div', { class: 'meta' }, item.labels.map((label) => el('span', { class: 'chip chip-label', text: label })))]
|
|
314
314
|
: []),
|
|
315
|
-
|
|
315
|
+
...(item.activeMainRun
|
|
316
|
+
? []
|
|
317
|
+
: [el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason })]),
|
|
318
|
+
...(item.activeMainRun ? [renderChildRun(item.activeMainRun)] : []),
|
|
316
319
|
...((item.activeChildRuns || []).map(renderChildRun)),
|
|
317
320
|
]));
|
|
318
321
|
return el('div', { class: 'col' + (items.length === 0 ? ' col-empty' : '') }, [
|
|
@@ -424,6 +427,22 @@ function renderItemDetails(detail) {
|
|
|
424
427
|
});
|
|
425
428
|
body.appendChild(retryBtn);
|
|
426
429
|
}
|
|
430
|
+
if (detail.item.issue.labels.includes('wake:scheduled-workflow')) {
|
|
431
|
+
const runNowBtn = el('button', { type: 'button', class: 'btn', text: 'Run now' });
|
|
432
|
+
runNowBtn.addEventListener('click', async () => {
|
|
433
|
+
runNowBtn.disabled = true;
|
|
434
|
+
runNowBtn.textContent = 'Requesting run...';
|
|
435
|
+
try {
|
|
436
|
+
await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/run');
|
|
437
|
+
runNowBtn.textContent = 'Run requested';
|
|
438
|
+
} catch (err) {
|
|
439
|
+
runNowBtn.disabled = false;
|
|
440
|
+
runNowBtn.textContent = 'Run now';
|
|
441
|
+
document.getElementById('status-summary').textContent = 'run request failed: ' + err.message;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
body.appendChild(runNowBtn);
|
|
445
|
+
}
|
|
427
446
|
const resources = detail.item.correlatedResources || [];
|
|
428
447
|
if (resources.length > 0) {
|
|
429
448
|
body.appendChild(el('h3', { text: 'Resources' }));
|
|
@@ -68,6 +68,9 @@ function deriveCondition(item, lastRun, config) {
|
|
|
68
68
|
if (lastRun?.sentinel === 'FAILED') {
|
|
69
69
|
return { condition: 'error', reason: 'last run failed; awaiting operator/retry policy' };
|
|
70
70
|
}
|
|
71
|
+
if (item.issue.labels.includes('wake:scheduled-workflow')) {
|
|
72
|
+
return { condition: 'scheduled', reason: 'scheduled workflow awaiting next run' };
|
|
73
|
+
}
|
|
71
74
|
return { condition: 'ready', reason: 'has a route and no blocking condition' };
|
|
72
75
|
}
|
|
73
76
|
function timeInStageMs(item, now) {
|
|
@@ -107,6 +110,18 @@ export async function buildBoard(input) {
|
|
|
107
110
|
const lastRun = item.wake.lastRunId === undefined ? null : (runsById.get(item.wake.lastRunId) ?? null);
|
|
108
111
|
const { condition, reason } = deriveCondition(item, lastRun, input.config);
|
|
109
112
|
const activeChildRuns = activeChildRunsForItem(item, runs, input.now);
|
|
113
|
+
const activeMainRun = lastRun?.status === 'running'
|
|
114
|
+
? {
|
|
115
|
+
runId: lastRun.runId,
|
|
116
|
+
action: lastRun.action,
|
|
117
|
+
startedAt: lastRun.startedAt,
|
|
118
|
+
ageMs: input.now.getTime() - Date.parse(lastRun.startedAt),
|
|
119
|
+
...(lastRun.routing?.runnerName === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { runnerName: lastRun.routing.runnerName }),
|
|
122
|
+
...(lastRun.routing?.tier === undefined ? {} : { tier: lastRun.routing.tier }),
|
|
123
|
+
}
|
|
124
|
+
: undefined;
|
|
110
125
|
return {
|
|
111
126
|
repo: item.issue.repo,
|
|
112
127
|
number: item.issue.number,
|
|
@@ -120,6 +135,7 @@ export async function buildBoard(input) {
|
|
|
120
135
|
lastRunAction: lastRun?.action,
|
|
121
136
|
lastRunSentinel: lastRun?.sentinel,
|
|
122
137
|
lastRunStatus: lastRun?.status,
|
|
138
|
+
...(activeMainRun === undefined ? {} : { activeMainRun }),
|
|
123
139
|
...(activeChildRuns.length === 0 ? {} : { activeChildRuns }),
|
|
124
140
|
sessionId: item.wake.sessionId,
|
|
125
141
|
workspacePath: item.wake.workspacePath,
|
|
@@ -162,6 +178,7 @@ export async function buildStatus(input) {
|
|
|
162
178
|
const counters = {
|
|
163
179
|
'needs-human': 0,
|
|
164
180
|
active: 0,
|
|
181
|
+
scheduled: 0,
|
|
165
182
|
ready: 0,
|
|
166
183
|
error: 0,
|
|
167
184
|
finished: 0,
|
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { createProjectionUpdater } from '../../core/projection-updater.js';
|
|
6
|
-
import { RETRY_REQUESTED_EVENT } from '../../domain/event-types.js';
|
|
6
|
+
import { RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT } from '../../domain/event-types.js';
|
|
7
7
|
import { configuredTicketSource } from '../../domain/sources.js';
|
|
8
8
|
import { createEventEnvelope } from '../../lib/event-log.js';
|
|
9
9
|
import { writeJsonFile } from '../../lib/json-file.js';
|
|
@@ -137,6 +137,46 @@ async function handleRequest(req, res, options, now, projectionUpdater) {
|
|
|
137
137
|
sendJson(res, 202, { workItemKey, retryEventId: retryId });
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
+
if (req.method === 'POST' &&
|
|
141
|
+
resource === 'work-items' &&
|
|
142
|
+
segments.length === 3 &&
|
|
143
|
+
segments[2] === 'run') {
|
|
144
|
+
const workItemKey = segments[1] ?? '';
|
|
145
|
+
const item = await stateStore.readIssueState(workItemKey);
|
|
146
|
+
if (item === null) {
|
|
147
|
+
sendJson(res, 404, { error: 'work item not found' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!item.issue.labels.includes('wake:scheduled-workflow')) {
|
|
151
|
+
sendJson(res, 409, { error: 'work item is not a scheduled workflow' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const occurredAt = now().toISOString();
|
|
155
|
+
const runRequestId = `run-${workItemKey}-${now().getTime()}`;
|
|
156
|
+
const runRequestEvent = createEventEnvelope({
|
|
157
|
+
eventId: runRequestId,
|
|
158
|
+
workItemKey,
|
|
159
|
+
streamScope: 'work-item',
|
|
160
|
+
direction: 'internal',
|
|
161
|
+
sourceSystem: 'wake',
|
|
162
|
+
sourceEventType: RUN_REQUESTED_EVENT,
|
|
163
|
+
sourceRefs: { repo: item.issue.repo, issueNumber: item.issue.number },
|
|
164
|
+
occurredAt,
|
|
165
|
+
ingestedAt: occurredAt,
|
|
166
|
+
trigger: 'immediate',
|
|
167
|
+
payload: { requestedBy: 'ui' },
|
|
168
|
+
});
|
|
169
|
+
const appended = await stateStore.appendEventEnvelope(runRequestEvent);
|
|
170
|
+
await projectionUpdater.rebuildFromEvents([appended]);
|
|
171
|
+
const tickRequest = {
|
|
172
|
+
requestId: randomUUID(),
|
|
173
|
+
requestedAt: now().toISOString(),
|
|
174
|
+
requestedBy: 'ui:run',
|
|
175
|
+
};
|
|
176
|
+
await writeJsonFile(stateStore.paths.tickRequestFile, tickRequest);
|
|
177
|
+
sendJson(res, 202, { workItemKey, runEventId: runRequestId });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
140
180
|
if (req.method === 'GET' &&
|
|
141
181
|
resource === 'work-items' &&
|
|
142
182
|
segments.length === 3 &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, WORKFLOW_SELECTED_EVENT, WORKSPACE_CLEANED_EVENT, } from '../domain/event-types.js';
|
|
1
|
+
import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, WORKFLOW_SELECTED_EVENT, WORKSPACE_CLEANED_EVENT, } from '../domain/event-types.js';
|
|
2
2
|
import { UNRESOLVED_WORK_ITEM_KEY, parseIssueStateRecord } from '../domain/schema.js';
|
|
3
3
|
import { doneRunnerSentinel, stageFromLabels } from '../domain/stages.js';
|
|
4
4
|
import { builtInDefaultWorkflowDefinition, defaultWorkflowName, selectWorkflowForEvent, workflowStageVocabulary, } from '../domain/workflows.js';
|
|
@@ -343,6 +343,16 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
343
343
|
},
|
|
344
344
|
});
|
|
345
345
|
}
|
|
346
|
+
if (event.sourceEventType === RUN_REQUESTED_EVENT) {
|
|
347
|
+
return parseIssueStateRecord({
|
|
348
|
+
...current,
|
|
349
|
+
wake: {
|
|
350
|
+
...current.wake,
|
|
351
|
+
syncedAt: event.ingestedAt,
|
|
352
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
346
356
|
if (event.sourceEventType === WORKSPACE_CLEANED_EVENT) {
|
|
347
357
|
return parseIssueStateRecord({
|
|
348
358
|
...current,
|
|
@@ -10,6 +10,7 @@ export const PUBLISH_FAILED_EVENT = 'wake.publish.failed';
|
|
|
10
10
|
export const PUBLISH_INTENT_REQUESTED_EVENT = 'wake.publish.intent.requested';
|
|
11
11
|
export const PUBLISH_SENT_UNCONFIRMED_EVENT = 'wake.publish.sent-unconfirmed';
|
|
12
12
|
export const RETRY_REQUESTED_EVENT = 'wake.retry.requested';
|
|
13
|
+
export const RUN_REQUESTED_EVENT = 'wake.run.requested';
|
|
13
14
|
export const RUN_CLAIMED_EVENT = 'wake.run.claimed';
|
|
14
15
|
export const RUN_COMPLETED_EVENT = 'wake.run.completed';
|
|
15
16
|
export const WORKFLOW_SELECTED_EVENT = 'wake.workflow.selected';
|
|
@@ -29,6 +30,7 @@ export const wakeEventTypeValues = [
|
|
|
29
30
|
PUBLISH_INTENT_REQUESTED_EVENT,
|
|
30
31
|
PUBLISH_SENT_UNCONFIRMED_EVENT,
|
|
31
32
|
RETRY_REQUESTED_EVENT,
|
|
33
|
+
RUN_REQUESTED_EVENT,
|
|
32
34
|
RUN_CLAIMED_EVENT,
|
|
33
35
|
RUN_COMPLETED_EVENT,
|
|
34
36
|
WORKFLOW_SELECTED_EVENT,
|
|
@@ -97,6 +99,11 @@ export const wakeEventTypeDefinitions = [
|
|
|
97
99
|
description: 'Requests that a failed work item be retried.',
|
|
98
100
|
payloadShape: '{ requestedBy }',
|
|
99
101
|
},
|
|
102
|
+
{
|
|
103
|
+
type: RUN_REQUESTED_EVENT,
|
|
104
|
+
description: 'Requests that a scheduled work item be run immediately.',
|
|
105
|
+
payloadShape: '{ requestedBy }',
|
|
106
|
+
},
|
|
100
107
|
{
|
|
101
108
|
type: RUN_CLAIMED_EVENT,
|
|
102
109
|
description: 'Records that Wake claimed a work item for an agent run.',
|
package/dist/src/version.js
CHANGED