cheekeong2025 commited on
Commit
6d2929b
Β·
verified Β·
1 Parent(s): c5cdcaf

Upload 2 files

Browse files
Files changed (2) hide show
  1. static/index-nodejs.html +1503 -0
  2. static/index.html +1503 -0
static/index-nodejs.html ADDED
@@ -0,0 +1,1503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>⚑ N8N Workflow Documentation</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
9
+ <style>
10
+ /* Modern CSS Reset and Base */
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ :root {
18
+ --primary: #3b82f6;
19
+ --primary-dark: #2563eb;
20
+ --success: #10b981;
21
+ --warning: #f59e0b;
22
+ --error: #ef4444;
23
+ --bg: #ffffff;
24
+ --bg-secondary: #f8fafc;
25
+ --bg-tertiary: #f1f5f9;
26
+ --text: #1e293b;
27
+ --text-secondary: #64748b;
28
+ --text-muted: #94a3b8;
29
+ --border: #e2e8f0;
30
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
31
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
32
+ }
33
+
34
+ [data-theme="dark"] {
35
+ --bg: #0f172a;
36
+ --bg-secondary: #1e293b;
37
+ --bg-tertiary: #334155;
38
+ --text: #f8fafc;
39
+ --text-secondary: #cbd5e1;
40
+ --text-muted: #64748b;
41
+ --border: #475569;
42
+ }
43
+
44
+ body {
45
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
+ background: var(--bg);
47
+ color: var(--text);
48
+ line-height: 1.6;
49
+ transition: all 0.2s ease;
50
+ }
51
+
52
+ .container {
53
+ max-width: 1200px;
54
+ margin: 0 auto;
55
+ padding: 0 1rem;
56
+ }
57
+
58
+ /* Header */
59
+ .header {
60
+ background: var(--bg-secondary);
61
+ border-bottom: 1px solid var(--border);
62
+ padding: 2rem 0;
63
+ text-align: center;
64
+ }
65
+
66
+ .title {
67
+ font-size: 2.5rem;
68
+ font-weight: 700;
69
+ margin-bottom: 0.5rem;
70
+ color: var(--primary);
71
+ }
72
+
73
+ .subtitle {
74
+ font-size: 1.125rem;
75
+ color: var(--text-secondary);
76
+ margin-bottom: 2rem;
77
+ }
78
+
79
+ .stats {
80
+ display: flex;
81
+ gap: 2rem;
82
+ justify-content: center;
83
+ flex-wrap: wrap;
84
+ }
85
+
86
+ .stat {
87
+ text-align: center;
88
+ min-width: 100px;
89
+ }
90
+
91
+ .stat-number {
92
+ display: block;
93
+ font-size: 1.875rem;
94
+ font-weight: 700;
95
+ color: var(--primary);
96
+ }
97
+
98
+ .stat-label {
99
+ font-size: 0.875rem;
100
+ color: var(--text-muted);
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.05em;
103
+ }
104
+
105
+ /* Controls */
106
+ .controls {
107
+ background: var(--bg);
108
+ border-bottom: 1px solid var(--border);
109
+ padding: 1.5rem 0;
110
+ position: sticky;
111
+ top: 0;
112
+ z-index: 100;
113
+ }
114
+
115
+ .search-section {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 1rem;
119
+ margin-bottom: 1rem;
120
+ }
121
+
122
+ .search-input {
123
+ flex: 1;
124
+ padding: 0.75rem 1rem;
125
+ border: 1px solid var(--border);
126
+ border-radius: 0.5rem;
127
+ background: var(--bg);
128
+ color: var(--text);
129
+ font-size: 1rem;
130
+ min-width: 300px;
131
+ }
132
+
133
+ .search-input:focus {
134
+ outline: none;
135
+ border-color: var(--primary);
136
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
137
+ }
138
+
139
+ .filter-section {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 1rem;
143
+ flex-wrap: wrap;
144
+ }
145
+
146
+ .filter-group {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.5rem;
150
+ }
151
+
152
+ .filter-group label {
153
+ font-size: 0.875rem;
154
+ font-weight: 500;
155
+ color: var(--text-secondary);
156
+ }
157
+
158
+ .filter-group select {
159
+ padding: 0.5rem;
160
+ border: 1px solid var(--border);
161
+ border-radius: 0.375rem;
162
+ background: var(--bg);
163
+ color: var(--text);
164
+ font-size: 0.875rem;
165
+ }
166
+
167
+ .theme-toggle {
168
+ background: var(--bg-tertiary);
169
+ border: 1px solid var(--border);
170
+ border-radius: 0.5rem;
171
+ padding: 0.5rem 1rem;
172
+ cursor: pointer;
173
+ font-size: 1rem;
174
+ margin-left: auto;
175
+ }
176
+
177
+ .results-info {
178
+ margin-top: 1rem;
179
+ font-size: 0.875rem;
180
+ color: var(--text-secondary);
181
+ }
182
+
183
+ /* Main Content */
184
+ .main {
185
+ padding: 2rem 0;
186
+ }
187
+
188
+ /* States */
189
+ .state {
190
+ text-align: center;
191
+ padding: 4rem 2rem;
192
+ }
193
+
194
+ .state .icon {
195
+ font-size: 4rem;
196
+ margin-bottom: 1rem;
197
+ }
198
+
199
+ .state h3 {
200
+ font-size: 1.5rem;
201
+ margin-bottom: 0.5rem;
202
+ color: var(--text);
203
+ }
204
+
205
+ .state p {
206
+ color: var(--text-secondary);
207
+ margin-bottom: 2rem;
208
+ }
209
+
210
+ .retry-btn {
211
+ background: var(--primary);
212
+ color: white;
213
+ border: none;
214
+ padding: 0.75rem 1.5rem;
215
+ border-radius: 0.5rem;
216
+ cursor: pointer;
217
+ font-size: 1rem;
218
+ font-weight: 500;
219
+ }
220
+
221
+ .retry-btn:hover {
222
+ background: var(--primary-dark);
223
+ }
224
+
225
+ /* Workflow Grid */
226
+ .workflow-grid {
227
+ display: grid;
228
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
229
+ gap: 1.5rem;
230
+ }
231
+
232
+ .workflow-card {
233
+ background: var(--bg-secondary);
234
+ border: 1px solid var(--border);
235
+ border-radius: 0.75rem;
236
+ padding: 1.5rem;
237
+ box-shadow: var(--shadow);
238
+ transition: all 0.2s ease;
239
+ cursor: pointer;
240
+ position: relative;
241
+ }
242
+
243
+ .workflow-card:hover {
244
+ box-shadow: var(--shadow-lg);
245
+ border-color: var(--primary);
246
+ transform: translateY(-2px);
247
+ }
248
+
249
+ .workflow-header {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: space-between;
253
+ margin-bottom: 1rem;
254
+ }
255
+
256
+ .workflow-meta {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 0.5rem;
260
+ font-size: 0.875rem;
261
+ color: var(--text-secondary);
262
+ }
263
+
264
+ .status-dot {
265
+ width: 8px;
266
+ height: 8px;
267
+ border-radius: 50%;
268
+ }
269
+
270
+ .status-active {
271
+ background: var(--success);
272
+ }
273
+
274
+ .status-inactive {
275
+ background: var(--text-muted);
276
+ }
277
+
278
+ .complexity-dot {
279
+ width: 8px;
280
+ height: 8px;
281
+ border-radius: 50%;
282
+ }
283
+
284
+ .complexity-low {
285
+ background: var(--success);
286
+ }
287
+
288
+ .complexity-medium {
289
+ background: var(--warning);
290
+ }
291
+
292
+ .complexity-high {
293
+ background: var(--error);
294
+ }
295
+
296
+ .trigger-badge {
297
+ background: var(--primary);
298
+ color: white;
299
+ padding: 0.25rem 0.5rem;
300
+ border-radius: 0.375rem;
301
+ font-size: 0.75rem;
302
+ font-weight: 500;
303
+ }
304
+
305
+ .category-badge {
306
+ background: var(--bg-tertiary);
307
+ color: var(--text-secondary);
308
+ padding: 0.125rem 0.375rem;
309
+ border-radius: 0.25rem;
310
+ font-size: 0.75rem;
311
+ border: 1px solid var(--border);
312
+ font-weight: 500;
313
+ }
314
+
315
+ .workflow-title {
316
+ font-size: 1.25rem;
317
+ font-weight: 600;
318
+ margin-bottom: 0.5rem;
319
+ color: var(--text);
320
+ line-height: 1.4;
321
+ }
322
+
323
+ .workflow-description {
324
+ color: var(--text-secondary);
325
+ margin-bottom: 1rem;
326
+ line-height: 1.5;
327
+ }
328
+
329
+ .workflow-integrations {
330
+ margin-top: 1rem;
331
+ }
332
+
333
+ .integrations-title {
334
+ font-size: 0.875rem;
335
+ font-weight: 500;
336
+ color: var(--text-secondary);
337
+ margin-bottom: 0.5rem;
338
+ }
339
+
340
+ .integrations-list {
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ gap: 0.25rem;
344
+ }
345
+
346
+ .integration-tag {
347
+ background: var(--bg-tertiary);
348
+ color: var(--text-secondary);
349
+ padding: 0.125rem 0.5rem;
350
+ border-radius: 0.25rem;
351
+ font-size: 0.75rem;
352
+ border: 1px solid var(--border);
353
+ }
354
+
355
+ .workflow-actions {
356
+ display: flex;
357
+ gap: 0.5rem;
358
+ margin-top: 1rem;
359
+ padding-top: 1rem;
360
+ border-top: 1px solid var(--border);
361
+ }
362
+
363
+ .action-btn {
364
+ padding: 0.5rem 1rem;
365
+ border: 1px solid var(--border);
366
+ border-radius: 0.375rem;
367
+ background: var(--bg);
368
+ color: var(--text);
369
+ text-decoration: none;
370
+ font-size: 0.875rem;
371
+ font-weight: 500;
372
+ cursor: pointer;
373
+ transition: all 0.2s ease;
374
+ }
375
+
376
+ .action-btn:hover {
377
+ background: var(--bg-tertiary);
378
+ border-color: var(--primary);
379
+ }
380
+
381
+ .action-btn.primary {
382
+ background: var(--primary);
383
+ color: white;
384
+ border-color: var(--primary);
385
+ }
386
+
387
+ .action-btn.primary:hover {
388
+ background: var(--primary-dark);
389
+ }
390
+
391
+ /* Load More */
392
+ .load-more {
393
+ text-align: center;
394
+ margin-top: 2rem;
395
+ }
396
+
397
+ .load-more-btn {
398
+ background: var(--primary);
399
+ color: white;
400
+ border: none;
401
+ padding: 0.75rem 2rem;
402
+ border-radius: 0.5rem;
403
+ cursor: pointer;
404
+ font-size: 1rem;
405
+ font-weight: 500;
406
+ }
407
+
408
+ .load-more-btn:hover {
409
+ background: var(--primary-dark);
410
+ }
411
+
412
+ /* Modal */
413
+ .modal {
414
+ position: fixed;
415
+ top: 0;
416
+ left: 0;
417
+ right: 0;
418
+ bottom: 0;
419
+ background: rgba(0, 0, 0, 0.5);
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ z-index: 1000;
424
+ padding: 1rem;
425
+ }
426
+
427
+ .modal-content {
428
+ background: var(--bg);
429
+ border-radius: 0.75rem;
430
+ max-width: 800px;
431
+ width: 100%;
432
+ max-height: 90vh;
433
+ overflow-y: auto;
434
+ position: relative;
435
+ }
436
+
437
+ .modal-header {
438
+ padding: 1.5rem;
439
+ border-bottom: 1px solid var(--border);
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: space-between;
443
+ }
444
+
445
+ .modal-title {
446
+ font-size: 1.25rem;
447
+ font-weight: 600;
448
+ }
449
+
450
+ .modal-close {
451
+ background: none;
452
+ border: none;
453
+ font-size: 1.5rem;
454
+ cursor: pointer;
455
+ padding: 0.25rem;
456
+ color: var(--text-secondary);
457
+ }
458
+
459
+ .modal-body {
460
+ padding: 1.5rem;
461
+ }
462
+
463
+ .workflow-detail {
464
+ margin-bottom: 1rem;
465
+ }
466
+
467
+ .workflow-detail h4 {
468
+ font-size: 0.875rem;
469
+ font-weight: 600;
470
+ color: var(--text-secondary);
471
+ text-transform: uppercase;
472
+ letter-spacing: 0.05em;
473
+ margin-bottom: 0.5rem;
474
+ }
475
+
476
+ .section-header {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: space-between;
480
+ margin-bottom: 0.5rem;
481
+ }
482
+
483
+ .copy-btn {
484
+ background: var(--primary);
485
+ color: white;
486
+ border: none;
487
+ padding: 0.25rem 0.5rem;
488
+ border-radius: 0.25rem;
489
+ font-size: 0.75rem;
490
+ cursor: pointer;
491
+ transition: all 0.2s ease;
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 0.25rem;
495
+ }
496
+
497
+ .copy-btn:hover {
498
+ background: var(--primary-dark);
499
+ }
500
+
501
+ .copy-btn.copied {
502
+ background: var(--success);
503
+ }
504
+
505
+ .copy-btn.copied:hover {
506
+ background: var(--success);
507
+ }
508
+
509
+ .json-viewer {
510
+ background: var(--bg-secondary);
511
+ border: 1px solid var(--border);
512
+ border-radius: 0.5rem;
513
+ padding: 1rem;
514
+ font-family: 'Courier New', monospace;
515
+ font-size: 0.875rem;
516
+ overflow-x: auto;
517
+ max-height: 400px;
518
+ white-space: pre-wrap;
519
+ }
520
+
521
+ .hidden {
522
+ display: none !important;
523
+ }
524
+
525
+ /* Mermaid diagram styling */
526
+ .mermaid {
527
+ background: var(--bg-secondary);
528
+ border: 1px solid var(--border);
529
+ border-radius: 0.5rem;
530
+ padding: 1rem;
531
+ text-align: center;
532
+ overflow-x: auto;
533
+ }
534
+
535
+ .mermaid svg {
536
+ max-width: 100%;
537
+ height: auto;
538
+ }
539
+
540
+ /* Responsive */
541
+ @media (max-width: 768px) {
542
+ .title {
543
+ font-size: 2rem;
544
+ }
545
+
546
+ .stats {
547
+ gap: 1rem;
548
+ }
549
+
550
+ .search-section,
551
+ .filter-section {
552
+ flex-direction: column;
553
+ align-items: stretch;
554
+ }
555
+
556
+ .search-input {
557
+ min-width: auto;
558
+ }
559
+
560
+ .theme-toggle {
561
+ margin-left: 0;
562
+ align-self: flex-start;
563
+ }
564
+
565
+ .workflow-grid {
566
+ grid-template-columns: 1fr;
567
+ }
568
+
569
+ .workflow-header {
570
+ flex-direction: column;
571
+ align-items: flex-start;
572
+ gap: 0.5rem;
573
+ }
574
+ }
575
+ </style>
576
+ </head>
577
+
578
+ <body>
579
+ <div id="app">
580
+ <!-- Header -->
581
+ <header class="header">
582
+ <div class="container">
583
+ <h1 class="title">⚑ N8N Workflow Documentation</h1>
584
+ <p class="subtitle">Lightning-fast workflow browser with instant search</p>
585
+ <div class="stats">
586
+ <div class="stat">
587
+ <span class="stat-number" id="totalCount">0</span>
588
+ <span class="stat-label">Total</span>
589
+ </div>
590
+ <div class="stat">
591
+ <span class="stat-number" id="activeCount">0</span>
592
+ <span class="stat-label">Active</span>
593
+ </div>
594
+ <div class="stat">
595
+ <span class="stat-number" id="nodeCount">0</span>
596
+ <span class="stat-label">Total Nodes</span>
597
+ </div>
598
+ <div class="stat">
599
+ <span class="stat-number" id="integrationCount">0</span>
600
+ <span class="stat-label">Integrations</span>
601
+ </div>
602
+ </div>
603
+ </div>
604
+ </header>
605
+
606
+ <!-- Controls -->
607
+ <div class="controls">
608
+ <div class="container">
609
+ <div class="search-section">
610
+ <input type="text" id="searchInput" class="search-input"
611
+ placeholder="Search workflows by name, description, or integration...">
612
+ </div>
613
+
614
+ <div class="filter-section">
615
+ <div class="filter-group">
616
+ <label for="triggerFilter">Trigger:</label>
617
+ <select id="triggerFilter">
618
+ <option value="all">All Types</option>
619
+ <option value="Webhook">Webhook</option>
620
+ <option value="Scheduled">Scheduled</option>
621
+ <option value="Manual">Manual</option>
622
+ <option value="Complex">Complex</option>
623
+ </select>
624
+ </div>
625
+
626
+ <div class="filter-group">
627
+ <label for="complexityFilter">Complexity:</label>
628
+ <select id="complexityFilter">
629
+ <option value="all">All Levels</option>
630
+ <option value="low">Low (≀5 nodes)</option>
631
+ <option value="medium">Medium (6-15 nodes)</option>
632
+ <option value="high">High (16+ nodes)</option>
633
+ </select>
634
+ </div>
635
+
636
+ <div class="filter-group">
637
+ <label for="categoryFilter">Category:</label>
638
+ <select id="categoryFilter">
639
+ <option value="all">All Categories</option>
640
+ <!-- Categories will be populated dynamically -->
641
+ </select>
642
+ </div>
643
+
644
+ <div class="filter-group">
645
+ <label>
646
+ <input type="checkbox" id="activeOnly">
647
+ Active only
648
+ </label>
649
+ </div>
650
+
651
+ <button id="themeToggle" class="theme-toggle">πŸŒ™</button>
652
+ </div>
653
+
654
+ <div class="results-info">
655
+ <span id="resultsCount">Loading...</span>
656
+ </div>
657
+ </div>
658
+ </div>
659
+
660
+ <!-- Main Content -->
661
+ <main class="main">
662
+ <div class="container">
663
+ <!-- Loading State -->
664
+ <div id="loadingState" class="state loading">
665
+ <div class="icon">⚑</div>
666
+ <h3>Loading workflows...</h3>
667
+ <p>Please wait while we fetch your workflow data</p>
668
+ </div>
669
+
670
+ <!-- Error State -->
671
+ <div id="errorState" class="state error hidden">
672
+ <div class="icon">❌</div>
673
+ <h3>Error Loading Workflows</h3>
674
+ <p id="errorMessage">Something went wrong. Please try again.</p>
675
+ <button id="retryBtn" class="retry-btn">Retry</button>
676
+ </div>
677
+
678
+ <!-- No Results State -->
679
+ <div id="noResultsState" class="state hidden">
680
+ <div class="icon">πŸ”</div>
681
+ <h3>No workflows found</h3>
682
+ <p>Try adjusting your search terms or filters</p>
683
+ </div>
684
+
685
+ <!-- Workflows Grid -->
686
+ <div id="workflowGrid" class="workflow-grid hidden">
687
+ <!-- Workflow cards will be inserted here -->
688
+ </div>
689
+
690
+ <!-- Load More -->
691
+ <div id="loadMoreContainer" class="load-more hidden">
692
+ <button id="loadMoreBtn" class="load-more-btn">Load More</button>
693
+ </div>
694
+ </div>
695
+ </main>
696
+
697
+ <!-- Workflow Detail Modal -->
698
+ <div id="workflowModal" class="modal hidden">
699
+ <div class="modal-content">
700
+ <div class="modal-header">
701
+ <h2 class="modal-title" id="modalTitle">Workflow Details</h2>
702
+ <button class="modal-close" id="modalClose">&times;</button>
703
+ </div>
704
+ <div class="modal-body">
705
+ <div class="workflow-detail">
706
+ <h4>Description</h4>
707
+ <p id="modalDescription">Loading...</p>
708
+ </div>
709
+
710
+ <div class="workflow-detail">
711
+ <h4>Statistics</h4>
712
+ <div id="modalStats">Loading...</div>
713
+ </div>
714
+
715
+ <div class="workflow-detail">
716
+ <h4>Integrations</h4>
717
+ <div id="modalIntegrations">Loading...</div>
718
+ </div>
719
+
720
+ <div class="workflow-detail">
721
+ <h4>Actions</h4>
722
+ <div class="workflow-actions">
723
+ <a id="downloadBtn" class="action-btn primary" href="#" download>πŸ“₯ Download JSON</a>
724
+ <button id="viewJsonBtn" class="action-btn">πŸ“„ View JSON</button>
725
+ <button id="viewDiagramBtn" class="action-btn">πŸ“Š View Diagram</button>
726
+ </div>
727
+ </div>
728
+
729
+ <div class="workflow-detail hidden" id="jsonSection">
730
+ <div class="section-header">
731
+ <h4>Workflow JSON</h4>
732
+ <button id="copyJsonBtn" class="copy-btn" title="Copy JSON to clipboard">
733
+ πŸ“‹ Copy
734
+ </button>
735
+ </div>
736
+ <div class="json-viewer" id="jsonViewer">Loading...</div>
737
+ </div>
738
+
739
+ <div class="workflow-detail hidden" id="diagramSection">
740
+ <div class="section-header">
741
+ <h4>Workflow Diagram</h4>
742
+ <button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
743
+ πŸ“‹ Copy
744
+ </button>
745
+ </div>
746
+ <div id="diagramViewer">Loading diagram...</div>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </div>
751
+ </div>
752
+
753
+ <script>
754
+ // Enhanced Workflow App with Full Functionality
755
+ class WorkflowApp {
756
+ constructor() {
757
+ this.state = {
758
+ workflows: [],
759
+ currentPage: 1,
760
+ totalPages: 1,
761
+ totalCount: 0,
762
+ perPage: 20,
763
+ isLoading: false,
764
+ searchQuery: '',
765
+ filters: {
766
+ trigger: 'all',
767
+ complexity: 'all',
768
+ category: 'all',
769
+ activeOnly: false
770
+ },
771
+ categories: [],
772
+ categoryMap: new Map()
773
+ };
774
+
775
+ this.elements = {
776
+ searchInput: document.getElementById('searchInput'),
777
+ triggerFilter: document.getElementById('triggerFilter'),
778
+ complexityFilter: document.getElementById('complexityFilter'),
779
+ categoryFilter: document.getElementById('categoryFilter'),
780
+ activeOnlyFilter: document.getElementById('activeOnly'),
781
+ themeToggle: document.getElementById('themeToggle'),
782
+ resultsCount: document.getElementById('resultsCount'),
783
+ workflowGrid: document.getElementById('workflowGrid'),
784
+ loadMoreContainer: document.getElementById('loadMoreContainer'),
785
+ loadMoreBtn: document.getElementById('loadMoreBtn'),
786
+ loadingState: document.getElementById('loadingState'),
787
+ errorState: document.getElementById('errorState'),
788
+ noResultsState: document.getElementById('noResultsState'),
789
+ errorMessage: document.getElementById('errorMessage'),
790
+ retryBtn: document.getElementById('retryBtn'),
791
+ totalCount: document.getElementById('totalCount'),
792
+ activeCount: document.getElementById('activeCount'),
793
+ nodeCount: document.getElementById('nodeCount'),
794
+ integrationCount: document.getElementById('integrationCount'),
795
+ // Modal elements
796
+ workflowModal: document.getElementById('workflowModal'),
797
+ modalTitle: document.getElementById('modalTitle'),
798
+ modalClose: document.getElementById('modalClose'),
799
+ modalDescription: document.getElementById('modalDescription'),
800
+ modalStats: document.getElementById('modalStats'),
801
+ modalIntegrations: document.getElementById('modalIntegrations'),
802
+ downloadBtn: document.getElementById('downloadBtn'),
803
+ viewJsonBtn: document.getElementById('viewJsonBtn'),
804
+ viewDiagramBtn: document.getElementById('viewDiagramBtn'),
805
+ jsonSection: document.getElementById('jsonSection'),
806
+ jsonViewer: document.getElementById('jsonViewer'),
807
+ diagramSection: document.getElementById('diagramSection'),
808
+ diagramViewer: document.getElementById('diagramViewer'),
809
+ copyJsonBtn: document.getElementById('copyJsonBtn'),
810
+ copyDiagramBtn: document.getElementById('copyDiagramBtn')
811
+ };
812
+
813
+ this.searchDebounceTimer = null;
814
+ this.currentWorkflow = null;
815
+ this.currentJsonData = null;
816
+ this.currentDiagramData = null;
817
+ this.init();
818
+ }
819
+
820
+ async init() {
821
+ this.setupEventListeners();
822
+ this.setupTheme();
823
+ this.initMermaid();
824
+ await this.loadInitialData();
825
+ }
826
+
827
+ initMermaid() {
828
+ // Initialize Mermaid with proper configuration
829
+ if (typeof mermaid !== 'undefined') {
830
+ mermaid.initialize({
831
+ startOnLoad: false,
832
+ theme: 'base',
833
+ themeVariables: {
834
+ primaryColor: '#3b82f6',
835
+ primaryTextColor: '#1e293b',
836
+ primaryBorderColor: '#2563eb',
837
+ lineColor: '#64748b',
838
+ secondaryColor: '#f1f5f9',
839
+ tertiaryColor: '#f8fafc'
840
+ }
841
+ });
842
+ }
843
+ }
844
+
845
+ setupEventListeners() {
846
+ // Search and filters
847
+ this.elements.searchInput.addEventListener('input', (e) => {
848
+ this.state.searchQuery = e.target.value;
849
+ this.debounceSearch();
850
+ });
851
+
852
+ this.elements.triggerFilter.addEventListener('change', (e) => {
853
+ this.state.filters.trigger = e.target.value;
854
+ this.state.currentPage = 1;
855
+ this.resetAndSearch();
856
+ });
857
+
858
+ this.elements.complexityFilter.addEventListener('change', (e) => {
859
+ this.state.filters.complexity = e.target.value;
860
+ this.state.currentPage = 1;
861
+ this.resetAndSearch();
862
+ });
863
+
864
+ this.elements.categoryFilter.addEventListener('change', (e) => {
865
+ const selectedCategory = e.target.value;
866
+ console.log(`Category filter changed to: ${selectedCategory}`);
867
+ console.log('Current category map size:', this.state.categoryMap.size);
868
+
869
+ this.state.filters.category = selectedCategory;
870
+ this.state.currentPage = 1;
871
+ this.resetAndSearch();
872
+ });
873
+
874
+ this.elements.activeOnlyFilter.addEventListener('change', (e) => {
875
+ this.state.filters.activeOnly = e.target.checked;
876
+ this.state.currentPage = 1;
877
+ this.resetAndSearch();
878
+ });
879
+
880
+ // Load more
881
+ this.elements.loadMoreBtn.addEventListener('click', () => {
882
+ this.loadMoreWorkflows();
883
+ });
884
+
885
+ // Retry
886
+ this.elements.retryBtn.addEventListener('click', () => {
887
+ this.loadInitialData();
888
+ });
889
+
890
+ // Theme toggle
891
+ this.elements.themeToggle.addEventListener('click', () => {
892
+ this.toggleTheme();
893
+ });
894
+
895
+ // Modal events
896
+ this.elements.modalClose.addEventListener('click', () => {
897
+ this.closeModal();
898
+ });
899
+
900
+ this.elements.workflowModal.addEventListener('click', (e) => {
901
+ if (e.target === this.elements.workflowModal) {
902
+ this.closeModal();
903
+ }
904
+ });
905
+
906
+ this.elements.viewJsonBtn.addEventListener('click', () => {
907
+ this.toggleJsonView();
908
+ });
909
+
910
+ this.elements.viewDiagramBtn.addEventListener('click', () => {
911
+ this.toggleDiagramView();
912
+ });
913
+
914
+ // Copy button events
915
+ this.elements.copyJsonBtn.addEventListener('click', () => {
916
+ this.copyToClipboard(this.currentJsonData, 'copyJsonBtn');
917
+ });
918
+
919
+ this.elements.copyDiagramBtn.addEventListener('click', () => {
920
+ this.copyToClipboard(this.currentDiagramData, 'copyDiagramBtn');
921
+ });
922
+
923
+ // Keyboard shortcuts
924
+ document.addEventListener('keydown', (e) => {
925
+ if (e.key === 'Escape') {
926
+ this.closeModal();
927
+ }
928
+ });
929
+ }
930
+
931
+ setupTheme() {
932
+ const savedTheme = localStorage.getItem('theme') || 'light';
933
+ document.documentElement.setAttribute('data-theme', savedTheme);
934
+ this.updateThemeToggle(savedTheme);
935
+ }
936
+
937
+ toggleTheme() {
938
+ const currentTheme = document.documentElement.getAttribute('data-theme');
939
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
940
+ document.documentElement.setAttribute('data-theme', newTheme);
941
+ localStorage.setItem('theme', newTheme);
942
+ this.updateThemeToggle(newTheme);
943
+ }
944
+
945
+ updateThemeToggle(theme) {
946
+ this.elements.themeToggle.textContent = theme === 'dark' ? 'β˜€οΈ' : 'πŸŒ™';
947
+ }
948
+
949
+ debounceSearch() {
950
+ clearTimeout(this.searchDebounceTimer);
951
+ this.searchDebounceTimer = setTimeout(() => {
952
+ this.state.currentPage = 1;
953
+ this.resetAndSearch();
954
+ }, 300);
955
+ }
956
+
957
+ async apiCall(endpoint, options = {}) {
958
+ const response = await fetch(`/api${endpoint}`, {
959
+ headers: {
960
+ 'Content-Type': 'application/json',
961
+ ...options.headers
962
+ },
963
+ ...options
964
+ });
965
+
966
+ if (!response.ok) {
967
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
968
+ }
969
+
970
+ return response.json();
971
+ }
972
+
973
+ async loadInitialData() {
974
+ this.showState('loading');
975
+
976
+ try {
977
+ // Load categories first, then stats and workflows
978
+ console.log('Loading categories...');
979
+ await this.loadCategories();
980
+
981
+ console.log('Categories loaded, populating filter...');
982
+ this.populateCategoryFilter();
983
+
984
+ // Load stats and workflows in parallel
985
+ console.log('Loading stats and workflows...');
986
+ const [stats] = await Promise.all([
987
+ this.apiCall('/stats'),
988
+ this.loadWorkflows(true)
989
+ ]);
990
+
991
+ this.updateStatsDisplay(stats);
992
+ console.log('Initial data loading complete');
993
+ } catch (error) {
994
+ console.error('Error during initial data loading:', error);
995
+ this.showError('Failed to load data: ' + error.message);
996
+ }
997
+ }
998
+
999
+ async loadCategories() {
1000
+ try {
1001
+ console.log('Loading categories from API...');
1002
+
1003
+ // Load categories and mappings in parallel from API
1004
+ const [categoriesResponse, mappingsResponse] = await Promise.all([
1005
+ this.apiCall('/categories'),
1006
+ this.apiCall('/category-mappings')
1007
+ ]);
1008
+
1009
+ // Set categories from API
1010
+ this.state.categories = categoriesResponse.categories || ['Uncategorized'];
1011
+
1012
+ // Build category map from API mappings
1013
+ const categoryMap = new Map();
1014
+ const mappings = mappingsResponse.mappings || {};
1015
+
1016
+ Object.entries(mappings).forEach(([filename, category]) => {
1017
+ categoryMap.set(filename, category || 'Uncategorized');
1018
+ });
1019
+
1020
+ this.state.categoryMap = categoryMap;
1021
+
1022
+ console.log(`Successfully loaded ${this.state.categories.length} categories from API:`, this.state.categories);
1023
+ console.log(`Loaded ${categoryMap.size} category mappings from API`);
1024
+
1025
+ return { categories: this.state.categories, mappings: mappings };
1026
+ } catch (error) {
1027
+ console.error('Failed to load categories from API:', error);
1028
+ // Set default categories if loading fails
1029
+ this.state.categories = ['Uncategorized'];
1030
+ this.state.categoryMap = new Map();
1031
+ return { categories: this.state.categories, mappings: {} };
1032
+ }
1033
+ }
1034
+
1035
+ populateCategoryFilter() {
1036
+ const select = this.elements.categoryFilter;
1037
+
1038
+ if (!select) {
1039
+ console.error('Category filter element not found');
1040
+ return;
1041
+ }
1042
+
1043
+ console.log('Populating category filter with:', this.state.categories);
1044
+
1045
+ // Clear existing options except "All Categories"
1046
+ while (select.children.length > 1) {
1047
+ select.removeChild(select.lastChild);
1048
+ }
1049
+
1050
+ if (this.state.categories.length === 0) {
1051
+ console.warn('No categories available to populate filter');
1052
+ return;
1053
+ }
1054
+
1055
+ // Add categories in alphabetical order
1056
+ this.state.categories.forEach(category => {
1057
+ const option = document.createElement('option');
1058
+ option.value = category;
1059
+ option.textContent = category;
1060
+ select.appendChild(option);
1061
+ console.log(`Added category option: ${category}`);
1062
+ });
1063
+
1064
+ console.log(`Category filter populated with ${select.options.length - 1} categories`);
1065
+ }
1066
+
1067
+ async loadWorkflows(reset = false) {
1068
+ if (reset) {
1069
+ this.state.currentPage = 1;
1070
+ this.state.workflows = [];
1071
+ }
1072
+
1073
+ this.state.isLoading = true;
1074
+
1075
+ try {
1076
+ // If category filtering is active, we need to load all workflows to filter properly
1077
+ const needsAllWorkflows = this.state.filters.category !== 'all' && reset;
1078
+
1079
+ let allWorkflows = [];
1080
+ let totalCount = 0;
1081
+ let totalPages = 1;
1082
+
1083
+ if (needsAllWorkflows) {
1084
+ // Load all workflows in batches for category filtering
1085
+ console.log('Loading all workflows for category filtering...');
1086
+ allWorkflows = await this.loadAllWorkflowsForCategoryFiltering();
1087
+
1088
+ // Apply client-side category filtering
1089
+ console.log(`Filtering ${allWorkflows.length} workflows for category: ${this.state.filters.category}`);
1090
+ console.log('Category map size:', this.state.categoryMap.size);
1091
+
1092
+ let matchCount = 0;
1093
+ const filteredWorkflows = allWorkflows.filter(workflow => {
1094
+ const workflowCategory = this.getWorkflowCategory(workflow.filename);
1095
+ const matches = workflowCategory === this.state.filters.category;
1096
+
1097
+ // Debug: log first few matches/non-matches
1098
+ if (matchCount < 5 || (!matches && matchCount < 3)) {
1099
+ console.log(`${workflow.filename}: ${workflowCategory} ${matches ? '===' : '!=='} ${this.state.filters.category}`);
1100
+ }
1101
+
1102
+ if (matches) matchCount++;
1103
+
1104
+ return matches;
1105
+ });
1106
+
1107
+ console.log(`Filtered from ${allWorkflows.length} to ${filteredWorkflows.length} workflows`);
1108
+ allWorkflows = filteredWorkflows;
1109
+ totalCount = filteredWorkflows.length;
1110
+ totalPages = 1; // All results loaded, no pagination needed
1111
+ } else {
1112
+ // Normal pagination
1113
+ const params = new URLSearchParams({
1114
+ q: this.state.searchQuery,
1115
+ trigger: this.state.filters.trigger,
1116
+ complexity: this.state.filters.complexity,
1117
+ active_only: this.state.filters.activeOnly,
1118
+ page: this.state.currentPage,
1119
+ per_page: this.state.perPage
1120
+ });
1121
+
1122
+ const response = await this.apiCall(`/workflows?${params}`);
1123
+ allWorkflows = response.workflows;
1124
+ totalCount = response.total;
1125
+ totalPages = response.pages;
1126
+ }
1127
+
1128
+ if (reset) {
1129
+ this.state.workflows = allWorkflows;
1130
+ this.state.totalCount = totalCount;
1131
+ this.state.totalPages = totalPages;
1132
+ } else {
1133
+ this.state.workflows.push(...allWorkflows);
1134
+ }
1135
+
1136
+ this.updateUI();
1137
+
1138
+ } catch (error) {
1139
+ this.showError('Failed to load workflows: ' + error.message);
1140
+ } finally {
1141
+ this.state.isLoading = false;
1142
+ }
1143
+ }
1144
+
1145
+ async loadAllWorkflowsForCategoryFiltering() {
1146
+ const allWorkflows = [];
1147
+ let currentPage = 1;
1148
+ const maxPerPage = 100; // API limit
1149
+
1150
+ while (true) {
1151
+ const params = new URLSearchParams({
1152
+ q: this.state.searchQuery,
1153
+ trigger: this.state.filters.trigger,
1154
+ complexity: this.state.filters.complexity,
1155
+ active_only: this.state.filters.activeOnly,
1156
+ page: currentPage,
1157
+ per_page: maxPerPage
1158
+ });
1159
+
1160
+ const response = await this.apiCall(`/workflows?${params}`);
1161
+ allWorkflows.push(...response.workflows);
1162
+
1163
+ console.log(`Loaded page ${currentPage}/${response.pages} (${response.workflows.length} workflows)`);
1164
+
1165
+ if (currentPage >= response.pages) {
1166
+ break;
1167
+ }
1168
+
1169
+ currentPage++;
1170
+ }
1171
+
1172
+ console.log(`Loaded total of ${allWorkflows.length} workflows for filtering`);
1173
+ return allWorkflows;
1174
+ }
1175
+
1176
+ getWorkflowCategory(filename) {
1177
+ const category = this.state.categoryMap.get(filename);
1178
+ const result = category && category.trim() ? category : 'Uncategorized';
1179
+ return result;
1180
+ }
1181
+
1182
+ async loadMoreWorkflows() {
1183
+ if (this.state.currentPage >= this.state.totalPages) return;
1184
+
1185
+ this.state.currentPage++;
1186
+ await this.loadWorkflows(false);
1187
+ }
1188
+
1189
+ resetAndSearch() {
1190
+ this.loadWorkflows(true);
1191
+ }
1192
+
1193
+ updateUI() {
1194
+ this.updateResultsCount();
1195
+ this.renderWorkflows();
1196
+ this.updateLoadMoreButton();
1197
+
1198
+ if (this.state.workflows.length === 0) {
1199
+ this.showState('no-results');
1200
+ } else {
1201
+ this.showState('content');
1202
+ }
1203
+ }
1204
+
1205
+ updateStatsDisplay(stats) {
1206
+ this.elements.totalCount.textContent = stats.total.toLocaleString();
1207
+ this.elements.activeCount.textContent = stats.active.toLocaleString();
1208
+ this.elements.nodeCount.textContent = stats.total_nodes.toLocaleString();
1209
+ this.elements.integrationCount.textContent = stats.unique_integrations.toLocaleString();
1210
+ }
1211
+
1212
+ updateResultsCount() {
1213
+ const count = this.state.totalCount;
1214
+ const query = this.state.searchQuery;
1215
+ const category = this.state.filters.category;
1216
+
1217
+ let text = `${count.toLocaleString()} workflows`;
1218
+
1219
+ if (query && category !== 'all') {
1220
+ text += ` found for "${query}" in "${category}"`;
1221
+ } else if (query) {
1222
+ text += ` found for "${query}"`;
1223
+ } else if (category !== 'all') {
1224
+ text += ` in "${category}"`;
1225
+ }
1226
+
1227
+ this.elements.resultsCount.textContent = text;
1228
+ }
1229
+
1230
+ renderWorkflows() {
1231
+ const html = this.state.workflows.map(workflow => this.createWorkflowCard(workflow)).join('');
1232
+ this.elements.workflowGrid.innerHTML = html;
1233
+
1234
+ // Add click handlers to cards
1235
+ this.elements.workflowGrid.querySelectorAll('.workflow-card').forEach((card, index) => {
1236
+ card.addEventListener('click', () => {
1237
+ this.openWorkflowDetail(this.state.workflows[index]);
1238
+ });
1239
+ });
1240
+ }
1241
+
1242
+ createWorkflowCard(workflow) {
1243
+ const statusClass = workflow.active ? 'status-active' : 'status-inactive';
1244
+ const complexityClass = `complexity-${workflow.complexity}`;
1245
+ const category = this.getWorkflowCategory(workflow.filename);
1246
+
1247
+ const integrations = workflow.integrations.slice(0, 5).map(integration =>
1248
+ `<span class="integration-tag">${this.escapeHtml(integration)}</span>`
1249
+ ).join('');
1250
+
1251
+ const moreIntegrations = workflow.integrations.length > 5
1252
+ ? `<span class="integration-tag">+${workflow.integrations.length - 5}</span>`
1253
+ : '';
1254
+
1255
+ return `
1256
+ <div class="workflow-card" data-filename="${workflow.filename}">
1257
+ <div class="workflow-header">
1258
+ <div class="workflow-meta">
1259
+ <div class="status-dot ${statusClass}"></div>
1260
+ <div class="complexity-dot ${complexityClass}"></div>
1261
+ <span>${workflow.node_count} nodes</span>
1262
+ <span class="category-badge">${this.escapeHtml(category)}</span>
1263
+ </div>
1264
+ <span class="trigger-badge">${this.escapeHtml(workflow.trigger_type)}</span>
1265
+ </div>
1266
+
1267
+ <h3 class="workflow-title">${this.escapeHtml(workflow.name)}</h3>
1268
+ <p class="workflow-description">${this.escapeHtml(workflow.description)}</p>
1269
+
1270
+ ${workflow.integrations.length > 0 ? `
1271
+ <div class="workflow-integrations">
1272
+ <h4 class="integrations-title">Integrations (${workflow.integrations.length})</h4>
1273
+ <div class="integrations-list">
1274
+ ${integrations}
1275
+ ${moreIntegrations}
1276
+ </div>
1277
+ </div>
1278
+ ` : ''}
1279
+ </div>
1280
+ `;
1281
+ }
1282
+
1283
+ async openWorkflowDetail(workflow) {
1284
+ this.currentWorkflow = workflow;
1285
+ this.elements.modalTitle.textContent = workflow.name;
1286
+ this.elements.modalDescription.textContent = workflow.description;
1287
+
1288
+ // Update stats
1289
+ const category = this.getWorkflowCategory(workflow.filename);
1290
+ this.elements.modalStats.innerHTML = `
1291
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
1292
+ <div><strong>Status:</strong> ${workflow.active ? 'Active' : 'Inactive'}</div>
1293
+ <div><strong>Trigger:</strong> ${workflow.trigger_type}</div>
1294
+ <div><strong>Complexity:</strong> ${workflow.complexity}</div>
1295
+ <div><strong>Nodes:</strong> ${workflow.node_count}</div>
1296
+ <div><strong>Category:</strong> ${this.escapeHtml(category)}</div>
1297
+ </div>
1298
+ `;
1299
+
1300
+ // Update integrations
1301
+ if (workflow.integrations.length > 0) {
1302
+ this.elements.modalIntegrations.innerHTML = workflow.integrations
1303
+ .map(integration => `<span class="integration-tag">${this.escapeHtml(integration)}</span>`)
1304
+ .join(' ');
1305
+ } else {
1306
+ this.elements.modalIntegrations.textContent = 'No integrations found';
1307
+ }
1308
+
1309
+ // Set download link
1310
+ this.elements.downloadBtn.href = `/api/workflows/${workflow.filename}/download`;
1311
+ this.elements.downloadBtn.download = workflow.filename;
1312
+
1313
+ // Reset view states
1314
+ this.elements.jsonSection.classList.add('hidden');
1315
+ this.elements.diagramSection.classList.add('hidden');
1316
+
1317
+ this.elements.workflowModal.classList.remove('hidden');
1318
+ }
1319
+
1320
+ closeModal() {
1321
+ this.elements.workflowModal.classList.add('hidden');
1322
+ this.currentWorkflow = null;
1323
+ this.currentJsonData = null;
1324
+ this.currentDiagramData = null;
1325
+
1326
+ // Reset button states
1327
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ View JSON';
1328
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š View Diagram';
1329
+
1330
+ // Reset copy button states
1331
+ this.resetCopyButton('copyJsonBtn');
1332
+ this.resetCopyButton('copyDiagramBtn');
1333
+ }
1334
+
1335
+ async toggleJsonView() {
1336
+ if (!this.currentWorkflow) return;
1337
+
1338
+ const isVisible = !this.elements.jsonSection.classList.contains('hidden');
1339
+
1340
+ if (isVisible) {
1341
+ this.elements.jsonSection.classList.add('hidden');
1342
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ View JSON';
1343
+ } else {
1344
+ try {
1345
+ this.elements.jsonViewer.textContent = 'Loading...';
1346
+ this.elements.jsonSection.classList.remove('hidden');
1347
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ Hide JSON';
1348
+
1349
+ const data = await this.apiCall(`/workflows/${this.currentWorkflow.filename}`);
1350
+ const jsonString = JSON.stringify(data.raw_json, null, 2);
1351
+ this.currentJsonData = jsonString;
1352
+ this.elements.jsonViewer.textContent = jsonString;
1353
+ } catch (error) {
1354
+ this.elements.jsonViewer.textContent = 'Error loading JSON: ' + error.message;
1355
+ this.currentJsonData = null;
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ async toggleDiagramView() {
1361
+ if (!this.currentWorkflow) return;
1362
+
1363
+ const isVisible = !this.elements.diagramSection.classList.contains('hidden');
1364
+
1365
+ if (isVisible) {
1366
+ this.elements.diagramSection.classList.add('hidden');
1367
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š View Diagram';
1368
+ } else {
1369
+ try {
1370
+ this.elements.diagramViewer.textContent = 'Loading diagram...';
1371
+ this.elements.diagramSection.classList.remove('hidden');
1372
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š Hide Diagram';
1373
+
1374
+ const data = await this.apiCall(`/workflows/${this.currentWorkflow.filename}/diagram`);
1375
+ this.currentDiagramData = data.diagram;
1376
+
1377
+ // Create a Mermaid diagram that will be rendered
1378
+ this.elements.diagramViewer.innerHTML = `
1379
+ <pre class="mermaid">${data.diagram}</pre>
1380
+ `;
1381
+
1382
+ // Re-initialize Mermaid for the new diagram
1383
+ if (typeof mermaid !== 'undefined') {
1384
+ mermaid.init(undefined, this.elements.diagramViewer.querySelector('.mermaid'));
1385
+ }
1386
+ } catch (error) {
1387
+ this.elements.diagramViewer.textContent = 'Error loading diagram: ' + error.message;
1388
+ this.currentDiagramData = null;
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ updateLoadMoreButton() {
1394
+ const hasMore = this.state.currentPage < this.state.totalPages;
1395
+
1396
+ if (hasMore && this.state.workflows.length > 0) {
1397
+ this.elements.loadMoreContainer.classList.remove('hidden');
1398
+ } else {
1399
+ this.elements.loadMoreContainer.classList.add('hidden');
1400
+ }
1401
+ }
1402
+
1403
+ showState(state) {
1404
+ // Hide all states
1405
+ this.elements.loadingState.classList.add('hidden');
1406
+ this.elements.errorState.classList.add('hidden');
1407
+ this.elements.noResultsState.classList.add('hidden');
1408
+ this.elements.workflowGrid.classList.add('hidden');
1409
+
1410
+ // Show the requested state
1411
+ switch (state) {
1412
+ case 'loading':
1413
+ this.elements.loadingState.classList.remove('hidden');
1414
+ break;
1415
+ case 'error':
1416
+ this.elements.errorState.classList.remove('hidden');
1417
+ break;
1418
+ case 'no-results':
1419
+ this.elements.noResultsState.classList.remove('hidden');
1420
+ break;
1421
+ case 'content':
1422
+ this.elements.workflowGrid.classList.remove('hidden');
1423
+ break;
1424
+ }
1425
+ }
1426
+
1427
+ showError(message) {
1428
+ this.elements.errorMessage.textContent = message;
1429
+ this.showState('error');
1430
+ }
1431
+
1432
+ escapeHtml(text) {
1433
+ const div = document.createElement('div');
1434
+ div.textContent = text;
1435
+ return div.innerHTML;
1436
+ }
1437
+
1438
+ async copyToClipboard(text, buttonId) {
1439
+ if (!text) {
1440
+ console.warn('No content to copy');
1441
+ return;
1442
+ }
1443
+
1444
+ try {
1445
+ await navigator.clipboard.writeText(text);
1446
+ this.showCopySuccess(buttonId);
1447
+ } catch (error) {
1448
+ // Fallback for older browsers
1449
+ this.fallbackCopyToClipboard(text, buttonId);
1450
+ }
1451
+ }
1452
+
1453
+ fallbackCopyToClipboard(text, buttonId) {
1454
+ const textArea = document.createElement('textarea');
1455
+ textArea.value = text;
1456
+ textArea.style.position = 'fixed';
1457
+ textArea.style.left = '-999999px';
1458
+ textArea.style.top = '-999999px';
1459
+ document.body.appendChild(textArea);
1460
+ textArea.focus();
1461
+ textArea.select();
1462
+
1463
+ try {
1464
+ document.execCommand('copy');
1465
+ this.showCopySuccess(buttonId);
1466
+ } catch (error) {
1467
+ console.error('Failed to copy text: ', error);
1468
+ } finally {
1469
+ document.body.removeChild(textArea);
1470
+ }
1471
+ }
1472
+
1473
+ showCopySuccess(buttonId) {
1474
+ const button = document.getElementById(buttonId);
1475
+ if (!button) return;
1476
+
1477
+ const originalText = button.innerHTML;
1478
+ button.innerHTML = 'βœ… Copied!';
1479
+ button.classList.add('copied');
1480
+
1481
+ setTimeout(() => {
1482
+ button.innerHTML = originalText;
1483
+ button.classList.remove('copied');
1484
+ }, 2000);
1485
+ }
1486
+
1487
+ resetCopyButton(buttonId) {
1488
+ const button = document.getElementById(buttonId);
1489
+ if (!button) return;
1490
+
1491
+ button.innerHTML = 'πŸ“‹ Copy';
1492
+ button.classList.remove('copied');
1493
+ }
1494
+ }
1495
+
1496
+ // Initialize the app
1497
+ document.addEventListener('DOMContentLoaded', () => {
1498
+ window.workflowApp = new WorkflowApp();
1499
+ });
1500
+ </script>
1501
+ </body>
1502
+
1503
+ </html>
static/index.html ADDED
@@ -0,0 +1,1503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>⚑ N8N Workflow Documentation</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
9
+ <style>
10
+ /* Modern CSS Reset and Base */
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ :root {
18
+ --primary: #3b82f6;
19
+ --primary-dark: #2563eb;
20
+ --success: #10b981;
21
+ --warning: #f59e0b;
22
+ --error: #ef4444;
23
+ --bg: #ffffff;
24
+ --bg-secondary: #f8fafc;
25
+ --bg-tertiary: #f1f5f9;
26
+ --text: #1e293b;
27
+ --text-secondary: #64748b;
28
+ --text-muted: #94a3b8;
29
+ --border: #e2e8f0;
30
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
31
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
32
+ }
33
+
34
+ [data-theme="dark"] {
35
+ --bg: #0f172a;
36
+ --bg-secondary: #1e293b;
37
+ --bg-tertiary: #334155;
38
+ --text: #f8fafc;
39
+ --text-secondary: #cbd5e1;
40
+ --text-muted: #64748b;
41
+ --border: #475569;
42
+ }
43
+
44
+ body {
45
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
+ background: var(--bg);
47
+ color: var(--text);
48
+ line-height: 1.6;
49
+ transition: all 0.2s ease;
50
+ }
51
+
52
+ .container {
53
+ max-width: 1200px;
54
+ margin: 0 auto;
55
+ padding: 0 1rem;
56
+ }
57
+
58
+ /* Header */
59
+ .header {
60
+ background: var(--bg-secondary);
61
+ border-bottom: 1px solid var(--border);
62
+ padding: 2rem 0;
63
+ text-align: center;
64
+ }
65
+
66
+ .title {
67
+ font-size: 2.5rem;
68
+ font-weight: 700;
69
+ margin-bottom: 0.5rem;
70
+ color: var(--primary);
71
+ }
72
+
73
+ .subtitle {
74
+ font-size: 1.125rem;
75
+ color: var(--text-secondary);
76
+ margin-bottom: 2rem;
77
+ }
78
+
79
+ .stats {
80
+ display: flex;
81
+ gap: 2rem;
82
+ justify-content: center;
83
+ flex-wrap: wrap;
84
+ }
85
+
86
+ .stat {
87
+ text-align: center;
88
+ min-width: 100px;
89
+ }
90
+
91
+ .stat-number {
92
+ display: block;
93
+ font-size: 1.875rem;
94
+ font-weight: 700;
95
+ color: var(--primary);
96
+ }
97
+
98
+ .stat-label {
99
+ font-size: 0.875rem;
100
+ color: var(--text-muted);
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.05em;
103
+ }
104
+
105
+ /* Controls */
106
+ .controls {
107
+ background: var(--bg);
108
+ border-bottom: 1px solid var(--border);
109
+ padding: 1.5rem 0;
110
+ position: sticky;
111
+ top: 0;
112
+ z-index: 100;
113
+ }
114
+
115
+ .search-section {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 1rem;
119
+ margin-bottom: 1rem;
120
+ }
121
+
122
+ .search-input {
123
+ flex: 1;
124
+ padding: 0.75rem 1rem;
125
+ border: 1px solid var(--border);
126
+ border-radius: 0.5rem;
127
+ background: var(--bg);
128
+ color: var(--text);
129
+ font-size: 1rem;
130
+ min-width: 300px;
131
+ }
132
+
133
+ .search-input:focus {
134
+ outline: none;
135
+ border-color: var(--primary);
136
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
137
+ }
138
+
139
+ .filter-section {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 1rem;
143
+ flex-wrap: wrap;
144
+ }
145
+
146
+ .filter-group {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.5rem;
150
+ }
151
+
152
+ .filter-group label {
153
+ font-size: 0.875rem;
154
+ font-weight: 500;
155
+ color: var(--text-secondary);
156
+ }
157
+
158
+ .filter-group select {
159
+ padding: 0.5rem;
160
+ border: 1px solid var(--border);
161
+ border-radius: 0.375rem;
162
+ background: var(--bg);
163
+ color: var(--text);
164
+ font-size: 0.875rem;
165
+ }
166
+
167
+ .theme-toggle {
168
+ background: var(--bg-tertiary);
169
+ border: 1px solid var(--border);
170
+ border-radius: 0.5rem;
171
+ padding: 0.5rem 1rem;
172
+ cursor: pointer;
173
+ font-size: 1rem;
174
+ margin-left: auto;
175
+ }
176
+
177
+ .results-info {
178
+ margin-top: 1rem;
179
+ font-size: 0.875rem;
180
+ color: var(--text-secondary);
181
+ }
182
+
183
+ /* Main Content */
184
+ .main {
185
+ padding: 2rem 0;
186
+ }
187
+
188
+ /* States */
189
+ .state {
190
+ text-align: center;
191
+ padding: 4rem 2rem;
192
+ }
193
+
194
+ .state .icon {
195
+ font-size: 4rem;
196
+ margin-bottom: 1rem;
197
+ }
198
+
199
+ .state h3 {
200
+ font-size: 1.5rem;
201
+ margin-bottom: 0.5rem;
202
+ color: var(--text);
203
+ }
204
+
205
+ .state p {
206
+ color: var(--text-secondary);
207
+ margin-bottom: 2rem;
208
+ }
209
+
210
+ .retry-btn {
211
+ background: var(--primary);
212
+ color: white;
213
+ border: none;
214
+ padding: 0.75rem 1.5rem;
215
+ border-radius: 0.5rem;
216
+ cursor: pointer;
217
+ font-size: 1rem;
218
+ font-weight: 500;
219
+ }
220
+
221
+ .retry-btn:hover {
222
+ background: var(--primary-dark);
223
+ }
224
+
225
+ /* Workflow Grid */
226
+ .workflow-grid {
227
+ display: grid;
228
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
229
+ gap: 1.5rem;
230
+ }
231
+
232
+ .workflow-card {
233
+ background: var(--bg-secondary);
234
+ border: 1px solid var(--border);
235
+ border-radius: 0.75rem;
236
+ padding: 1.5rem;
237
+ box-shadow: var(--shadow);
238
+ transition: all 0.2s ease;
239
+ cursor: pointer;
240
+ position: relative;
241
+ }
242
+
243
+ .workflow-card:hover {
244
+ box-shadow: var(--shadow-lg);
245
+ border-color: var(--primary);
246
+ transform: translateY(-2px);
247
+ }
248
+
249
+ .workflow-header {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: space-between;
253
+ margin-bottom: 1rem;
254
+ }
255
+
256
+ .workflow-meta {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 0.5rem;
260
+ font-size: 0.875rem;
261
+ color: var(--text-secondary);
262
+ }
263
+
264
+ .status-dot {
265
+ width: 8px;
266
+ height: 8px;
267
+ border-radius: 50%;
268
+ }
269
+
270
+ .status-active {
271
+ background: var(--success);
272
+ }
273
+
274
+ .status-inactive {
275
+ background: var(--text-muted);
276
+ }
277
+
278
+ .complexity-dot {
279
+ width: 8px;
280
+ height: 8px;
281
+ border-radius: 50%;
282
+ }
283
+
284
+ .complexity-low {
285
+ background: var(--success);
286
+ }
287
+
288
+ .complexity-medium {
289
+ background: var(--warning);
290
+ }
291
+
292
+ .complexity-high {
293
+ background: var(--error);
294
+ }
295
+
296
+ .trigger-badge {
297
+ background: var(--primary);
298
+ color: white;
299
+ padding: 0.25rem 0.5rem;
300
+ border-radius: 0.375rem;
301
+ font-size: 0.75rem;
302
+ font-weight: 500;
303
+ }
304
+
305
+ .category-badge {
306
+ background: var(--bg-tertiary);
307
+ color: var(--text-secondary);
308
+ padding: 0.125rem 0.375rem;
309
+ border-radius: 0.25rem;
310
+ font-size: 0.75rem;
311
+ border: 1px solid var(--border);
312
+ font-weight: 500;
313
+ }
314
+
315
+ .workflow-title {
316
+ font-size: 1.25rem;
317
+ font-weight: 600;
318
+ margin-bottom: 0.5rem;
319
+ color: var(--text);
320
+ line-height: 1.4;
321
+ }
322
+
323
+ .workflow-description {
324
+ color: var(--text-secondary);
325
+ margin-bottom: 1rem;
326
+ line-height: 1.5;
327
+ }
328
+
329
+ .workflow-integrations {
330
+ margin-top: 1rem;
331
+ }
332
+
333
+ .integrations-title {
334
+ font-size: 0.875rem;
335
+ font-weight: 500;
336
+ color: var(--text-secondary);
337
+ margin-bottom: 0.5rem;
338
+ }
339
+
340
+ .integrations-list {
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ gap: 0.25rem;
344
+ }
345
+
346
+ .integration-tag {
347
+ background: var(--bg-tertiary);
348
+ color: var(--text-secondary);
349
+ padding: 0.125rem 0.5rem;
350
+ border-radius: 0.25rem;
351
+ font-size: 0.75rem;
352
+ border: 1px solid var(--border);
353
+ }
354
+
355
+ .workflow-actions {
356
+ display: flex;
357
+ gap: 0.5rem;
358
+ margin-top: 1rem;
359
+ padding-top: 1rem;
360
+ border-top: 1px solid var(--border);
361
+ }
362
+
363
+ .action-btn {
364
+ padding: 0.5rem 1rem;
365
+ border: 1px solid var(--border);
366
+ border-radius: 0.375rem;
367
+ background: var(--bg);
368
+ color: var(--text);
369
+ text-decoration: none;
370
+ font-size: 0.875rem;
371
+ font-weight: 500;
372
+ cursor: pointer;
373
+ transition: all 0.2s ease;
374
+ }
375
+
376
+ .action-btn:hover {
377
+ background: var(--bg-tertiary);
378
+ border-color: var(--primary);
379
+ }
380
+
381
+ .action-btn.primary {
382
+ background: var(--primary);
383
+ color: white;
384
+ border-color: var(--primary);
385
+ }
386
+
387
+ .action-btn.primary:hover {
388
+ background: var(--primary-dark);
389
+ }
390
+
391
+ /* Load More */
392
+ .load-more {
393
+ text-align: center;
394
+ margin-top: 2rem;
395
+ }
396
+
397
+ .load-more-btn {
398
+ background: var(--primary);
399
+ color: white;
400
+ border: none;
401
+ padding: 0.75rem 2rem;
402
+ border-radius: 0.5rem;
403
+ cursor: pointer;
404
+ font-size: 1rem;
405
+ font-weight: 500;
406
+ }
407
+
408
+ .load-more-btn:hover {
409
+ background: var(--primary-dark);
410
+ }
411
+
412
+ /* Modal */
413
+ .modal {
414
+ position: fixed;
415
+ top: 0;
416
+ left: 0;
417
+ right: 0;
418
+ bottom: 0;
419
+ background: rgba(0, 0, 0, 0.5);
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ z-index: 1000;
424
+ padding: 1rem;
425
+ }
426
+
427
+ .modal-content {
428
+ background: var(--bg);
429
+ border-radius: 0.75rem;
430
+ max-width: 800px;
431
+ width: 100%;
432
+ max-height: 90vh;
433
+ overflow-y: auto;
434
+ position: relative;
435
+ }
436
+
437
+ .modal-header {
438
+ padding: 1.5rem;
439
+ border-bottom: 1px solid var(--border);
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: space-between;
443
+ }
444
+
445
+ .modal-title {
446
+ font-size: 1.25rem;
447
+ font-weight: 600;
448
+ }
449
+
450
+ .modal-close {
451
+ background: none;
452
+ border: none;
453
+ font-size: 1.5rem;
454
+ cursor: pointer;
455
+ padding: 0.25rem;
456
+ color: var(--text-secondary);
457
+ }
458
+
459
+ .modal-body {
460
+ padding: 1.5rem;
461
+ }
462
+
463
+ .workflow-detail {
464
+ margin-bottom: 1rem;
465
+ }
466
+
467
+ .workflow-detail h4 {
468
+ font-size: 0.875rem;
469
+ font-weight: 600;
470
+ color: var(--text-secondary);
471
+ text-transform: uppercase;
472
+ letter-spacing: 0.05em;
473
+ margin-bottom: 0.5rem;
474
+ }
475
+
476
+ .section-header {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: space-between;
480
+ margin-bottom: 0.5rem;
481
+ }
482
+
483
+ .copy-btn {
484
+ background: var(--primary);
485
+ color: white;
486
+ border: none;
487
+ padding: 0.25rem 0.5rem;
488
+ border-radius: 0.25rem;
489
+ font-size: 0.75rem;
490
+ cursor: pointer;
491
+ transition: all 0.2s ease;
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 0.25rem;
495
+ }
496
+
497
+ .copy-btn:hover {
498
+ background: var(--primary-dark);
499
+ }
500
+
501
+ .copy-btn.copied {
502
+ background: var(--success);
503
+ }
504
+
505
+ .copy-btn.copied:hover {
506
+ background: var(--success);
507
+ }
508
+
509
+ .json-viewer {
510
+ background: var(--bg-secondary);
511
+ border: 1px solid var(--border);
512
+ border-radius: 0.5rem;
513
+ padding: 1rem;
514
+ font-family: 'Courier New', monospace;
515
+ font-size: 0.875rem;
516
+ overflow-x: auto;
517
+ max-height: 400px;
518
+ white-space: pre-wrap;
519
+ }
520
+
521
+ .hidden {
522
+ display: none !important;
523
+ }
524
+
525
+ /* Mermaid diagram styling */
526
+ .mermaid {
527
+ background: var(--bg-secondary);
528
+ border: 1px solid var(--border);
529
+ border-radius: 0.5rem;
530
+ padding: 1rem;
531
+ text-align: center;
532
+ overflow-x: auto;
533
+ }
534
+
535
+ .mermaid svg {
536
+ max-width: 100%;
537
+ height: auto;
538
+ }
539
+
540
+ /* Responsive */
541
+ @media (max-width: 768px) {
542
+ .title {
543
+ font-size: 2rem;
544
+ }
545
+
546
+ .stats {
547
+ gap: 1rem;
548
+ }
549
+
550
+ .search-section,
551
+ .filter-section {
552
+ flex-direction: column;
553
+ align-items: stretch;
554
+ }
555
+
556
+ .search-input {
557
+ min-width: auto;
558
+ }
559
+
560
+ .theme-toggle {
561
+ margin-left: 0;
562
+ align-self: flex-start;
563
+ }
564
+
565
+ .workflow-grid {
566
+ grid-template-columns: 1fr;
567
+ }
568
+
569
+ .workflow-header {
570
+ flex-direction: column;
571
+ align-items: flex-start;
572
+ gap: 0.5rem;
573
+ }
574
+ }
575
+ </style>
576
+ </head>
577
+
578
+ <body>
579
+ <div id="app">
580
+ <!-- Header -->
581
+ <header class="header">
582
+ <div class="container">
583
+ <h1 class="title">⚑ N8N Workflow Documentation</h1>
584
+ <p class="subtitle">Lightning-fast workflow browser with instant search</p>
585
+ <div class="stats">
586
+ <div class="stat">
587
+ <span class="stat-number" id="totalCount">0</span>
588
+ <span class="stat-label">Total</span>
589
+ </div>
590
+ <div class="stat">
591
+ <span class="stat-number" id="activeCount">0</span>
592
+ <span class="stat-label">Active</span>
593
+ </div>
594
+ <div class="stat">
595
+ <span class="stat-number" id="nodeCount">0</span>
596
+ <span class="stat-label">Total Nodes</span>
597
+ </div>
598
+ <div class="stat">
599
+ <span class="stat-number" id="integrationCount">0</span>
600
+ <span class="stat-label">Integrations</span>
601
+ </div>
602
+ </div>
603
+ </div>
604
+ </header>
605
+
606
+ <!-- Controls -->
607
+ <div class="controls">
608
+ <div class="container">
609
+ <div class="search-section">
610
+ <input type="text" id="searchInput" class="search-input"
611
+ placeholder="Search workflows by name, description, or integration...">
612
+ </div>
613
+
614
+ <div class="filter-section">
615
+ <div class="filter-group">
616
+ <label for="triggerFilter">Trigger:</label>
617
+ <select id="triggerFilter">
618
+ <option value="all">All Types</option>
619
+ <option value="Webhook">Webhook</option>
620
+ <option value="Scheduled">Scheduled</option>
621
+ <option value="Manual">Manual</option>
622
+ <option value="Complex">Complex</option>
623
+ </select>
624
+ </div>
625
+
626
+ <div class="filter-group">
627
+ <label for="complexityFilter">Complexity:</label>
628
+ <select id="complexityFilter">
629
+ <option value="all">All Levels</option>
630
+ <option value="low">Low (≀5 nodes)</option>
631
+ <option value="medium">Medium (6-15 nodes)</option>
632
+ <option value="high">High (16+ nodes)</option>
633
+ </select>
634
+ </div>
635
+
636
+ <div class="filter-group">
637
+ <label for="categoryFilter">Category:</label>
638
+ <select id="categoryFilter">
639
+ <option value="all">All Categories</option>
640
+ <!-- Categories will be populated dynamically -->
641
+ </select>
642
+ </div>
643
+
644
+ <div class="filter-group">
645
+ <label>
646
+ <input type="checkbox" id="activeOnly">
647
+ Active only
648
+ </label>
649
+ </div>
650
+
651
+ <button id="themeToggle" class="theme-toggle">πŸŒ™</button>
652
+ </div>
653
+
654
+ <div class="results-info">
655
+ <span id="resultsCount">Loading...</span>
656
+ </div>
657
+ </div>
658
+ </div>
659
+
660
+ <!-- Main Content -->
661
+ <main class="main">
662
+ <div class="container">
663
+ <!-- Loading State -->
664
+ <div id="loadingState" class="state loading">
665
+ <div class="icon">⚑</div>
666
+ <h3>Loading workflows...</h3>
667
+ <p>Please wait while we fetch your workflow data</p>
668
+ </div>
669
+
670
+ <!-- Error State -->
671
+ <div id="errorState" class="state error hidden">
672
+ <div class="icon">❌</div>
673
+ <h3>Error Loading Workflows</h3>
674
+ <p id="errorMessage">Something went wrong. Please try again.</p>
675
+ <button id="retryBtn" class="retry-btn">Retry</button>
676
+ </div>
677
+
678
+ <!-- No Results State -->
679
+ <div id="noResultsState" class="state hidden">
680
+ <div class="icon">πŸ”</div>
681
+ <h3>No workflows found</h3>
682
+ <p>Try adjusting your search terms or filters</p>
683
+ </div>
684
+
685
+ <!-- Workflows Grid -->
686
+ <div id="workflowGrid" class="workflow-grid hidden">
687
+ <!-- Workflow cards will be inserted here -->
688
+ </div>
689
+
690
+ <!-- Load More -->
691
+ <div id="loadMoreContainer" class="load-more hidden">
692
+ <button id="loadMoreBtn" class="load-more-btn">Load More</button>
693
+ </div>
694
+ </div>
695
+ </main>
696
+
697
+ <!-- Workflow Detail Modal -->
698
+ <div id="workflowModal" class="modal hidden">
699
+ <div class="modal-content">
700
+ <div class="modal-header">
701
+ <h2 class="modal-title" id="modalTitle">Workflow Details</h2>
702
+ <button class="modal-close" id="modalClose">&times;</button>
703
+ </div>
704
+ <div class="modal-body">
705
+ <div class="workflow-detail">
706
+ <h4>Description</h4>
707
+ <p id="modalDescription">Loading...</p>
708
+ </div>
709
+
710
+ <div class="workflow-detail">
711
+ <h4>Statistics</h4>
712
+ <div id="modalStats">Loading...</div>
713
+ </div>
714
+
715
+ <div class="workflow-detail">
716
+ <h4>Integrations</h4>
717
+ <div id="modalIntegrations">Loading...</div>
718
+ </div>
719
+
720
+ <div class="workflow-detail">
721
+ <h4>Actions</h4>
722
+ <div class="workflow-actions">
723
+ <a id="downloadBtn" class="action-btn primary" href="#" download>πŸ“₯ Download JSON</a>
724
+ <button id="viewJsonBtn" class="action-btn">πŸ“„ View JSON</button>
725
+ <button id="viewDiagramBtn" class="action-btn">πŸ“Š View Diagram</button>
726
+ </div>
727
+ </div>
728
+
729
+ <div class="workflow-detail hidden" id="jsonSection">
730
+ <div class="section-header">
731
+ <h4>Workflow JSON</h4>
732
+ <button id="copyJsonBtn" class="copy-btn" title="Copy JSON to clipboard">
733
+ πŸ“‹ Copy
734
+ </button>
735
+ </div>
736
+ <div class="json-viewer" id="jsonViewer">Loading...</div>
737
+ </div>
738
+
739
+ <div class="workflow-detail hidden" id="diagramSection">
740
+ <div class="section-header">
741
+ <h4>Workflow Diagram</h4>
742
+ <button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
743
+ πŸ“‹ Copy
744
+ </button>
745
+ </div>
746
+ <div id="diagramViewer">Loading diagram...</div>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </div>
751
+ </div>
752
+
753
+ <script>
754
+ // Enhanced Workflow App with Full Functionality
755
+ class WorkflowApp {
756
+ constructor() {
757
+ this.state = {
758
+ workflows: [],
759
+ currentPage: 1,
760
+ totalPages: 1,
761
+ totalCount: 0,
762
+ perPage: 20,
763
+ isLoading: false,
764
+ searchQuery: '',
765
+ filters: {
766
+ trigger: 'all',
767
+ complexity: 'all',
768
+ category: 'all',
769
+ activeOnly: false
770
+ },
771
+ categories: [],
772
+ categoryMap: new Map()
773
+ };
774
+
775
+ this.elements = {
776
+ searchInput: document.getElementById('searchInput'),
777
+ triggerFilter: document.getElementById('triggerFilter'),
778
+ complexityFilter: document.getElementById('complexityFilter'),
779
+ categoryFilter: document.getElementById('categoryFilter'),
780
+ activeOnlyFilter: document.getElementById('activeOnly'),
781
+ themeToggle: document.getElementById('themeToggle'),
782
+ resultsCount: document.getElementById('resultsCount'),
783
+ workflowGrid: document.getElementById('workflowGrid'),
784
+ loadMoreContainer: document.getElementById('loadMoreContainer'),
785
+ loadMoreBtn: document.getElementById('loadMoreBtn'),
786
+ loadingState: document.getElementById('loadingState'),
787
+ errorState: document.getElementById('errorState'),
788
+ noResultsState: document.getElementById('noResultsState'),
789
+ errorMessage: document.getElementById('errorMessage'),
790
+ retryBtn: document.getElementById('retryBtn'),
791
+ totalCount: document.getElementById('totalCount'),
792
+ activeCount: document.getElementById('activeCount'),
793
+ nodeCount: document.getElementById('nodeCount'),
794
+ integrationCount: document.getElementById('integrationCount'),
795
+ // Modal elements
796
+ workflowModal: document.getElementById('workflowModal'),
797
+ modalTitle: document.getElementById('modalTitle'),
798
+ modalClose: document.getElementById('modalClose'),
799
+ modalDescription: document.getElementById('modalDescription'),
800
+ modalStats: document.getElementById('modalStats'),
801
+ modalIntegrations: document.getElementById('modalIntegrations'),
802
+ downloadBtn: document.getElementById('downloadBtn'),
803
+ viewJsonBtn: document.getElementById('viewJsonBtn'),
804
+ viewDiagramBtn: document.getElementById('viewDiagramBtn'),
805
+ jsonSection: document.getElementById('jsonSection'),
806
+ jsonViewer: document.getElementById('jsonViewer'),
807
+ diagramSection: document.getElementById('diagramSection'),
808
+ diagramViewer: document.getElementById('diagramViewer'),
809
+ copyJsonBtn: document.getElementById('copyJsonBtn'),
810
+ copyDiagramBtn: document.getElementById('copyDiagramBtn')
811
+ };
812
+
813
+ this.searchDebounceTimer = null;
814
+ this.currentWorkflow = null;
815
+ this.currentJsonData = null;
816
+ this.currentDiagramData = null;
817
+ this.init();
818
+ }
819
+
820
+ async init() {
821
+ this.setupEventListeners();
822
+ this.setupTheme();
823
+ this.initMermaid();
824
+ await this.loadInitialData();
825
+ }
826
+
827
+ initMermaid() {
828
+ // Initialize Mermaid with proper configuration
829
+ if (typeof mermaid !== 'undefined') {
830
+ mermaid.initialize({
831
+ startOnLoad: false,
832
+ theme: 'base',
833
+ themeVariables: {
834
+ primaryColor: '#3b82f6',
835
+ primaryTextColor: '#1e293b',
836
+ primaryBorderColor: '#2563eb',
837
+ lineColor: '#64748b',
838
+ secondaryColor: '#f1f5f9',
839
+ tertiaryColor: '#f8fafc'
840
+ }
841
+ });
842
+ }
843
+ }
844
+
845
+ setupEventListeners() {
846
+ // Search and filters
847
+ this.elements.searchInput.addEventListener('input', (e) => {
848
+ this.state.searchQuery = e.target.value;
849
+ this.debounceSearch();
850
+ });
851
+
852
+ this.elements.triggerFilter.addEventListener('change', (e) => {
853
+ this.state.filters.trigger = e.target.value;
854
+ this.state.currentPage = 1;
855
+ this.resetAndSearch();
856
+ });
857
+
858
+ this.elements.complexityFilter.addEventListener('change', (e) => {
859
+ this.state.filters.complexity = e.target.value;
860
+ this.state.currentPage = 1;
861
+ this.resetAndSearch();
862
+ });
863
+
864
+ this.elements.categoryFilter.addEventListener('change', (e) => {
865
+ const selectedCategory = e.target.value;
866
+ console.log(`Category filter changed to: ${selectedCategory}`);
867
+ console.log('Current category map size:', this.state.categoryMap.size);
868
+
869
+ this.state.filters.category = selectedCategory;
870
+ this.state.currentPage = 1;
871
+ this.resetAndSearch();
872
+ });
873
+
874
+ this.elements.activeOnlyFilter.addEventListener('change', (e) => {
875
+ this.state.filters.activeOnly = e.target.checked;
876
+ this.state.currentPage = 1;
877
+ this.resetAndSearch();
878
+ });
879
+
880
+ // Load more
881
+ this.elements.loadMoreBtn.addEventListener('click', () => {
882
+ this.loadMoreWorkflows();
883
+ });
884
+
885
+ // Retry
886
+ this.elements.retryBtn.addEventListener('click', () => {
887
+ this.loadInitialData();
888
+ });
889
+
890
+ // Theme toggle
891
+ this.elements.themeToggle.addEventListener('click', () => {
892
+ this.toggleTheme();
893
+ });
894
+
895
+ // Modal events
896
+ this.elements.modalClose.addEventListener('click', () => {
897
+ this.closeModal();
898
+ });
899
+
900
+ this.elements.workflowModal.addEventListener('click', (e) => {
901
+ if (e.target === this.elements.workflowModal) {
902
+ this.closeModal();
903
+ }
904
+ });
905
+
906
+ this.elements.viewJsonBtn.addEventListener('click', () => {
907
+ this.toggleJsonView();
908
+ });
909
+
910
+ this.elements.viewDiagramBtn.addEventListener('click', () => {
911
+ this.toggleDiagramView();
912
+ });
913
+
914
+ // Copy button events
915
+ this.elements.copyJsonBtn.addEventListener('click', () => {
916
+ this.copyToClipboard(this.currentJsonData, 'copyJsonBtn');
917
+ });
918
+
919
+ this.elements.copyDiagramBtn.addEventListener('click', () => {
920
+ this.copyToClipboard(this.currentDiagramData, 'copyDiagramBtn');
921
+ });
922
+
923
+ // Keyboard shortcuts
924
+ document.addEventListener('keydown', (e) => {
925
+ if (e.key === 'Escape') {
926
+ this.closeModal();
927
+ }
928
+ });
929
+ }
930
+
931
+ setupTheme() {
932
+ const savedTheme = localStorage.getItem('theme') || 'light';
933
+ document.documentElement.setAttribute('data-theme', savedTheme);
934
+ this.updateThemeToggle(savedTheme);
935
+ }
936
+
937
+ toggleTheme() {
938
+ const currentTheme = document.documentElement.getAttribute('data-theme');
939
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
940
+ document.documentElement.setAttribute('data-theme', newTheme);
941
+ localStorage.setItem('theme', newTheme);
942
+ this.updateThemeToggle(newTheme);
943
+ }
944
+
945
+ updateThemeToggle(theme) {
946
+ this.elements.themeToggle.textContent = theme === 'dark' ? 'β˜€οΈ' : 'πŸŒ™';
947
+ }
948
+
949
+ debounceSearch() {
950
+ clearTimeout(this.searchDebounceTimer);
951
+ this.searchDebounceTimer = setTimeout(() => {
952
+ this.state.currentPage = 1;
953
+ this.resetAndSearch();
954
+ }, 300);
955
+ }
956
+
957
+ async apiCall(endpoint, options = {}) {
958
+ const response = await fetch(`/api${endpoint}`, {
959
+ headers: {
960
+ 'Content-Type': 'application/json',
961
+ ...options.headers
962
+ },
963
+ ...options
964
+ });
965
+
966
+ if (!response.ok) {
967
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
968
+ }
969
+
970
+ return response.json();
971
+ }
972
+
973
+ async loadInitialData() {
974
+ this.showState('loading');
975
+
976
+ try {
977
+ // Load categories first, then stats and workflows
978
+ console.log('Loading categories...');
979
+ await this.loadCategories();
980
+
981
+ console.log('Categories loaded, populating filter...');
982
+ this.populateCategoryFilter();
983
+
984
+ // Load stats and workflows in parallel
985
+ console.log('Loading stats and workflows...');
986
+ const [stats] = await Promise.all([
987
+ this.apiCall('/stats'),
988
+ this.loadWorkflows(true)
989
+ ]);
990
+
991
+ this.updateStatsDisplay(stats);
992
+ console.log('Initial data loading complete');
993
+ } catch (error) {
994
+ console.error('Error during initial data loading:', error);
995
+ this.showError('Failed to load data: ' + error.message);
996
+ }
997
+ }
998
+
999
+ async loadCategories() {
1000
+ try {
1001
+ console.log('Loading categories from API...');
1002
+
1003
+ // Load categories and mappings in parallel from API
1004
+ const [categoriesResponse, mappingsResponse] = await Promise.all([
1005
+ this.apiCall('/categories'),
1006
+ this.apiCall('/category-mappings')
1007
+ ]);
1008
+
1009
+ // Set categories from API
1010
+ this.state.categories = categoriesResponse.categories || ['Uncategorized'];
1011
+
1012
+ // Build category map from API mappings
1013
+ const categoryMap = new Map();
1014
+ const mappings = mappingsResponse.mappings || {};
1015
+
1016
+ Object.entries(mappings).forEach(([filename, category]) => {
1017
+ categoryMap.set(filename, category || 'Uncategorized');
1018
+ });
1019
+
1020
+ this.state.categoryMap = categoryMap;
1021
+
1022
+ console.log(`Successfully loaded ${this.state.categories.length} categories from API:`, this.state.categories);
1023
+ console.log(`Loaded ${categoryMap.size} category mappings from API`);
1024
+
1025
+ return { categories: this.state.categories, mappings: mappings };
1026
+ } catch (error) {
1027
+ console.error('Failed to load categories from API:', error);
1028
+ // Set default categories if loading fails
1029
+ this.state.categories = ['Uncategorized'];
1030
+ this.state.categoryMap = new Map();
1031
+ return { categories: this.state.categories, mappings: {} };
1032
+ }
1033
+ }
1034
+
1035
+ populateCategoryFilter() {
1036
+ const select = this.elements.categoryFilter;
1037
+
1038
+ if (!select) {
1039
+ console.error('Category filter element not found');
1040
+ return;
1041
+ }
1042
+
1043
+ console.log('Populating category filter with:', this.state.categories);
1044
+
1045
+ // Clear existing options except "All Categories"
1046
+ while (select.children.length > 1) {
1047
+ select.removeChild(select.lastChild);
1048
+ }
1049
+
1050
+ if (this.state.categories.length === 0) {
1051
+ console.warn('No categories available to populate filter');
1052
+ return;
1053
+ }
1054
+
1055
+ // Add categories in alphabetical order
1056
+ this.state.categories.forEach(category => {
1057
+ const option = document.createElement('option');
1058
+ option.value = category;
1059
+ option.textContent = category;
1060
+ select.appendChild(option);
1061
+ console.log(`Added category option: ${category}`);
1062
+ });
1063
+
1064
+ console.log(`Category filter populated with ${select.options.length - 1} categories`);
1065
+ }
1066
+
1067
+ async loadWorkflows(reset = false) {
1068
+ if (reset) {
1069
+ this.state.currentPage = 1;
1070
+ this.state.workflows = [];
1071
+ }
1072
+
1073
+ this.state.isLoading = true;
1074
+
1075
+ try {
1076
+ // If category filtering is active, we need to load all workflows to filter properly
1077
+ const needsAllWorkflows = this.state.filters.category !== 'all' && reset;
1078
+
1079
+ let allWorkflows = [];
1080
+ let totalCount = 0;
1081
+ let totalPages = 1;
1082
+
1083
+ if (needsAllWorkflows) {
1084
+ // Load all workflows in batches for category filtering
1085
+ console.log('Loading all workflows for category filtering...');
1086
+ allWorkflows = await this.loadAllWorkflowsForCategoryFiltering();
1087
+
1088
+ // Apply client-side category filtering
1089
+ console.log(`Filtering ${allWorkflows.length} workflows for category: ${this.state.filters.category}`);
1090
+ console.log('Category map size:', this.state.categoryMap.size);
1091
+
1092
+ let matchCount = 0;
1093
+ const filteredWorkflows = allWorkflows.filter(workflow => {
1094
+ const workflowCategory = this.getWorkflowCategory(workflow.filename);
1095
+ const matches = workflowCategory === this.state.filters.category;
1096
+
1097
+ // Debug: log first few matches/non-matches
1098
+ if (matchCount < 5 || (!matches && matchCount < 3)) {
1099
+ console.log(`${workflow.filename}: ${workflowCategory} ${matches ? '===' : '!=='} ${this.state.filters.category}`);
1100
+ }
1101
+
1102
+ if (matches) matchCount++;
1103
+
1104
+ return matches;
1105
+ });
1106
+
1107
+ console.log(`Filtered from ${allWorkflows.length} to ${filteredWorkflows.length} workflows`);
1108
+ allWorkflows = filteredWorkflows;
1109
+ totalCount = filteredWorkflows.length;
1110
+ totalPages = 1; // All results loaded, no pagination needed
1111
+ } else {
1112
+ // Normal pagination
1113
+ const params = new URLSearchParams({
1114
+ q: this.state.searchQuery,
1115
+ trigger: this.state.filters.trigger,
1116
+ complexity: this.state.filters.complexity,
1117
+ active_only: this.state.filters.activeOnly,
1118
+ page: this.state.currentPage,
1119
+ per_page: this.state.perPage
1120
+ });
1121
+
1122
+ const response = await this.apiCall(`/workflows?${params}`);
1123
+ allWorkflows = response.workflows;
1124
+ totalCount = response.total;
1125
+ totalPages = response.pages;
1126
+ }
1127
+
1128
+ if (reset) {
1129
+ this.state.workflows = allWorkflows;
1130
+ this.state.totalCount = totalCount;
1131
+ this.state.totalPages = totalPages;
1132
+ } else {
1133
+ this.state.workflows.push(...allWorkflows);
1134
+ }
1135
+
1136
+ this.updateUI();
1137
+
1138
+ } catch (error) {
1139
+ this.showError('Failed to load workflows: ' + error.message);
1140
+ } finally {
1141
+ this.state.isLoading = false;
1142
+ }
1143
+ }
1144
+
1145
+ async loadAllWorkflowsForCategoryFiltering() {
1146
+ const allWorkflows = [];
1147
+ let currentPage = 1;
1148
+ const maxPerPage = 100; // API limit
1149
+
1150
+ while (true) {
1151
+ const params = new URLSearchParams({
1152
+ q: this.state.searchQuery,
1153
+ trigger: this.state.filters.trigger,
1154
+ complexity: this.state.filters.complexity,
1155
+ active_only: this.state.filters.activeOnly,
1156
+ page: currentPage,
1157
+ per_page: maxPerPage
1158
+ });
1159
+
1160
+ const response = await this.apiCall(`/workflows?${params}`);
1161
+ allWorkflows.push(...response.workflows);
1162
+
1163
+ console.log(`Loaded page ${currentPage}/${response.pages} (${response.workflows.length} workflows)`);
1164
+
1165
+ if (currentPage >= response.pages) {
1166
+ break;
1167
+ }
1168
+
1169
+ currentPage++;
1170
+ }
1171
+
1172
+ console.log(`Loaded total of ${allWorkflows.length} workflows for filtering`);
1173
+ return allWorkflows;
1174
+ }
1175
+
1176
+ getWorkflowCategory(filename) {
1177
+ const category = this.state.categoryMap.get(filename);
1178
+ const result = category && category.trim() ? category : 'Uncategorized';
1179
+ return result;
1180
+ }
1181
+
1182
+ async loadMoreWorkflows() {
1183
+ if (this.state.currentPage >= this.state.totalPages) return;
1184
+
1185
+ this.state.currentPage++;
1186
+ await this.loadWorkflows(false);
1187
+ }
1188
+
1189
+ resetAndSearch() {
1190
+ this.loadWorkflows(true);
1191
+ }
1192
+
1193
+ updateUI() {
1194
+ this.updateResultsCount();
1195
+ this.renderWorkflows();
1196
+ this.updateLoadMoreButton();
1197
+
1198
+ if (this.state.workflows.length === 0) {
1199
+ this.showState('no-results');
1200
+ } else {
1201
+ this.showState('content');
1202
+ }
1203
+ }
1204
+
1205
+ updateStatsDisplay(stats) {
1206
+ this.elements.totalCount.textContent = stats.total.toLocaleString();
1207
+ this.elements.activeCount.textContent = stats.active.toLocaleString();
1208
+ this.elements.nodeCount.textContent = stats.total_nodes.toLocaleString();
1209
+ this.elements.integrationCount.textContent = stats.unique_integrations.toLocaleString();
1210
+ }
1211
+
1212
+ updateResultsCount() {
1213
+ const count = this.state.totalCount;
1214
+ const query = this.state.searchQuery;
1215
+ const category = this.state.filters.category;
1216
+
1217
+ let text = `${count.toLocaleString()} workflows`;
1218
+
1219
+ if (query && category !== 'all') {
1220
+ text += ` found for "${query}" in "${category}"`;
1221
+ } else if (query) {
1222
+ text += ` found for "${query}"`;
1223
+ } else if (category !== 'all') {
1224
+ text += ` in "${category}"`;
1225
+ }
1226
+
1227
+ this.elements.resultsCount.textContent = text;
1228
+ }
1229
+
1230
+ renderWorkflows() {
1231
+ const html = this.state.workflows.map(workflow => this.createWorkflowCard(workflow)).join('');
1232
+ this.elements.workflowGrid.innerHTML = html;
1233
+
1234
+ // Add click handlers to cards
1235
+ this.elements.workflowGrid.querySelectorAll('.workflow-card').forEach((card, index) => {
1236
+ card.addEventListener('click', () => {
1237
+ this.openWorkflowDetail(this.state.workflows[index]);
1238
+ });
1239
+ });
1240
+ }
1241
+
1242
+ createWorkflowCard(workflow) {
1243
+ const statusClass = workflow.active ? 'status-active' : 'status-inactive';
1244
+ const complexityClass = `complexity-${workflow.complexity}`;
1245
+ const category = this.getWorkflowCategory(workflow.filename);
1246
+
1247
+ const integrations = workflow.integrations.slice(0, 5).map(integration =>
1248
+ `<span class="integration-tag">${this.escapeHtml(integration)}</span>`
1249
+ ).join('');
1250
+
1251
+ const moreIntegrations = workflow.integrations.length > 5
1252
+ ? `<span class="integration-tag">+${workflow.integrations.length - 5}</span>`
1253
+ : '';
1254
+
1255
+ return `
1256
+ <div class="workflow-card" data-filename="${workflow.filename}">
1257
+ <div class="workflow-header">
1258
+ <div class="workflow-meta">
1259
+ <div class="status-dot ${statusClass}"></div>
1260
+ <div class="complexity-dot ${complexityClass}"></div>
1261
+ <span>${workflow.node_count} nodes</span>
1262
+ <span class="category-badge">${this.escapeHtml(category)}</span>
1263
+ </div>
1264
+ <span class="trigger-badge">${this.escapeHtml(workflow.trigger_type)}</span>
1265
+ </div>
1266
+
1267
+ <h3 class="workflow-title">${this.escapeHtml(workflow.name)}</h3>
1268
+ <p class="workflow-description">${this.escapeHtml(workflow.description)}</p>
1269
+
1270
+ ${workflow.integrations.length > 0 ? `
1271
+ <div class="workflow-integrations">
1272
+ <h4 class="integrations-title">Integrations (${workflow.integrations.length})</h4>
1273
+ <div class="integrations-list">
1274
+ ${integrations}
1275
+ ${moreIntegrations}
1276
+ </div>
1277
+ </div>
1278
+ ` : ''}
1279
+ </div>
1280
+ `;
1281
+ }
1282
+
1283
+ async openWorkflowDetail(workflow) {
1284
+ this.currentWorkflow = workflow;
1285
+ this.elements.modalTitle.textContent = workflow.name;
1286
+ this.elements.modalDescription.textContent = workflow.description;
1287
+
1288
+ // Update stats
1289
+ const category = this.getWorkflowCategory(workflow.filename);
1290
+ this.elements.modalStats.innerHTML = `
1291
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
1292
+ <div><strong>Status:</strong> ${workflow.active ? 'Active' : 'Inactive'}</div>
1293
+ <div><strong>Trigger:</strong> ${workflow.trigger_type}</div>
1294
+ <div><strong>Complexity:</strong> ${workflow.complexity}</div>
1295
+ <div><strong>Nodes:</strong> ${workflow.node_count}</div>
1296
+ <div><strong>Category:</strong> ${this.escapeHtml(category)}</div>
1297
+ </div>
1298
+ `;
1299
+
1300
+ // Update integrations
1301
+ if (workflow.integrations.length > 0) {
1302
+ this.elements.modalIntegrations.innerHTML = workflow.integrations
1303
+ .map(integration => `<span class="integration-tag">${this.escapeHtml(integration)}</span>`)
1304
+ .join(' ');
1305
+ } else {
1306
+ this.elements.modalIntegrations.textContent = 'No integrations found';
1307
+ }
1308
+
1309
+ // Set download link
1310
+ this.elements.downloadBtn.href = `/api/workflows/${workflow.filename}/download`;
1311
+ this.elements.downloadBtn.download = workflow.filename;
1312
+
1313
+ // Reset view states
1314
+ this.elements.jsonSection.classList.add('hidden');
1315
+ this.elements.diagramSection.classList.add('hidden');
1316
+
1317
+ this.elements.workflowModal.classList.remove('hidden');
1318
+ }
1319
+
1320
+ closeModal() {
1321
+ this.elements.workflowModal.classList.add('hidden');
1322
+ this.currentWorkflow = null;
1323
+ this.currentJsonData = null;
1324
+ this.currentDiagramData = null;
1325
+
1326
+ // Reset button states
1327
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ View JSON';
1328
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š View Diagram';
1329
+
1330
+ // Reset copy button states
1331
+ this.resetCopyButton('copyJsonBtn');
1332
+ this.resetCopyButton('copyDiagramBtn');
1333
+ }
1334
+
1335
+ async toggleJsonView() {
1336
+ if (!this.currentWorkflow) return;
1337
+
1338
+ const isVisible = !this.elements.jsonSection.classList.contains('hidden');
1339
+
1340
+ if (isVisible) {
1341
+ this.elements.jsonSection.classList.add('hidden');
1342
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ View JSON';
1343
+ } else {
1344
+ try {
1345
+ this.elements.jsonViewer.textContent = 'Loading...';
1346
+ this.elements.jsonSection.classList.remove('hidden');
1347
+ this.elements.viewJsonBtn.textContent = 'πŸ“„ Hide JSON';
1348
+
1349
+ const data = await this.apiCall(`/workflows/${this.currentWorkflow.filename}`);
1350
+ const jsonString = JSON.stringify(data.raw_json, null, 2);
1351
+ this.currentJsonData = jsonString;
1352
+ this.elements.jsonViewer.textContent = jsonString;
1353
+ } catch (error) {
1354
+ this.elements.jsonViewer.textContent = 'Error loading JSON: ' + error.message;
1355
+ this.currentJsonData = null;
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ async toggleDiagramView() {
1361
+ if (!this.currentWorkflow) return;
1362
+
1363
+ const isVisible = !this.elements.diagramSection.classList.contains('hidden');
1364
+
1365
+ if (isVisible) {
1366
+ this.elements.diagramSection.classList.add('hidden');
1367
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š View Diagram';
1368
+ } else {
1369
+ try {
1370
+ this.elements.diagramViewer.textContent = 'Loading diagram...';
1371
+ this.elements.diagramSection.classList.remove('hidden');
1372
+ this.elements.viewDiagramBtn.textContent = 'πŸ“Š Hide Diagram';
1373
+
1374
+ const data = await this.apiCall(`/workflows/${this.currentWorkflow.filename}/diagram`);
1375
+ this.currentDiagramData = data.diagram;
1376
+
1377
+ // Create a Mermaid diagram that will be rendered
1378
+ this.elements.diagramViewer.innerHTML = `
1379
+ <pre class="mermaid">${data.diagram}</pre>
1380
+ `;
1381
+
1382
+ // Re-initialize Mermaid for the new diagram
1383
+ if (typeof mermaid !== 'undefined') {
1384
+ mermaid.init(undefined, this.elements.diagramViewer.querySelector('.mermaid'));
1385
+ }
1386
+ } catch (error) {
1387
+ this.elements.diagramViewer.textContent = 'Error loading diagram: ' + error.message;
1388
+ this.currentDiagramData = null;
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ updateLoadMoreButton() {
1394
+ const hasMore = this.state.currentPage < this.state.totalPages;
1395
+
1396
+ if (hasMore && this.state.workflows.length > 0) {
1397
+ this.elements.loadMoreContainer.classList.remove('hidden');
1398
+ } else {
1399
+ this.elements.loadMoreContainer.classList.add('hidden');
1400
+ }
1401
+ }
1402
+
1403
+ showState(state) {
1404
+ // Hide all states
1405
+ this.elements.loadingState.classList.add('hidden');
1406
+ this.elements.errorState.classList.add('hidden');
1407
+ this.elements.noResultsState.classList.add('hidden');
1408
+ this.elements.workflowGrid.classList.add('hidden');
1409
+
1410
+ // Show the requested state
1411
+ switch (state) {
1412
+ case 'loading':
1413
+ this.elements.loadingState.classList.remove('hidden');
1414
+ break;
1415
+ case 'error':
1416
+ this.elements.errorState.classList.remove('hidden');
1417
+ break;
1418
+ case 'no-results':
1419
+ this.elements.noResultsState.classList.remove('hidden');
1420
+ break;
1421
+ case 'content':
1422
+ this.elements.workflowGrid.classList.remove('hidden');
1423
+ break;
1424
+ }
1425
+ }
1426
+
1427
+ showError(message) {
1428
+ this.elements.errorMessage.textContent = message;
1429
+ this.showState('error');
1430
+ }
1431
+
1432
+ escapeHtml(text) {
1433
+ const div = document.createElement('div');
1434
+ div.textContent = text;
1435
+ return div.innerHTML;
1436
+ }
1437
+
1438
+ async copyToClipboard(text, buttonId) {
1439
+ if (!text) {
1440
+ console.warn('No content to copy');
1441
+ return;
1442
+ }
1443
+
1444
+ try {
1445
+ await navigator.clipboard.writeText(text);
1446
+ this.showCopySuccess(buttonId);
1447
+ } catch (error) {
1448
+ // Fallback for older browsers
1449
+ this.fallbackCopyToClipboard(text, buttonId);
1450
+ }
1451
+ }
1452
+
1453
+ fallbackCopyToClipboard(text, buttonId) {
1454
+ const textArea = document.createElement('textarea');
1455
+ textArea.value = text;
1456
+ textArea.style.position = 'fixed';
1457
+ textArea.style.left = '-999999px';
1458
+ textArea.style.top = '-999999px';
1459
+ document.body.appendChild(textArea);
1460
+ textArea.focus();
1461
+ textArea.select();
1462
+
1463
+ try {
1464
+ document.execCommand('copy');
1465
+ this.showCopySuccess(buttonId);
1466
+ } catch (error) {
1467
+ console.error('Failed to copy text: ', error);
1468
+ } finally {
1469
+ document.body.removeChild(textArea);
1470
+ }
1471
+ }
1472
+
1473
+ showCopySuccess(buttonId) {
1474
+ const button = document.getElementById(buttonId);
1475
+ if (!button) return;
1476
+
1477
+ const originalText = button.innerHTML;
1478
+ button.innerHTML = 'βœ… Copied!';
1479
+ button.classList.add('copied');
1480
+
1481
+ setTimeout(() => {
1482
+ button.innerHTML = originalText;
1483
+ button.classList.remove('copied');
1484
+ }, 2000);
1485
+ }
1486
+
1487
+ resetCopyButton(buttonId) {
1488
+ const button = document.getElementById(buttonId);
1489
+ if (!button) return;
1490
+
1491
+ button.innerHTML = 'πŸ“‹ Copy';
1492
+ button.classList.remove('copied');
1493
+ }
1494
+ }
1495
+
1496
+ // Initialize the app
1497
+ document.addEventListener('DOMContentLoaded', () => {
1498
+ window.workflowApp = new WorkflowApp();
1499
+ });
1500
+ </script>
1501
+ </body>
1502
+
1503
+ </html>