@synkro/ui 0.1.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 buemura
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @synkro/ui
2
+
3
+ Dashboard UI for [@synkro/core](https://github.com/buemura/synkro). Mount it on any HTTP endpoint to visualize your registered events and workflows in real time.
4
+
5
+ ## Screenshots
6
+
7
+ ### Dashboard
8
+
9
+ Overview of all registered events and workflows with paginated tables.
10
+
11
+ ![Dashboard](./docs/dashboard-screenshot.png)
12
+
13
+ ### Event Detail
14
+
15
+ Click any event to view its message metrics — received, completed, and failed counts tracked via Redis.
16
+
17
+ ![Event Detail](./docs/event-detail-screenshot.png)
18
+
19
+ ### Workflow Detail
20
+
21
+ Click any workflow to see a branching flow diagram with SVG connectors (green for success, red for failure) and a detailed steps table.
22
+
23
+ ![Workflow Detail](./docs/workflow-detail-screenshot.png)
24
+
25
+ ## Features
26
+
27
+ - **Events overview** — Paginated table of all standalone events with retry configuration
28
+ - **Event detail** — Click an event to see received/completed/failed message counts (Redis-persisted)
29
+ - **Workflows overview** — Paginated table of all workflows with step counts and callback badges
30
+ - **Workflow detail** — Branching flow diagram with SVG bezier curves for onSuccess/onFailure paths, plus a detailed steps table
31
+ - **Stats at a glance** — Total counts for events, workflows, and workflow steps
32
+ - **Pagination** — 5 items per page with independent pagination for events and workflows
33
+ - **Framework-agnostic** — Works with Express, Fastify, raw Node.js HTTP, or any framework that supports `(IncomingMessage, ServerResponse)` handlers
34
+ - **Zero dependencies** — Self-contained HTML/CSS/JS dashboard with no external assets
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @synkro/ui
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```typescript
45
+ import { createDashboardHandler } from "@synkro/ui";
46
+ import { Synkro } from "@synkro/core";
47
+
48
+ const synkro = await Synkro.start({
49
+ transport: "redis",
50
+ connectionUrl: "redis://localhost:6379",
51
+ events: [/* ... */],
52
+ workflows: [/* ... */],
53
+ });
54
+ ```
55
+
56
+ ### Express
57
+
58
+ ```typescript
59
+ import express from "express";
60
+
61
+ const app = express();
62
+
63
+ app.use("/synkro", createDashboardHandler(synkro, { basePath: "/synkro" }));
64
+
65
+ app.listen(3000);
66
+ // Dashboard available at http://localhost:3000/synkro
67
+ ```
68
+
69
+ ### Raw Node.js HTTP
70
+
71
+ ```typescript
72
+ import http from "node:http";
73
+
74
+ const server = http.createServer(createDashboardHandler(synkro));
75
+
76
+ server.listen(3000);
77
+ // Dashboard available at http://localhost:3000
78
+ ```
79
+
80
+ ## API
81
+
82
+ ### `createDashboardHandler(synkro, options?)`
83
+
84
+ Returns a standard Node.js HTTP request handler `(IncomingMessage, ServerResponse) => void`.
85
+
86
+ **Parameters:**
87
+
88
+ | Parameter | Type | Description |
89
+ |---|---|---|
90
+ | `synkro` | `Synkro` | A started Synkro instance |
91
+ | `options.basePath` | `string` | Base path where the dashboard is mounted (default: `"/"`) |
92
+
93
+ **Served routes (relative to basePath):**
94
+
95
+ | Route | Description |
96
+ |---|---|
97
+ | `GET /` | Dashboard HTML page |
98
+ | `GET /api/introspection` | JSON payload with all registered events and workflows |
99
+ | `GET /api/events/:type` | JSON payload with message metrics for a specific event |
100
+
101
+ ## License
102
+
103
+ ISC
@@ -0,0 +1,2 @@
1
+ export declare function getDashboardHtml(): string;
2
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,IAAI,MAAM,CAs5BzC"}
@@ -0,0 +1,920 @@
1
+ export function getDashboardHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Synkro Dashboard</title>
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --bg: #0a0a0f;
13
+ --surface: #12121a;
14
+ --surface-hover: #1a1a25;
15
+ --border: #1e1e2e;
16
+ --text: #e2e2e8;
17
+ --text-muted: #6e6e82;
18
+ --accent: #7c6af6;
19
+ --accent-dim: #7c6af620;
20
+ --success: #34d399;
21
+ --success-dim: #34d39920;
22
+ --danger: #f87171;
23
+ --danger-dim: #f8717120;
24
+ --warning: #fbbf24;
25
+ --warning-dim: #fbbf2420;
26
+ --radius: 10px;
27
+ }
28
+
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ line-height: 1.5;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ .container {
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ padding: 32px 24px;
41
+ }
42
+
43
+ header {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ margin-bottom: 40px;
48
+ }
49
+
50
+ .logo {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 12px;
54
+ }
55
+
56
+ .logo-icon {
57
+ width: 36px;
58
+ height: 36px;
59
+ background: var(--accent);
60
+ border-radius: 8px;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ font-weight: 700;
65
+ font-size: 16px;
66
+ color: #fff;
67
+ }
68
+
69
+ .logo h1 {
70
+ font-size: 22px;
71
+ font-weight: 600;
72
+ letter-spacing: -0.5px;
73
+ }
74
+
75
+ .logo h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
76
+
77
+ .btn {
78
+ background: var(--surface);
79
+ border: 1px solid var(--border);
80
+ color: var(--text);
81
+ padding: 8px 16px;
82
+ border-radius: 8px;
83
+ cursor: pointer;
84
+ font-size: 13px;
85
+ transition: background 0.15s;
86
+ text-decoration: none;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ }
91
+
92
+ .btn:hover { background: var(--surface-hover); }
93
+
94
+ .stats {
95
+ display: grid;
96
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
97
+ gap: 16px;
98
+ margin-bottom: 40px;
99
+ }
100
+
101
+ .stat-card {
102
+ background: var(--surface);
103
+ border: 1px solid var(--border);
104
+ border-radius: var(--radius);
105
+ padding: 20px;
106
+ }
107
+
108
+ .stat-card .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
109
+ .stat-card .value { font-size: 32px; font-weight: 700; margin-top: 4px; }
110
+ .stat-card.accent .value { color: var(--accent); }
111
+ .stat-card.success .value { color: var(--success); }
112
+ .stat-card.danger .value { color: var(--danger); }
113
+
114
+ .section { margin-bottom: 40px; }
115
+
116
+ .section-header {
117
+ font-size: 16px;
118
+ font-weight: 600;
119
+ margin-bottom: 16px;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 8px;
123
+ }
124
+
125
+ .section-header .count {
126
+ background: var(--accent-dim);
127
+ color: var(--accent);
128
+ font-size: 12px;
129
+ padding: 2px 8px;
130
+ border-radius: 99px;
131
+ font-weight: 500;
132
+ }
133
+
134
+ /* Events Table */
135
+ .events-table {
136
+ width: 100%;
137
+ border-collapse: collapse;
138
+ background: var(--surface);
139
+ border: 1px solid var(--border);
140
+ border-radius: var(--radius);
141
+ overflow: hidden;
142
+ }
143
+
144
+ .events-table th {
145
+ text-align: left;
146
+ padding: 12px 16px;
147
+ font-size: 11px;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.5px;
150
+ color: var(--text-muted);
151
+ border-bottom: 1px solid var(--border);
152
+ background: var(--surface);
153
+ }
154
+
155
+ .events-table td {
156
+ padding: 12px 16px;
157
+ border-bottom: 1px solid var(--border);
158
+ font-size: 14px;
159
+ }
160
+
161
+ .events-table tr:last-child td { border-bottom: none; }
162
+ .events-table tr.clickable { cursor: pointer; transition: background 0.15s; }
163
+ .events-table tr.clickable:hover { background: var(--surface-hover); }
164
+
165
+ .event-type {
166
+ font-family: 'SF Mono', 'Fira Code', monospace;
167
+ font-size: 13px;
168
+ color: var(--accent);
169
+ }
170
+
171
+ .badge {
172
+ display: inline-block;
173
+ font-size: 11px;
174
+ padding: 2px 8px;
175
+ border-radius: 99px;
176
+ font-weight: 500;
177
+ }
178
+
179
+ .badge-retry {
180
+ background: var(--warning-dim);
181
+ color: var(--warning);
182
+ }
183
+
184
+ .badge-none {
185
+ background: var(--border);
186
+ color: var(--text-muted);
187
+ }
188
+
189
+ /* Workflow Cards */
190
+ .workflow-card {
191
+ background: var(--surface);
192
+ border: 1px solid var(--border);
193
+ border-radius: var(--radius);
194
+ padding: 24px;
195
+ margin-bottom: 16px;
196
+ }
197
+
198
+ .workflow-name {
199
+ font-family: 'SF Mono', 'Fira Code', monospace;
200
+ font-size: 15px;
201
+ font-weight: 600;
202
+ margin-bottom: 20px;
203
+ color: var(--accent);
204
+ }
205
+
206
+ .workflow-callbacks {
207
+ display: flex;
208
+ gap: 12px;
209
+ margin-top: 16px;
210
+ padding-top: 16px;
211
+ border-top: 1px solid var(--border);
212
+ flex-wrap: wrap;
213
+ }
214
+
215
+ .callback-tag {
216
+ font-size: 12px;
217
+ padding: 4px 10px;
218
+ border-radius: 6px;
219
+ font-family: 'SF Mono', 'Fira Code', monospace;
220
+ }
221
+
222
+ .callback-complete { background: var(--accent-dim); color: var(--accent); }
223
+ .callback-success { background: var(--success-dim); color: var(--success); }
224
+ .callback-failure { background: var(--danger-dim); color: var(--danger); }
225
+
226
+ /* Workflow Flow Diagram */
227
+ .workflow-flow {
228
+ position: relative;
229
+ overflow-x: auto;
230
+ padding: 8px 0;
231
+ }
232
+
233
+ .workflow-flow svg {
234
+ position: absolute;
235
+ top: 0;
236
+ left: 0;
237
+ pointer-events: none;
238
+ }
239
+
240
+ .flow-grid {
241
+ display: grid;
242
+ grid-auto-flow: column;
243
+ grid-template-rows: auto auto auto;
244
+ gap: 16px 40px;
245
+ align-items: center;
246
+ position: relative;
247
+ }
248
+
249
+ .flow-node {
250
+ background: var(--bg);
251
+ border: 1px solid var(--border);
252
+ border-radius: 8px;
253
+ padding: 12px 16px;
254
+ min-width: 140px;
255
+ text-align: center;
256
+ }
257
+
258
+ .flow-node.branch-success { border-color: var(--success); border-width: 1px; }
259
+ .flow-node.branch-failure { border-color: var(--danger); border-width: 1px; }
260
+
261
+ .flow-node .node-type {
262
+ font-family: 'SF Mono', 'Fira Code', monospace;
263
+ font-size: 12px;
264
+ font-weight: 500;
265
+ }
266
+
267
+ .flow-node .node-label {
268
+ font-size: 10px;
269
+ margin-top: 4px;
270
+ }
271
+
272
+ .flow-node .node-label.label-success { color: var(--success); }
273
+ .flow-node .node-label.label-failure { color: var(--danger); }
274
+
275
+ .flow-spacer {
276
+ visibility: hidden;
277
+ min-width: 140px;
278
+ padding: 12px 16px;
279
+ }
280
+
281
+ .empty-state {
282
+ text-align: center;
283
+ padding: 48px;
284
+ color: var(--text-muted);
285
+ background: var(--surface);
286
+ border: 1px solid var(--border);
287
+ border-radius: var(--radius);
288
+ }
289
+
290
+ .empty-state p { font-size: 14px; }
291
+
292
+ .loading {
293
+ text-align: center;
294
+ padding: 80px;
295
+ color: var(--text-muted);
296
+ }
297
+
298
+ /* Event Detail */
299
+ .back-link {
300
+ color: var(--text-muted);
301
+ text-decoration: none;
302
+ font-size: 13px;
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: 6px;
306
+ margin-bottom: 24px;
307
+ cursor: pointer;
308
+ transition: color 0.15s;
309
+ }
310
+
311
+ .back-link:hover { color: var(--text); }
312
+
313
+ .detail-header {
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: space-between;
317
+ margin-bottom: 32px;
318
+ }
319
+
320
+ .detail-title {
321
+ font-family: 'SF Mono', 'Fira Code', monospace;
322
+ font-size: 20px;
323
+ font-weight: 600;
324
+ color: var(--accent);
325
+ }
326
+
327
+ .detail-badge { margin-left: 12px; }
328
+
329
+ /* Pagination */
330
+ .pagination {
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: space-between;
334
+ padding: 12px 16px;
335
+ background: var(--surface);
336
+ border: 1px solid var(--border);
337
+ border-top: none;
338
+ border-radius: 0 0 var(--radius) var(--radius);
339
+ font-size: 13px;
340
+ color: var(--text-muted);
341
+ }
342
+
343
+ .pagination-buttons {
344
+ display: flex;
345
+ gap: 8px;
346
+ }
347
+
348
+ .pagination-btn {
349
+ background: var(--bg);
350
+ border: 1px solid var(--border);
351
+ color: var(--text);
352
+ padding: 4px 12px;
353
+ border-radius: 6px;
354
+ cursor: pointer;
355
+ font-size: 12px;
356
+ transition: background 0.15s;
357
+ }
358
+
359
+ .pagination-btn:hover:not(:disabled) { background: var(--surface-hover); }
360
+ .pagination-btn:disabled { opacity: 0.3; cursor: default; }
361
+
362
+ .events-table.has-pagination { border-radius: var(--radius) var(--radius) 0 0; }
363
+ </style>
364
+ </head>
365
+ <body>
366
+ <div class="container">
367
+ <header>
368
+ <div class="logo">
369
+ <div class="logo-icon">S</div>
370
+ <h1>Synkro <span>Dashboard</span></h1>
371
+ </div>
372
+ <button class="btn" id="header-action">Refresh</button>
373
+ </header>
374
+ <div id="content">
375
+ <div class="loading">Loading...</div>
376
+ </div>
377
+ </div>
378
+
379
+ <script>
380
+ function getBase() {
381
+ const p = window.location.pathname;
382
+ return p.endsWith('/') ? p : p + '/';
383
+ }
384
+
385
+ let cachedIntrospection = null;
386
+ var PAGE_SIZE = 5;
387
+ var eventsPage = 0;
388
+ var workflowsPage = 0;
389
+
390
+ async function fetchIntrospection() {
391
+ const res = await fetch(getBase() + 'api/introspection');
392
+ cachedIntrospection = await res.json();
393
+ return cachedIntrospection;
394
+ }
395
+
396
+ async function fetchEventMetrics(eventType) {
397
+ const res = await fetch(getBase() + 'api/events/' + encodeURIComponent(eventType));
398
+ return await res.json();
399
+ }
400
+
401
+ function route() {
402
+ const hash = window.location.hash || '#/';
403
+ const eventMatch = hash.match(/^#\\/events\\/(.+)$/);
404
+ const workflowMatch = hash.match(/^#\\/workflows\\/(.+)$/);
405
+
406
+ if (eventMatch) {
407
+ const eventType = decodeURIComponent(eventMatch[1]);
408
+ showEventDetail(eventType);
409
+ } else if (workflowMatch) {
410
+ const workflowName = decodeURIComponent(workflowMatch[1]);
411
+ showWorkflowDetail(workflowName);
412
+ } else {
413
+ showDashboard();
414
+ }
415
+ }
416
+
417
+ async function showDashboard() {
418
+ const btn = document.getElementById('header-action');
419
+ btn.textContent = 'Refresh';
420
+ btn.onclick = function() { eventsPage = 0; workflowsPage = 0; showDashboard(); };
421
+
422
+ try {
423
+ const data = await fetchIntrospection();
424
+ renderDashboard(data);
425
+ } catch (err) {
426
+ document.getElementById('content').innerHTML =
427
+ '<div class="empty-state"><p>Failed to load data. Check the console for errors.</p></div>';
428
+ console.error('Synkro Dashboard: Failed to fetch introspection data', err);
429
+ }
430
+ }
431
+
432
+ async function showEventDetail(eventType) {
433
+ const btn = document.getElementById('header-action');
434
+ btn.textContent = 'Refresh';
435
+ btn.onclick = () => showEventDetail(eventType);
436
+
437
+ document.getElementById('content').innerHTML = '<div class="loading">Loading...</div>';
438
+
439
+ try {
440
+ if (!cachedIntrospection) await fetchIntrospection();
441
+ const eventInfo = cachedIntrospection.events.find(e => e.type === eventType);
442
+ const metrics = await fetchEventMetrics(eventType);
443
+ renderEventDetail(eventType, eventInfo, metrics);
444
+ } catch (err) {
445
+ document.getElementById('content').innerHTML =
446
+ '<div class="empty-state"><p>Failed to load event data.</p></div>';
447
+ console.error('Synkro Dashboard: Failed to fetch event metrics', err);
448
+ }
449
+ }
450
+
451
+ async function showWorkflowDetail(workflowName) {
452
+ const btn = document.getElementById('header-action');
453
+ btn.textContent = 'Refresh';
454
+ btn.onclick = () => showWorkflowDetail(workflowName);
455
+
456
+ document.getElementById('content').innerHTML = '<div class="loading">Loading...</div>';
457
+
458
+ try {
459
+ if (!cachedIntrospection) await fetchIntrospection();
460
+ const wf = cachedIntrospection.workflows.find(w => w.name === workflowName);
461
+ if (!wf) {
462
+ document.getElementById('content').innerHTML =
463
+ '<div class="empty-state"><p>Workflow not found.</p></div>';
464
+ return;
465
+ }
466
+ renderWorkflowDetail(wf);
467
+ } catch (err) {
468
+ document.getElementById('content').innerHTML =
469
+ '<div class="empty-state"><p>Failed to load workflow data.</p></div>';
470
+ console.error('Synkro Dashboard: Failed to fetch workflow data', err);
471
+ }
472
+ }
473
+
474
+ function renderWorkflowDetail(wf) {
475
+ let html = '';
476
+
477
+ html += '<a class="back-link" onclick="window.location.hash=\\'#/\\'">\u2190 Back to Dashboard</a>';
478
+
479
+ html += '<div class="detail-header">';
480
+ html += '<div>';
481
+ html += '<div class="detail-title">' + esc(wf.name) + '</div>';
482
+ html += '</div>';
483
+ html += '</div>';
484
+
485
+ // Stats
486
+ var branchTargets = new Set();
487
+ for (var s = 0; s < wf.steps.length; s++) {
488
+ if (wf.steps[s].onSuccess) branchTargets.add(wf.steps[s].onSuccess);
489
+ if (wf.steps[s].onFailure) branchTargets.add(wf.steps[s].onFailure);
490
+ }
491
+ var mainCount = wf.steps.filter(function(st) { return !branchTargets.has(st.type); }).length;
492
+
493
+ html += '<div class="stats">';
494
+ html += statCard('Total Steps', wf.steps.length, 'accent');
495
+ html += statCard('Main Flow', mainCount);
496
+ html += statCard('Branches', branchTargets.size);
497
+ html += '</div>';
498
+
499
+ // Flow diagram
500
+ html += '<div class="section">';
501
+ html += '<div class="section-header">Flow Diagram</div>';
502
+ html += workflowCard(wf);
503
+ html += '</div>';
504
+
505
+ // Steps table
506
+ html += '<div class="section">';
507
+ html += '<div class="section-header">Steps <span class="count">' + wf.steps.length + '</span></div>';
508
+ html += '<table class="events-table">';
509
+ html += '<thead><tr><th>Step</th><th>Retries</th><th>On Success</th><th>On Failure</th></tr></thead>';
510
+ html += '<tbody>';
511
+ for (var i = 0; i < wf.steps.length; i++) {
512
+ var step = wf.steps[i];
513
+ var retryBadge = step.retry
514
+ ? '<span class="badge badge-retry">' + step.retry.maxRetries + ' retries</span>'
515
+ : '<span class="badge badge-none">No retry</span>';
516
+ var successBadge = step.onSuccess
517
+ ? '<span class="badge badge-retry">' + esc(step.onSuccess) + '</span>'
518
+ : '<span class="badge badge-none">\u2014</span>';
519
+ var failBadge = step.onFailure
520
+ ? '<span class="badge" style="background:var(--danger-dim);color:var(--danger)">' + esc(step.onFailure) + '</span>'
521
+ : '<span class="badge badge-none">\u2014</span>';
522
+ html += '<tr><td class="event-type">' + esc(step.type) + '</td><td>' + retryBadge + '</td><td>' + successBadge + '</td><td>' + failBadge + '</td></tr>';
523
+ }
524
+ html += '</tbody></table>';
525
+ html += '</div>';
526
+
527
+ document.getElementById('content').innerHTML = html;
528
+ requestAnimationFrame(drawFlowConnections);
529
+ }
530
+
531
+ function renderDashboard(data) {
532
+ const { events, workflows } = data;
533
+ let html = '';
534
+
535
+ // Stats
536
+ html += '<div class="stats">';
537
+ html += statCard('Events', events.length);
538
+ html += statCard('Workflows', workflows.length);
539
+ const totalSteps = workflows.reduce((sum, w) => sum + w.steps.length, 0);
540
+ html += statCard('Workflow Steps', totalSteps);
541
+ html += '</div>';
542
+
543
+ // Events
544
+ html += '<div class="section">';
545
+ html += '<div class="section-header">Events <span class="count">' + events.length + '</span></div>';
546
+ if (events.length === 0) {
547
+ html += '<div class="empty-state"><p>No events registered</p></div>';
548
+ } else {
549
+ var eTotalPages = Math.ceil(events.length / PAGE_SIZE);
550
+ if (eventsPage >= eTotalPages) eventsPage = eTotalPages - 1;
551
+ var eStart = eventsPage * PAGE_SIZE;
552
+ var eSlice = events.slice(eStart, eStart + PAGE_SIZE);
553
+ var needsEPag = events.length > PAGE_SIZE;
554
+
555
+ html += '<table class="events-table' + (needsEPag ? ' has-pagination' : '') + '">';
556
+ html += '<thead><tr><th>Event Type</th><th>Retries</th></tr></thead>';
557
+ html += '<tbody>';
558
+ for (var ei = 0; ei < eSlice.length; ei++) {
559
+ var event = eSlice[ei];
560
+ var retryBadge = event.retry
561
+ ? '<span class="badge badge-retry">' + event.retry.maxRetries + ' retries</span>'
562
+ : '<span class="badge badge-none">No retry</span>';
563
+ html += '<tr class="clickable" onclick="window.location.hash=\\'#/events/' + encodeURIComponent(event.type) + '\\'"><td class="event-type">' + esc(event.type) + '</td><td>' + retryBadge + '</td></tr>';
564
+ }
565
+ html += '</tbody></table>';
566
+
567
+ if (needsEPag) {
568
+ html += '<div class="pagination">';
569
+ html += '<span>' + (eStart + 1) + '\u2013' + Math.min(eStart + PAGE_SIZE, events.length) + ' of ' + events.length + '</span>';
570
+ html += '<div class="pagination-buttons">';
571
+ html += '<button class="pagination-btn" id="events-prev"' + (eventsPage === 0 ? ' disabled' : '') + '>\u2190 Prev</button>';
572
+ html += '<button class="pagination-btn" id="events-next"' + (eventsPage >= eTotalPages - 1 ? ' disabled' : '') + '>Next \u2192</button>';
573
+ html += '</div></div>';
574
+ }
575
+ }
576
+ html += '</div>';
577
+
578
+ // Workflows
579
+ html += '<div class="section">';
580
+ html += '<div class="section-header">Workflows <span class="count">' + workflows.length + '</span></div>';
581
+ if (workflows.length === 0) {
582
+ html += '<div class="empty-state"><p>No workflows registered</p></div>';
583
+ } else {
584
+ var wTotalPages = Math.ceil(workflows.length / PAGE_SIZE);
585
+ if (workflowsPage >= wTotalPages) workflowsPage = wTotalPages - 1;
586
+ var wStart = workflowsPage * PAGE_SIZE;
587
+ var wSlice = workflows.slice(wStart, wStart + PAGE_SIZE);
588
+ var needsWPag = workflows.length > PAGE_SIZE;
589
+
590
+ html += '<table class="events-table' + (needsWPag ? ' has-pagination' : '') + '">';
591
+ html += '<thead><tr><th>Workflow Name</th><th>Steps</th><th>Callbacks</th></tr></thead>';
592
+ html += '<tbody>';
593
+ for (var wi = 0; wi < wSlice.length; wi++) {
594
+ var wf = wSlice[wi];
595
+ var callbacks = [];
596
+ if (wf.onComplete) callbacks.push('onComplete');
597
+ if (wf.onSuccess) callbacks.push('onSuccess');
598
+ if (wf.onFailure) callbacks.push('onFailure');
599
+ var callbacksHtml = callbacks.length > 0
600
+ ? callbacks.map(function(c) { return '<span class="badge badge-' + (c === 'onComplete' ? 'none' : c === 'onSuccess' ? 'retry' : 'none') + '">' + c + '</span>'; }).join(' ')
601
+ : '<span class="badge badge-none">None</span>';
602
+ html += '<tr class="clickable" onclick="window.location.hash=\\'#/workflows/' + encodeURIComponent(wf.name) + '\\'"><td class="event-type">' + esc(wf.name) + '</td><td>' + wf.steps.length + ' steps</td><td>' + callbacksHtml + '</td></tr>';
603
+ }
604
+ html += '</tbody></table>';
605
+
606
+ if (needsWPag) {
607
+ html += '<div class="pagination">';
608
+ html += '<span>' + (wStart + 1) + '\u2013' + Math.min(wStart + PAGE_SIZE, workflows.length) + ' of ' + workflows.length + '</span>';
609
+ html += '<div class="pagination-buttons">';
610
+ html += '<button class="pagination-btn" id="workflows-prev"' + (workflowsPage === 0 ? ' disabled' : '') + '>\u2190 Prev</button>';
611
+ html += '<button class="pagination-btn" id="workflows-next"' + (workflowsPage >= wTotalPages - 1 ? ' disabled' : '') + '>Next \u2192</button>';
612
+ html += '</div></div>';
613
+ }
614
+ }
615
+ html += '</div>';
616
+
617
+ document.getElementById('content').innerHTML = html;
618
+
619
+ // Bind pagination buttons
620
+ var eprev = document.getElementById('events-prev');
621
+ var enext = document.getElementById('events-next');
622
+ var wprev = document.getElementById('workflows-prev');
623
+ var wnext = document.getElementById('workflows-next');
624
+ if (eprev) eprev.onclick = function() { eventsPage--; renderDashboard(data); };
625
+ if (enext) enext.onclick = function() { eventsPage++; renderDashboard(data); };
626
+ if (wprev) wprev.onclick = function() { workflowsPage--; renderDashboard(data); };
627
+ if (wnext) wnext.onclick = function() { workflowsPage++; renderDashboard(data); };
628
+ }
629
+
630
+ function drawFlowConnections() {
631
+ var flows = document.querySelectorAll('.workflow-flow');
632
+ flows.forEach(function(flow) {
633
+ var grid = flow.querySelector('.flow-grid');
634
+ if (!grid) return;
635
+
636
+ var nodes = grid.querySelectorAll('.flow-node');
637
+ if (nodes.length === 0) return;
638
+
639
+ // Remove old SVG
640
+ var oldSvg = flow.querySelector('svg');
641
+ if (oldSvg) oldSvg.remove();
642
+
643
+ var flowRect = flow.getBoundingClientRect();
644
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
645
+ svg.setAttribute('width', grid.scrollWidth);
646
+ svg.setAttribute('height', grid.scrollHeight);
647
+ svg.style.width = grid.scrollWidth + 'px';
648
+ svg.style.height = grid.scrollHeight + 'px';
649
+
650
+ var gridRect = grid.getBoundingClientRect();
651
+
652
+ function nodeRect(id) {
653
+ var el = grid.querySelector('[data-id="' + id + '"]');
654
+ if (!el) return null;
655
+ var r = el.getBoundingClientRect();
656
+ return {
657
+ left: r.left - gridRect.left,
658
+ right: r.right - gridRect.left,
659
+ top: r.top - gridRect.top,
660
+ bottom: r.bottom - gridRect.top,
661
+ cx: (r.left + r.right) / 2 - gridRect.left,
662
+ cy: (r.top + r.bottom) / 2 - gridRect.top
663
+ };
664
+ }
665
+
666
+ function makePath(d, color, dashed) {
667
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
668
+ path.setAttribute('d', d);
669
+ path.setAttribute('fill', 'none');
670
+ path.setAttribute('stroke', color);
671
+ path.setAttribute('stroke-width', '2');
672
+ if (dashed) path.setAttribute('stroke-dasharray', '6 4');
673
+ svg.appendChild(path);
674
+ }
675
+
676
+ function makeArrowHead(x, y, color) {
677
+ var poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
678
+ poly.setAttribute('points', (x - 6) + ',' + (y - 4) + ' ' + x + ',' + y + ' ' + (x - 6) + ',' + (y + 4));
679
+ poly.setAttribute('fill', color);
680
+ svg.appendChild(poly);
681
+ }
682
+
683
+ // Find how many main columns exist
684
+ var mainNodes = grid.querySelectorAll('[data-id^="main-"]');
685
+ var colCount = mainNodes.length;
686
+
687
+ for (var col = 0; col < colCount; col++) {
688
+ var main = nodeRect('main-' + col);
689
+ var nextMain = nodeRect('main-' + (col + 1));
690
+ var succBranch = nodeRect('branch-success-' + col);
691
+ var failBranch = nodeRect('branch-failure-' + col);
692
+
693
+ var seqColor = '#6e6e82';
694
+ var successColor = '#34d399';
695
+ var failColor = '#f87171';
696
+
697
+ if (succBranch && failBranch) {
698
+ // Has branches: draw curves from main to success (up) and failure (down)
699
+ // Main -> Success branch (curve up-right)
700
+ var sx = main.right;
701
+ var sy = main.cy;
702
+ var ex = succBranch.left;
703
+ var ey = succBranch.cy;
704
+ var cpx = sx + (ex - sx) * 0.5;
705
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, successColor);
706
+ makeArrowHead(ex, ey, successColor);
707
+
708
+ // Main -> Failure branch (curve down-right)
709
+ ey = failBranch.cy;
710
+ ex = failBranch.left;
711
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, failColor);
712
+ makeArrowHead(ex, ey, failColor);
713
+
714
+ // Success branch -> next main (curve down-right to converge)
715
+ if (nextMain) {
716
+ sx = succBranch.right;
717
+ sy = succBranch.cy;
718
+ ex = nextMain.left;
719
+ ey = nextMain.cy;
720
+ cpx = sx + (ex - sx) * 0.5;
721
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, successColor, true);
722
+
723
+ // Failure branch -> next main (curve up-right to converge)
724
+ sx = failBranch.right;
725
+ sy = failBranch.cy;
726
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, failColor, true);
727
+ makeArrowHead(ex, ey, seqColor);
728
+ }
729
+ } else if (succBranch) {
730
+ // Only success branch
731
+ var sx = main.right; var sy = main.cy;
732
+ var ex = succBranch.left; var ey = succBranch.cy;
733
+ var cpx = sx + (ex - sx) * 0.5;
734
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, successColor);
735
+ makeArrowHead(ex, ey, successColor);
736
+ if (nextMain) {
737
+ sx = succBranch.right; sy = succBranch.cy;
738
+ ex = nextMain.left; ey = nextMain.cy;
739
+ cpx = sx + (ex - sx) * 0.5;
740
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, successColor, true);
741
+ makeArrowHead(ex, ey, seqColor);
742
+ }
743
+ // Also draw sequential from main to next if no failure branch
744
+ if (nextMain) {
745
+ sx = main.right; sy = main.cy;
746
+ ex = nextMain.left; ey = nextMain.cy;
747
+ makePath('M' + sx + ',' + sy + ' L' + ex + ',' + ey, seqColor);
748
+ makeArrowHead(ex, ey, seqColor);
749
+ }
750
+ } else if (failBranch) {
751
+ // Only failure branch
752
+ var sx = main.right; var sy = main.cy;
753
+ var ex = failBranch.left; var ey = failBranch.cy;
754
+ var cpx = sx + (ex - sx) * 0.5;
755
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, failColor);
756
+ makeArrowHead(ex, ey, failColor);
757
+ if (nextMain) {
758
+ sx = failBranch.right; sy = failBranch.cy;
759
+ ex = nextMain.left; ey = nextMain.cy;
760
+ cpx = sx + (ex - sx) * 0.5;
761
+ makePath('M' + sx + ',' + sy + ' C' + cpx + ',' + sy + ' ' + cpx + ',' + ey + ' ' + ex + ',' + ey, failColor, true);
762
+ makeArrowHead(ex, ey, seqColor);
763
+ }
764
+ // Also draw sequential from main to next
765
+ if (nextMain) {
766
+ sx = main.right; sy = main.cy;
767
+ ex = nextMain.left; ey = nextMain.cy;
768
+ makePath('M' + sx + ',' + sy + ' L' + ex + ',' + ey, seqColor);
769
+ makeArrowHead(ex, ey, seqColor);
770
+ }
771
+ } else if (nextMain) {
772
+ // No branches - straight sequential arrow
773
+ var sx = main.right;
774
+ var sy = main.cy;
775
+ var ex = nextMain.left;
776
+ var ey = nextMain.cy;
777
+ makePath('M' + sx + ',' + sy + ' L' + ex + ',' + ey, seqColor);
778
+ makeArrowHead(ex, ey, seqColor);
779
+ }
780
+ }
781
+
782
+ grid.insertBefore(svg, grid.firstChild);
783
+ });
784
+ }
785
+
786
+ function renderEventDetail(eventType, eventInfo, metrics) {
787
+ let html = '';
788
+
789
+ html += '<a class="back-link" onclick="window.location.hash=\\'#/\\'">\u2190 Back to Dashboard</a>';
790
+
791
+ html += '<div class="detail-header">';
792
+ html += '<div>';
793
+ html += '<div class="detail-title">' + esc(eventType) + '</div>';
794
+ if (eventInfo && eventInfo.retry) {
795
+ html += '<span class="badge badge-retry detail-badge">' + eventInfo.retry.maxRetries + ' retries</span>';
796
+ }
797
+ html += '</div>';
798
+ html += '</div>';
799
+
800
+ html += '<div class="stats">';
801
+ html += statCard('Received', metrics.received, 'accent');
802
+ html += statCard('Completed', metrics.completed, 'success');
803
+ html += statCard('Failed', metrics.failed, 'danger');
804
+ html += '</div>';
805
+
806
+ document.getElementById('content').innerHTML = html;
807
+ }
808
+
809
+ function statCard(label, value, variant) {
810
+ const cls = variant ? ' ' + variant : '';
811
+ return '<div class="stat-card' + cls + '"><div class="label">' + label + '</div><div class="value">' + value + '</div></div>';
812
+ }
813
+
814
+ function workflowCard(wf) {
815
+ let html = '<div class="workflow-card" data-workflow="' + esc(wf.name) + '">';
816
+
817
+ // Identify branch targets (steps referenced by onSuccess/onFailure)
818
+ var branchTargets = new Set();
819
+ var branchMap = {}; // parentType -> { onSuccess: stepType, onFailure: stepType }
820
+ for (var s = 0; s < wf.steps.length; s++) {
821
+ var st = wf.steps[s];
822
+ if (st.onSuccess) { branchTargets.add(st.onSuccess); branchMap[st.type] = branchMap[st.type] || {}; branchMap[st.type].onSuccess = st.onSuccess; }
823
+ if (st.onFailure) { branchTargets.add(st.onFailure); branchMap[st.type] = branchMap[st.type] || {}; branchMap[st.type].onFailure = st.onFailure; }
824
+ }
825
+
826
+ // Build main flow (skip branch targets)
827
+ var mainSteps = [];
828
+ for (var s = 0; s < wf.steps.length; s++) {
829
+ if (!branchTargets.has(wf.steps[s].type)) {
830
+ mainSteps.push(wf.steps[s]);
831
+ }
832
+ }
833
+
834
+ // Find branch step data by type
835
+ function findStep(type) {
836
+ for (var s = 0; s < wf.steps.length; s++) {
837
+ if (wf.steps[s].type === type) return wf.steps[s];
838
+ }
839
+ return null;
840
+ }
841
+
842
+ // Build grid: 3 rows, columns advance separately for branches
843
+ // Row 1 = success branches, Row 2 = main flow, Row 3 = failure branches
844
+ html += '<div class="workflow-flow"><div class="flow-grid">';
845
+
846
+ var gridCol = 1;
847
+ for (var col = 0; col < mainSteps.length; col++) {
848
+ var ms = mainSteps[col];
849
+ var branches = branchMap[ms.type];
850
+
851
+ // Main step column
852
+ html += '<div class="flow-spacer" style="grid-row:1;grid-column:' + gridCol + '"></div>';
853
+ html += '<div class="flow-node" data-id="main-' + col + '" style="grid-row:2;grid-column:' + gridCol + '">';
854
+ html += '<div class="node-type">' + esc(ms.type) + '</div>';
855
+ if (ms.retry) html += '<span class="badge badge-retry">' + ms.retry.maxRetries + ' retries</span>';
856
+ html += '</div>';
857
+ html += '<div class="flow-spacer" style="grid-row:3;grid-column:' + gridCol + '"></div>';
858
+
859
+ // If this step has branches, add them in the next column
860
+ if (branches && (branches.onSuccess || branches.onFailure)) {
861
+ gridCol++;
862
+
863
+ if (branches.onSuccess) {
864
+ var succStep = findStep(branches.onSuccess);
865
+ html += '<div class="flow-node branch-success" data-id="branch-success-' + col + '" style="grid-row:1;grid-column:' + gridCol + '">';
866
+ html += '<div class="node-type">' + esc(branches.onSuccess) + '</div>';
867
+ html += '<div class="node-label label-success">on success</div>';
868
+ if (succStep && succStep.retry) html += '<span class="badge badge-retry">' + succStep.retry.maxRetries + ' retries</span>';
869
+ html += '</div>';
870
+ } else {
871
+ html += '<div class="flow-spacer" style="grid-row:1;grid-column:' + gridCol + '"></div>';
872
+ }
873
+
874
+ // Empty middle row for branches column
875
+ html += '<div class="flow-spacer" style="grid-row:2;grid-column:' + gridCol + '"></div>';
876
+
877
+ if (branches.onFailure) {
878
+ var failStep = findStep(branches.onFailure);
879
+ html += '<div class="flow-node branch-failure" data-id="branch-failure-' + col + '" style="grid-row:3;grid-column:' + gridCol + '">';
880
+ html += '<div class="node-type">' + esc(branches.onFailure) + '</div>';
881
+ html += '<div class="node-label label-failure">on failure</div>';
882
+ if (failStep && failStep.retry) html += '<span class="badge badge-retry">' + failStep.retry.maxRetries + ' retries</span>';
883
+ html += '</div>';
884
+ } else {
885
+ html += '<div class="flow-spacer" style="grid-row:3;grid-column:' + gridCol + '"></div>';
886
+ }
887
+ }
888
+
889
+ gridCol++;
890
+ }
891
+
892
+ html += '</div></div>';
893
+
894
+ // Workflow-level callbacks
895
+ var hasCallbacks = wf.onComplete || wf.onSuccess || wf.onFailure;
896
+ if (hasCallbacks) {
897
+ html += '<div class="workflow-callbacks">';
898
+ if (wf.onComplete) html += '<span class="callback-tag callback-complete">onComplete \u2192 ' + esc(wf.onComplete) + '</span>';
899
+ if (wf.onSuccess) html += '<span class="callback-tag callback-success">onSuccess \u2192 ' + esc(wf.onSuccess) + '</span>';
900
+ if (wf.onFailure) html += '<span class="callback-tag callback-failure">onFailure \u2192 ' + esc(wf.onFailure) + '</span>';
901
+ html += '</div>';
902
+ }
903
+
904
+ html += '</div>';
905
+ return html;
906
+ }
907
+
908
+ function esc(str) {
909
+ const div = document.createElement('div');
910
+ div.textContent = str;
911
+ return div.innerHTML;
912
+ }
913
+
914
+ window.addEventListener('hashchange', route);
915
+ route();
916
+ </script>
917
+ </body>
918
+ </html>`;
919
+ }
920
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAo5BD,CAAC;AACT,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Synkro } from "@synkro/core";
3
+ export type DashboardOptions = {
4
+ basePath?: string;
5
+ };
6
+ export declare function createDashboardHandler(synkro: Synkro, options?: DashboardOptions): (req: IncomingMessage, res: ServerResponse) => void;
7
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAI3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CA6CrD"}
@@ -0,0 +1,46 @@
1
+ import { getDashboardHtml } from "./dashboard.js";
2
+ export function createDashboardHandler(synkro, options) {
3
+ const basePath = normalizeBasePath(options?.basePath ?? "/");
4
+ return (req, res) => {
5
+ if (req.method !== "GET") {
6
+ res.writeHead(405);
7
+ res.end("Method Not Allowed");
8
+ return;
9
+ }
10
+ const url = req.url ?? "/";
11
+ const path = basePath ? url.replace(basePath, "") || "/" : url;
12
+ if (path === "/" || path === "") {
13
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
14
+ res.end(getDashboardHtml());
15
+ return;
16
+ }
17
+ if (path === "/api/introspection") {
18
+ const data = synkro.introspect();
19
+ res.writeHead(200, { "Content-Type": "application/json" });
20
+ res.end(JSON.stringify(data));
21
+ return;
22
+ }
23
+ const eventMetricsMatch = path.match(/^\/api\/events\/(.+)$/);
24
+ if (eventMetricsMatch?.[1]) {
25
+ const eventType = decodeURIComponent(eventMetricsMatch[1]);
26
+ synkro
27
+ .getEventMetrics(eventType)
28
+ .then((data) => {
29
+ res.writeHead(200, { "Content-Type": "application/json" });
30
+ res.end(JSON.stringify(data));
31
+ })
32
+ .catch(() => {
33
+ res.writeHead(500);
34
+ res.end("Internal Server Error");
35
+ });
36
+ return;
37
+ }
38
+ res.writeHead(404);
39
+ res.end("Not Found");
40
+ };
41
+ }
42
+ function normalizeBasePath(path) {
43
+ const normalized = "/" + path.replace(/^\/+|\/+$/g, "");
44
+ return normalized === "/" ? "" : normalized;
45
+ }
46
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAMlD,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,OAA0B;IAE1B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,EAAE,QAAQ,IAAI,GAAG,CAAC,CAAC;IAE7D,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAE,EAAE;QACnD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YACzB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;QAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAE/D,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YAChC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAC;YACnE,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,IAAI,IAAI,KAAK,oBAAoB,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;YACjC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAC9D,IAAI,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3D,MAAM;iBACH,eAAe,CAAC,SAAS,CAAC;iBAC1B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAChC,CAAC,CAAC;iBACD,KAAK,CAAC,GAAG,EAAE;gBACV,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC;YACL,OAAO;QACT,CAAC;QAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,UAAU,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACxD,OAAO,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { createDashboardHandler } from "./handler.js";
2
+ export type { DashboardOptions } from "./handler.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createDashboardHandler } from "./handler.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@synkro/ui",
3
+ "version": "0.1.0",
4
+ "description": "Dashboard UI for @synkro/core",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "keywords": [
19
+ "workflow",
20
+ "state-machine",
21
+ "orchestrator",
22
+ "event-driven",
23
+ "dashboard"
24
+ ],
25
+ "author": "buemura",
26
+ "license": "ISC",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/buemura/synkro.git"
30
+ },
31
+ "homepage": "https://github.com/buemura/synkro/packages/ui#readme",
32
+ "peerDependencies": {
33
+ "@synkro/core": "^0.7.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.3.3",
37
+ "typescript": "^5.7.0",
38
+ "@synkro/core": "0.7.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "type-check": "tsc --noEmit"
43
+ }
44
+ }