go-duck-cli 1.3.372 → 1.4.6

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.
@@ -2,9 +2,12 @@ package router
2
2
 
3
3
  import (
4
4
  "context"
5
+ "encoding/json"
5
6
  "fmt"
7
+ "io"
6
8
  "log"
7
9
  "net/http"
10
+ "os"
8
11
  "strings"
9
12
  "time"
10
13
 
@@ -15,6 +18,9 @@ import (
15
18
  "gorm.io/driver/postgres"
16
19
  "gorm.io/gorm"
17
20
  "gorm.io/plugin/opentelemetry/tracing"
21
+ "github.com/prometheus/client_golang/prometheus/promhttp"
22
+ "github.com/shirou/gopsutil/v3/cpu"
23
+ "github.com/shirou/gopsutil/v3/mem"
18
24
  {{#if multitenancy_enabled}}
19
25
  "{{app_name}}/management"
20
26
  {{/if}}
@@ -148,6 +154,8 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
148
154
  log.Println("✅ Connected to master MongoDB")
149
155
  }
150
156
 
157
+ // Intercept Gin standard output for the live log stream
158
+ gin.DefaultWriter = io.MultiWriter(os.Stdout, logger.GlobalLogBroker)
151
159
  r := gin.Default()
152
160
 
153
161
  // 11. Global Middleware (OTel, Rate Limit & CORS)
@@ -157,15 +165,119 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
157
165
  r.Use(middleware.RateLimitMiddleware(appConfig))
158
166
  r.Use(middleware.CORSMiddleware(appConfig))
159
167
 
168
+ if appConfig.GoDuck.Telemetry.Metrics.PrometheusEnabled || appConfig.GoDuck.Telemetry.Metrics.StreamEnabled {
169
+ r.Use(telemetry.MetricsTrackingMiddleware(appConfig))
170
+ }
171
+
160
172
  // Health Check
161
173
  r.GET("/health", func(c *gin.Context) {
162
174
  c.JSON(http.StatusOK, gin.H{"status": "UP"})
163
175
  })
164
176
 
177
+ // Prometheus Metrics
178
+ if appConfig.GoDuck.Telemetry.Metrics.PrometheusEnabled {
179
+ r.GET("/metrics", gin.WrapH(promhttp.Handler()))
180
+ }
181
+
182
+ // Full JSON System Metrics (JHipster Parity)
183
+ if appConfig.GoDuck.Telemetry.Metrics.PrometheusEnabled || appConfig.GoDuck.Telemetry.Metrics.StreamEnabled {
184
+ r.GET("/api/system/metrics", func(c *gin.Context) {
185
+ sysMetrics := telemetry.CollectSystemMetrics()
186
+ appMetrics := telemetry.GetGlobalMetrics()
187
+
188
+ appMetrics.mu.RLock()
189
+ defer appMetrics.mu.RUnlock()
190
+
191
+ c.JSON(http.StatusOK, gin.H{
192
+ "system": sysMetrics,
193
+ "endpoints": appMetrics.Endpoints,
194
+ "status_codes": appMetrics.StatusCodes,
195
+ "failed_calls": appMetrics.FailedCalls,
196
+ })
197
+ })
198
+ }
199
+
200
+ // Live Server Logs Stream (SSE)
201
+ if appConfig.GoDuck.Telemetry.Metrics.StreamEnabled {
202
+ r.GET("/api/system/logs/stream", func(c *gin.Context) {
203
+ c.Header("Content-Type", "text/event-stream")
204
+ c.Header("Cache-Control", "no-cache")
205
+ c.Header("Connection", "keep-alive")
206
+ c.Header("Access-Control-Allow-Origin", "*")
207
+
208
+ clientChan := make(chan []byte, 100)
209
+ logger.GlobalLogBroker.AddClient(clientChan)
210
+
211
+ defer func() {
212
+ logger.GlobalLogBroker.RemoveClient(clientChan)
213
+ }()
214
+
215
+ c.Stream(func(w io.Writer) bool {
216
+ select {
217
+ case <-c.Request.Context().Done():
218
+ return false
219
+ case msg := <-clientChan:
220
+ c.SSEvent("log", string(msg))
221
+ return true
222
+ }
223
+ })
224
+ })
225
+ }
226
+
227
+ // Real-Time System Stream (SSE)
228
+ if appConfig.GoDuck.Telemetry.Metrics.StreamEnabled {
229
+ r.GET("/api/system/stream", func(c *gin.Context) {
230
+ c.Header("Content-Type", "text/event-stream")
231
+ c.Header("Cache-Control", "no-cache")
232
+ c.Header("Connection", "keep-alive")
233
+ c.Header("Access-Control-Allow-Origin", "*")
234
+
235
+ interval := appConfig.GoDuck.Telemetry.Metrics.StreamInterval
236
+ if interval == 0 {
237
+ interval = time.Second
238
+ }
239
+
240
+ ticker := time.NewTicker(interval)
241
+ defer ticker.Stop()
242
+
243
+ c.Stream(func(w io.Writer) bool {
244
+ select {
245
+ case <-c.Request.Context().Done():
246
+ return false
247
+ case <-ticker.C:
248
+ cpuPercent, _ := cpu.Percent(0, false)
249
+ memStats, _ := mem.VirtualMemory()
250
+
251
+ var cpuUsage float64
252
+ if len(cpuPercent) > 0 {
253
+ cpuUsage = cpuPercent[0]
254
+ }
255
+
256
+ // Simple load calculation
257
+ loadPct := (cpuUsage + memStats.UsedPercent) / 2.0
258
+
259
+ payload := map[string]interface{}{
260
+ "timestamp": time.Now().Format(time.RFC3339),
261
+ "cpu_percent": cpuUsage,
262
+ "mem_percent": memStats.UsedPercent,
263
+ "mem_used_mb": memStats.Used / 1024 / 1024,
264
+ "mem_total_mb": memStats.Total / 1024 / 1024,
265
+ "load_percentage": loadPct,
266
+ }
267
+
268
+ data, _ := json.Marshal(payload)
269
+ c.SSEvent("message", string(data))
270
+ return true
271
+ }
272
+ })
273
+ })
274
+ }
275
+
165
276
  // Swagger Docs & UI
166
277
  // Expose standard OpenAPI endpoints (JHipster / Spring / WSO2 compatibility)
167
278
  r.StaticFile("/v3/api-docs", "./docs/swagger.json")
168
279
  r.StaticFile("/swagger.json", "./docs/swagger.json") // Legacy fallback
280
+ r.StaticFile("/api/mqtt-topics", "./docs/mqtt_topics.json5")
169
281
  r.StaticFile("/logo.png", "./docs/web/logo.png")
170
282
  r.GET("/swagger", func(c *gin.Context) {
171
283
  swaggerHTML := `<!DOCTYPE html>
@@ -175,6 +287,7 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
175
287
  <title>Swagger UI (Secured)</title>
176
288
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
177
289
  <script src="https://cdn.jsdelivr.net/npm/keycloak-js@24.0.4/dist/keycloak.min.js"></script>
290
+ <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
178
291
  <style>
179
292
  html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
180
293
  *, *:before, *:after { box-sizing: inherit; }
@@ -271,6 +384,157 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
271
384
  background: #2ecc71;
272
385
  box-shadow: 0 0 8px #2ecc71;
273
386
  }
387
+
388
+ /* MQTT Topics Viewer */
389
+ #mqtt-modal {
390
+ display: none;
391
+ position: fixed;
392
+ top: 0; left: 0; width: 100%; height: 100%;
393
+ background: rgba(0,0,0,0.6);
394
+ z-index: 2000;
395
+ justify-content: center;
396
+ align-items: center;
397
+ }
398
+ .mqtt-content {
399
+ background: #000;
400
+ width: 80%; max-width: 900px;
401
+ border-radius: 8px;
402
+ padding: 20px;
403
+ color: #00ff00;
404
+ font-family: monospace;
405
+ position: relative;
406
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
407
+ }
408
+ .mqtt-header {
409
+ display: flex;
410
+ justify-content: space-between;
411
+ margin-bottom: 10px;
412
+ border-bottom: 1px solid #333;
413
+ padding-bottom: 10px;
414
+ }
415
+ .mqtt-close { color: white; cursor: pointer; font-size: 1.5rem; }
416
+ .mqtt-dl { background: #333; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; }
417
+ .mqtt-dl:hover { background: #555; }
418
+
419
+ .syntax-punct { color: #ff0000; }
420
+
421
+ /* Interactive MQTT Actions */
422
+ .mqtt-action-btn {
423
+ margin-left: 10px; padding: 2px 8px; border: none; border-radius: 4px;
424
+ font-size: 0.8rem; font-weight: bold; color: white; cursor: pointer;
425
+ vertical-align: middle;
426
+ }
427
+ #mqtt-action-modal {
428
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
429
+ background: rgba(0,0,0,0.8); z-index: 3000; justify-content: center; align-items: center;
430
+ }
431
+ .mqtt-action-content {
432
+ background: #1e1e1e; width: 80%; max-width: 800px; border-radius: 8px; padding: 20px;
433
+ color: #fff; font-family: monospace; position: relative;
434
+ }
435
+ .mqtt-console {
436
+ background: #000; color: #0f0; padding: 10px; height: 300px; overflow-y: auto;
437
+ border-radius: 4px; margin-top: 10px; border: 1px solid #333; white-space: pre-wrap;
438
+ }
439
+ .mqtt-editor {
440
+ width: 100%; height: 200px; background: #000; color: #0f0; font-family: monospace;
441
+ padding: 10px; border: 1px solid #333; border-radius: 4px; margin-top: 10px;
442
+ }
443
+ .broker-input {
444
+ background: #333; color: #fff; border: 1px solid #555; padding: 5px;
445
+ border-radius: 4px; width: 250px; margin-left: 15px; font-family: monospace;
446
+ }
447
+
448
+ /* System Metrics Dashboard */
449
+ #sys-metrics-modal {
450
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
451
+ background: rgba(0,0,0,0.8); z-index: 4000; justify-content: center; align-items: center;
452
+ }
453
+ .metrics-content {
454
+ background: rgba(255, 255, 255, 0.95);
455
+ backdrop-filter: blur(20px);
456
+ width: 90%; max-width: 1200px; height: 85vh; border-radius: 12px;
457
+ padding: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.2);
458
+ display: flex; flex-direction: column;
459
+ }
460
+ .metrics-header {
461
+ display: flex; justify-content: space-between; align-items: center;
462
+ border-bottom: 2px solid #eaeaea; padding-bottom: 15px; margin-bottom: 20px;
463
+ }
464
+ .metrics-header h3 { margin: 0; font-size: 1.5rem; color: #2c3e50; }
465
+ .metrics-close { color: #888; cursor: pointer; font-size: 2rem; line-height: 1; }
466
+ .metrics-close:hover { color: #333; }
467
+
468
+ .metrics-tabs { display: flex; gap: 10px; margin-bottom: 20px; }
469
+ .metrics-tab {
470
+ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer;
471
+ font-weight: 600; background: #eee; color: #555; transition: all 0.2s;
472
+ }
473
+ .metrics-tab.active { background: #4a90e2; color: white; }
474
+
475
+ .metrics-dashboard-grid {
476
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;
477
+ overflow-y: auto; padding-right: 10px;
478
+ }
479
+ .metric-card {
480
+ background: white; border-radius: 8px; padding: 15px;
481
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05); border: 1px solid #f0f0f0;
482
+ }
483
+ .metric-card.wide { grid-column: span 4; }
484
+ .metric-card.half { grid-column: span 2; }
485
+ .metric-title { font-size: 0.85rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
486
+ .metric-value { font-size: 1.8rem; font-weight: 700; color: #2c3e50; display: flex; align-items: baseline; gap: 5px;}
487
+ .metric-unit { font-size: 1rem; color: #999; font-weight: normal; }
488
+
489
+ .progress-bar-container {
490
+ width: 100%; height: 8px; background: #eee; border-radius: 4px; margin-top: 10px; overflow: hidden;
491
+ }
492
+ .progress-bar { height: 100%; background: #2ecc71; transition: width 0.5s ease-out, background 0.3s; }
493
+
494
+ .endpoint-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
495
+ .endpoint-table th, .endpoint-table td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
496
+ .endpoint-table th { color: #888; font-weight: 600; text-transform: uppercase; font-size: 0.8rem; }
497
+
498
+ #metrics-raw-view {
499
+ display: none; background: #1e1e1e; color: #d4d4d4; padding: 20px;
500
+ border-radius: 8px; overflow: auto; height: 100%; font-family: monospace;
501
+ }
502
+ .json-key { color: #9cdcfe; }
503
+ .json-string { color: #ce9178; }
504
+ .json-number { color: #b5cea8; }
505
+ .json-boolean { color: #569cd6; }
506
+
507
+ /* Live Server Logs */
508
+ #live-logs-modal {
509
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
510
+ background: rgba(0,0,0,0.8); z-index: 4000; justify-content: center; align-items: center;
511
+ }
512
+ .logs-content {
513
+ background: #1e1e1e; width: 90%; max-width: 1400px; height: 85vh; border-radius: 12px;
514
+ padding: 0; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
515
+ display: flex; flex-direction: column; overflow: hidden;
516
+ }
517
+ .logs-header {
518
+ background: #252526; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center;
519
+ border-bottom: 1px solid #333; color: #fff;
520
+ }
521
+ .logs-controls { display: flex; align-items: center; gap: 15px; }
522
+ .logs-search {
523
+ background: #3c3c3c; color: #fff; border: 1px solid #555; padding: 5px 10px;
524
+ border-radius: 4px; width: 250px; outline: none;
525
+ }
526
+ .logs-search:focus { border-color: #007acc; }
527
+ .logs-status { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; }
528
+ .logs-status-dot { width: 10px; height: 10px; border-radius: 50%; background: #2ecc71; box-shadow: 0 0 8px #2ecc71; }
529
+ .logs-status-dot.paused { background: #f39c12; box-shadow: 0 0 8px #f39c12; }
530
+ .logs-body {
531
+ flex: 1; padding: 15px; overflow-y: auto; background: #1e1e1e;
532
+ color: #d4d4d4; font-family: monospace; font-size: 0.9rem; line-height: 1.4;
533
+ white-space: pre-wrap; word-wrap: break-word;
534
+ }
535
+ .log-line { margin: 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
536
+ .log-line:hover { background: rgba(255,255,255,0.02); }
537
+ .log-line.hidden { display: none; }
274
538
  </style>
275
539
  </head>
276
540
  <body>
@@ -292,11 +556,164 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
292
556
  <input type="text" id="tenant-input" class="tenant-input" placeholder="X-Tenant-ID" value="tenant_1" title="Multi-tenant DB Target" />
293
557
  <button id="login-btn" class="btn btn-login">Login with Keycloak</button>
294
558
  <button id="logout-btn" class="btn btn-logout">Logout</button>
559
+ <button id="mqtt-btn" class="btn" style="background: #2c3e50; color: white;">MQTT Topics</button>
560
+ <button id="sys-metrics-btn" class="btn" style="background: #8e44ad; color: white;">System Metrics</button>
561
+ <button id="live-logs-btn" class="btn" style="background: #d35400; color: white;" title="Stream Server Console Logs">Live Logs</button>
295
562
  </div>
296
563
  </div>
297
564
 
298
565
  <div id="swagger-ui"></div>
299
566
 
567
+ <div id="mqtt-modal">
568
+ <div class="mqtt-content">
569
+ <div class="mqtt-header">
570
+ <h3 style="margin: 0; color: white;">MQTT Topics Dictionary (JSON5)</h3>
571
+ <div>
572
+ <button class="mqtt-dl" id="mqtt-dl-btn">Download JSON5</button>
573
+ <span class="mqtt-close" id="mqtt-close">&times;</span>
574
+ </div>
575
+ </div>
576
+ <pre id="mqtt-viewer" style="overflow-x: auto; max-height: 70vh; margin: 0;"></pre>
577
+ </div>
578
+ </div>
579
+
580
+ <div id="mqtt-action-modal">
581
+ <div class="mqtt-action-content">
582
+ <div style="display: flex; justify-content: space-between; border-bottom: 1px solid #333; padding-bottom: 10px;">
583
+ <div>
584
+ <h3 style="margin: 0; display: inline-block;"><span id="action-title">SUBSCRIBE</span>: <span id="action-topic" style="color: #3498db;"></span></h3>
585
+ <input type="text" id="broker-url" class="broker-input" value="ws://localhost:9001" title="Broker WS URL" />
586
+ <span id="broker-status" style="margin-left: 10px; font-size: 0.9rem; color: #f39c12;">Disconnected</span>
587
+ </div>
588
+ <span class="mqtt-close" id="mqtt-action-close">&times;</span>
589
+ </div>
590
+
591
+ <!-- Subscribe View -->
592
+ <div id="view-subscribe" style="display: none;">
593
+ <div class="mqtt-console" id="mqtt-console">Waiting for messages...\n</div>
594
+ </div>
595
+
596
+ <!-- Publish View -->
597
+ <div id="view-publish" style="display: none;">
598
+ <textarea id="mqtt-payload" class="mqtt-editor">{\n "example": "payload"\n}</textarea>
599
+ <button id="mqtt-publish-btn" class="btn" style="background: #e74c3c; color: white; margin-top: 15px; width: 100%;">PUBLISH PAYLOAD</button>
600
+ </div>
601
+ </div>
602
+ </div>
603
+
604
+ <div id="sys-metrics-modal">
605
+ <div class="metrics-content">
606
+ <div class="metrics-header">
607
+ <h3>System Metrics Dashboard</h3>
608
+ <span class="metrics-close" id="sys-metrics-close">&times;</span>
609
+ </div>
610
+
611
+ <div class="metrics-tabs">
612
+ <button class="metrics-tab active" id="tab-dashboard">Dashboard</button>
613
+ <button class="metrics-tab" id="tab-raw">Raw JSON</button>
614
+ </div>
615
+
616
+ <div id="metrics-dashboard-view" class="metrics-dashboard-grid">
617
+ <!-- CPU Card -->
618
+ <div class="metric-card">
619
+ <div class="metric-title">Process CPU</div>
620
+ <div class="metric-value"><span id="m-cpu">0.00</span><span class="metric-unit">%</span></div>
621
+ <div class="progress-bar-container"><div id="p-cpu" class="progress-bar" style="width: 0%"></div></div>
622
+ </div>
623
+ <!-- Memory Card -->
624
+ <div class="metric-card">
625
+ <div class="metric-title">Heap Alloc</div>
626
+ <div class="metric-value"><span id="m-heap">0</span><span class="metric-unit">MB</span></div>
627
+ <div class="progress-bar-container"><div id="p-heap" class="progress-bar" style="width: 0%"></div></div>
628
+ </div>
629
+ <!-- Goroutines Card -->
630
+ <div class="metric-card">
631
+ <div class="metric-title">Goroutines</div>
632
+ <div class="metric-value"><span id="m-goroutines">0</span></div>
633
+ </div>
634
+ <!-- Uptime Card -->
635
+ <div class="metric-card">
636
+ <div class="metric-title">Uptime</div>
637
+ <div class="metric-value" style="font-size: 1.2rem; margin-top: 5px;"><span id="m-uptime">-</span></div>
638
+ </div>
639
+
640
+ <!-- GC Card -->
641
+ <div class="metric-card half">
642
+ <div class="metric-title">Garbage Collection</div>
643
+ <div style="display: flex; justify-content: space-around; margin-top: 15px;">
644
+ <div style="text-align: center;">
645
+ <div class="metric-value" style="justify-content: center;"><span id="m-gc-count">0</span></div>
646
+ <div style="font-size: 0.8rem; color: #888;">Total Runs</div>
647
+ </div>
648
+ <div style="text-align: center;">
649
+ <div class="metric-value" style="justify-content: center;"><span id="m-gc-pause">0</span><span class="metric-unit">ms</span></div>
650
+ <div style="font-size: 0.8rem; color: #888;">Total Pause Time</div>
651
+ </div>
652
+ </div>
653
+ </div>
654
+
655
+ <!-- HTTP Traffic Statuses -->
656
+ <div class="metric-card half">
657
+ <div class="metric-title">HTTP Traffic</div>
658
+ <div style="display: flex; justify-content: space-around; margin-top: 15px;">
659
+ <div style="text-align: center;">
660
+ <div class="metric-value" style="justify-content: center; color: #2ecc71;"><span id="m-http-ok">0</span></div>
661
+ <div style="font-size: 0.8rem; color: #888;">200 OK</div>
662
+ </div>
663
+ <div style="text-align: center;">
664
+ <div class="metric-value" style="justify-content: center; color: #e74c3c;"><span id="m-http-fail">0</span></div>
665
+ <div style="font-size: 0.8rem; color: #888;">Failed Calls (400+)</div>
666
+ </div>
667
+ </div>
668
+ </div>
669
+
670
+ <!-- Endpoints Table -->
671
+ <div class="metric-card wide">
672
+ <div class="metric-title">API Endpoints Performance</div>
673
+ <table class="endpoint-table">
674
+ <thead>
675
+ <tr>
676
+ <th>Endpoint</th>
677
+ <th>Hits</th>
678
+ <th>Mean Latency</th>
679
+ <th>Max Latency</th>
680
+ </tr>
681
+ </thead>
682
+ <tbody id="m-endpoints-body">
683
+ <tr><td colspan="4" style="text-align: center; color: #888;">No data available</td></tr>
684
+ </tbody>
685
+ </table>
686
+ </div>
687
+ </div>
688
+
689
+ <pre id="metrics-raw-view"></pre>
690
+ </div>
691
+ </div>
692
+
693
+ <div id="live-logs-modal">
694
+ <div class="logs-content">
695
+ <div class="logs-header">
696
+ <div style="display: flex; align-items: center; gap: 15px;">
697
+ <h3 style="margin: 0; font-size: 1.2rem; display: flex; align-items: center; gap: 10px;">
698
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
699
+ Server Terminal
700
+ </h3>
701
+ <div class="logs-status">
702
+ <div class="logs-status-dot" id="log-status-dot"></div>
703
+ <span id="log-status-text">Streaming (Space to Pause)</span>
704
+ </div>
705
+ </div>
706
+ <div class="logs-controls">
707
+ <input type="text" id="log-search" class="logs-search" placeholder="Filter logs (Ctrl+F behavior)..." />
708
+ <span class="metrics-close" id="live-logs-close" style="color: #fff; margin-left: 10px;">&times;</span>
709
+ </div>
710
+ </div>
711
+ <div class="logs-body" id="log-console-body">
712
+ <div style="color: #555;">Connecting to live log stream...</div>
713
+ </div>
714
+ </div>
715
+ </div>
716
+
300
717
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
301
718
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
302
719
  <script>
@@ -385,6 +802,397 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
385
802
  layout: "StandaloneLayout"
386
803
  });
387
804
  });
805
+
806
+ // MQTT Viewer Logic
807
+ const mqttModal = document.getElementById('mqtt-modal');
808
+ const actionModal = document.getElementById('mqtt-action-modal');
809
+ const actionTitle = document.getElementById('action-title');
810
+ const actionTopic = document.getElementById('action-topic');
811
+ const viewSub = document.getElementById('view-subscribe');
812
+ const viewPub = document.getElementById('view-publish');
813
+ const brokerInput = document.getElementById('broker-url');
814
+ const brokerStatus = document.getElementById('broker-status');
815
+ const mqttConsole = document.getElementById('mqtt-console');
816
+ const payloadEditor = document.getElementById('mqtt-payload');
817
+
818
+ let rawJson5 = '';
819
+ let mqttClient = null;
820
+ let currentTopic = '';
821
+
822
+ // Open Action Modal Logic (Global so inline onClick can call it)
823
+ window.openMqttAction = function(topic, action) {
824
+ currentTopic = topic;
825
+ actionTitle.innerText = action;
826
+ actionTopic.innerText = topic;
827
+
828
+ if (action === 'SEND') {
829
+ viewSub.style.display = 'none';
830
+ viewPub.style.display = 'block';
831
+ } else {
832
+ viewSub.style.display = 'block';
833
+ viewPub.style.display = 'none';
834
+ mqttConsole.innerHTML = 'Waiting for messages on ' + topic + '...\\n';
835
+ }
836
+
837
+ actionModal.style.display = 'flex';
838
+ };
839
+
840
+ document.getElementById('mqtt-action-close').onclick = () => {
841
+ if (mqttClient) { mqttClient.end(); mqttClient = null; }
842
+ brokerStatus.innerText = 'Disconnected';
843
+ brokerStatus.style.color = '#f39c12';
844
+ actionModal.style.display = 'none';
845
+ };
846
+
847
+ // Metrics Dashboard Logic
848
+ const sysMetricsModal = document.getElementById('sys-metrics-modal');
849
+ let metricsInterval = null;
850
+
851
+ function syntaxHighlight(json) {
852
+ if (typeof json != 'string') {
853
+ json = JSON.stringify(json, undefined, 2);
854
+ }
855
+ json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
856
+ return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
857
+ var cls = 'json-number';
858
+ if (/^"/.test(match)) {
859
+ if (/:$/.test(match)) {
860
+ cls = 'json-key';
861
+ } else {
862
+ cls = 'json-string';
863
+ }
864
+ } else if (/true|false/.test(match)) {
865
+ cls = 'json-boolean';
866
+ } else if (/null/.test(match)) {
867
+ cls = 'json-null';
868
+ }
869
+ return '<span class="' + cls + '">' + match + '</span>';
870
+ });
871
+ }
872
+
873
+ function updateProgressBar(id, value, max) {
874
+ const el = document.getElementById(id);
875
+ let pct = (value / max) * 100;
876
+ if (pct > 100) pct = 100;
877
+ el.style.width = pct + "%";
878
+ if (pct > 85) el.style.background = '#e74c3c';
879
+ else if (pct > 60) el.style.background = '#f1c40f';
880
+ else el.style.background = '#2ecc71';
881
+ }
882
+
883
+ function fetchMetrics() {
884
+ const headers = {};
885
+ if (keycloak && keycloak.token) {
886
+ headers['Authorization'] = 'Bearer ' + keycloak.token;
887
+ }
888
+ const tenantId = document.getElementById('tenant-input').value;
889
+ if (tenantId) headers['X-Tenant-ID'] = tenantId;
890
+
891
+ fetch('/api/system/metrics', { headers })
892
+ .then(res => {
893
+ if (!res.ok) throw new Error('Status ' + res.status);
894
+ return res.json();
895
+ })
896
+ .then(data => {
897
+ // Update Dashboard UI
898
+ const sys = data.system;
899
+ document.getElementById('m-cpu').innerText = sys.process_cpu_usage.toFixed(2);
900
+ updateProgressBar('p-cpu', sys.process_cpu_usage, 100);
901
+
902
+ document.getElementById('m-heap').innerText = sys.heap_alloc_mb;
903
+ updateProgressBar('p-heap', sys.heap_alloc_mb, sys.heap_sys_mb > 0 ? sys.heap_sys_mb : 1024);
904
+
905
+ document.getElementById('m-goroutines').innerText = sys.goroutines;
906
+ document.getElementById('m-uptime').innerText = sys.uptime;
907
+ document.getElementById('m-gc-count').innerText = sys.num_gc;
908
+ document.getElementById('m-gc-pause').innerText = sys.gc_pause_total_ms;
909
+
910
+ document.getElementById('m-http-ok').innerText = data.status_codes['200'] ? data.status_codes['200'].count : 0;
911
+ document.getElementById('m-http-fail').innerText = data.failed_calls;
912
+
913
+ // Endpoints Table
914
+ const tbody = document.getElementById('m-endpoints-body');
915
+ tbody.innerHTML = '';
916
+ const endpoints = Object.entries(data.endpoints).sort((a, b) => b[1].count - a[1].count);
917
+ if (endpoints.length === 0) {
918
+ tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #888;">No data available</td></tr>';
919
+ } else {
920
+ endpoints.forEach(([path, stats]) => {
921
+ tbody.innerHTML += '<tr>' +
922
+ '<td style="font-family: monospace; font-weight: bold;">' + path + '</td>' +
923
+ '<td>' + stats.count + '</td>' +
924
+ '<td>' + stats.mean_time_ms.toFixed(2) + ' ms</td>' +
925
+ '<td>' + stats.max_time_ms.toFixed(2) + ' ms</td>' +
926
+ '</tr>';
927
+ });
928
+ }
929
+
930
+ // Raw JSON View
931
+ document.getElementById('metrics-raw-view').innerHTML = syntaxHighlight(data);
932
+ })
933
+ .catch(err => {
934
+ console.error('Failed to fetch metrics', err);
935
+ });
936
+ }
937
+
938
+ document.getElementById('sys-metrics-btn').onclick = () => {
939
+ if (!keycloak.authenticated) {
940
+ alert('Please login to view System Metrics');
941
+ return;
942
+ }
943
+ sysMetricsModal.style.display = 'flex';
944
+ fetchMetrics();
945
+ metricsInterval = setInterval(fetchMetrics, 3000);
946
+ };
947
+
948
+ document.getElementById('sys-metrics-close').onclick = () => {
949
+ sysMetricsModal.style.display = 'none';
950
+ if (metricsInterval) {
951
+ clearInterval(metricsInterval);
952
+ metricsInterval = null;
953
+ }
954
+ };
955
+
956
+ document.getElementById('tab-dashboard').onclick = (e) => {
957
+ e.target.classList.add('active');
958
+ document.getElementById('tab-raw').classList.remove('active');
959
+ document.getElementById('metrics-dashboard-view').style.display = 'grid';
960
+ document.getElementById('metrics-raw-view').style.display = 'none';
961
+ };
962
+
963
+ document.getElementById('tab-raw').onclick = (e) => {
964
+ e.target.classList.add('active');
965
+ document.getElementById('tab-dashboard').classList.remove('active');
966
+ document.getElementById('metrics-dashboard-view').style.display = 'none';
967
+ document.getElementById('metrics-raw-view').style.display = 'block';
968
+ };
969
+
970
+ function connectMqtt() {
971
+ if (mqttClient) {
972
+ if (mqttClient.options.href === brokerInput.value && mqttClient.connected) {
973
+ // Already connected to this broker, just subscribe if needed
974
+ if (actionTitle.innerText === 'SUBSCRIBE') mqttClient.subscribe(currentTopic);
975
+ return;
976
+ }
977
+ mqttClient.end();
978
+ }
979
+
980
+ brokerStatus.innerText = "Connecting...";
981
+ brokerStatus.style.color = "#f39c12";
982
+
983
+ try {
984
+ mqttClient = mqtt.connect(brokerInput.value);
985
+ } catch(e) {
986
+ brokerStatus.innerText = "Error: " + e.message;
987
+ brokerStatus.style.color = "#e74c3c";
988
+ return;
989
+ }
990
+
991
+ mqttClient.on('connect', () => {
992
+ brokerStatus.innerText = "Connected";
993
+ brokerStatus.style.color = "#2ecc71";
994
+ if (actionTitle.innerText === 'SUBSCRIBE') {
995
+ mqttClient.subscribe(currentTopic, (err) => {
996
+ if(!err) mqttConsole.innerHTML += 'Subscribed to ' + currentTopic + '\\n';
997
+ });
998
+ }
999
+ });
1000
+
1001
+ mqttClient.on('message', (topic, message) => {
1002
+ if (topic === currentTopic) {
1003
+ const time = new Date().toLocaleTimeString();
1004
+ let msgStr = message.toString();
1005
+ try { msgStr = JSON.stringify(JSON.parse(msgStr), null, 2); } catch(e){}
1006
+ mqttConsole.innerHTML += '\\n<span style="color:#888">[' + time + ']</span>\\n' + msgStr + '\\n';
1007
+ mqttConsole.scrollTop = mqttConsole.scrollHeight;
1008
+ }
1009
+ });
1010
+
1011
+ mqttClient.on('error', (err) => {
1012
+ brokerStatus.innerText = "Error";
1013
+ brokerStatus.style.color = "#e74c3c";
1014
+ console.error("MQTT Error:", err);
1015
+ });
1016
+ }
1017
+
1018
+ brokerInput.addEventListener('change', connectMqtt);
1019
+
1020
+ document.getElementById('mqtt-publish-btn').onclick = () => {
1021
+ if (!mqttClient || !mqttClient.connected) {
1022
+ alert("Broker not connected!");
1023
+ return;
1024
+ }
1025
+ const payload = payloadEditor.value;
1026
+ mqttClient.publish(currentTopic, payload, {}, (err) => {
1027
+ if (err) {
1028
+ alert("Failed to publish: " + err);
1029
+ } else {
1030
+ alert("Message published to " + currentTopic);
1031
+ actionModal.style.display = 'none';
1032
+ }
1033
+ });
1034
+ };
1035
+
1036
+ document.getElementById('mqtt-action-close').onclick = () => {
1037
+ if (mqttClient && actionTitle.innerText === 'SUBSCRIBE') {
1038
+ mqttClient.unsubscribe(currentTopic);
1039
+ }
1040
+ actionModal.style.display = 'none';
1041
+ };
1042
+
1043
+ document.getElementById('mqtt-btn').onclick = () => {
1044
+ fetch('/api/mqtt-topics')
1045
+ .then(res => {
1046
+ if (!res.ok) throw new Error("Status " + res.status);
1047
+ return res.text();
1048
+ })
1049
+ .then(text => {
1050
+ rawJson5 = text;
1051
+ let highlighted = text
1052
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
1053
+ .replace(/([{}[\]:,])/g, '<span class="syntax-punct">$1</span>');
1054
+
1055
+ // Inject buttons next to topics (Only if authenticated)
1056
+ highlighted = highlighted.replace(/"([^"]+)"(<span class="syntax-punct">.*?<\\/span>)/g, (match, topic, punct) => {
1057
+ if (topic.includes('/')) {
1058
+ let btnHtml = "";
1059
+ if (keycloak && keycloak.authenticated) {
1060
+ const isSend = /patch|put|post|create|update/i.test(topic);
1061
+ const btnLabel = isSend ? "SEND" : "SUBSCRIBE";
1062
+ const btnColor = isSend ? "#e74c3c" : "#3498db";
1063
+ btnHtml = ' <button class="mqtt-action-btn" style="background:' + btnColor + '" onclick="openMqttAction(\\'' + topic + '\\', \\'' + btnLabel + '\\')">' + btnLabel + '</button>';
1064
+ }
1065
+ return '"' + topic + '"' + punct + btnHtml;
1066
+ }
1067
+ return match;
1068
+ });
1069
+
1070
+ document.getElementById('mqtt-viewer').innerHTML = highlighted;
1071
+ mqttModal.style.display = 'flex';
1072
+ }).catch(err => alert('Failed to fetch MQTT topics: ' + err));
1073
+ };
1074
+
1075
+ document.getElementById('mqtt-close').onclick = () => mqttModal.style.display = 'none';
1076
+
1077
+ document.getElementById('mqtt-dl-btn').onclick = () => {
1078
+ const blob = new Blob([rawJson5], { type: 'application/json5' });
1079
+ const url = URL.createObjectURL(blob);
1080
+ const a = document.createElement('a');
1081
+ a.href = url;
1082
+ a.download = 'mqtt_topics.json5';
1083
+ document.body.appendChild(a);
1084
+ a.click();
1085
+ document.body.removeChild(a);
1086
+ URL.revokeObjectURL(url);
1087
+ };
1088
+
1089
+ // Live Logs Logic
1090
+ const liveLogsModal = document.getElementById('live-logs-modal');
1091
+ const logBody = document.getElementById('log-console-body');
1092
+ const logSearch = document.getElementById('log-search');
1093
+ const logStatusDot = document.getElementById('log-status-dot');
1094
+ const logStatusText = document.getElementById('log-status-text');
1095
+
1096
+ let logsEventSource = null;
1097
+ let autoScroll = true;
1098
+ let currentFilter = "";
1099
+
1100
+ function appendLogLine(text) {
1101
+ const line = document.createElement('div');
1102
+ line.className = 'log-line';
1103
+
1104
+ // Basic ANSI color stripping for cleaner display if backend sends colors
1105
+ text = text.replace(/\\x1B\\[[0-9;]*[a-zA-Z]/g, '');
1106
+ line.innerText = text;
1107
+
1108
+ if (currentFilter && !text.toLowerCase().includes(currentFilter)) {
1109
+ line.classList.add('hidden');
1110
+ }
1111
+
1112
+ logBody.appendChild(line);
1113
+
1114
+ // Prevent DOM overflow (keep last 2000 lines)
1115
+ if (logBody.children.length > 2000) {
1116
+ logBody.removeChild(logBody.firstChild);
1117
+ }
1118
+
1119
+ if (autoScroll) {
1120
+ logBody.scrollTop = logBody.scrollHeight;
1121
+ }
1122
+ }
1123
+
1124
+ document.getElementById('live-logs-btn').onclick = () => {
1125
+ if (!keycloak.authenticated) {
1126
+ alert('Please login to view Server Logs');
1127
+ return;
1128
+ }
1129
+ liveLogsModal.style.display = 'flex';
1130
+ logBody.innerHTML = '<div style="color: #555;">Connecting to live log stream...</div>';
1131
+ autoScroll = true;
1132
+ updateLogStatusUI();
1133
+
1134
+ // Connect SSE
1135
+ logsEventSource = new EventSource('/api/system/logs/stream');
1136
+
1137
+ logsEventSource.onmessage = function(event) {
1138
+ // Remove connecting message on first log
1139
+ if (logBody.children.length === 1 && logBody.firstChild.innerText.includes('Connecting')) {
1140
+ logBody.innerHTML = '';
1141
+ }
1142
+ appendLogLine(event.data);
1143
+ };
1144
+
1145
+ logsEventSource.onerror = function() {
1146
+ appendLogLine('[SSE Error] Disconnected from log stream.');
1147
+ logsEventSource.close();
1148
+ };
1149
+ };
1150
+
1151
+ document.getElementById('live-logs-close').onclick = () => {
1152
+ liveLogsModal.style.display = 'none';
1153
+ if (logsEventSource) {
1154
+ logsEventSource.close();
1155
+ logsEventSource = null;
1156
+ }
1157
+ };
1158
+
1159
+ // Filter Logic
1160
+ logSearch.addEventListener('input', (e) => {
1161
+ currentFilter = e.target.value.toLowerCase();
1162
+ const lines = logBody.getElementsByClassName('log-line');
1163
+ for (let i = 0; i < lines.length; i++) {
1164
+ const text = lines[i].innerText.toLowerCase();
1165
+ if (currentFilter && !text.includes(currentFilter)) {
1166
+ lines[i].classList.add('hidden');
1167
+ } else {
1168
+ lines[i].classList.remove('hidden');
1169
+ }
1170
+ }
1171
+ if (autoScroll) logBody.scrollTop = logBody.scrollHeight;
1172
+ });
1173
+
1174
+ // Spacebar Pause/Play
1175
+ document.addEventListener('keydown', (e) => {
1176
+ // Only trigger if modal is open and user is not typing in the search box
1177
+ if (liveLogsModal.style.display === 'flex' && document.activeElement !== logSearch) {
1178
+ if (e.code === 'Space') {
1179
+ e.preventDefault(); // Prevent page scroll
1180
+ autoScroll = !autoScroll;
1181
+ updateLogStatusUI();
1182
+ }
1183
+ }
1184
+ });
1185
+
1186
+ function updateLogStatusUI() {
1187
+ if (autoScroll) {
1188
+ logStatusDot.classList.remove('paused');
1189
+ logStatusText.innerText = 'Streaming (Space to Pause)';
1190
+ } else {
1191
+ logStatusDot.classList.add('paused');
1192
+ logStatusText.innerText = 'Paused Auto-scroll (Space to Resume)';
1193
+ }
1194
+ }
1195
+
388
1196
  };
389
1197
  </script>
390
1198
  </body>