@thotischner/observability-mcp 1.3.4 → 1.4.1
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/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +370 -0
- package/dist/cli/lib.d.ts +95 -0
- package/dist/cli/lib.js +185 -0
- package/dist/cli/lib.test.d.ts +1 -0
- package/dist/cli/lib.test.js +134 -0
- package/dist/config/loader.test.js +3 -3
- package/dist/connectors/hub.d.ts +48 -0
- package/dist/connectors/hub.js +51 -0
- package/dist/connectors/hub.test.d.ts +1 -0
- package/dist/connectors/hub.test.js +52 -0
- package/dist/connectors/install.d.ts +24 -0
- package/dist/connectors/install.js +100 -0
- package/dist/connectors/install.test.d.ts +1 -0
- package/dist/connectors/install.test.js +58 -0
- package/dist/connectors/loader.d.ts +48 -0
- package/dist/connectors/loader.js +222 -0
- package/dist/connectors/loki.js +14 -6
- package/dist/connectors/loki.test.js +27 -0
- package/dist/connectors/registry.d.ts +3 -0
- package/dist/connectors/registry.js +16 -16
- package/dist/connectors/tls.test.js +3 -3
- package/dist/connectors/verify.d.ts +19 -0
- package/dist/connectors/verify.js +87 -0
- package/dist/connectors/verify.test.d.ts +1 -0
- package/dist/connectors/verify.test.js +63 -0
- package/dist/index.js +389 -26
- package/dist/metrics/instrument-connector.d.ts +8 -0
- package/dist/metrics/instrument-connector.js +41 -0
- package/dist/metrics/self.d.ts +12 -0
- package/dist/metrics/self.js +61 -0
- package/dist/openapi.d.ts +2 -0
- package/dist/openapi.js +186 -0
- package/dist/sdk/index.d.ts +52 -0
- package/dist/sdk/index.js +13 -0
- package/dist/sdk/manifest-schema.d.ts +28 -0
- package/dist/sdk/manifest-schema.js +47 -0
- package/dist/sdk/manifest-schema.test.d.ts +1 -0
- package/dist/sdk/manifest-schema.test.js +50 -0
- package/dist/tools/get-service-health.js +3 -2
- package/dist/ui/index.html +687 -115
- package/dist/util/sanitize.d.ts +1 -0
- package/dist/util/sanitize.js +6 -0
- package/package.json +21 -8
package/dist/ui/index.html
CHANGED
|
@@ -4,122 +4,482 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Observability MCP Gateway</title>
|
|
7
|
+
<link rel="preconnect" href="https://rsms.me/">
|
|
8
|
+
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
|
9
|
+
<style>
|
|
10
|
+
@supports (font-variation-settings: normal) {
|
|
11
|
+
:root { font-family: 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
7
14
|
<style>
|
|
8
15
|
:root {
|
|
9
|
-
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
16
|
+
/* Surface — neutral slate, three tiers of elevation */
|
|
17
|
+
--bg: #0b0d12;
|
|
18
|
+
--surface: #11141b;
|
|
19
|
+
--surface-2: #161a23;
|
|
20
|
+
--surface-3: #1c212c;
|
|
21
|
+
--border: rgba(255,255,255,0.06);
|
|
22
|
+
--border-strong: rgba(255,255,255,0.10);
|
|
23
|
+
|
|
24
|
+
/* Text — four levels */
|
|
25
|
+
--text: #e8ecf2;
|
|
26
|
+
--text-muted: #a1a8b3;
|
|
27
|
+
--text-dim: #6b7280;
|
|
28
|
+
--text-inv: #0b0d12;
|
|
29
|
+
|
|
30
|
+
/* One accent — refined cyan-blue, not GitHub blue */
|
|
31
|
+
--accent: #4f8cff;
|
|
32
|
+
--accent-2: #6ea3ff;
|
|
33
|
+
--accent-soft: rgba(79,140,255,0.10);
|
|
34
|
+
--accent-ring: rgba(79,140,255,0.35);
|
|
35
|
+
|
|
36
|
+
/* Semantic — desaturated, professional */
|
|
37
|
+
--success: #4ade80;
|
|
38
|
+
--success-soft: rgba(74,222,128,0.10);
|
|
39
|
+
--warning: #f5b341;
|
|
40
|
+
--warning-soft: rgba(245,179,65,0.10);
|
|
41
|
+
--danger: #ef5b6e;
|
|
42
|
+
--danger-soft: rgba(239,91,110,0.10);
|
|
43
|
+
--info: #a78bfa;
|
|
44
|
+
--info-soft: rgba(167,139,250,0.10);
|
|
45
|
+
|
|
46
|
+
/* Legacy aliases so nothing breaks during the rolling refactor */
|
|
47
|
+
--text2: var(--text-muted);
|
|
48
|
+
--surface2: var(--surface-2);
|
|
49
|
+
--green: var(--success);
|
|
50
|
+
--red: var(--danger);
|
|
51
|
+
--yellow: var(--warning);
|
|
52
|
+
--purple: var(--info);
|
|
53
|
+
|
|
54
|
+
/* Type scale (Major-Third-ish) */
|
|
55
|
+
--fs-xs: 11px;
|
|
56
|
+
--fs-sm: 12px;
|
|
57
|
+
--fs-md: 13px;
|
|
58
|
+
--fs-lg: 14px;
|
|
59
|
+
--fs-xl: 16px;
|
|
60
|
+
--fs-2xl: 20px;
|
|
61
|
+
--fs-3xl: 28px;
|
|
62
|
+
--fs-4xl: 36px;
|
|
63
|
+
|
|
64
|
+
/* Spacing — 4-based scale */
|
|
65
|
+
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
|
66
|
+
--sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
|
|
67
|
+
|
|
68
|
+
/* Radii */
|
|
69
|
+
--radius-sm: 6px;
|
|
70
|
+
--radius: 8px;
|
|
71
|
+
--radius-lg: 12px;
|
|
72
|
+
--radius-pill: 999px;
|
|
73
|
+
|
|
74
|
+
/* Elevation */
|
|
75
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
|
76
|
+
--shadow: 0 4px 12px rgba(0,0,0,0.25), 0 0 0 1px var(--border);
|
|
77
|
+
--shadow-lg: 0 12px 32px rgba(0,0,0,0.35), 0 0 0 1px var(--border-strong);
|
|
78
|
+
|
|
79
|
+
/* Motion */
|
|
80
|
+
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
81
|
+
--t-fast: 120ms;
|
|
82
|
+
}
|
|
83
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
84
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
85
|
+
body {
|
|
86
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
87
|
+
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
|
88
|
+
font-size: var(--fs-md);
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
background: var(--bg);
|
|
91
|
+
color: var(--text);
|
|
92
|
+
-webkit-font-smoothing: antialiased;
|
|
93
|
+
text-rendering: optimizeLegibility;
|
|
94
|
+
}
|
|
95
|
+
code, .mono, pre { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-ligatures: none; }
|
|
96
|
+
a { color: var(--accent); text-decoration: none; }
|
|
97
|
+
a:hover { color: var(--accent-2); }
|
|
98
|
+
:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; border-radius: 4px; }
|
|
99
|
+
|
|
100
|
+
.header {
|
|
101
|
+
background: linear-gradient(180deg, var(--surface), var(--surface) 70%, rgba(0,0,0,0.0));
|
|
102
|
+
border-bottom: 1px solid var(--border);
|
|
103
|
+
padding: var(--sp-3) var(--sp-6);
|
|
104
|
+
display: flex; align-items: center; gap: var(--sp-4);
|
|
105
|
+
position: sticky; top: 0; z-index: 20;
|
|
106
|
+
backdrop-filter: saturate(160%) blur(8px);
|
|
107
|
+
}
|
|
108
|
+
.header h1 { font-size: var(--fs-lg); font-weight: 600; letter-spacing: -0.01em; }
|
|
109
|
+
.header h1::before {
|
|
110
|
+
content: ''; display: inline-block; width: 14px; height: 14px;
|
|
111
|
+
margin-right: 10px; vertical-align: -2px; border-radius: 3px;
|
|
112
|
+
background: linear-gradient(135deg, var(--accent), var(--info));
|
|
113
|
+
box-shadow: 0 0 0 1px var(--border-strong) inset;
|
|
114
|
+
}
|
|
115
|
+
.badge {
|
|
116
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
117
|
+
padding: 3px 10px;
|
|
118
|
+
border-radius: var(--radius-pill);
|
|
119
|
+
font-size: var(--fs-xs); font-weight: 600;
|
|
120
|
+
letter-spacing: 0.02em;
|
|
121
|
+
}
|
|
122
|
+
.badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
|
|
123
|
+
.badge-ok { background: var(--success-soft); color: var(--success); border: 1px solid rgba(74,222,128,0.25); }
|
|
124
|
+
.badge-err { background: var(--danger-soft); color: var(--danger); border: 1px solid rgba(239,91,110,0.30); }
|
|
125
|
+
.hidden { display: none !important; }
|
|
126
|
+
|
|
127
|
+
.nav { display: flex; gap: 2px; margin-left: var(--sp-6); }
|
|
128
|
+
.nav-btn {
|
|
129
|
+
background: none; border: none;
|
|
130
|
+
color: var(--text-muted);
|
|
131
|
+
padding: 6px 14px;
|
|
132
|
+
border-radius: var(--radius-sm);
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
font-size: var(--fs-md); font-weight: 500;
|
|
135
|
+
transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
|
|
136
|
+
}
|
|
137
|
+
.nav-btn:hover { background: var(--surface-2); color: var(--text); }
|
|
138
|
+
.nav-btn.active { background: var(--surface-2); color: var(--text); box-shadow: inset 0 -2px 0 0 var(--accent); }
|
|
139
|
+
.container { max-width: 1240px; margin: 0 auto; padding: var(--sp-6); }
|
|
140
|
+
.page { display: none; } .page.active { display: block; animation: fadeIn 180ms var(--ease); }
|
|
141
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
|
|
142
|
+
|
|
143
|
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-4); margin-bottom: var(--sp-6); }
|
|
144
|
+
.stat-card {
|
|
145
|
+
background: var(--surface);
|
|
146
|
+
border: 1px solid var(--border);
|
|
147
|
+
border-radius: var(--radius);
|
|
148
|
+
padding: var(--sp-5);
|
|
149
|
+
position: relative; overflow: hidden;
|
|
150
|
+
text-align: left;
|
|
151
|
+
transition: border-color var(--t-fast) var(--ease);
|
|
152
|
+
}
|
|
153
|
+
.stat-card:hover { border-color: var(--border-strong); }
|
|
154
|
+
.stat-card::after {
|
|
155
|
+
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
|
|
156
|
+
background: linear-gradient(180deg, var(--accent), transparent);
|
|
157
|
+
opacity: 0.5;
|
|
158
|
+
}
|
|
159
|
+
.stat-card .label {
|
|
160
|
+
font-size: var(--fs-xs); color: var(--text-muted);
|
|
161
|
+
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500;
|
|
162
|
+
margin-bottom: var(--sp-2);
|
|
163
|
+
}
|
|
164
|
+
.stat-card .value {
|
|
165
|
+
font-size: var(--fs-3xl); font-weight: 600; letter-spacing: -0.02em;
|
|
166
|
+
color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.1;
|
|
167
|
+
}
|
|
168
|
+
.stat-card .context {
|
|
169
|
+
font-size: var(--fs-xs); color: var(--text-muted);
|
|
170
|
+
margin-top: var(--sp-2);
|
|
171
|
+
font-variant-numeric: tabular-nums;
|
|
172
|
+
}
|
|
173
|
+
.stat-card .context.good { color: var(--success, #3fb950); }
|
|
174
|
+
.stat-card .context.warn { color: var(--warn, #d29922); }
|
|
175
|
+
|
|
176
|
+
.live-indicator {
|
|
177
|
+
display: inline-flex; align-items: center; gap: var(--sp-2);
|
|
178
|
+
font-size: var(--fs-xs); color: var(--text-muted);
|
|
179
|
+
text-transform: uppercase; letter-spacing: 0.08em;
|
|
180
|
+
}
|
|
181
|
+
.live-indicator::before {
|
|
182
|
+
content: ''; width: 8px; height: 8px; border-radius: 50%;
|
|
183
|
+
background: var(--success, #3fb950);
|
|
184
|
+
box-shadow: 0 0 0 0 rgba(63, 185, 80, 0.5);
|
|
185
|
+
animation: live-pulse 2s var(--ease) infinite;
|
|
186
|
+
}
|
|
187
|
+
@keyframes live-pulse {
|
|
188
|
+
0% { box-shadow: 0 0 0 0 rgba(63, 185, 80, 0.4); }
|
|
189
|
+
70% { box-shadow: 0 0 0 6px rgba(63, 185, 80, 0); }
|
|
190
|
+
100% { box-shadow: 0 0 0 0 rgba(63, 185, 80, 0); }
|
|
191
|
+
}
|
|
192
|
+
.dashboard-meta {
|
|
193
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
194
|
+
gap: var(--sp-4); margin-bottom: var(--sp-4);
|
|
195
|
+
}
|
|
196
|
+
.dashboard-meta h1 {
|
|
197
|
+
font-size: var(--fs-lg); font-weight: 600; color: var(--text);
|
|
198
|
+
letter-spacing: -0.01em; margin: 0;
|
|
199
|
+
}
|
|
200
|
+
.card {
|
|
201
|
+
background: var(--surface);
|
|
202
|
+
border: 1px solid var(--border);
|
|
203
|
+
border-radius: var(--radius);
|
|
204
|
+
padding: var(--sp-5);
|
|
205
|
+
margin-bottom: var(--sp-4);
|
|
206
|
+
}
|
|
207
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); }
|
|
208
|
+
.card-header h2 { font-size: var(--fs-lg); font-weight: 600; letter-spacing: -0.01em; color: var(--text); }
|
|
209
|
+
.source-row {
|
|
210
|
+
display: flex; align-items: center; gap: var(--sp-3);
|
|
211
|
+
padding: var(--sp-3) var(--sp-4);
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
border-radius: var(--radius-sm);
|
|
214
|
+
margin-bottom: var(--sp-2);
|
|
215
|
+
background: var(--surface);
|
|
216
|
+
transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
|
|
217
|
+
}
|
|
218
|
+
.source-row:hover { background: var(--surface-2); border-color: var(--border-strong); }
|
|
219
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; position: relative; }
|
|
220
|
+
.dot-up { background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); }
|
|
221
|
+
.dot-up::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; background: var(--success); opacity: 0.4; animation: pulse 2s var(--ease) infinite; }
|
|
222
|
+
@keyframes pulse { 0%, 100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 0; transform: scale(1.8); } }
|
|
223
|
+
.dot-down { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
|
|
224
|
+
.dot-disabled { background: var(--text-dim); box-shadow: 0 0 0 3px rgba(107,114,128,0.15); }
|
|
39
225
|
.source-info { flex: 1; min-width: 0; }
|
|
40
|
-
.source-info .name { font-weight: 600; font-size:
|
|
41
|
-
.source-info .url {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
.tag
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
226
|
+
.source-info .name { font-weight: 600; font-size: var(--fs-lg); letter-spacing: -0.01em; }
|
|
227
|
+
.source-info .url {
|
|
228
|
+
color: var(--text-muted); font-size: var(--fs-sm);
|
|
229
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace; font-variant-ligatures: none;
|
|
230
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
231
|
+
}
|
|
232
|
+
.source-actions { display: flex; gap: var(--sp-1); flex-shrink: 0; }
|
|
233
|
+
.tag {
|
|
234
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
235
|
+
padding: 2px 8px;
|
|
236
|
+
border-radius: var(--radius-pill);
|
|
237
|
+
font-size: var(--fs-xs); font-weight: 500;
|
|
238
|
+
letter-spacing: 0.02em;
|
|
239
|
+
margin-left: 6px;
|
|
240
|
+
}
|
|
241
|
+
.tag-metrics { color: var(--accent); background: var(--accent-soft); border: 1px solid rgba(79,140,255,0.25); }
|
|
242
|
+
.tag-logs { color: var(--info); background: var(--info-soft); border: 1px solid rgba(167,139,250,0.25); }
|
|
243
|
+
.tag-type { color: var(--text-muted); background: var(--surface-2); border: 1px solid var(--border-strong); text-transform: capitalize; }
|
|
244
|
+
.tag-latency { color: var(--text-muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
|
|
245
|
+
.service-row {
|
|
246
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
247
|
+
padding: var(--sp-3) var(--sp-4);
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
border-radius: var(--radius-sm);
|
|
250
|
+
margin-bottom: var(--sp-2);
|
|
251
|
+
background: var(--surface);
|
|
252
|
+
transition: background var(--t-fast) var(--ease);
|
|
253
|
+
}
|
|
254
|
+
.service-row:hover { background: var(--surface-2); }
|
|
255
|
+
.service-row .name { font-weight: 600; font-size: var(--fs-lg); }
|
|
256
|
+
.btn {
|
|
257
|
+
padding: 7px 14px;
|
|
258
|
+
border-radius: var(--radius-sm);
|
|
259
|
+
font: 500 var(--fs-md)/1 inherit;
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
border: 1px solid var(--border-strong);
|
|
262
|
+
background: var(--surface-2);
|
|
263
|
+
color: var(--text);
|
|
264
|
+
transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
|
|
265
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
266
|
+
}
|
|
267
|
+
.btn:hover { background: var(--surface-3); border-color: rgba(255,255,255,0.18); }
|
|
268
|
+
.btn:active { transform: translateY(1px); }
|
|
269
|
+
.btn-primary {
|
|
270
|
+
background: var(--accent);
|
|
271
|
+
border-color: var(--accent);
|
|
272
|
+
color: var(--text-inv);
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
}
|
|
275
|
+
.btn-primary:hover { background: var(--accent-2); border-color: var(--accent-2); }
|
|
276
|
+
.btn-ghost { background: none; border-color: transparent; color: var(--text-muted); }
|
|
277
|
+
.btn-ghost:hover { background: var(--surface-2); color: var(--text); border-color: transparent; }
|
|
278
|
+
.btn-danger { background: none; color: var(--danger); border-color: transparent; }
|
|
279
|
+
.btn-danger:hover { background: var(--danger-soft); border-color: transparent; }
|
|
280
|
+
.btn-sm { padding: 4px 10px; font-size: var(--fs-sm); }
|
|
281
|
+
.btn-icon { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--fs-xl); transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease); }
|
|
282
|
+
.btn-icon:hover { background: var(--surface-2); color: var(--text); }
|
|
283
|
+
.toggle { position: relative; width: 38px; height: 22px; cursor: pointer; flex-shrink: 0; }
|
|
57
284
|
.toggle input { display: none; }
|
|
58
|
-
.toggle .slider {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
285
|
+
.toggle .slider {
|
|
286
|
+
position: absolute; inset: 0;
|
|
287
|
+
background: var(--surface-3);
|
|
288
|
+
border: 1px solid var(--border-strong);
|
|
289
|
+
border-radius: var(--radius-pill);
|
|
290
|
+
transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
|
|
291
|
+
}
|
|
292
|
+
.toggle .slider::before {
|
|
293
|
+
content: ''; position: absolute;
|
|
294
|
+
width: 16px; height: 16px; left: 2px; top: 2px;
|
|
295
|
+
background: var(--text);
|
|
296
|
+
border-radius: 50%;
|
|
297
|
+
transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
|
|
298
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
299
|
+
}
|
|
300
|
+
.toggle input:checked + .slider { background: var(--accent); border-color: var(--accent); }
|
|
301
|
+
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
|
|
302
|
+
.toggle:hover .slider { border-color: rgba(255,255,255,0.18); }
|
|
303
|
+
.modal-overlay {
|
|
304
|
+
display: none; position: fixed; inset: 0;
|
|
305
|
+
background: rgba(8,10,15,0.72);
|
|
306
|
+
backdrop-filter: blur(4px);
|
|
307
|
+
z-index: 100; align-items: center; justify-content: center;
|
|
308
|
+
}
|
|
309
|
+
.modal-overlay.open { display: flex; animation: fadeIn 140ms var(--ease); }
|
|
310
|
+
.modal {
|
|
311
|
+
background: var(--surface);
|
|
312
|
+
border: 1px solid var(--border-strong);
|
|
313
|
+
border-radius: var(--radius-lg);
|
|
314
|
+
box-shadow: var(--shadow-lg);
|
|
315
|
+
width: 560px; max-width: 92vw; max-height: 88vh;
|
|
316
|
+
overflow-y: auto;
|
|
317
|
+
animation: modalIn 180ms var(--ease);
|
|
318
|
+
}
|
|
319
|
+
@keyframes modalIn { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
320
|
+
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border); }
|
|
321
|
+
.modal-header h3 { font-size: var(--fs-xl); font-weight: 600; letter-spacing: -0.01em; }
|
|
322
|
+
.modal-body { padding: var(--sp-5); }
|
|
323
|
+
.modal-footer { display: flex; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-4) var(--sp-5); border-top: 1px solid var(--border); background: var(--surface); position: sticky; bottom: 0; }
|
|
324
|
+
.form-group { margin-bottom: var(--sp-4); }
|
|
325
|
+
.form-group label { display: block; font-size: var(--fs-sm); font-weight: 500; color: var(--text-muted); margin-bottom: 6px; letter-spacing: 0.02em; }
|
|
326
|
+
.form-group input, .form-group select, .form-group textarea {
|
|
327
|
+
width: 100%; padding: 9px 12px;
|
|
328
|
+
background: var(--bg);
|
|
329
|
+
border: 1px solid var(--border-strong);
|
|
330
|
+
border-radius: var(--radius-sm);
|
|
331
|
+
color: var(--text);
|
|
332
|
+
font: var(--fs-lg)/1.4 inherit;
|
|
333
|
+
transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
|
|
334
|
+
}
|
|
335
|
+
.form-group input:hover, .form-group select:hover, .form-group textarea:hover { border-color: rgba(255,255,255,0.16); }
|
|
336
|
+
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
|
337
|
+
outline: none;
|
|
338
|
+
border-color: var(--accent);
|
|
339
|
+
box-shadow: 0 0 0 3px var(--accent-ring);
|
|
340
|
+
}
|
|
341
|
+
.form-group select {
|
|
342
|
+
appearance: none;
|
|
343
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23a1a8b3'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
|
|
344
|
+
background-repeat: no-repeat; background-position: right 12px center;
|
|
345
|
+
padding-right: 36px;
|
|
346
|
+
}
|
|
347
|
+
.form-group textarea { resize: vertical; min-height: 96px; font: var(--fs-md)/1.55 'JetBrains Mono', ui-monospace, monospace; }
|
|
348
|
+
.form-hint { font-size: var(--fs-xs); color: var(--text-dim); margin-top: 6px; line-height: 1.5; }
|
|
349
|
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
|
|
350
|
+
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--sp-3); }
|
|
351
|
+
.test-result { padding: 10px 14px; border-radius: var(--radius-sm); font-size: var(--fs-md); margin-top: var(--sp-3); display: none; }
|
|
79
352
|
.test-result.show { display: block; }
|
|
80
|
-
.test-result.success { background:
|
|
81
|
-
.test-result.failure { background:
|
|
82
|
-
.endpoint-bar {
|
|
83
|
-
|
|
84
|
-
|
|
353
|
+
.test-result.success { background: var(--success-soft); border: 1px solid rgba(74,222,128,0.30); color: var(--success); }
|
|
354
|
+
.test-result.failure { background: var(--danger-soft); border: 1px solid rgba(239,91,110,0.35); color: var(--danger); }
|
|
355
|
+
.endpoint-bar {
|
|
356
|
+
background: var(--surface);
|
|
357
|
+
border: 1px solid var(--border);
|
|
358
|
+
border-radius: var(--radius);
|
|
359
|
+
padding: 10px 16px;
|
|
360
|
+
font: var(--fs-md)/1 'JetBrains Mono', ui-monospace, monospace;
|
|
361
|
+
color: var(--accent);
|
|
362
|
+
margin-bottom: var(--sp-6);
|
|
363
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
364
|
+
}
|
|
365
|
+
.endpoint-bar::before { content: 'POST'; display: inline-block; padding: 2px 6px; margin-right: 10px; background: var(--accent-soft); color: var(--accent); border-radius: 3px; font-size: var(--fs-xs); font-weight: 700; font-family: 'Inter var', sans-serif; }
|
|
366
|
+
.empty { color: var(--text-dim); text-align: center; padding: var(--sp-8) var(--sp-4); font-size: var(--fs-md); }
|
|
367
|
+
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; }
|
|
85
368
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
86
369
|
/* Tabs inside settings */
|
|
87
|
-
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom:
|
|
88
|
-
.tab-btn { background: none; border: none; border-bottom: 2px solid transparent; color: var(--
|
|
370
|
+
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: var(--sp-5); }
|
|
371
|
+
.tab-btn { background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); padding: 10px 18px; cursor: pointer; font-size: var(--fs-md); font-weight: 500; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
|
|
89
372
|
.tab-btn:hover { color: var(--text); }
|
|
90
|
-
.tab-btn.active { color: var(--
|
|
373
|
+
.tab-btn.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
91
374
|
.tab-content { display: none; } .tab-content.active { display: block; }
|
|
92
375
|
/* Threshold cards */
|
|
93
|
-
.threshold-group { display: grid; grid-template-columns: 1fr 1fr; gap:
|
|
94
|
-
.threshold-card { background: var(--
|
|
95
|
-
.threshold-card h4 { font-size:
|
|
376
|
+
.threshold-group { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); margin-bottom: var(--sp-5); }
|
|
377
|
+
.threshold-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-4); }
|
|
378
|
+
.threshold-card h4 { font-size: var(--fs-md); font-weight: 600; margin-bottom: var(--sp-3); letter-spacing: 0.01em; }
|
|
96
379
|
/* Metric table */
|
|
97
380
|
.metric-table { width: 100%; border-collapse: collapse; }
|
|
98
|
-
.metric-table th { text-align: left; font-size:
|
|
99
|
-
.metric-table td { padding:
|
|
100
|
-
.metric-table .query { font-family: monospace; font-size:
|
|
101
|
-
.metric-table tr
|
|
102
|
-
.
|
|
103
|
-
.toast
|
|
381
|
+
.metric-table th { text-align: left; font-size: var(--fs-xs); color: var(--text-muted); font-weight: 500; padding: 8px 10px; border-bottom: 1px solid var(--border-strong); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
382
|
+
.metric-table td { padding: 10px; border-bottom: 1px solid var(--border); font-size: var(--fs-md); }
|
|
383
|
+
.metric-table .query { font-family: 'JetBrains Mono', ui-monospace, monospace; font-variant-ligatures: none; font-size: var(--fs-xs); color: var(--text-muted); max-width: 400px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
384
|
+
.metric-table tr { transition: background var(--t-fast) var(--ease); }
|
|
385
|
+
.metric-table tbody tr:hover td { background: var(--surface-2); }
|
|
386
|
+
.toast {
|
|
387
|
+
position: fixed; bottom: var(--sp-6); right: var(--sp-6);
|
|
388
|
+
background: var(--surface);
|
|
389
|
+
color: var(--text);
|
|
390
|
+
padding: 12px 18px;
|
|
391
|
+
border-radius: var(--radius);
|
|
392
|
+
border: 1px solid var(--border-strong);
|
|
393
|
+
box-shadow: var(--shadow);
|
|
394
|
+
font-size: var(--fs-md); font-weight: 500;
|
|
395
|
+
display: flex; align-items: center; gap: 10px;
|
|
396
|
+
opacity: 0; transform: translateY(8px);
|
|
397
|
+
transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
|
|
398
|
+
pointer-events: none; z-index: 200;
|
|
399
|
+
}
|
|
400
|
+
.toast::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); }
|
|
401
|
+
.toast.show { opacity: 1; transform: none; }
|
|
402
|
+
.toast.toast-error::before { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
|
|
104
403
|
/* Health cards */
|
|
105
|
-
.health-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding:
|
|
106
|
-
.health-card
|
|
107
|
-
.health-card .hc-
|
|
108
|
-
.health-card .hc-
|
|
109
|
-
.health-card .hc-score
|
|
110
|
-
.health-card .hc-score.
|
|
111
|
-
.health-card .hc-score.
|
|
112
|
-
.health-card .hc-
|
|
113
|
-
.hc-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.hc-
|
|
118
|
-
.
|
|
119
|
-
.hc-
|
|
120
|
-
.hc-
|
|
121
|
-
.hc-
|
|
122
|
-
.
|
|
404
|
+
.health-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--sp-5); transition: border-color var(--t-fast) var(--ease); }
|
|
405
|
+
.health-card:hover { border-color: var(--border-strong); }
|
|
406
|
+
.health-card .hc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); gap: var(--sp-3); }
|
|
407
|
+
.health-card .hc-name { font-size: var(--fs-xl); font-weight: 600; letter-spacing: -0.01em; }
|
|
408
|
+
.health-card .hc-score { font-size: var(--fs-3xl); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1; }
|
|
409
|
+
.health-card .hc-score.healthy { color: var(--success); }
|
|
410
|
+
.health-card .hc-score.degraded { color: var(--warning); }
|
|
411
|
+
.health-card .hc-score.critical { color: var(--danger); }
|
|
412
|
+
.health-card .hc-spark {
|
|
413
|
+
display: block; width: 100%; height: 36px;
|
|
414
|
+
margin: var(--sp-1) 0 var(--sp-3); opacity: 0.85;
|
|
415
|
+
}
|
|
416
|
+
.health-card .hc-spark.healthy .spark-fill { fill: rgba(63, 185, 80, 0.12); }
|
|
417
|
+
.health-card .hc-spark.healthy .spark-line { stroke: var(--success); }
|
|
418
|
+
.health-card .hc-spark.degraded .spark-fill { fill: rgba(210, 153, 34, 0.12); }
|
|
419
|
+
.health-card .hc-spark.degraded .spark-line { stroke: var(--warning); }
|
|
420
|
+
.health-card .hc-spark.critical .spark-fill { fill: rgba(248, 81, 73, 0.12); }
|
|
421
|
+
.health-card .hc-spark.critical .spark-line { stroke: var(--danger); }
|
|
422
|
+
.health-card .hc-spark .spark-line { stroke-width: 1.5; fill: none; }
|
|
423
|
+
.health-card .hc-spark .spark-dot { fill: currentColor; }
|
|
424
|
+
.health-card .hc-spark .spark-empty {
|
|
425
|
+
stroke: var(--border); stroke-dasharray: 3 3; stroke-width: 1; fill: none;
|
|
426
|
+
}
|
|
427
|
+
.health-card .hc-status {
|
|
428
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
429
|
+
padding: 3px 10px;
|
|
430
|
+
border-radius: var(--radius-pill);
|
|
431
|
+
font-size: var(--fs-xs); font-weight: 600;
|
|
432
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
433
|
+
}
|
|
434
|
+
.hc-status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
435
|
+
.hc-status.healthy { background: var(--success-soft); color: var(--success); border: 1px solid rgba(74,222,128,0.25); }
|
|
436
|
+
.hc-status.degraded { background: var(--warning-soft); color: var(--warning); border: 1px solid rgba(245,179,65,0.30); }
|
|
437
|
+
.hc-status.critical { background: var(--danger-soft); color: var(--danger); border: 1px solid rgba(239,91,110,0.35); }
|
|
438
|
+
.hc-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); margin-top: var(--sp-3); }
|
|
439
|
+
.hc-metric { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; }
|
|
440
|
+
.hc-metric .label { font-size: var(--fs-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; }
|
|
441
|
+
.hc-metric .val { font-size: var(--fs-lg); font-weight: 600; margin-top: 4px; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
|
442
|
+
.hc-anomalies { margin-top: var(--sp-3); }
|
|
443
|
+
.hc-anomaly {
|
|
444
|
+
background: var(--danger-soft);
|
|
445
|
+
border: 1px solid rgba(239,91,110,0.30);
|
|
446
|
+
border-left: 3px solid var(--danger);
|
|
447
|
+
border-radius: var(--radius-sm);
|
|
448
|
+
padding: 10px 12px; margin-top: 6px;
|
|
449
|
+
font-size: var(--fs-sm); color: var(--text); line-height: 1.5;
|
|
450
|
+
}
|
|
451
|
+
.hc-correlation {
|
|
452
|
+
background: var(--info-soft);
|
|
453
|
+
border: 1px solid rgba(167,139,250,0.25);
|
|
454
|
+
border-left: 3px solid var(--info);
|
|
455
|
+
border-radius: var(--radius-sm);
|
|
456
|
+
padding: 10px 12px; margin-top: 6px;
|
|
457
|
+
font-size: var(--fs-sm); color: var(--text); line-height: 1.5;
|
|
458
|
+
}
|
|
459
|
+
.info-footer {
|
|
460
|
+
max-width: 1240px; margin: var(--sp-8) auto var(--sp-6);
|
|
461
|
+
padding: var(--sp-4) var(--sp-6);
|
|
462
|
+
border-top: 1px solid var(--border);
|
|
463
|
+
color: var(--text-muted);
|
|
464
|
+
font-size: var(--fs-xs);
|
|
465
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
|
|
466
|
+
letter-spacing: 0.02em;
|
|
467
|
+
}
|
|
468
|
+
.info-footer code { color: var(--text); font-size: var(--fs-xs); }
|
|
469
|
+
.info-footer strong { color: var(--text); font-weight: 600; }
|
|
470
|
+
.footer-sep { color: var(--text-dim); }
|
|
471
|
+
.footer-plugins { display: inline-flex; flex-wrap: wrap; gap: var(--sp-1); }
|
|
472
|
+
.footer-pill {
|
|
473
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
474
|
+
padding: 2px 8px;
|
|
475
|
+
background: var(--surface-2);
|
|
476
|
+
border: 1px solid var(--border-strong);
|
|
477
|
+
border-radius: var(--radius-pill);
|
|
478
|
+
font-size: var(--fs-xs);
|
|
479
|
+
color: var(--text);
|
|
480
|
+
}
|
|
481
|
+
.footer-pill-meta { color: var(--text-muted); font-weight: 400; }
|
|
482
|
+
|
|
123
483
|
@media (max-width: 768px) { .stats { grid-template-columns: repeat(2, 1fr); } .threshold-group { grid-template-columns: 1fr; } .form-row, .form-row-3 { grid-template-columns: 1fr; } }
|
|
124
484
|
</style>
|
|
125
485
|
</head>
|
|
@@ -131,6 +491,7 @@
|
|
|
131
491
|
<button class="nav-btn active" data-page="dashboard" onclick="showPage('dashboard')">Dashboard</button>
|
|
132
492
|
<button class="nav-btn" data-page="sources" onclick="showPage('sources')">Sources</button>
|
|
133
493
|
<button class="nav-btn" data-page="services" onclick="showPage('services')">Services</button>
|
|
494
|
+
<button class="nav-btn" data-page="connectors" onclick="showPage('connectors')">Connectors</button>
|
|
134
495
|
<button class="nav-btn" data-page="health" onclick="showPage('health')">Health</button>
|
|
135
496
|
<button class="nav-btn" data-page="settings" onclick="showPage('settings')">Settings</button>
|
|
136
497
|
</div>
|
|
@@ -141,11 +502,31 @@
|
|
|
141
502
|
<div class="container">
|
|
142
503
|
<!-- ===== Dashboard ===== -->
|
|
143
504
|
<div class="page active" id="page-dashboard">
|
|
505
|
+
<div class="dashboard-meta">
|
|
506
|
+
<h1>Overview</h1>
|
|
507
|
+
<span class="live-indicator" id="dash-live">Live</span>
|
|
508
|
+
</div>
|
|
144
509
|
<div class="stats">
|
|
145
|
-
<div class="stat-card"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
510
|
+
<div class="stat-card">
|
|
511
|
+
<div class="label">Data Sources</div>
|
|
512
|
+
<div class="value" id="stat-sources">-</div>
|
|
513
|
+
<div class="context" id="stat-sources-ctx">configured</div>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="stat-card">
|
|
516
|
+
<div class="label">Sources Up</div>
|
|
517
|
+
<div class="value" id="stat-sources-up">-</div>
|
|
518
|
+
<div class="context" id="stat-sources-up-ctx">connected</div>
|
|
519
|
+
</div>
|
|
520
|
+
<div class="stat-card">
|
|
521
|
+
<div class="label">Services</div>
|
|
522
|
+
<div class="value" id="stat-services">-</div>
|
|
523
|
+
<div class="context" id="stat-services-ctx">discovered</div>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="stat-card">
|
|
526
|
+
<div class="label">MCP Tools</div>
|
|
527
|
+
<div class="value" id="stat-tools">6</div>
|
|
528
|
+
<div class="context">available</div>
|
|
529
|
+
</div>
|
|
149
530
|
</div>
|
|
150
531
|
<div class="endpoint-bar">
|
|
151
532
|
<span>MCP Endpoint: <strong id="mcp-url">http://localhost:3000/mcp</strong></span>
|
|
@@ -170,6 +551,40 @@
|
|
|
170
551
|
<div class="card"><div class="card-header"><h2>Discovered Services</h2></div><div id="services-list"><div class="empty">Loading...</div></div></div>
|
|
171
552
|
</div>
|
|
172
553
|
|
|
554
|
+
<!-- ===== Connectors ===== -->
|
|
555
|
+
<div class="page" id="page-connectors">
|
|
556
|
+
<div class="card" style="padding:0;">
|
|
557
|
+
<div class="tabs">
|
|
558
|
+
<button class="tab-btn active" onclick="showTab('installed')">Installed</button>
|
|
559
|
+
<button class="tab-btn" onclick="showTab('hub')">Connector Hub</button>
|
|
560
|
+
<button class="tab-btn" onclick="showTab('upload')">Upload bundle</button>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<!-- Installed Tab -->
|
|
564
|
+
<div class="tab-content active" id="tab-installed" style="padding:20px;">
|
|
565
|
+
<div class="card-header" style="margin-bottom:12px"><h2>Installed connectors</h2><button class="btn btn-ghost btn-sm" onclick="loadConnectors()">Refresh</button></div>
|
|
566
|
+
<div id="conn-installed"><div class="empty">Loading...</div></div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<!-- Connector Hub Tab -->
|
|
570
|
+
<div class="tab-content" id="tab-hub" style="padding:20px;">
|
|
571
|
+
<div class="card-header" style="margin-bottom:12px"><h2>Available from the Connector Hub</h2>
|
|
572
|
+
<a class="btn btn-ghost btn-sm" href="https://thotischner.github.io/observability-mcp/hub/" target="_blank" rel="noopener">Open Hub ↗</a></div>
|
|
573
|
+
<div id="conn-hub"><div class="empty">Loading...</div></div>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<!-- Upload bundle Tab -->
|
|
577
|
+
<div class="tab-content" id="tab-upload" style="padding:20px;">
|
|
578
|
+
<div class="card-header" style="margin-bottom:12px"><h2>Upload a connector bundle</h2></div>
|
|
579
|
+
<p class="empty" style="margin:0 0 12px">Install a signed connector <code>.tgz</code> directly — handy for air-gapped environments. The bundle is always verified against the configured trust root before it is loaded.</p>
|
|
580
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
581
|
+
<input type="file" id="conn-upload-file" accept=".tgz,.tar.gz,application/octet-stream">
|
|
582
|
+
<button class="btn btn-primary btn-sm" onclick="uploadConnector(this)">Upload & install</button>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
173
588
|
<!-- ===== Health ===== -->
|
|
174
589
|
<div class="page" id="page-health">
|
|
175
590
|
<div id="health-cards" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px;">
|
|
@@ -365,12 +780,88 @@ function showPage(name) {
|
|
|
365
780
|
document.querySelector(`.nav-btn[data-page="${name}"]`).classList.add('active');
|
|
366
781
|
if(name==='settings') loadSettingsData();
|
|
367
782
|
if(name==='health') loadHealthData();
|
|
783
|
+
if(name==='connectors') loadConnectors();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function escHtml(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|
787
|
+
|
|
788
|
+
async function loadConnectors(){
|
|
789
|
+
const inst=document.getElementById('conn-installed');
|
|
790
|
+
const hub=document.getElementById('conn-hub');
|
|
791
|
+
try {
|
|
792
|
+
const d=await(await fetch('/api/connectors')).json();
|
|
793
|
+
const cs=(d.connectors||[]);
|
|
794
|
+
inst.innerHTML = cs.length ? cs.map(c=>{
|
|
795
|
+
const caps=Object.entries(c.capabilities||{}).filter(([,v])=>v).map(([k])=>k).join(', ')||'—';
|
|
796
|
+
return `<div class="card" style="margin:0 0 10px">
|
|
797
|
+
<div class="card-header"><h2 style="font-size:15px">${escHtml(c.displayName)}
|
|
798
|
+
<span class="badge">${escHtml(c.source)}</span>
|
|
799
|
+
${c.version?`<span class="badge">v${escHtml(c.version)}</span>`:''}</h2></div>
|
|
800
|
+
<div style="color:var(--text-muted);font-size:13px">${escHtml(c.description)||'<em>no description</em>'}</div>
|
|
801
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:6px">type <code>${escHtml(c.name)}</code> · signals ${(c.signalTypes||[]).map(escHtml).join(', ')||'—'} · ${escHtml(caps)}</div>
|
|
802
|
+
</div>`;
|
|
803
|
+
}).join('') : '<div class="empty">No connectors loaded.</div>';
|
|
804
|
+
} catch(e){ inst.innerHTML='<div class="empty">Failed to load connectors.</div>'; }
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const d=await(await fetch('/api/hub/catalog')).json();
|
|
808
|
+
if(d.error){ hub.innerHTML=`<div class="empty">Hub catalog unreachable (${escHtml(d.url)}).<br>Set <code>HUB_CATALOG_URL</code> for a mirror. ${escHtml(d.error)}</div>`; return; }
|
|
809
|
+
const cs=(d.connectors||[]);
|
|
810
|
+
hub.innerHTML = cs.length ? cs.map(c=>{
|
|
811
|
+
const v=(c.versions&&c.versions[0])||{};
|
|
812
|
+
const ver=c.latest||v.version||'';
|
|
813
|
+
const status = c.installed
|
|
814
|
+
? `<span class="badge badge-ok">installed${c.installedVersion?` v${escHtml(c.installedVersion)}`:''}</span>`
|
|
815
|
+
: (c.builtin?'<span class="badge">builtin</span>':'<span class="badge">available</span>');
|
|
816
|
+
const cmd = c.builtin
|
|
817
|
+
? `# ${escHtml(c.displayName)} ships in the server image — no install needed.`
|
|
818
|
+
: `curl -fsSL -o plugin-signing.pub.pem https://raw.githubusercontent.com/ThoTischner/observability-mcp/main/docs/plugin-signing.pub.pem\nomcp plugin install ${escHtml(c.name)}@${escHtml(ver)} --trust-root plugin-signing.pub.pem`;
|
|
819
|
+
const id='ci-'+c.name;
|
|
820
|
+
return `<div class="card" style="margin:0 0 10px">
|
|
821
|
+
<div class="card-header"><h2 style="font-size:15px">${escHtml(c.displayName)}
|
|
822
|
+
<span class="badge">${escHtml(c.tier)}</span> ${status}</h2>
|
|
823
|
+
${c.installed||c.builtin?'':`<span style="display:flex;gap:6px"><button class="btn btn-primary btn-sm" onclick="installConnector('${escHtml(c.name)}',this)">Install</button><button class="btn btn-ghost btn-sm" onclick="document.getElementById('${id}').classList.toggle('hidden')">CLI…</button></span>`}</div>
|
|
824
|
+
<div style="color:var(--text-muted);font-size:13px">${escHtml(c.description)}</div>
|
|
825
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:6px">type <code>${escHtml(c.name)}</code> · signals ${(c.signalTypes||[]).map(escHtml).join(', ')||'—'} · latest <code>${escHtml(ver)||'—'}</code></div>
|
|
826
|
+
<pre id="${id}" class="hidden" style="margin-top:8px;background:var(--surface-2);padding:10px;border-radius:6px;font-size:12px;overflow:auto">${escHtml(cmd)}</pre>
|
|
827
|
+
</div>`;
|
|
828
|
+
}).join('') : '<div class="empty">Catalog empty.</div>';
|
|
829
|
+
} catch(e){ hub.innerHTML='<div class="empty">Failed to reach the hub catalog.</div>'; }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function installConnector(name, btn){
|
|
833
|
+
if(btn){ btn.disabled=true; btn.textContent='Installing…'; }
|
|
834
|
+
try {
|
|
835
|
+
const r=await fetch('/api/connectors/install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
|
|
836
|
+
const d=await r.json().catch(()=>({}));
|
|
837
|
+
if(r.ok){ toast(`Installed ${name}${d.version?(' v'+d.version):''}`); loadConnectors(); loadTypes(); }
|
|
838
|
+
else if(r.status===403){ toast('UI install disabled — use the CLI tab, or set ENABLE_UI_INSTALL=true + PLUGIN_TRUST_ROOT.'); if(btn){btn.disabled=false;btn.textContent='Install';} }
|
|
839
|
+
else { toast('Install failed: '+(d.error||r.status)); if(btn){btn.disabled=false;btn.textContent='Install';} }
|
|
840
|
+
} catch(e){ toast('Install error: '+e.message); if(btn){btn.disabled=false;btn.textContent='Install';} }
|
|
841
|
+
}
|
|
842
|
+
async function uploadConnector(btn){
|
|
843
|
+
const inp=document.getElementById('conn-upload-file');
|
|
844
|
+
const f=inp&&inp.files&&inp.files[0];
|
|
845
|
+
if(!f){ toast('Choose a connector .tgz first'); return; }
|
|
846
|
+
if(btn){ btn.disabled=true; btn.textContent='Uploading…'; }
|
|
847
|
+
try {
|
|
848
|
+
const r=await fetch('/api/connectors/upload',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:f});
|
|
849
|
+
const d=await r.json().catch(()=>({}));
|
|
850
|
+
if(r.ok){ toast(`Installed ${d.name||'connector'}${d.version?(' v'+d.version):''}`); inp.value=''; loadConnectors(); loadTypes(); }
|
|
851
|
+
else if(r.status===403){ toast('UI install disabled — set ENABLE_UI_INSTALL=true + PLUGIN_TRUST_ROOT.'); }
|
|
852
|
+
else { toast('Upload failed: '+(d.error||r.status)); }
|
|
853
|
+
} catch(e){ toast('Upload error: '+e.message); }
|
|
854
|
+
finally { if(btn){btn.disabled=false;btn.textContent='Upload & install';} }
|
|
368
855
|
}
|
|
369
856
|
function showTab(name) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
857
|
+
// Scope to the tab group's card so independent tab sets (Settings,
|
|
858
|
+
// Connectors) don't reset each other.
|
|
859
|
+
const scope = (event && event.target.closest('.card')) || document;
|
|
860
|
+
scope.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
|
|
861
|
+
scope.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
|
862
|
+
const el = document.getElementById('tab-'+name);
|
|
863
|
+
if (el) el.classList.add('active');
|
|
864
|
+
if (event && event.target) event.target.classList.add('active');
|
|
374
865
|
if(name==='metrics') populateMetricsSourceSelect();
|
|
375
866
|
}
|
|
376
867
|
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
|
@@ -386,9 +877,29 @@ async function loadSettingsData() {
|
|
|
386
877
|
]); populateSettingsForm(); populateHealthForm(); populateMetricsSourceSelect(); } catch(e){ console.error(e); }
|
|
387
878
|
}
|
|
388
879
|
function updateStats() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
880
|
+
const total = sourcesData.length;
|
|
881
|
+
const up = sourcesData.filter(s=>s.status==='up').length;
|
|
882
|
+
const enabled = sourcesData.filter(s=>s.enabled).length;
|
|
883
|
+
document.getElementById('stat-sources').textContent = total;
|
|
884
|
+
document.getElementById('stat-sources-up').textContent = up;
|
|
885
|
+
document.getElementById('stat-services').textContent = servicesData.length;
|
|
886
|
+
|
|
887
|
+
const srcCtx = document.getElementById('stat-sources-ctx');
|
|
888
|
+
if (srcCtx) srcCtx.textContent = enabled === total ? `${enabled} enabled` : `${enabled}/${total} enabled`;
|
|
889
|
+
|
|
890
|
+
const upCtx = document.getElementById('stat-sources-up-ctx');
|
|
891
|
+
if (upCtx) {
|
|
892
|
+
upCtx.textContent = total === 0 ? 'no sources' : `${up}/${total} connected`;
|
|
893
|
+
upCtx.classList.toggle('good', total > 0 && up === total);
|
|
894
|
+
upCtx.classList.toggle('warn', total > 0 && up < total);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const svcCtx = document.getElementById('stat-services-ctx');
|
|
898
|
+
if (svcCtx) {
|
|
899
|
+
const backends = new Set(servicesData.map(s => s.source || s.sourceName).filter(Boolean));
|
|
900
|
+
svcCtx.textContent = backends.size > 0 ? `across ${backends.size} backend${backends.size===1?'':'s'}` : 'discovered';
|
|
901
|
+
}
|
|
902
|
+
|
|
392
903
|
const allUp=sourcesData.length>0&&sourcesData.filter(s=>s.enabled).every(s=>s.status==='up');
|
|
393
904
|
const b=document.getElementById('status-badge');
|
|
394
905
|
b.textContent=sourcesData.length===0?'No sources':allUp?'All systems operational':'Issues detected';
|
|
@@ -631,6 +1142,39 @@ function saveMetric() {
|
|
|
631
1142
|
// --- Health Dashboard ---
|
|
632
1143
|
let healthData={};
|
|
633
1144
|
let healthInterval=null;
|
|
1145
|
+
// Score history per service, kept client-side. The server doesn't yet
|
|
1146
|
+
// expose a per-service score timeseries — this gives an at-a-glance trend
|
|
1147
|
+
// for the last ~7.5 minutes (30 points × 15s refresh).
|
|
1148
|
+
const SPARK_MAX = 30;
|
|
1149
|
+
const scoreHistory = {};
|
|
1150
|
+
function pushScore(name, score) {
|
|
1151
|
+
if (typeof score !== 'number') return;
|
|
1152
|
+
const arr = scoreHistory[name] || (scoreHistory[name] = []);
|
|
1153
|
+
arr.push(score);
|
|
1154
|
+
if (arr.length > SPARK_MAX) arr.shift();
|
|
1155
|
+
}
|
|
1156
|
+
function sparkSvg(name, status) {
|
|
1157
|
+
const pts = scoreHistory[name] || [];
|
|
1158
|
+
const w = 100, h = 36, pad = 2;
|
|
1159
|
+
if (pts.length < 2) {
|
|
1160
|
+
// Placeholder dashed midline until we have ≥2 samples.
|
|
1161
|
+
return `<svg class="hc-spark ${status}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true">`
|
|
1162
|
+
+ `<line class="spark-empty" x1="0" y1="${h/2}" x2="${w}" y2="${h/2}"/></svg>`;
|
|
1163
|
+
}
|
|
1164
|
+
const min = 0, max = 100;
|
|
1165
|
+
const step = (w - pad*2) / (pts.length - 1);
|
|
1166
|
+
const y = v => h - pad - ((v - min) / (max - min)) * (h - pad*2);
|
|
1167
|
+
const coords = pts.map((v, i) => `${pad + i*step},${y(v)}`);
|
|
1168
|
+
const line = coords.join(' ');
|
|
1169
|
+
const area = `M${coords[0]} L${line.split(' ').join(' L')} L${pad + (pts.length-1)*step},${h-pad} L${pad},${h-pad} Z`;
|
|
1170
|
+
const last = coords[coords.length - 1].split(',');
|
|
1171
|
+
return `<svg class="hc-spark ${status}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true">`
|
|
1172
|
+
+ `<path class="spark-fill" d="${area}"/>`
|
|
1173
|
+
+ `<polyline class="spark-line" points="${line}"/>`
|
|
1174
|
+
+ `<circle class="spark-dot" cx="${last[0]}" cy="${last[1]}" r="1.8"/>`
|
|
1175
|
+
+ `</svg>`;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
634
1178
|
async function loadHealthData() {
|
|
635
1179
|
try {
|
|
636
1180
|
healthData=await(await fetch('/api/health')).json();
|
|
@@ -647,11 +1191,13 @@ function renderHealthCards() {
|
|
|
647
1191
|
const s=h.status||'healthy';
|
|
648
1192
|
const m=h.signals?.metrics||{};
|
|
649
1193
|
const l=h.signals?.logs||{};
|
|
1194
|
+
pushScore(name, h.score);
|
|
650
1195
|
return `<div class="health-card">
|
|
651
1196
|
<div class="hc-header">
|
|
652
1197
|
<div><span class="hc-name">${esc(name)}</span> <span class="hc-status ${s}">${s}</span></div>
|
|
653
1198
|
<span class="hc-score ${s}">${h.score ?? '-'}</span>
|
|
654
1199
|
</div>
|
|
1200
|
+
${sparkSvg(name, s)}
|
|
655
1201
|
<div class="hc-metrics">
|
|
656
1202
|
<div class="hc-metric"><div class="label">CPU</div><div class="val">${typeof m.cpu==='number'?m.cpu.toFixed(1)+'%':'-'}</div></div>
|
|
657
1203
|
<div class="hc-metric"><div class="label">Memory</div><div class="val">${typeof m.memory==='number'?m.memory.toFixed(0)+' MB':'-'}</div></div>
|
|
@@ -669,7 +1215,33 @@ function renderHealthCards() {
|
|
|
669
1215
|
// --- Utils ---
|
|
670
1216
|
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
|
671
1217
|
async function refresh(){await Promise.all([loadSources(),loadServices()]);}
|
|
672
|
-
|
|
1218
|
+
|
|
1219
|
+
async function loadInfo(){
|
|
1220
|
+
try{
|
|
1221
|
+
const r=await fetch('/api/info'); if(!r.ok) return;
|
|
1222
|
+
const d=await r.json();
|
|
1223
|
+
const footer=document.getElementById('info-footer');
|
|
1224
|
+
if(!footer) return;
|
|
1225
|
+
const plugins=(d.plugins||[]).map(p=>`<span class="footer-pill">${esc(p.name)}<span class="footer-pill-meta">${esc(p.source)}${p.version?` · ${esc(p.version)}`:''}</span></span>`).join('');
|
|
1226
|
+
const sha=d.build&&d.build.commit?` · <code class="mono">${esc(d.build.commit.slice(0,7))}</code>`:'';
|
|
1227
|
+
footer.innerHTML=`
|
|
1228
|
+
<span><strong>${esc(d.name)}</strong> v${esc(d.version)}${sha}</span>
|
|
1229
|
+
<span class="footer-sep">·</span>
|
|
1230
|
+
<span>MCP ${esc(d.mcpProtocolVersion)}</span>
|
|
1231
|
+
<span class="footer-sep">·</span>
|
|
1232
|
+
<span>${esc(d.runtime?d.runtime.node:'')} ${esc(d.runtime?d.runtime.platform+'/'+d.runtime.arch:'')}</span>
|
|
1233
|
+
<span class="footer-sep">·</span>
|
|
1234
|
+
<span class="footer-plugins">${plugins||'<em>no plugins loaded</em>'}</span>
|
|
1235
|
+
`;
|
|
1236
|
+
}catch{/* server too old or /api/info disabled */}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Show the endpoint the user is actually reaching the server through
|
|
1240
|
+
// (localhost, a port-forward, or an ingress) — not a hardcoded host.
|
|
1241
|
+
document.getElementById('mcp-url').textContent = window.location.origin + '/mcp';
|
|
1242
|
+
(async()=>{await loadTypes();await refresh();await loadInfo();setInterval(refresh,15000);})();
|
|
673
1243
|
</script>
|
|
1244
|
+
|
|
1245
|
+
<footer id="info-footer" class="info-footer"><span class="spinner"></span> loading server info…</footer>
|
|
674
1246
|
</body>
|
|
675
1247
|
</html>
|