pms_md 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +93 -0
  2. package/node-monitor/ARCHITECTURE.md +341 -0
  3. package/node-monitor/CHANGELOG.md +105 -0
  4. package/node-monitor/CONTRIBUTING.md +96 -0
  5. package/node-monitor/DESIGN_IMPROVEMENTS.md +286 -0
  6. package/node-monitor/FILTER_BUTTONS_FIX.md +303 -0
  7. package/node-monitor/GETTING_STARTED.md +416 -0
  8. package/node-monitor/INSTALLATION.md +470 -0
  9. package/node-monitor/LICENSE +22 -0
  10. package/node-monitor/PUBLISHING_GUIDE.md +331 -0
  11. package/node-monitor/QUICK_REFERENCE.md +252 -0
  12. package/node-monitor/README.md +458 -0
  13. package/node-monitor/READY_TO_PUBLISH.md +272 -0
  14. package/node-monitor/SETUP_GUIDE.md +479 -0
  15. package/node-monitor/examples/EMAIL_SETUP_GUIDE.md +282 -0
  16. package/node-monitor/examples/ERROR_LOGGING_GUIDE.md +405 -0
  17. package/node-monitor/examples/GET_APP_PASSWORD.md +145 -0
  18. package/node-monitor/examples/LOG_FILES_REFERENCE.md +336 -0
  19. package/node-monitor/examples/QUICK_START_EMAIL.md +126 -0
  20. package/node-monitor/examples/express-app.js +499 -0
  21. package/node-monitor/examples/package-lock.json +1295 -0
  22. package/node-monitor/examples/package.json +18 -0
  23. package/node-monitor/examples/public/css/style.css +718 -0
  24. package/node-monitor/examples/public/js/dashboard.js +207 -0
  25. package/node-monitor/examples/public/js/health.js +114 -0
  26. package/node-monitor/examples/public/js/main.js +89 -0
  27. package/node-monitor/examples/public/js/metrics.js +225 -0
  28. package/node-monitor/examples/public/js/theme.js +138 -0
  29. package/node-monitor/examples/views/dashboard.ejs +20 -0
  30. package/node-monitor/examples/views/error-logs.ejs +1129 -0
  31. package/node-monitor/examples/views/health.ejs +21 -0
  32. package/node-monitor/examples/views/home.ejs +341 -0
  33. package/node-monitor/examples/views/layout.ejs +50 -0
  34. package/node-monitor/examples/views/metrics.ejs +16 -0
  35. package/node-monitor/examples/views/partials/footer.ejs +16 -0
  36. package/node-monitor/examples/views/partials/header.ejs +35 -0
  37. package/node-monitor/examples/views/partials/nav.ejs +23 -0
  38. package/node-monitor/examples/views/status.ejs +390 -0
  39. package/node-monitor/package-lock.json +4300 -0
  40. package/node-monitor/package.json +76 -0
  41. package/node-monitor/pre-publish-check.js +200 -0
  42. package/node-monitor/src/config/monitoringConfig.js +255 -0
  43. package/node-monitor/src/index.js +300 -0
  44. package/node-monitor/src/logger/errorLogger.js +297 -0
  45. package/node-monitor/src/monitors/apiErrorMonitor.js +156 -0
  46. package/node-monitor/src/monitors/dbConnectionMonitor.js +389 -0
  47. package/node-monitor/src/monitors/serverHealthMonitor.js +320 -0
  48. package/node-monitor/src/monitors/systemResourceMonitor.js +357 -0
  49. package/node-monitor/src/notifiers/emailNotifier.js +248 -0
  50. package/node-monitor/src/notifiers/notificationManager.js +96 -0
  51. package/node-monitor/src/notifiers/slackNotifier.js +209 -0
  52. package/node-monitor/src/views/dashboard.html +530 -0
  53. package/node-monitor/src/views/health.html +399 -0
  54. package/node-monitor/src/views/metrics.html +406 -0
  55. package/package.json +22 -0
@@ -0,0 +1,1129 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %> - Node Monitor</title>
7
+ <link rel="stylesheet" href="/css/style.css?v=3.0">
8
+ <script src="/js/theme.js"></script>
9
+ <!-- Filter Buttons Fix v3.0 - All buttons start in simple state -->
10
+ <style>
11
+ /* Page Header with Gradient */
12
+ .page-header-gradient {
13
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
+ margin: 0 0 20px 0;
15
+ padding: 18px 30px;
16
+ border-radius: 15px;
17
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
18
+ }
19
+
20
+ .page-header-content {
21
+ max-width: 1400px;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ .header-text {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 12px;
29
+ }
30
+
31
+ .header-icon {
32
+ font-size: 2em;
33
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
34
+ }
35
+
36
+ .page-title {
37
+ font-size: 1.5em;
38
+ font-weight: 700;
39
+ color: white;
40
+ margin: 0;
41
+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
42
+ }
43
+
44
+ .page-subtitle {
45
+ font-size: 0.85em;
46
+ color: rgba(255, 255, 255, 0.9);
47
+ margin: 3px 0 0 0;
48
+ font-weight: 400;
49
+ }
50
+
51
+ .error-logs-container {
52
+ max-width: 1400px;
53
+ margin: 0 auto;
54
+ padding: 20px;
55
+ }
56
+
57
+ .logs-header {
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: center;
61
+ margin-bottom: 30px;
62
+ flex-wrap: wrap;
63
+ gap: 15px;
64
+ background: var(--card-bg);
65
+ padding: 20px;
66
+ border-radius: 12px;
67
+ box-shadow: 0 2px 8px var(--card-shadow);
68
+ transition: background 0.3s ease;
69
+ }
70
+
71
+ .header-stats-inline {
72
+ display: flex;
73
+ gap: 30px;
74
+ flex-wrap: wrap;
75
+ }
76
+
77
+ .inline-stat {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 8px 16px;
82
+ background: var(--nav-bg);
83
+ border-radius: 8px;
84
+ border-left: 3px solid #667eea;
85
+ transition: background 0.3s ease;
86
+ }
87
+
88
+ .stat-icon {
89
+ font-size: 1.2em;
90
+ }
91
+
92
+ .stat-text {
93
+ font-size: 0.95em;
94
+ color: var(--text-secondary);
95
+ }
96
+
97
+ .stat-text strong {
98
+ color: var(--text-primary);
99
+ font-size: 1.1em;
100
+ }
101
+
102
+ .file-selector {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 12px;
106
+ background: var(--nav-bg);
107
+ padding: 12px 20px;
108
+ border-radius: 10px;
109
+ border: 2px solid var(--border-color);
110
+ transition: background 0.3s ease;
111
+ }
112
+
113
+ .file-selector label {
114
+ font-weight: 600;
115
+ color: var(--text-primary);
116
+ font-size: 0.95em;
117
+ }
118
+
119
+ .file-selector select {
120
+ padding: 10px 16px;
121
+ border: 2px solid var(--border-color);
122
+ border-radius: 8px;
123
+ font-size: 14px;
124
+ background: var(--card-bg);
125
+ color: var(--text-primary);
126
+ cursor: pointer;
127
+ min-width: 220px;
128
+ font-weight: 500;
129
+ transition: all 0.3s ease;
130
+ }
131
+
132
+ .file-selector select:hover {
133
+ border-color: #667eea;
134
+ }
135
+
136
+ .file-selector select:focus {
137
+ outline: none;
138
+ border-color: #667eea;
139
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
140
+ }
141
+
142
+ /* Filter Buttons Section */
143
+ .filter-buttons-container {
144
+ background: var(--card-bg);
145
+ padding: 25px;
146
+ border-radius: 12px;
147
+ box-shadow: 0 2px 8px var(--card-shadow);
148
+ margin-bottom: 30px;
149
+ transition: background 0.3s ease;
150
+ }
151
+
152
+ .filter-section-title {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 10px;
156
+ font-size: 1.1em;
157
+ font-weight: 600;
158
+ color: var(--text-primary);
159
+ margin-bottom: 20px;
160
+ padding-bottom: 15px;
161
+ border-bottom: 2px solid var(--border-color);
162
+ }
163
+
164
+ .filter-icon {
165
+ font-size: 1.3em;
166
+ }
167
+
168
+ .filter-buttons-grid {
169
+ display: grid;
170
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
171
+ gap: 15px;
172
+ }
173
+
174
+ .filter-btn {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 10px;
178
+ padding: 12px 20px;
179
+ border: 2px solid var(--border-color);
180
+ background: var(--card-bg);
181
+ border-radius: 8px;
182
+ cursor: pointer;
183
+ transition: all 0.2s ease;
184
+ font-weight: 500;
185
+ color: var(--text-secondary);
186
+ }
187
+
188
+ .filter-btn-icon {
189
+ font-size: 1.2em;
190
+ }
191
+
192
+ .filter-btn-text {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ flex: 1;
197
+ }
198
+
199
+ .filter-btn-label {
200
+ font-size: 0.9em;
201
+ color: var(--text-secondary);
202
+ font-weight: 500;
203
+ }
204
+
205
+ .filter-btn-count {
206
+ font-size: 1.1em;
207
+ color: var(--text-primary);
208
+ font-weight: 700;
209
+ }
210
+
211
+ /* Hover State */
212
+ .filter-btn:hover {
213
+ border-color: #667eea;
214
+ background: var(--nav-bg);
215
+ }
216
+
217
+ /* Active State */
218
+ .filter-btn.active {
219
+ border-color: #667eea;
220
+ background: #667eea;
221
+ color: white;
222
+ }
223
+
224
+ .filter-btn.active .filter-btn-label {
225
+ color: white;
226
+ }
227
+
228
+ .filter-btn.active .filter-btn-count {
229
+ color: white;
230
+ }
231
+
232
+ .filter-btn:active {
233
+ transform: translateY(0);
234
+ }
235
+
236
+ .log-actions {
237
+ display: flex;
238
+ gap: 10px;
239
+ }
240
+
241
+ .action-btn {
242
+ padding: 12px 24px;
243
+ border: none;
244
+ border-radius: 10px;
245
+ font-size: 14px;
246
+ font-weight: 600;
247
+ cursor: pointer;
248
+ transition: all 0.3s ease;
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
252
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
253
+ position: relative;
254
+ overflow: hidden;
255
+ }
256
+
257
+ .action-btn::before {
258
+ content: '';
259
+ position: absolute;
260
+ top: 50%;
261
+ left: 50%;
262
+ width: 0;
263
+ height: 0;
264
+ border-radius: 50%;
265
+ background: rgba(255, 255, 255, 0.3);
266
+ transform: translate(-50%, -50%);
267
+ transition: width 0.6s, height 0.6s;
268
+ }
269
+
270
+ .action-btn:hover::before {
271
+ width: 300px;
272
+ height: 300px;
273
+ }
274
+
275
+ .action-btn span {
276
+ position: relative;
277
+ z-index: 1;
278
+ }
279
+
280
+ .download-btn {
281
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
282
+ color: white;
283
+ }
284
+
285
+ .download-btn:hover {
286
+ transform: translateY(-3px);
287
+ box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
288
+ }
289
+
290
+ .refresh-btn {
291
+ background: linear-gradient(135deg, #10b981, #059669);
292
+ color: white;
293
+ }
294
+
295
+ .refresh-btn:hover {
296
+ transform: translateY(-3px);
297
+ box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
298
+ }
299
+
300
+ .clear-btn {
301
+ background: linear-gradient(135deg, #ef4444, #dc2626);
302
+ color: white;
303
+ }
304
+
305
+ .action-btn:active {
306
+ transform: translateY(0);
307
+ }
308
+
309
+ .stats-summary {
310
+ display: grid;
311
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
312
+ gap: 20px;
313
+ margin-bottom: 30px;
314
+ }
315
+
316
+ .stat-card {
317
+ background: var(--card-bg);
318
+ padding: 25px;
319
+ border-radius: 12px;
320
+ box-shadow: 0 2px 8px var(--card-shadow);
321
+ border-left: 4px solid #667eea;
322
+ transition: all 0.3s ease;
323
+ position: relative;
324
+ overflow: hidden;
325
+ }
326
+
327
+ .stat-card::before {
328
+ content: '';
329
+ position: absolute;
330
+ top: 0;
331
+ right: 0;
332
+ width: 80px;
333
+ height: 80px;
334
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
335
+ border-radius: 0 0 0 100%;
336
+ }
337
+
338
+ .stat-card:hover {
339
+ transform: translateY(-4px);
340
+ box-shadow: 0 6px 20px var(--card-shadow);
341
+ }
342
+
343
+ .stat-label {
344
+ font-size: 0.9em;
345
+ color: var(--text-secondary);
346
+ margin-bottom: 12px;
347
+ font-weight: 600;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.5px;
350
+ }
351
+
352
+ .stat-value {
353
+ font-size: 2em;
354
+ font-weight: 700;
355
+ color: var(--text-primary);
356
+ position: relative;
357
+ z-index: 1;
358
+ }
359
+
360
+ .error-list {
361
+ background: var(--card-bg);
362
+ border-radius: 12px;
363
+ box-shadow: 0 2px 8px var(--card-shadow);
364
+ overflow: hidden;
365
+ transition: background 0.3s ease;
366
+ }
367
+
368
+ .error-item {
369
+ border-bottom: 1px solid #e2e8f0;
370
+ padding: 24px;
371
+ transition: all 0.3s ease;
372
+ position: relative;
373
+ border-left: 4px solid transparent;
374
+ background: rgba(0, 0, 0, 0.01);
375
+ }
376
+
377
+ .error-item:hover {
378
+ background: #f8fafc;
379
+ border-left-color: #667eea;
380
+ padding-left: 28px;
381
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
382
+ }
383
+
384
+ .error-item:last-child {
385
+ border-bottom: none;
386
+ }
387
+
388
+ /* Light mode */
389
+ [data-theme="light"] .error-item,
390
+ :root:not([data-theme="dark"]) .error-item {
391
+ border-bottom-color: #e2e8f0 !important;
392
+ background: rgba(0, 0, 0, 0.01) !important;
393
+ }
394
+
395
+ [data-theme="light"] .error-item:hover,
396
+ :root:not([data-theme="dark"]) .error-item:hover {
397
+ background: #f8fafc !important;
398
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
399
+ }
400
+
401
+ /* Dark mode */
402
+ [data-theme="dark"] .error-item {
403
+ border-bottom-color: #2d3748 !important;
404
+ background: rgba(255, 255, 255, 0.01) !important;
405
+ }
406
+
407
+ [data-theme="dark"] .error-item:hover {
408
+ background: #0f1623 !important;
409
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
410
+ }
411
+
412
+ .error-header {
413
+ display: flex;
414
+ justify-content: space-between;
415
+ align-items: flex-start;
416
+ margin-bottom: 12px;
417
+ gap: 15px;
418
+ }
419
+
420
+ .error-title {
421
+ flex: 1;
422
+ }
423
+
424
+ .error-message {
425
+ font-size: 17px;
426
+ font-weight: 700;
427
+ color: #1e293b;
428
+ margin-bottom: 8px;
429
+ line-height: 1.4;
430
+ }
431
+
432
+ .error-meta {
433
+ display: flex;
434
+ gap: 15px;
435
+ flex-wrap: wrap;
436
+ font-size: 14px;
437
+ color: #475569;
438
+ font-weight: 600;
439
+ }
440
+
441
+ .error-meta span {
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 5px;
445
+ }
446
+
447
+ /* Light mode */
448
+ [data-theme="light"] .error-message,
449
+ :root:not([data-theme="dark"]) .error-message {
450
+ color: #0f172a !important;
451
+ text-shadow: none !important;
452
+ }
453
+
454
+ [data-theme="light"] .error-meta,
455
+ :root:not([data-theme="dark"]) .error-meta {
456
+ color: #475569 !important;
457
+ }
458
+
459
+ [data-theme="light"] .error-meta span,
460
+ :root:not([data-theme="dark"]) .error-meta span {
461
+ text-shadow: none !important;
462
+ }
463
+
464
+ /* Dark mode */
465
+ [data-theme="dark"] .error-message {
466
+ color: #ffffff !important;
467
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5) !important;
468
+ }
469
+
470
+ [data-theme="dark"] .error-meta {
471
+ color: #cbd5e1 !important;
472
+ }
473
+
474
+ [data-theme="dark"] .error-meta span {
475
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
476
+ }
477
+
478
+ .error-badge {
479
+ padding: 6px 14px;
480
+ border-radius: 8px;
481
+ font-size: 12px;
482
+ font-weight: 700;
483
+ white-space: nowrap;
484
+ box-shadow: 0 2px 4px var(--card-shadow);
485
+ transition: all 0.3s ease;
486
+ }
487
+
488
+ .error-badge:hover {
489
+ transform: scale(1.05);
490
+ }
491
+
492
+ .badge-error {
493
+ background: linear-gradient(135deg, #f8b1b1, #f77e7e);
494
+ color: #f0eded;
495
+ border: 1px solid #f64949;
496
+ }
497
+
498
+ .badge-warning {
499
+ background: linear-gradient(135deg, #fef3c7, #fde68a);
500
+ color: #d97706;
501
+ border: 1px solid #fcd34d;
502
+ }
503
+
504
+ .error-details {
505
+ margin-top: 12px;
506
+ padding: 18px;
507
+ background: #1a202c;
508
+ border-radius: 10px;
509
+ font-family: 'Courier New', monospace;
510
+ transition: background 0.3s ease;
511
+ font-size: 14px;
512
+ border: 1px solid #2d3748;
513
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
514
+ }
515
+
516
+ /* Light mode - lighter background */
517
+ [data-theme="light"] .error-details,
518
+ :root:not([data-theme="dark"]) .error-details {
519
+ background: #f8fafc !important;
520
+ border-color: #cbd5e1 !important;
521
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05) !important;
522
+ }
523
+
524
+ /* Dark mode - darker background */
525
+ [data-theme="dark"] .error-details {
526
+ background: #0a0f1e !important;
527
+ border-color: #2d3748 !important;
528
+ }
529
+
530
+ .detail-row {
531
+ display: grid;
532
+ grid-template-columns: 150px 1fr;
533
+ gap: 12px;
534
+ margin-bottom: 10px;
535
+ padding: 8px;
536
+ background: rgba(0, 0, 0, 0.02);
537
+ border-radius: 6px;
538
+ }
539
+
540
+ .detail-row:hover {
541
+ background: rgba(0, 0, 0, 0.05);
542
+ }
543
+
544
+ /* Light mode */
545
+ [data-theme="light"] .detail-row,
546
+ :root:not([data-theme="dark"]) .detail-row {
547
+ background: rgba(0, 0, 0, 0.02) !important;
548
+ }
549
+
550
+ [data-theme="light"] .detail-row:hover,
551
+ :root:not([data-theme="dark"]) .detail-row:hover {
552
+ background: rgba(0, 0, 0, 0.05) !important;
553
+ }
554
+
555
+ /* Dark mode */
556
+ [data-theme="dark"] .detail-row {
557
+ background: rgba(255, 255, 255, 0.02) !important;
558
+ }
559
+
560
+ [data-theme="dark"] .detail-row:hover {
561
+ background: rgba(255, 255, 255, 0.05) !important;
562
+ }
563
+
564
+ .detail-label {
565
+ font-weight: 800;
566
+ color: #64748b;
567
+ text-transform: uppercase;
568
+ font-size: 13px;
569
+ letter-spacing: 1px;
570
+ }
571
+
572
+ .detail-value {
573
+ color: #1e293b;
574
+ word-break: break-all;
575
+ font-weight: 600;
576
+ font-size: 14px;
577
+ }
578
+
579
+ /* Light mode */
580
+ [data-theme="light"] .detail-label,
581
+ :root:not([data-theme="dark"]) .detail-label {
582
+ color: #475569 !important;
583
+ text-shadow: none !important;
584
+ }
585
+
586
+ [data-theme="light"] .detail-value,
587
+ :root:not([data-theme="dark"]) .detail-value {
588
+ color: #0f172a !important;
589
+ text-shadow: none !important;
590
+ }
591
+
592
+ /* Dark mode */
593
+ [data-theme="dark"] .detail-label {
594
+ color: #94a3b8 !important;
595
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) !important;
596
+ }
597
+
598
+ [data-theme="dark"] .detail-value {
599
+ color: #ffffff !important;
600
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
601
+ }
602
+
603
+ .stack-trace {
604
+ margin-top: 12px;
605
+ padding: 18px;
606
+ background: #1a202c;
607
+ color: #1e293b;
608
+ border-radius: 8px;
609
+ border: 1px solid #cbd5e1;
610
+ font-family: 'Courier New', monospace;
611
+ font-size: 13px;
612
+ overflow-x: auto;
613
+ white-space: pre-wrap;
614
+ word-break: break-word;
615
+ max-height: 300px;
616
+ overflow-y: auto;
617
+ line-height: 1.7;
618
+ font-weight: 500;
619
+ }
620
+
621
+ /* Light mode */
622
+ [data-theme="light"] .stack-trace,
623
+ :root:not([data-theme="dark"]) .stack-trace {
624
+ background: #f8fafc !important;
625
+ color: #0f172a !important;
626
+ border-color: #cbd5e1 !important;
627
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05) !important;
628
+ }
629
+
630
+ /* Dark mode */
631
+ [data-theme="dark"] .stack-trace {
632
+ background: #0a0f1e !important;
633
+ color: #ffffff !important;
634
+ border-color: #2d3748 !important;
635
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4) !important;
636
+ }
637
+
638
+ .toggle-stack {
639
+ margin-top: 12px;
640
+ padding: 8px 16px;
641
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
642
+ border: none;
643
+ border-radius: 8px;
644
+ font-size: 13px;
645
+ cursor: pointer;
646
+ color: #ffffff;
647
+ font-weight: 600;
648
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
649
+ transition: all 0.2s ease;
650
+ }
651
+
652
+ .toggle-stack:hover {
653
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
654
+ transform: translateY(-1px);
655
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
656
+ }
657
+
658
+ .toggle-stack:active {
659
+ transform: translateY(0);
660
+ }
661
+
662
+ .no-errors {
663
+ text-align: center;
664
+ padding: 60px 20px;
665
+ color: #6b7280;
666
+ }
667
+
668
+ .no-errors-icon {
669
+ font-size: 64px;
670
+ margin-bottom: 20px;
671
+ }
672
+
673
+ .loading {
674
+ text-align: center;
675
+ padding: 40px;
676
+ color: #6b7280;
677
+ }
678
+
679
+ .spinner {
680
+ border: 3px solid #f3f4f6;
681
+ border-top: 3px solid #8b5cf6;
682
+ border-radius: 50%;
683
+ width: 40px;
684
+ height: 40px;
685
+ animation: spin 1s linear infinite;
686
+ margin: 0 auto 20px;
687
+ }
688
+
689
+ @keyframes spin {
690
+ 0% { transform: rotate(0deg); }
691
+ 100% { transform: rotate(360deg); }
692
+ }
693
+
694
+ /* Responsive Design */
695
+ @media (max-width: 768px) {
696
+ .page-header-gradient {
697
+ margin: 0 0 20px 0;
698
+ padding: 30px 20px;
699
+ }
700
+
701
+ .header-text {
702
+ flex-direction: column;
703
+ text-align: center;
704
+ gap: 15px;
705
+ }
706
+
707
+ .header-icon {
708
+ font-size: 2.5em;
709
+ }
710
+
711
+ .page-title {
712
+ font-size: 1.8em;
713
+ }
714
+
715
+ .page-subtitle {
716
+ font-size: 1em;
717
+ }
718
+
719
+ .logs-header {
720
+ flex-direction: column;
721
+ align-items: stretch;
722
+ }
723
+
724
+ .header-stats-inline {
725
+ flex-direction: column;
726
+ gap: 10px;
727
+ }
728
+
729
+ .file-selector {
730
+ flex-direction: column;
731
+ align-items: stretch;
732
+ }
733
+
734
+ .file-selector select {
735
+ width: 100%;
736
+ }
737
+
738
+ .log-actions {
739
+ flex-direction: column;
740
+ width: 100%;
741
+ }
742
+
743
+ .action-btn {
744
+ width: 100%;
745
+ justify-content: center;
746
+ }
747
+
748
+ .filter-buttons-grid {
749
+ grid-template-columns: 1fr;
750
+ }
751
+
752
+ .stats-summary {
753
+ grid-template-columns: 1fr;
754
+ }
755
+ }
756
+
757
+ /* Smooth Animations */
758
+ * {
759
+ scroll-behavior: smooth;
760
+ }
761
+
762
+ .error-item {
763
+ animation: fadeIn 0.5s ease-in-out;
764
+ }
765
+
766
+ @keyframes fadeIn {
767
+ from {
768
+ opacity: 0;
769
+ transform: translateY(10px);
770
+ }
771
+ to {
772
+ opacity: 1;
773
+ transform: translateY(0);
774
+ }
775
+ }
776
+ </style>
777
+ </head>
778
+ <body>
779
+ <%- include('partials/nav') %>
780
+
781
+ <!-- Page Header with Gradient -->
782
+ <!-- <div class="page-header-gradient">
783
+ <div class="page-header-content">
784
+ <div class="header-text">
785
+ <div class="header-icon">📋</div>
786
+ <div>
787
+ <h1 class="page-title">API Error Logs</h1>
788
+ <p class="page-subtitle">View and analyze API errors in real-time</p>
789
+ </div>
790
+ </div>
791
+ </div>
792
+ </div> -->
793
+
794
+ <div class="error-logs-container">
795
+ <div class="logs-header">
796
+ <div class="header-stats-inline">
797
+ <div class="inline-stat">
798
+ <span class="stat-icon">📊</span>
799
+ <span class="stat-text">Total: <strong id="totalCount">0</strong></span>
800
+ </div>
801
+ <div class="inline-stat">
802
+ <span class="stat-icon">🔴</span>
803
+ <span class="stat-text">API: <strong id="apiCount">0</strong></span>
804
+ </div>
805
+ <div class="inline-stat">
806
+ <span class="stat-icon">⚠️</span>
807
+ <span class="stat-text">Server: <strong id="serverCount">0</strong></span>
808
+ </div>
809
+ </div>
810
+
811
+ <div class="file-selector">
812
+ <label for="logFile">📅 Log File:</label>
813
+ <select id="logFile" onchange="loadLogFile()">
814
+ <% if (files.length === 0) { %>
815
+ <option value="">No log files available</option>
816
+ <% } else { %>
817
+ <% files.forEach(file => { %>
818
+ <option value="<%= file %>"><%= file %></option>
819
+ <% }); %>
820
+ <% } %>
821
+ </select>
822
+ </div>
823
+
824
+ <div class="log-actions">
825
+ <button class="action-btn refresh-btn" onclick="loadLogFile()">
826
+ <span>🔄</span>
827
+ <span>Refresh</span>
828
+ </button>
829
+ <button class="action-btn download-btn" onclick="downloadLog()">
830
+ <span>📥</span>
831
+ <span>Download</span>
832
+ </button>
833
+ </div>
834
+ </div>
835
+
836
+ <div class="stats-summary" id="stats">
837
+ <div class="stat-card">
838
+ <div class="stat-label">Total Errors</div>
839
+ <div class="stat-value" id="totalErrors">-</div>
840
+ </div>
841
+ <div class="stat-card">
842
+ <div class="stat-label">Latest Error</div>
843
+ <div class="stat-value" style="font-size: 16px;" id="latestError">-</div>
844
+ </div>
845
+ <div class="stat-card">
846
+ <div class="stat-label">Most Common</div>
847
+ <div class="stat-value" style="font-size: 16px;" id="commonError">-</div>
848
+ </div>
849
+ </div>
850
+
851
+ <!-- Filter Buttons -->
852
+ <div class="filter-buttons-container">
853
+ <div class="filter-section-title">
854
+ <span class="filter-icon">🔍</span>
855
+ <span>Filter by Type</span>
856
+ </div>
857
+ <div class="filter-buttons-grid">
858
+ <button class="filter-btn filter-all" onclick="filterErrors('ALL')" id="filter-all">
859
+ <span class="filter-btn-icon">📊</span>
860
+ <span class="filter-btn-text">
861
+ <span class="filter-btn-label">All Errors</span>
862
+ <span class="filter-btn-count" id="count-all">0</span>
863
+ </span>
864
+ </button>
865
+ <button class="filter-btn filter-api" onclick="filterErrors('API_ERROR')" id="filter-api">
866
+ <span class="filter-btn-icon">🔗</span>
867
+ <span class="filter-btn-text">
868
+ <span class="filter-btn-label">API Errors</span>
869
+ <span class="filter-btn-count" id="count-api">0</span>
870
+ </span>
871
+ </button>
872
+ <button class="filter-btn filter-server" onclick="filterErrors('SERVER_ERROR')" id="filter-server">
873
+ <span class="filter-btn-icon">⚙️</span>
874
+ <span class="filter-btn-text">
875
+ <span class="filter-btn-label">Server Errors</span>
876
+ <span class="filter-btn-count" id="count-server">0</span>
877
+ </span>
878
+ </button>
879
+ <button class="filter-btn filter-db" onclick="filterErrors('DATABASE_ERROR')" id="filter-db">
880
+ <span class="filter-btn-icon">🗄️</span>
881
+ <span class="filter-btn-text">
882
+ <span class="filter-btn-label">Database Errors</span>
883
+ <span class="filter-btn-count" id="count-db">0</span>
884
+ </span>
885
+ </button>
886
+ </div>
887
+ </div>
888
+
889
+ <div class="error-list" id="errorList">
890
+ <div class="loading">
891
+ <div class="spinner"></div>
892
+ <p>Loading error logs...</p>
893
+ </div>
894
+ </div>
895
+ </div>
896
+
897
+ <%- include('partials/footer') %>
898
+
899
+ <script>
900
+ let currentErrors = [];
901
+
902
+ async function loadLogFile() {
903
+ const select = document.getElementById('logFile');
904
+ const filename = select.value;
905
+
906
+ if (!filename || filename === '') {
907
+ document.getElementById('errorList').innerHTML = `
908
+ <div class="no-errors">
909
+ <div class="no-errors-icon">📋</div>
910
+ <h3>No log files available</h3>
911
+ <p>Error logs will appear here once errors occur.</p>
912
+ <p style="margin-top: 15px; color: #6b7280;">Try triggering an error by clicking the "✗ Error" button on the home page.</p>
913
+ </div>
914
+ `;
915
+ return;
916
+ }
917
+
918
+ document.getElementById('errorList').innerHTML = `
919
+ <div class="loading">
920
+ <div class="spinner"></div>
921
+ <p>Loading error logs...</p>
922
+ </div>
923
+ `;
924
+
925
+ try {
926
+ const response = await fetch(`/monitor/error-logs/${filename}?format=json&limit=100`);
927
+ const data = await response.json();
928
+
929
+ currentErrors = data.errors || [];
930
+ renderErrors(currentErrors);
931
+ updateStats(currentErrors, data.total);
932
+ } catch (error) {
933
+ document.getElementById('errorList').innerHTML = `
934
+ <div class="no-errors">
935
+ <div class="no-errors-icon">❌</div>
936
+ <h3>Failed to load error logs</h3>
937
+ <p>${error.message}</p>
938
+ </div>
939
+ `;
940
+ }
941
+ }
942
+
943
+ function renderErrors(errors) {
944
+ const container = document.getElementById('errorList');
945
+
946
+ if (errors.length === 0) {
947
+ container.innerHTML = `
948
+ <div class="no-errors">
949
+ <div class="no-errors-icon">✅</div>
950
+ <h3>No errors found</h3>
951
+ <p>This log file contains no errors. Great job!</p>
952
+ </div>
953
+ `;
954
+ return;
955
+ }
956
+
957
+ container.innerHTML = errors.map((error, index) => {
958
+ const hasApiEndpoint = error.url || error.path || error.method;
959
+ const errorType = error.type || 'UNKNOWN_ERROR';
960
+
961
+ return `
962
+ <div class="error-item">
963
+ <div class="error-header">
964
+ <div class="error-title">
965
+ <div class="error-message">${escapeHtml(error.message || 'Unknown Error')}</div>
966
+ <div class="error-meta">
967
+ <span>🕐 ${formatDate(error.timestamp)}</span>
968
+ ${error.ip ? `<span>📍 ${error.ip}</span>` : ''}
969
+ <span style="background: ${errorType === 'API_ERROR' ? '#dc2626' : '#f59e0b'}; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold;">
970
+ ${errorType.replace('_', ' ')}
971
+ </span>
972
+ </div>
973
+ </div>
974
+ <span class="error-badge badge-error">${error.statusCode || error.code || '500'}</span>
975
+ </div>
976
+
977
+ <!-- API Endpoint - Only show if it exists -->
978
+ ${hasApiEndpoint ? `
979
+ <div class="detail-row" style="background: linear-gradient(135deg, #fff3cd 0%, #ffe8a1 100%); padding: 14px; border-radius: 8px; margin: 12px 0; border-left: 5px solid #ff6b00; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
980
+ <span class="detail-label" style="font-weight: bold; color: #ff6b00; font-size: 13px;">🔗 API ENDPOINT:</span>
981
+ <span class="detail-value" style="font-family: 'Courier New', monospace; font-weight: bold; color: #d63384; font-size: 15px; display: block; margin-top: 6px;">
982
+ <span style="background: #dc2626; color: white; padding: 4px 8px; border-radius: 4px; margin-right: 8px;">${error.method || 'GET'}</span>
983
+ ${error.url || error.path || 'N/A'}
984
+ </span>
985
+ </div>
986
+ ` : ''}
987
+
988
+ <div class="error-details">
989
+ <div class="detail-row">
990
+ <span class="detail-label">Type:</span>
991
+ <span class="detail-value">${error.type || 'API_ERROR'}</span>
992
+ </div>
993
+ <div class="detail-row">
994
+ <span class="detail-label">Environment:</span>
995
+ <span class="detail-value">${error.app?.environment || 'N/A'}</span>
996
+ </div>
997
+ <div class="detail-row">
998
+ <span class="detail-label">Application:</span>
999
+ <span class="detail-value">${error.app?.name || 'N/A'} (v${error.app?.version || '1.0.0'})</span>
1000
+ </div>
1001
+ <div class="detail-row">
1002
+ <span class="detail-label">User Agent:</span>
1003
+ <span class="detail-value">${escapeHtml(error.userAgent || 'Unknown')}</span>
1004
+ </div>
1005
+ </div>
1006
+
1007
+ ${error.stack ? `
1008
+ <button class="toggle-stack" onclick="toggleStack(${index})">
1009
+ <span id="stack-toggle-${index}">▶ Show Stack Trace</span>
1010
+ </button>
1011
+ <div class="stack-trace" id="stack-${index}" style="display: none;">${escapeHtml(error.stack)}</div>
1012
+ ` : ''}
1013
+ </div>
1014
+ `;
1015
+ }).join('');
1016
+ }
1017
+
1018
+ function updateStats(errors, total) {
1019
+ document.getElementById('totalErrors').textContent = total || errors.length;
1020
+
1021
+ // Update filter counts
1022
+ const apiErrors = errors.filter(e => e.type === 'API_ERROR').length;
1023
+ const serverErrors = errors.filter(e => e.type === 'SERVER_ERROR').length;
1024
+ const dbErrors = errors.filter(e => e.type === 'DATABASE_ERROR').length;
1025
+
1026
+ // Update filter button counts
1027
+ document.getElementById('count-all').textContent = errors.length;
1028
+ document.getElementById('count-api').textContent = apiErrors;
1029
+ document.getElementById('count-server').textContent = serverErrors;
1030
+ document.getElementById('count-db').textContent = dbErrors;
1031
+
1032
+ // Update header stats counts
1033
+ document.getElementById('totalCount').textContent = errors.length;
1034
+ document.getElementById('apiCount').textContent = apiErrors;
1035
+ document.getElementById('serverCount').textContent = serverErrors;
1036
+
1037
+ if (errors.length > 0) {
1038
+ const latest = errors[0];
1039
+ document.getElementById('latestError').textContent = formatDate(latest.timestamp);
1040
+
1041
+ // Find most common error
1042
+ const errorCounts = {};
1043
+ errors.forEach(e => {
1044
+ const msg = e.message || 'Unknown';
1045
+ errorCounts[msg] = (errorCounts[msg] || 0) + 1;
1046
+ });
1047
+
1048
+ const mostCommon = Object.entries(errorCounts)
1049
+ .sort((a, b) => b[1] - a[1])[0];
1050
+
1051
+ if (mostCommon) {
1052
+ document.getElementById('commonError').textContent =
1053
+ mostCommon[0].substring(0, 30) + (mostCommon[0].length > 30 ? '...' : '');
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ let currentFilter = 'ALL';
1059
+
1060
+ function filterErrors(type) {
1061
+ currentFilter = type;
1062
+
1063
+ // Reset all buttons
1064
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1065
+ btn.classList.remove('active');
1066
+ });
1067
+
1068
+ // Activate clicked button
1069
+ const activeBtn = document.getElementById(`filter-${type.toLowerCase().replace('_error', '')}`);
1070
+ if (activeBtn) {
1071
+ activeBtn.classList.add('active');
1072
+ }
1073
+
1074
+ // Filter and render
1075
+ const filtered = type === 'ALL' ? currentErrors : currentErrors.filter(e => e.type === type);
1076
+ renderErrors(filtered);
1077
+ }
1078
+
1079
+ function toggleStack(index) {
1080
+ const stack = document.getElementById(`stack-${index}`);
1081
+ const toggle = document.getElementById(`stack-toggle-${index}`);
1082
+
1083
+ if (stack.style.display === 'none') {
1084
+ stack.style.display = 'block';
1085
+ toggle.textContent = '▼ Hide Stack Trace';
1086
+ } else {
1087
+ stack.style.display = 'none';
1088
+ toggle.textContent = '▶ Show Stack Trace';
1089
+ }
1090
+ }
1091
+
1092
+ function downloadLog() {
1093
+ const select = document.getElementById('logFile');
1094
+ const filename = select.value;
1095
+ if (filename) {
1096
+ window.location.href = `/monitor/error-logs/${filename}`;
1097
+ }
1098
+ }
1099
+
1100
+ function formatDate(timestamp) {
1101
+ const date = new Date(timestamp);
1102
+ return date.toLocaleString();
1103
+ }
1104
+
1105
+ function escapeHtml(text) {
1106
+ const div = document.createElement('div');
1107
+ div.textContent = text;
1108
+ return div.innerHTML;
1109
+ }
1110
+
1111
+ // Load first file on page load
1112
+ window.addEventListener('load', () => {
1113
+ loadLogFile();
1114
+ });
1115
+
1116
+ // Auto-refresh every 30 seconds
1117
+ setInterval(loadLogFile, 30000);
1118
+ </script>
1119
+
1120
+ </div> <!-- Close mainContent -->
1121
+ </div> <!-- Close container -->
1122
+
1123
+ <!-- Footer -->
1124
+ <!-- <div class="footer">
1125
+ <p>Node Monitor v1.0.0 | Built with ❤️ By Manish Desai</p>
1126
+ </div> -->
1127
+ </body>
1128
+ </html>
1129
+