@atolis-hq/wake 0.1.20 → 0.1.22
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.
|
@@ -35,6 +35,9 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
35
35
|
.topbar .version { color: rgba(255, 255, 255, 0.7); font-size: 0.78rem; }
|
|
36
36
|
.statusbar { display: flex; align-items: center; gap: 1rem; padding: 0.45rem 1rem; background: var(--brand-dark); border-top: 1px solid rgba(0, 0, 0, 0.18); flex-wrap: wrap; font-size: 0.8rem; }
|
|
37
37
|
.statusbar .meta { color: rgba(255, 255, 255, 0.72); }
|
|
38
|
+
.statusbar button { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); color: #fff; border-radius: 6px; padding: 0.22rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
|
|
39
|
+
.statusbar button:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
|
|
40
|
+
.statusbar button:disabled { cursor: wait; opacity: 0.62; }
|
|
38
41
|
.pill { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
|
39
42
|
.pill-idle { background: #1f3d2c; color: #7fe3a3; }
|
|
40
43
|
.pill-polling { background: #1f3350; color: #7fb3ff; }
|
|
@@ -80,6 +83,7 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
80
83
|
</header>
|
|
81
84
|
<div class="statusbar">
|
|
82
85
|
<span id="loop-pill" class="pill">…</span>
|
|
86
|
+
<button id="force-tick" type="button">Tick now</button>
|
|
83
87
|
<span id="status-summary" class="meta"></span>
|
|
84
88
|
</div>
|
|
85
89
|
<nav>
|
|
@@ -102,6 +106,12 @@ async function getJson(path) {
|
|
|
102
106
|
return res.json();
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
async function postJson(path) {
|
|
110
|
+
const res = await fetch(API + path, { method: 'POST' });
|
|
111
|
+
if (!res.ok) throw new Error(path + ' -> ' + res.status);
|
|
112
|
+
return res.json();
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function fmtMs(ms) {
|
|
106
116
|
if (ms === undefined || ms === null) return '—';
|
|
107
117
|
const s = Math.floor(ms / 1000);
|
|
@@ -142,6 +152,24 @@ async function renderStatusBar() {
|
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
async function forceTickNow() {
|
|
156
|
+
const button = document.getElementById('force-tick');
|
|
157
|
+
button.disabled = true;
|
|
158
|
+
const original = button.textContent;
|
|
159
|
+
button.textContent = 'Requested';
|
|
160
|
+
try {
|
|
161
|
+
await postJson('/tick');
|
|
162
|
+
await renderStatusBar();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
document.getElementById('status-summary').textContent = 'tick request failed: ' + err.message;
|
|
165
|
+
} finally {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
button.disabled = false;
|
|
168
|
+
button.textContent = original;
|
|
169
|
+
}, 900);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
async function renderBoard() {
|
|
146
174
|
const board = await getJson('/board');
|
|
147
175
|
const main = document.getElementById('main');
|
|
@@ -320,6 +348,7 @@ for (const btn of document.querySelectorAll('nav button')) {
|
|
|
320
348
|
document.getElementById('drawer-close').addEventListener('click', () => {
|
|
321
349
|
document.getElementById('drawer').classList.remove('open');
|
|
322
350
|
});
|
|
351
|
+
document.getElementById('force-tick').addEventListener('click', forceTickNow);
|
|
323
352
|
|
|
324
353
|
renderStatusBar();
|
|
325
354
|
switchView('board');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { configuredTicketSource } from '../../domain/sources.js';
|
|
4
|
+
import { writeJsonFile } from '../../lib/json-file.js';
|
|
3
5
|
import { indexHtml } from './ui-assets.js';
|
|
4
6
|
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
5
7
|
function sendJson(res, status, body) {
|
|
@@ -77,12 +79,6 @@ async function handleRequest(req, res, options, now) {
|
|
|
77
79
|
res.writeHead(404).end('not found');
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
80
|
-
if (req.method !== 'GET') {
|
|
81
|
-
sendJson(res, 405, {
|
|
82
|
-
error: 'this build only serves read endpoints; mutations are not implemented',
|
|
83
|
-
});
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
82
|
const { stateStore, resourceIndex, config } = options;
|
|
87
83
|
const segments = url.pathname
|
|
88
84
|
.slice('/api/v1/'.length)
|
|
@@ -90,6 +86,20 @@ async function handleRequest(req, res, options, now) {
|
|
|
90
86
|
.filter((part) => part.length > 0)
|
|
91
87
|
.map((s) => decodeURIComponent(s));
|
|
92
88
|
const resource = segments[0];
|
|
89
|
+
if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
|
|
90
|
+
const request = {
|
|
91
|
+
requestId: randomUUID(),
|
|
92
|
+
requestedAt: now().toISOString(),
|
|
93
|
+
requestedBy: 'ui',
|
|
94
|
+
};
|
|
95
|
+
await writeJsonFile(stateStore.paths.tickRequestFile, request);
|
|
96
|
+
sendJson(res, 202, request);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (req.method !== 'GET') {
|
|
100
|
+
sendJson(res, 405, { error: `method not allowed for ${url.pathname}` });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
93
103
|
if (resource === 'status' && segments.length === 1) {
|
|
94
104
|
sendJson(res, 200, await buildStatus({ stateStore, config, now: now() }));
|
|
95
105
|
return;
|
|
@@ -6,11 +6,43 @@ export function createControlPlane(deps) {
|
|
|
6
6
|
let consecutiveIdleTicks = 0;
|
|
7
7
|
let consecutiveIntakeIdleTicks = 0;
|
|
8
8
|
let consecutiveRunnerIdleTicks = 0;
|
|
9
|
+
let lastSingleTickRequest;
|
|
10
|
+
let lastIntakeTickRequest;
|
|
11
|
+
let lastRunnerTickRequest;
|
|
9
12
|
const maxIntervalMs = deps.maxIntervalMs ?? deps.intervalMs * 16;
|
|
10
13
|
function nextSleepMs(consecutiveTicks) {
|
|
11
14
|
const backoffMs = deps.intervalMs * 2 ** Math.min(consecutiveTicks, 20);
|
|
12
15
|
return Math.min(backoffMs, maxIntervalMs);
|
|
13
16
|
}
|
|
17
|
+
async function tickRequestPending(input) {
|
|
18
|
+
const request = (await deps.readTickRequest?.()) ?? null;
|
|
19
|
+
if (request === null || request === input.getLastRequest()) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
input.setLastRequest(request);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
async function sleepUntilNextTick(ms, requestState) {
|
|
26
|
+
if (deps.readTickRequest === undefined || ms <= 0) {
|
|
27
|
+
await deps.sleep(ms);
|
|
28
|
+
return { forced: false };
|
|
29
|
+
}
|
|
30
|
+
if (await tickRequestPending(requestState)) {
|
|
31
|
+
return { forced: true };
|
|
32
|
+
}
|
|
33
|
+
const deadline = Date.now() + ms;
|
|
34
|
+
while (running) {
|
|
35
|
+
const remainingMs = deadline - Date.now();
|
|
36
|
+
if (remainingMs <= 0) {
|
|
37
|
+
return { forced: false };
|
|
38
|
+
}
|
|
39
|
+
await deps.sleep(Math.min(remainingMs, 250));
|
|
40
|
+
if (await tickRequestPending(requestState)) {
|
|
41
|
+
return { forced: true };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { forced: false };
|
|
45
|
+
}
|
|
14
46
|
async function isPaused() {
|
|
15
47
|
return Boolean(await deps.isPaused());
|
|
16
48
|
}
|
|
@@ -58,8 +90,13 @@ export function createControlPlane(deps) {
|
|
|
58
90
|
input.setIdleTicks(0);
|
|
59
91
|
}
|
|
60
92
|
else {
|
|
61
|
-
await
|
|
62
|
-
|
|
93
|
+
const wait = await sleepUntilNextTick(nextSleepMs(input.getIdleTicks()), {
|
|
94
|
+
getLastRequest: input.getLastRequest,
|
|
95
|
+
setLastRequest: input.setLastRequest,
|
|
96
|
+
});
|
|
97
|
+
if (!wait.forced) {
|
|
98
|
+
input.setIdleTicks(input.getIdleTicks() + 1);
|
|
99
|
+
}
|
|
63
100
|
}
|
|
64
101
|
}
|
|
65
102
|
}
|
|
@@ -94,6 +131,10 @@ export function createControlPlane(deps) {
|
|
|
94
131
|
setIdleTicks: (value) => {
|
|
95
132
|
consecutiveIntakeIdleTicks = value;
|
|
96
133
|
},
|
|
134
|
+
getLastRequest: () => lastIntakeTickRequest,
|
|
135
|
+
setLastRequest: (value) => {
|
|
136
|
+
lastIntakeTickRequest = value;
|
|
137
|
+
},
|
|
97
138
|
}),
|
|
98
139
|
startLoop({
|
|
99
140
|
run: deps.tickRunner.runRunnerTick,
|
|
@@ -102,6 +143,10 @@ export function createControlPlane(deps) {
|
|
|
102
143
|
setIdleTicks: (value) => {
|
|
103
144
|
consecutiveRunnerIdleTicks = value;
|
|
104
145
|
},
|
|
146
|
+
getLastRequest: () => lastRunnerTickRequest,
|
|
147
|
+
setLastRequest: (value) => {
|
|
148
|
+
lastRunnerTickRequest = value;
|
|
149
|
+
},
|
|
105
150
|
}),
|
|
106
151
|
]);
|
|
107
152
|
return;
|
|
@@ -122,8 +167,15 @@ export function createControlPlane(deps) {
|
|
|
122
167
|
consecutiveIdleTicks = 0;
|
|
123
168
|
}
|
|
124
169
|
else {
|
|
125
|
-
await
|
|
126
|
-
|
|
170
|
+
const wait = await sleepUntilNextTick(nextSleepMs(consecutiveIdleTicks), {
|
|
171
|
+
getLastRequest: () => lastSingleTickRequest,
|
|
172
|
+
setLastRequest: (value) => {
|
|
173
|
+
lastSingleTickRequest = value;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (!wait.forced) {
|
|
177
|
+
consecutiveIdleTicks += 1;
|
|
178
|
+
}
|
|
127
179
|
}
|
|
128
180
|
}
|
|
129
181
|
},
|
package/dist/src/lib/paths.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createWakePaths(wakeRoot) {
|
|
|
12
12
|
configFile: join(wakeRoot, 'config.json'),
|
|
13
13
|
ledgerFile: join(wakeRoot, 'ledger.json'),
|
|
14
14
|
pauseFile: join(wakeRoot, 'PAUSE'),
|
|
15
|
+
tickRequestFile: join(wakeRoot, 'control', 'tick-request.json'),
|
|
15
16
|
tickLockFile: join(wakeRoot, 'locks', 'tick.lock'),
|
|
16
17
|
runnerLockFile: join(wakeRoot, 'locks', 'runner.lock'),
|
|
17
18
|
issueFixtureFile: join(wakeRoot, 'fixtures', 'issues.json'),
|
package/dist/src/main.js
CHANGED
|
@@ -26,6 +26,7 @@ import { createControlPlane } from './core/control-plane.js';
|
|
|
26
26
|
import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
|
|
27
27
|
import { createTickRunner } from './core/tick-runner.js';
|
|
28
28
|
import { systemClock } from './lib/clock.js';
|
|
29
|
+
import { readJsonFile } from './lib/json-file.js';
|
|
29
30
|
import { configuredTicketSource } from './domain/sources.js';
|
|
30
31
|
import { wakeVersion } from './version.js';
|
|
31
32
|
function commandArgsBeforeTerminator(args) {
|
|
@@ -135,6 +136,17 @@ async function runCommandCapture(command, args) {
|
|
|
135
136
|
});
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
async function readTickRequestId(path) {
|
|
140
|
+
try {
|
|
141
|
+
const request = await readJsonFile(path);
|
|
142
|
+
return typeof request.requestId === 'string' && request.requestId.length > 0
|
|
143
|
+
? request.requestId
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
138
150
|
async function inspectDockerImage(image) {
|
|
139
151
|
return await new Promise((resolveInspect, reject) => {
|
|
140
152
|
const child = spawn('docker', ['image', 'inspect', image], {
|
|
@@ -318,6 +330,9 @@ async function runStart(args) {
|
|
|
318
330
|
setTimeout(resolveSleep, ms);
|
|
319
331
|
});
|
|
320
332
|
},
|
|
333
|
+
readTickRequest() {
|
|
334
|
+
return readTickRequestId(runtime.stateStore.paths.tickRequestFile);
|
|
335
|
+
},
|
|
321
336
|
});
|
|
322
337
|
const stop = () => controlPlane.stop();
|
|
323
338
|
process.on('SIGINT', stop);
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "0.1.
|
|
1
|
+
export const wakeVersion = "0.1.22+gf88059c";
|