@ryanfw/prompt-orchestration-pipeline 0.11.0 → 0.12.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/package.json +2 -1
- package/src/components/DAGGrid.jsx +157 -47
- package/src/components/ui/RestartJobModal.jsx +26 -6
- package/src/components/ui/StopJobModal.jsx +183 -0
- package/src/core/config.js +7 -3
- package/src/core/lifecycle-policy.js +62 -0
- package/src/core/pipeline-runner.js +312 -217
- package/src/core/status-writer.js +84 -0
- package/src/pages/Code.jsx +8 -1
- package/src/pages/PipelineDetail.jsx +85 -3
- package/src/pages/PromptPipelineDashboard.jsx +10 -11
- package/src/ui/client/adapters/job-adapter.js +60 -0
- package/src/ui/client/api.js +233 -8
- package/src/ui/client/hooks/useJobList.js +14 -1
- package/src/ui/dist/app.js +262 -0
- package/src/ui/dist/assets/{index-DeDzq-Kk.js → index-B320avRx.js} +4854 -2104
- package/src/ui/dist/assets/index-B320avRx.js.map +1 -0
- package/src/ui/dist/assets/style-BYCoLBnK.css +62 -0
- package/src/ui/dist/favicon.svg +12 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/file-endpoints.js +330 -0
- package/src/ui/endpoints/job-control-endpoints.js +1001 -0
- package/src/ui/endpoints/job-endpoints.js +62 -0
- package/src/ui/endpoints/sse-endpoints.js +223 -0
- package/src/ui/endpoints/state-endpoint.js +85 -0
- package/src/ui/endpoints/upload-endpoints.js +406 -0
- package/src/ui/express-app.js +182 -0
- package/src/ui/server.js +38 -1880
- package/src/ui/sse-broadcast.js +93 -0
- package/src/ui/utils/http-utils.js +139 -0
- package/src/ui/utils/mime-types.js +196 -0
- package/src/ui/vite.config.js +22 -0
- package/src/utils/jobs.js +39 -0
- package/src/ui/dist/assets/style-aBtD_Yrs.css +0 -62
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend application for Pipeline Orchestrator UI
|
|
3
|
+
* Handles SSE connection, state updates, and UI rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// DOM elements
|
|
7
|
+
const elements = {
|
|
8
|
+
changeCount: document.getElementById("changeCount"),
|
|
9
|
+
updatedAt: document.getElementById("updatedAt"),
|
|
10
|
+
changesList: document.getElementById("changesList"),
|
|
11
|
+
watchedPaths: document.getElementById("watchedPaths"),
|
|
12
|
+
connectionStatus: document.getElementById("connectionStatus"),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Connection state
|
|
16
|
+
let eventSource = null;
|
|
17
|
+
let reconnectTimer = null;
|
|
18
|
+
const RECONNECT_DELAY = 3000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format timestamp as relative time
|
|
22
|
+
*/
|
|
23
|
+
function formatRelativeTime(timestamp) {
|
|
24
|
+
if (!timestamp) return "Never";
|
|
25
|
+
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const then = new Date(timestamp);
|
|
28
|
+
const diffMs = now - then;
|
|
29
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
30
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
31
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
32
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
33
|
+
|
|
34
|
+
if (diffSec < 10) return "Just now";
|
|
35
|
+
if (diffSec < 60) return `${diffSec} seconds ago`;
|
|
36
|
+
if (diffMin === 1) return "1 minute ago";
|
|
37
|
+
if (diffMin < 60) return `${diffMin} minutes ago`;
|
|
38
|
+
if (diffHour === 1) return "1 hour ago";
|
|
39
|
+
if (diffHour < 24) return `${diffHour} hours ago`;
|
|
40
|
+
if (diffDay === 1) return "1 day ago";
|
|
41
|
+
return `${diffDay} days ago`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format absolute timestamp
|
|
46
|
+
*/
|
|
47
|
+
function formatAbsoluteTime(timestamp) {
|
|
48
|
+
if (!timestamp) return "Never";
|
|
49
|
+
const date = new Date(timestamp);
|
|
50
|
+
return date.toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Update connection status indicator
|
|
55
|
+
*/
|
|
56
|
+
function updateConnectionStatus(status) {
|
|
57
|
+
const statusDot = elements.connectionStatus.querySelector(".status-dot");
|
|
58
|
+
const statusText = elements.connectionStatus.querySelector(".status-text");
|
|
59
|
+
|
|
60
|
+
statusDot.className = "status-dot";
|
|
61
|
+
|
|
62
|
+
switch (status) {
|
|
63
|
+
case "connected":
|
|
64
|
+
statusDot.classList.add("connected");
|
|
65
|
+
statusText.textContent = "Connected";
|
|
66
|
+
break;
|
|
67
|
+
case "reconnecting":
|
|
68
|
+
statusDot.classList.add("reconnecting");
|
|
69
|
+
statusText.textContent = "Reconnecting...";
|
|
70
|
+
break;
|
|
71
|
+
case "disconnected":
|
|
72
|
+
statusDot.classList.add("disconnected");
|
|
73
|
+
statusText.textContent = "Disconnected";
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
statusText.textContent = "Connecting...";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the UI with current state
|
|
82
|
+
*/
|
|
83
|
+
function renderState(state) {
|
|
84
|
+
// Update change count
|
|
85
|
+
elements.changeCount.textContent = state.changeCount || 0;
|
|
86
|
+
|
|
87
|
+
// Update last updated time
|
|
88
|
+
elements.updatedAt.textContent = formatRelativeTime(state.updatedAt);
|
|
89
|
+
elements.updatedAt.title = formatAbsoluteTime(state.updatedAt);
|
|
90
|
+
|
|
91
|
+
// Update watched paths
|
|
92
|
+
if (state.watchedPaths && state.watchedPaths.length > 0) {
|
|
93
|
+
const pathsSpan = elements.watchedPaths.querySelector(".paths");
|
|
94
|
+
pathsSpan.textContent = state.watchedPaths.join(", ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Render recent changes
|
|
98
|
+
if (state.recentChanges && state.recentChanges.length > 0) {
|
|
99
|
+
elements.changesList.innerHTML = state.recentChanges
|
|
100
|
+
.map((change) => {
|
|
101
|
+
const typeClass = `change-type-${change.type}`;
|
|
102
|
+
const typeLabel =
|
|
103
|
+
change.type.charAt(0).toUpperCase() + change.type.slice(1);
|
|
104
|
+
const relativeTime = formatRelativeTime(change.timestamp);
|
|
105
|
+
const absoluteTime = formatAbsoluteTime(change.timestamp);
|
|
106
|
+
|
|
107
|
+
return `
|
|
108
|
+
<div class="change-item">
|
|
109
|
+
<div class="change-header">
|
|
110
|
+
<span class="change-type ${typeClass}">${typeLabel}</span>
|
|
111
|
+
<span class="change-time" title="${absoluteTime}">${relativeTime}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="change-path">${escapeHtml(change.path)}</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
})
|
|
117
|
+
.join("");
|
|
118
|
+
} else {
|
|
119
|
+
elements.changesList.innerHTML =
|
|
120
|
+
'<div class="empty-state">No changes yet. Modify files in watched directories to see updates.</div>';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Escape HTML to prevent XSS
|
|
126
|
+
*/
|
|
127
|
+
function escapeHtml(text) {
|
|
128
|
+
const div = document.createElement("div");
|
|
129
|
+
div.textContent = text;
|
|
130
|
+
return div.innerHTML;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fetch initial state from API
|
|
135
|
+
*/
|
|
136
|
+
async function fetchInitialState() {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch("/api/state");
|
|
139
|
+
if (!response.ok) throw new Error("Failed to fetch state");
|
|
140
|
+
const state = await response.json();
|
|
141
|
+
renderState(state);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("Error fetching initial state:", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Connect to SSE endpoint
|
|
149
|
+
*/
|
|
150
|
+
function connectSSE() {
|
|
151
|
+
// Clean up existing connection
|
|
152
|
+
if (eventSource) {
|
|
153
|
+
eventSource.close();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clear reconnect timer
|
|
157
|
+
if (reconnectTimer) {
|
|
158
|
+
clearTimeout(reconnectTimer);
|
|
159
|
+
reconnectTimer = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateConnectionStatus("connecting");
|
|
163
|
+
|
|
164
|
+
// Create new EventSource
|
|
165
|
+
eventSource = new EventSource("/api/events");
|
|
166
|
+
|
|
167
|
+
// Handle state updates
|
|
168
|
+
eventSource.addEventListener("state", (event) => {
|
|
169
|
+
try {
|
|
170
|
+
const state = JSON.parse(event.data);
|
|
171
|
+
renderState(state);
|
|
172
|
+
// Do not derive connection health from receipt of state payloads.
|
|
173
|
+
// Connection status should be driven by EventSource.readyState (open/error)
|
|
174
|
+
// or an explicit health/ping endpoint. Keep this handler focused on state updates.
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error("Error parsing state event:", error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Handle connection open
|
|
181
|
+
eventSource.addEventListener("open", () => {
|
|
182
|
+
console.log("SSE connection established");
|
|
183
|
+
updateConnectionStatus("connected");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Handle errors
|
|
187
|
+
eventSource.addEventListener("error", (error) => {
|
|
188
|
+
console.error("SSE connection error:", error);
|
|
189
|
+
|
|
190
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
191
|
+
updateConnectionStatus("disconnected");
|
|
192
|
+
scheduleReconnect();
|
|
193
|
+
} else {
|
|
194
|
+
updateConnectionStatus("reconnecting");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Schedule reconnection attempt
|
|
201
|
+
*/
|
|
202
|
+
function scheduleReconnect() {
|
|
203
|
+
if (reconnectTimer) return;
|
|
204
|
+
|
|
205
|
+
updateConnectionStatus("reconnecting");
|
|
206
|
+
|
|
207
|
+
reconnectTimer = setTimeout(() => {
|
|
208
|
+
console.log("Attempting to reconnect...");
|
|
209
|
+
connectSSE();
|
|
210
|
+
}, RECONNECT_DELAY);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Initialize the application
|
|
215
|
+
*/
|
|
216
|
+
async function init() {
|
|
217
|
+
console.log("Initializing Pipeline Orchestrator UI...");
|
|
218
|
+
|
|
219
|
+
// Fetch initial state
|
|
220
|
+
await fetchInitialState();
|
|
221
|
+
|
|
222
|
+
// Connect to SSE
|
|
223
|
+
connectSSE();
|
|
224
|
+
|
|
225
|
+
// Update relative times every 10 seconds
|
|
226
|
+
setInterval(() => {
|
|
227
|
+
const updatedAt = elements.updatedAt.textContent;
|
|
228
|
+
if (updatedAt !== "Never") {
|
|
229
|
+
const timestamp = elements.updatedAt.title;
|
|
230
|
+
if (timestamp) {
|
|
231
|
+
elements.updatedAt.textContent = formatRelativeTime(
|
|
232
|
+
new Date(timestamp)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Update change times
|
|
238
|
+
document.querySelectorAll(".change-time").forEach((el) => {
|
|
239
|
+
const absoluteTime = el.title;
|
|
240
|
+
if (absoluteTime) {
|
|
241
|
+
el.textContent = formatRelativeTime(new Date(absoluteTime));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}, 10000);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Start the application when DOM is ready
|
|
248
|
+
if (document.readyState === "loading") {
|
|
249
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
250
|
+
} else {
|
|
251
|
+
init();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Clean up on page unload
|
|
255
|
+
window.addEventListener("beforeunload", () => {
|
|
256
|
+
if (eventSource) {
|
|
257
|
+
eventSource.close();
|
|
258
|
+
}
|
|
259
|
+
if (reconnectTimer) {
|
|
260
|
+
clearTimeout(reconnectTimer);
|
|
261
|
+
}
|
|
262
|
+
});
|