go-duck-cli 1.3.372 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/generators/config.js +8 -0
- package/generators/devops.js +25 -18
- package/generators/logger.js +50 -1
- package/generators/mqtt-topics.js +54 -0
- package/generators/postman.js +2 -1
- package/generators/swagger.js +16 -16
- package/generators/telemetry.js +167 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/templates/docs/pages/configuration.hbs +46 -5
- package/templates/docs/pages/mosquitto.hbs +6 -0
- package/templates/docs/pages/observability.hbs +55 -0
- package/templates/go/router.go.hbs +808 -0
|
@@ -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">×</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">×</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">×</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;">×</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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
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>
|