go-duck-cli 1.4.10 → 1.4.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.4.10",
3
+ "version": "1.4.12",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -40,6 +40,227 @@ import (
40
40
  "{{app_name}}/internal/search"
41
41
  )
42
42
 
43
+ const compactWidgetHTML = `<!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>Live Telemetry</title>
48
+ <style>
49
+ html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #121212; color: #fff; font-family: system-ui, sans-serif; overflow: hidden; }
50
+ .bento-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; gap: 8px; width: 100%; height: 100%; padding: 8px; box-sizing: border-box; }
51
+ .bento-card { background: #1e1e1e; border-radius: 12px; display: flex; flex-direction: column; justify-content: center; align-items: center; position: relative; border: 1px solid #333; transition: all 0.2s; }
52
+ .bento-title { position: absolute; top: 10px; left: 10px; font-size: 0.75rem; color: #888; text-transform: uppercase; letter-spacing: 1px; }
53
+ .bento-badge { position: absolute; top: 10px; right: 10px; font-size: 0.6rem; padding: 2px 6px; border-radius: 4px; background: #e74c3c; color: #fff; display: none; }
54
+ .bento-value { font-size: 2.5rem; font-weight: 700; color: #fff; margin-top: 15px; display: flex; align-items: baseline; gap: 4px; }
55
+ .bento-unit { font-size: 1rem; color: #888; font-weight: normal; }
56
+ .bento-sub { font-size: 0.75rem; color: #666; margin-top: 5px; }
57
+
58
+ @keyframes alarmFlash {
59
+ 0% { background: #1e1e1e; border-color: #333; }
60
+ 50% { background: rgba(231, 76, 60, 0.2); border-color: #e74c3c; box-shadow: 0 0 15px rgba(231, 76, 60, 0.4); }
61
+ 100% { background: #1e1e1e; border-color: #333; }
62
+ }
63
+ .alarm-active { animation: alarmFlash 1s infinite; }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="bento-grid">
68
+ <div class="bento-card" id="card-cpu">
69
+ <div class="bento-title" id="cpu-title">CPU</div>
70
+ <div class="bento-badge" id="cpu-badge">QUOTA</div>
71
+ <div class="bento-value"><span id="cpu-val">0</span><span class="bento-unit">%</span></div>
72
+ </div>
73
+ <div class="bento-card" id="card-mem">
74
+ <div class="bento-title">RAM</div>
75
+ <div class="bento-value"><span id="mem-val">0</span><span class="bento-unit">%</span></div>
76
+ <div class="bento-sub"><span id="mem-raw">0 / 0 MB</span></div>
77
+ </div>
78
+ </div>
79
+ <script>
80
+ // Apply custom background color if provided by Mission Control grid
81
+ const bg = new URLSearchParams(window.location.search).get('bg');
82
+ if (bg) {
83
+ document.body.style.background = bg;
84
+ document.querySelectorAll('.bento-card').forEach(c => {
85
+ c.style.background = 'transparent';
86
+ c.style.borderColor = 'rgba(255,255,255,0.1)';
87
+ });
88
+ }
89
+
90
+ const evtSource = new EventSource('/api/system/stream');
91
+ evtSource.addEventListener('metrics', function(e) {
92
+ const data = JSON.parse(e.data);
93
+ const sys = data.system;
94
+
95
+ // CPU
96
+ let cpuPct = 0;
97
+ if (sys.is_pod_cpu_limited) {
98
+ document.getElementById('cpu-title').innerText = 'CPU';
99
+ document.getElementById('cpu-badge').style.display = 'block';
100
+ cpuPct = sys.pod_cpu_limit_pct;
101
+ } else {
102
+ document.getElementById('cpu-title').innerText = 'PROCESS CPU';
103
+ document.getElementById('cpu-badge').style.display = 'none';
104
+ cpuPct = sys.process_cpu_usage;
105
+ }
106
+ document.getElementById('cpu-val').innerText = cpuPct.toFixed(1);
107
+
108
+ if (cpuPct > 90) document.getElementById('card-cpu').classList.add('alarm-active');
109
+ else document.getElementById('card-cpu').classList.remove('alarm-active');
110
+
111
+ // Memory
112
+ let memPct = 0;
113
+ if (sys.total_mem_mb > 0) {
114
+ memPct = (sys.heap_alloc_mb / sys.total_mem_mb) * 100;
115
+ }
116
+ document.getElementById('mem-val').innerText = memPct.toFixed(1);
117
+ document.getElementById('mem-raw').innerText = sys.heap_alloc_mb + " / " + sys.total_mem_mb + " MB";
118
+
119
+ if (memPct > 90) document.getElementById('card-mem').classList.add('alarm-active');
120
+ else document.getElementById('card-mem').classList.remove('alarm-active');
121
+ });
122
+ evtSource.onerror = function() {
123
+ document.getElementById('cpu-val').innerText = "OFF";
124
+ document.getElementById('mem-val').innerText = "OFF";
125
+ document.getElementById('mem-raw').innerText = "Disconnected";
126
+ };
127
+ </script>
128
+ </body>
129
+ </html>`
130
+
131
+ const gridWidgetHTML = `<!DOCTYPE html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
135
+ <title>Mission Control Grid</title>
136
+ <style>
137
+ html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; font-family: system-ui, sans-serif; overflow: auto; }
138
+ .dashboard { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-auto-rows: minmax(30vh, 1fr); gap: 15px; padding: 15px; box-sizing: border-box; }
139
+ .grid-item { position: relative; width: 100%; height: 100%; background: #1a1a1a; border-radius: 12px; border: 1px solid #333; overflow: hidden; display: flex; flex-direction: column; transition: background 0.3s; }
140
+ .grid-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); }
141
+ .grid-title { font-size: 0.85rem; font-weight: bold; color: #fff; text-transform: uppercase; letter-spacing: 1px; }
142
+ .grid-actions { display: flex; gap: 8px; align-items: center; }
143
+ .color-picker { width: 20px; height: 20px; padding: 0; border: none; border-radius: 4px; cursor: pointer; background: transparent; }
144
+ .close-btn { background: transparent; color: #888; border: none; font-size: 1.2rem; cursor: pointer; line-height: 1; transition: color 0.2s; }
145
+ .close-btn:hover { color: #e74c3c; }
146
+ .grid-item iframe { width: 100%; flex: 1; border: none; }
147
+
148
+ .add-item { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s; color: #888; border: 2px dashed #444; border-radius: 12px; }
149
+ .add-item:hover { background: #222; color: #fff; border-color: #666; }
150
+ .add-icon { font-size: 3rem; font-weight: 300; margin-bottom: 10px; }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div class="dashboard" id="dashboard">
155
+ <div class="add-item" id="add-btn" onclick="promptAddService()">
156
+ <div class="add-icon">+</div>
157
+ <div>Add Service</div>
158
+ </div>
159
+ </div>
160
+ <script>
161
+ let gridState = JSON.parse(localStorage.getItem('goduck_mission_control')) || [];
162
+
163
+ function saveState() {
164
+ localStorage.setItem('goduck_mission_control', JSON.stringify(gridState));
165
+ }
166
+
167
+ function renderGrid() {
168
+ // Clear all grid items except add button
169
+ document.querySelectorAll('.grid-item').forEach(el => el.remove());
170
+
171
+ // Add default widget if state is completely empty
172
+ if (gridState.length === 0) {
173
+ gridState.push({ id: 'default', name: '{{app_name}} (Host)', url: '/api/system/widget', color: '#1a1a1a' });
174
+ saveState();
175
+ }
176
+
177
+ const dash = document.getElementById('dashboard');
178
+ const addBtn = document.getElementById('add-btn');
179
+
180
+ gridState.forEach(widget => {
181
+ const wrapper = document.createElement('div');
182
+ wrapper.className = 'grid-item';
183
+ wrapper.style.backgroundColor = widget.color;
184
+
185
+ const header = document.createElement('div');
186
+ header.className = 'grid-header';
187
+
188
+ const title = document.createElement('div');
189
+ title.className = 'grid-title';
190
+ title.innerText = widget.name;
191
+
192
+ const actions = document.createElement('div');
193
+ actions.className = 'grid-actions';
194
+
195
+ const colorPicker = document.createElement('input');
196
+ colorPicker.type = 'color';
197
+ colorPicker.className = 'color-picker';
198
+ colorPicker.value = widget.color || '#1a1a1a';
199
+ colorPicker.title = 'Change Widget Color';
200
+ colorPicker.onchange = (e) => {
201
+ widget.color = e.target.value;
202
+ saveState();
203
+ renderGrid(); // Re-render to pass new color to iframe
204
+ };
205
+
206
+ const closeBtn = document.createElement('button');
207
+ closeBtn.className = 'close-btn';
208
+ closeBtn.innerHTML = '×';
209
+ closeBtn.title = 'Remove Widget';
210
+ closeBtn.onclick = () => {
211
+ if(confirm("Remove " + widget.name + " from Mission Control?")) {
212
+ gridState = gridState.filter(w => w.id !== widget.id);
213
+ saveState();
214
+ renderGrid();
215
+ }
216
+ };
217
+
218
+ actions.appendChild(colorPicker);
219
+ actions.appendChild(closeBtn);
220
+ header.appendChild(title);
221
+ header.appendChild(actions);
222
+
223
+ const iframe = document.createElement('iframe');
224
+ // Append color parameter to URL to theme the iframe natively
225
+ try {
226
+ const urlObj = new URL(widget.url, window.location.origin);
227
+ urlObj.searchParams.set('bg', widget.color);
228
+ iframe.src = urlObj.toString();
229
+ } catch (e) {
230
+ iframe.src = widget.url; // Fallback if URL is invalid
231
+ }
232
+
233
+ wrapper.appendChild(header);
234
+ wrapper.appendChild(iframe);
235
+
236
+ dash.insertBefore(wrapper, addBtn);
237
+ });
238
+ }
239
+
240
+ function promptAddService() {
241
+ const name = prompt("Enter a Name for this Service (e.g., 'Auth Service'):");
242
+ if (!name) return;
243
+
244
+ const url = prompt("Enter the remote widget URL:\\n(e.g., http://localhost:8081/api/system/widget)");
245
+ if (!url) return;
246
+
247
+ gridState.push({
248
+ id: Date.now().toString(),
249
+ name: name,
250
+ url: url,
251
+ color: '#1a1a1a'
252
+ });
253
+
254
+ saveState();
255
+ renderGrid();
256
+ }
257
+
258
+ // Initial render
259
+ renderGrid();
260
+ </script>
261
+ </body>
262
+ </html>`
263
+
43
264
  func SetupRouter(appConfig *config.Config) *gin.Engine {
44
265
  // 1. Initialize Logging & Observability
45
266
  logger.InitLogger(appConfig)
@@ -232,45 +453,39 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
232
453
  c.Header("Connection", "keep-alive")
233
454
  c.Header("Access-Control-Allow-Origin", "*")
234
455
 
235
- interval := appConfig.GoDuck.Telemetry.Metrics.StreamInterval
236
- if interval == 0 {
237
- interval = time.Second
238
- }
456
+ clientChan := make(chan telemetry.AppMetrics, 100)
457
+ telemetry.GlobalMetricsBroker.AddClient(clientChan)
239
458
 
240
- ticker := time.NewTicker(interval)
241
- defer ticker.Stop()
459
+ defer func() {
460
+ telemetry.GlobalMetricsBroker.RemoveClient(clientChan)
461
+ }()
242
462
 
243
463
  c.Stream(func(w io.Writer) bool {
244
464
  select {
245
465
  case <-c.Request.Context().Done():
246
466
  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))
467
+ case appMetrics := <-clientChan:
468
+ sysMetrics := telemetry.CollectSystemMetrics()
469
+ c.SSEvent("metrics", gin.H{
470
+ "system": sysMetrics,
471
+ "endpoints": appMetrics.Endpoints,
472
+ "status_codes": appMetrics.StatusCodes,
473
+ "failed_calls": appMetrics.FailedCalls,
474
+ })
270
475
  return true
271
476
  }
272
477
  })
273
478
  })
479
+
480
+ // Compact Bento Telemetry Widget
481
+ r.GET("/api/system/widget", func(c *gin.Context) {
482
+ c.Data(200, "text/html; charset=utf-8", []byte(compactWidgetHTML))
483
+ })
484
+
485
+ // Mission Control Iframe Grid
486
+ r.GET("/api/system/grid", func(c *gin.Context) {
487
+ c.Data(200, "text/html; charset=utf-8", []byte(gridWidgetHTML))
488
+ })
274
489
  }
275
490
 
276
491
  // Swagger Docs & UI
@@ -293,6 +508,15 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
293
508
  *, *:before, *:after { box-sizing: inherit; }
294
509
  body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
295
510
 
511
+ .nav-trigger-zone {
512
+ position: fixed;
513
+ top: 0;
514
+ left: 0;
515
+ right: 0;
516
+ height: 30px;
517
+ z-index: 999;
518
+ }
519
+
296
520
  .top-nav {
297
521
  background: rgba(255, 255, 255, 0.8);
298
522
  backdrop-filter: blur(10px);
@@ -302,11 +526,20 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
302
526
  display: flex;
303
527
  justify-content: space-between;
304
528
  align-items: center;
305
- position: sticky;
306
- top: 0;
529
+ position: fixed;
530
+ width: 100%;
531
+ top: -100px;
532
+ opacity: 0;
533
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
307
534
  z-index: 1000;
308
535
  box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
309
536
  }
537
+
538
+ .nav-trigger-zone:hover + .top-nav,
539
+ .top-nav:hover {
540
+ top: 0;
541
+ opacity: 1;
542
+ }
310
543
 
311
544
  .nav-brand {
312
545
  font-weight: 700;
@@ -579,7 +812,8 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
579
812
  .log-line.hidden { display: none; }
580
813
  </style>
581
814
  </head>
582
- <body>
815
+ <body id="swagger-body">
816
+ <div class="nav-trigger-zone"></div>
583
817
  <div class="top-nav">
584
818
  <div class="nav-brand">
585
819
  <img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
@@ -600,6 +834,7 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
600
834
  <button id="logout-btn" class="btn btn-logout">Logout</button>
601
835
  <button id="mqtt-btn" class="btn" style="background: #2c3e50; color: white;">MQTT Topics</button>
602
836
  <button id="sys-metrics-btn" class="btn" style="background: #8e44ad; color: white;">System Metrics</button>
837
+ <button id="compact-widget-btn" class="btn" style="background: #27ae60; color: white; margin-right: 5px;" title="Open Mission Control Grid">Mission Control</button>
603
838
  <button id="live-logs-btn" class="btn" style="background: #d35400; color: white;" title="Stream Server Console Logs">Live Logs</button>
604
839
  </div>
605
840
  </div>
@@ -1305,7 +1540,7 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
1305
1540
  }
1306
1541
 
1307
1542
  document.getElementById('live-logs-btn').onclick = () => {
1308
- if (!keycloak.authenticated) {
1543
+ if (!keycloak.authenticated && {{appConfig.GoDuck.Security.OIDC.Enabled}}) {
1309
1544
  alert('Please login to view Server Logs');
1310
1545
  return;
1311
1546
  }
@@ -1317,13 +1552,13 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
1317
1552
  // Connect SSE
1318
1553
  logsEventSource = new EventSource('/api/system/logs/stream');
1319
1554
 
1320
- logsEventSource.onmessage = function(event) {
1555
+ logsEventSource.addEventListener('log', function(event) {
1321
1556
  // Remove connecting message on first log
1322
1557
  if (logBody.children.length === 1 && logBody.firstChild.innerText.includes('Connecting')) {
1323
1558
  logBody.innerHTML = '';
1324
1559
  }
1325
1560
  appendLogLine(event.data);
1326
- };
1561
+ });
1327
1562
 
1328
1563
  logsEventSource.onerror = function() {
1329
1564
  appendLogLine('[SSE Error] Disconnected from log stream.');
@@ -1331,6 +1566,14 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
1331
1566
  };
1332
1567
  };
1333
1568
 
1569
+ document.getElementById('compact-widget-btn').onclick = () => {
1570
+ if (!keycloak.authenticated && {{appConfig.GoDuck.Security.OIDC.Enabled}}) {
1571
+ alert('Please login to view system telemetry.');
1572
+ return;
1573
+ }
1574
+ window.open('/api/system/grid', 'MissionControlGrid', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no');
1575
+ };
1576
+
1334
1577
  document.getElementById('live-logs-close').onclick = () => {
1335
1578
  liveLogsModal.style.display = 'none';
1336
1579
  if (logsEventSource) {