@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.
Files changed (34) hide show
  1. package/package.json +2 -1
  2. package/src/components/DAGGrid.jsx +157 -47
  3. package/src/components/ui/RestartJobModal.jsx +26 -6
  4. package/src/components/ui/StopJobModal.jsx +183 -0
  5. package/src/core/config.js +7 -3
  6. package/src/core/lifecycle-policy.js +62 -0
  7. package/src/core/pipeline-runner.js +312 -217
  8. package/src/core/status-writer.js +84 -0
  9. package/src/pages/Code.jsx +8 -1
  10. package/src/pages/PipelineDetail.jsx +85 -3
  11. package/src/pages/PromptPipelineDashboard.jsx +10 -11
  12. package/src/ui/client/adapters/job-adapter.js +60 -0
  13. package/src/ui/client/api.js +233 -8
  14. package/src/ui/client/hooks/useJobList.js +14 -1
  15. package/src/ui/dist/app.js +262 -0
  16. package/src/ui/dist/assets/{index-DeDzq-Kk.js → index-B320avRx.js} +4854 -2104
  17. package/src/ui/dist/assets/index-B320avRx.js.map +1 -0
  18. package/src/ui/dist/assets/style-BYCoLBnK.css +62 -0
  19. package/src/ui/dist/favicon.svg +12 -0
  20. package/src/ui/dist/index.html +2 -2
  21. package/src/ui/endpoints/file-endpoints.js +330 -0
  22. package/src/ui/endpoints/job-control-endpoints.js +1001 -0
  23. package/src/ui/endpoints/job-endpoints.js +62 -0
  24. package/src/ui/endpoints/sse-endpoints.js +223 -0
  25. package/src/ui/endpoints/state-endpoint.js +85 -0
  26. package/src/ui/endpoints/upload-endpoints.js +406 -0
  27. package/src/ui/express-app.js +182 -0
  28. package/src/ui/server.js +38 -1880
  29. package/src/ui/sse-broadcast.js +93 -0
  30. package/src/ui/utils/http-utils.js +139 -0
  31. package/src/ui/utils/mime-types.js +196 -0
  32. package/src/ui/vite.config.js +22 -0
  33. package/src/utils/jobs.js +39 -0
  34. 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
+ });