@ryanfw/prompt-orchestration-pipeline 0.0.1

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.
@@ -0,0 +1,260 @@
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
+ updateConnectionStatus("connected");
173
+ } catch (error) {
174
+ console.error("Error parsing state event:", error);
175
+ }
176
+ });
177
+
178
+ // Handle connection open
179
+ eventSource.addEventListener("open", () => {
180
+ console.log("SSE connection established");
181
+ updateConnectionStatus("connected");
182
+ });
183
+
184
+ // Handle errors
185
+ eventSource.addEventListener("error", (error) => {
186
+ console.error("SSE connection error:", error);
187
+
188
+ if (eventSource.readyState === EventSource.CLOSED) {
189
+ updateConnectionStatus("disconnected");
190
+ scheduleReconnect();
191
+ } else {
192
+ updateConnectionStatus("reconnecting");
193
+ }
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Schedule reconnection attempt
199
+ */
200
+ function scheduleReconnect() {
201
+ if (reconnectTimer) return;
202
+
203
+ updateConnectionStatus("reconnecting");
204
+
205
+ reconnectTimer = setTimeout(() => {
206
+ console.log("Attempting to reconnect...");
207
+ connectSSE();
208
+ }, RECONNECT_DELAY);
209
+ }
210
+
211
+ /**
212
+ * Initialize the application
213
+ */
214
+ async function init() {
215
+ console.log("Initializing Pipeline Orchestrator UI...");
216
+
217
+ // Fetch initial state
218
+ await fetchInitialState();
219
+
220
+ // Connect to SSE
221
+ connectSSE();
222
+
223
+ // Update relative times every 10 seconds
224
+ setInterval(() => {
225
+ const updatedAt = elements.updatedAt.textContent;
226
+ if (updatedAt !== "Never") {
227
+ const timestamp = elements.updatedAt.title;
228
+ if (timestamp) {
229
+ elements.updatedAt.textContent = formatRelativeTime(
230
+ new Date(timestamp)
231
+ );
232
+ }
233
+ }
234
+
235
+ // Update change times
236
+ document.querySelectorAll(".change-time").forEach((el) => {
237
+ const absoluteTime = el.title;
238
+ if (absoluteTime) {
239
+ el.textContent = formatRelativeTime(new Date(absoluteTime));
240
+ }
241
+ });
242
+ }, 10000);
243
+ }
244
+
245
+ // Start the application when DOM is ready
246
+ if (document.readyState === "loading") {
247
+ document.addEventListener("DOMContentLoaded", init);
248
+ } else {
249
+ init();
250
+ }
251
+
252
+ // Clean up on page unload
253
+ window.addEventListener("beforeunload", () => {
254
+ if (eventSource) {
255
+ eventSource.close();
256
+ }
257
+ if (reconnectTimer) {
258
+ clearTimeout(reconnectTimer);
259
+ }
260
+ });
@@ -0,0 +1,53 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Pipeline Orchestrator - File Watcher</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1>Pipeline Orchestrator</h1>
13
+ <div class="connection-status" id="connectionStatus">
14
+ <span class="status-dot"></span>
15
+ <span class="status-text">Connecting...</span>
16
+ </div>
17
+ </header>
18
+
19
+ <main>
20
+ <section class="stats-section">
21
+ <div class="stat-card">
22
+ <div class="stat-label">Total Changes</div>
23
+ <div class="stat-value" id="changeCount">0</div>
24
+ </div>
25
+ <div class="stat-card">
26
+ <div class="stat-label">Last Updated</div>
27
+ <div class="stat-value stat-time" id="updatedAt">Never</div>
28
+ </div>
29
+ </section>
30
+
31
+ <section class="changes-section">
32
+ <h2>Recent Changes</h2>
33
+ <div class="watched-paths" id="watchedPaths">
34
+ <span class="label">Watching:</span>
35
+ <span class="paths">Loading...</span>
36
+ </div>
37
+ <div class="changes-list" id="changesList">
38
+ <div class="empty-state">
39
+ No changes yet. Modify files in watched directories to see
40
+ updates.
41
+ </div>
42
+ </div>
43
+ </section>
44
+ </main>
45
+
46
+ <footer>
47
+ <p>Watching for file changes in real-time via Server-Sent Events</p>
48
+ </footer>
49
+ </div>
50
+
51
+ <script src="/app.js"></script>
52
+ </body>
53
+ </html>
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Pipeline Orchestrator UI Styles
3
+ * Modern, clean, dark-mode friendly design
4
+ */
5
+
6
+ :root {
7
+ --bg-primary: #0f172a;
8
+ --bg-secondary: #1e293b;
9
+ --bg-tertiary: #334155;
10
+ --text-primary: #f1f5f9;
11
+ --text-secondary: #cbd5e1;
12
+ --text-muted: #94a3b8;
13
+ --border-color: #334155;
14
+ --accent-blue: #3b82f6;
15
+ --accent-green: #10b981;
16
+ --accent-yellow: #f59e0b;
17
+ --accent-red: #ef4444;
18
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family:
29
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
30
+ Arial, sans-serif;
31
+ background: var(--bg-primary);
32
+ color: var(--text-primary);
33
+ line-height: 1.6;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ .container {
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ padding: 2rem;
41
+ }
42
+
43
+ /* Header */
44
+ header {
45
+ display: flex;
46
+ justify-content: space-between;
47
+ align-items: center;
48
+ margin-bottom: 3rem;
49
+ padding-bottom: 1.5rem;
50
+ border-bottom: 2px solid var(--border-color);
51
+ }
52
+
53
+ h1 {
54
+ font-size: 2rem;
55
+ font-weight: 700;
56
+ color: var(--text-primary);
57
+ }
58
+
59
+ h2 {
60
+ font-size: 1.5rem;
61
+ font-weight: 600;
62
+ margin-bottom: 1rem;
63
+ color: var(--text-primary);
64
+ }
65
+
66
+ /* Connection Status */
67
+ .connection-status {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.5rem;
71
+ padding: 0.5rem 1rem;
72
+ background: var(--bg-secondary);
73
+ border-radius: 0.5rem;
74
+ border: 1px solid var(--border-color);
75
+ }
76
+
77
+ .status-dot {
78
+ width: 10px;
79
+ height: 10px;
80
+ border-radius: 50%;
81
+ background: var(--text-muted);
82
+ animation: pulse 2s ease-in-out infinite;
83
+ }
84
+
85
+ .status-dot.connected {
86
+ background: var(--accent-green);
87
+ animation: none;
88
+ }
89
+
90
+ .status-dot.reconnecting {
91
+ background: var(--accent-yellow);
92
+ }
93
+
94
+ .status-dot.disconnected {
95
+ background: var(--accent-red);
96
+ animation: none;
97
+ }
98
+
99
+ .status-text {
100
+ font-size: 0.875rem;
101
+ font-weight: 500;
102
+ color: var(--text-secondary);
103
+ }
104
+
105
+ @keyframes pulse {
106
+ 0%,
107
+ 100% {
108
+ opacity: 1;
109
+ }
110
+ 50% {
111
+ opacity: 0.5;
112
+ }
113
+ }
114
+
115
+ /* Stats Section */
116
+ .stats-section {
117
+ display: grid;
118
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
119
+ gap: 1.5rem;
120
+ margin-bottom: 3rem;
121
+ }
122
+
123
+ .stat-card {
124
+ background: var(--bg-secondary);
125
+ padding: 2rem;
126
+ border-radius: 0.75rem;
127
+ border: 1px solid var(--border-color);
128
+ box-shadow: var(--shadow);
129
+ transition:
130
+ transform 0.2s ease,
131
+ box-shadow 0.2s ease;
132
+ }
133
+
134
+ .stat-card:hover {
135
+ transform: translateY(-2px);
136
+ box-shadow: 0 8px 12px -2px rgba(0, 0, 0, 0.4);
137
+ }
138
+
139
+ .stat-label {
140
+ font-size: 0.875rem;
141
+ font-weight: 500;
142
+ color: var(--text-muted);
143
+ text-transform: uppercase;
144
+ letter-spacing: 0.05em;
145
+ margin-bottom: 0.5rem;
146
+ }
147
+
148
+ .stat-value {
149
+ font-size: 3rem;
150
+ font-weight: 700;
151
+ color: var(--accent-blue);
152
+ line-height: 1;
153
+ }
154
+
155
+ .stat-value.stat-time {
156
+ font-size: 1.5rem;
157
+ color: var(--text-primary);
158
+ }
159
+
160
+ /* Changes Section */
161
+ .changes-section {
162
+ background: var(--bg-secondary);
163
+ padding: 2rem;
164
+ border-radius: 0.75rem;
165
+ border: 1px solid var(--border-color);
166
+ box-shadow: var(--shadow);
167
+ }
168
+
169
+ .watched-paths {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 0.5rem;
173
+ margin-bottom: 1.5rem;
174
+ padding: 0.75rem 1rem;
175
+ background: var(--bg-tertiary);
176
+ border-radius: 0.5rem;
177
+ font-size: 0.875rem;
178
+ }
179
+
180
+ .watched-paths .label {
181
+ font-weight: 600;
182
+ color: var(--text-secondary);
183
+ }
184
+
185
+ .watched-paths .paths {
186
+ color: var(--accent-blue);
187
+ font-family: "Monaco", "Courier New", monospace;
188
+ }
189
+
190
+ /* Changes List */
191
+ .changes-list {
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 0.75rem;
195
+ }
196
+
197
+ .empty-state {
198
+ padding: 3rem 2rem;
199
+ text-align: center;
200
+ color: var(--text-muted);
201
+ font-size: 0.875rem;
202
+ }
203
+
204
+ .change-item {
205
+ background: var(--bg-tertiary);
206
+ padding: 1rem;
207
+ border-radius: 0.5rem;
208
+ border: 1px solid var(--border-color);
209
+ transition:
210
+ background 0.2s ease,
211
+ transform 0.2s ease;
212
+ animation: slideIn 0.3s ease;
213
+ }
214
+
215
+ .change-item:hover {
216
+ background: #3f4b5e;
217
+ transform: translateX(4px);
218
+ }
219
+
220
+ @keyframes slideIn {
221
+ from {
222
+ opacity: 0;
223
+ transform: translateY(-10px);
224
+ }
225
+ to {
226
+ opacity: 1;
227
+ transform: translateY(0);
228
+ }
229
+ }
230
+
231
+ .change-header {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ align-items: center;
235
+ margin-bottom: 0.5rem;
236
+ }
237
+
238
+ .change-type {
239
+ display: inline-block;
240
+ padding: 0.25rem 0.75rem;
241
+ border-radius: 0.25rem;
242
+ font-size: 0.75rem;
243
+ font-weight: 600;
244
+ text-transform: uppercase;
245
+ letter-spacing: 0.05em;
246
+ }
247
+
248
+ .change-type-created {
249
+ background: rgba(16, 185, 129, 0.2);
250
+ color: var(--accent-green);
251
+ }
252
+
253
+ .change-type-modified {
254
+ background: rgba(59, 130, 246, 0.2);
255
+ color: var(--accent-blue);
256
+ }
257
+
258
+ .change-type-deleted {
259
+ background: rgba(239, 68, 68, 0.2);
260
+ color: var(--accent-red);
261
+ }
262
+
263
+ .change-time {
264
+ font-size: 0.75rem;
265
+ color: var(--text-muted);
266
+ cursor: help;
267
+ }
268
+
269
+ .change-path {
270
+ font-family: "Monaco", "Courier New", monospace;
271
+ font-size: 0.875rem;
272
+ color: var(--text-secondary);
273
+ word-break: break-all;
274
+ }
275
+
276
+ /* Footer */
277
+ footer {
278
+ margin-top: 3rem;
279
+ padding-top: 2rem;
280
+ border-top: 1px solid var(--border-color);
281
+ text-align: center;
282
+ color: var(--text-muted);
283
+ font-size: 0.875rem;
284
+ }
285
+
286
+ /* Responsive Design */
287
+ @media (max-width: 768px) {
288
+ .container {
289
+ padding: 1rem;
290
+ }
291
+
292
+ header {
293
+ flex-direction: column;
294
+ gap: 1rem;
295
+ align-items: flex-start;
296
+ }
297
+
298
+ h1 {
299
+ font-size: 1.5rem;
300
+ }
301
+
302
+ .stats-section {
303
+ grid-template-columns: 1fr;
304
+ }
305
+
306
+ .stat-value {
307
+ font-size: 2.5rem;
308
+ }
309
+
310
+ .watched-paths {
311
+ flex-direction: column;
312
+ align-items: flex-start;
313
+ }
314
+ }
315
+
316
+ /* Accessibility */
317
+ @media (prefers-reduced-motion: reduce) {
318
+ * {
319
+ animation-duration: 0.01ms !important;
320
+ animation-iteration-count: 1 !important;
321
+ transition-duration: 0.01ms !important;
322
+ }
323
+ }
324
+
325
+ /* Light mode support (optional) */
326
+ @media (prefers-color-scheme: light) {
327
+ :root {
328
+ --bg-primary: #f8fafc;
329
+ --bg-secondary: #ffffff;
330
+ --bg-tertiary: #f1f5f9;
331
+ --text-primary: #0f172a;
332
+ --text-secondary: #475569;
333
+ --text-muted: #64748b;
334
+ --border-color: #e2e8f0;
335
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
336
+ }
337
+
338
+ .change-item:hover {
339
+ background: #e2e8f0;
340
+ }
341
+ }