@thotischner/observability-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/sources.yaml +45 -0
- package/dist/analysis/anomaly.d.ts +24 -0
- package/dist/analysis/anomaly.js +50 -0
- package/dist/analysis/anomaly.test.d.ts +1 -0
- package/dist/analysis/anomaly.test.js +87 -0
- package/dist/analysis/correlator.d.ts +7 -0
- package/dist/analysis/correlator.js +31 -0
- package/dist/analysis/correlator.test.d.ts +1 -0
- package/dist/analysis/correlator.test.js +53 -0
- package/dist/analysis/health.d.ts +19 -0
- package/dist/analysis/health.js +34 -0
- package/dist/analysis/health.test.d.ts +1 -0
- package/dist/analysis/health.test.js +70 -0
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.js +81 -0
- package/dist/config/loader.test.d.ts +1 -0
- package/dist/config/loader.test.js +163 -0
- package/dist/connectors/interface.d.ts +17 -0
- package/dist/connectors/interface.js +1 -0
- package/dist/connectors/loki.d.ts +25 -0
- package/dist/connectors/loki.js +182 -0
- package/dist/connectors/loki.test.d.ts +1 -0
- package/dist/connectors/loki.test.js +111 -0
- package/dist/connectors/prometheus.d.ts +28 -0
- package/dist/connectors/prometheus.js +196 -0
- package/dist/connectors/prometheus.test.d.ts +1 -0
- package/dist/connectors/prometheus.test.js +103 -0
- package/dist/connectors/registry.d.ts +18 -0
- package/dist/connectors/registry.js +90 -0
- package/dist/connectors/registry.test.d.ts +1 -0
- package/dist/connectors/registry.test.js +93 -0
- package/dist/connectors/tls.d.ts +7 -0
- package/dist/connectors/tls.js +25 -0
- package/dist/connectors/tls.test.d.ts +1 -0
- package/dist/connectors/tls.test.js +99 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +421 -0
- package/dist/tools/detect-anomalies.d.ts +33 -0
- package/dist/tools/detect-anomalies.js +137 -0
- package/dist/tools/get-service-health.d.ts +25 -0
- package/dist/tools/get-service-health.js +111 -0
- package/dist/tools/handlers.test.d.ts +1 -0
- package/dist/tools/handlers.test.js +138 -0
- package/dist/tools/list-services.d.ts +22 -0
- package/dist/tools/list-services.js +57 -0
- package/dist/tools/list-sources.d.ts +15 -0
- package/dist/tools/list-sources.js +27 -0
- package/dist/tools/query-logs.d.ts +49 -0
- package/dist/tools/query-logs.js +93 -0
- package/dist/tools/query-metrics.d.ts +44 -0
- package/dist/tools/query-metrics.js +91 -0
- package/dist/tools/validation.d.ts +17 -0
- package/dist/tools/validation.js +45 -0
- package/dist/tools/validation.test.d.ts +1 -0
- package/dist/tools/validation.test.js +84 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.js +1 -0
- package/dist/ui/index.html +675 -0
- package/package.json +35 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Observability MCP Gateway</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f1117; --surface: #161b22; --surface2: #1c2128; --border: #30363d;
|
|
10
|
+
--text: #e1e4e8; --text2: #8b949e; --accent: #58a6ff; --green: #3fb950;
|
|
11
|
+
--red: #f85149; --yellow: #d29922; --purple: #d2a8ff; --radius: 8px;
|
|
12
|
+
}
|
|
13
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); }
|
|
15
|
+
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 14px 24px; display: flex; align-items: center; gap: 16px; }
|
|
16
|
+
.header h1 { font-size: 18px; font-weight: 600; }
|
|
17
|
+
.badge { padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
|
|
18
|
+
.badge-ok { background: var(--green); color: #fff; }
|
|
19
|
+
.badge-err { background: var(--red); color: #fff; }
|
|
20
|
+
.nav { display: flex; gap: 2px; margin-left: 24px; }
|
|
21
|
+
.nav-btn { background: none; border: none; color: var(--text2); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
22
|
+
.nav-btn:hover { background: var(--surface2); color: var(--text); }
|
|
23
|
+
.nav-btn.active { background: var(--surface2); color: var(--accent); }
|
|
24
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
25
|
+
.page { display: none; } .page.active { display: block; }
|
|
26
|
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
|
27
|
+
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; text-align: center; }
|
|
28
|
+
.stat-card .value { font-size: 28px; font-weight: 700; color: var(--accent); }
|
|
29
|
+
.stat-card .label { font-size: 12px; color: var(--text2); margin-top: 4px; }
|
|
30
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; }
|
|
31
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
32
|
+
.card-header h2 { font-size: 15px; font-weight: 600; }
|
|
33
|
+
.source-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; transition: border-color 0.15s; }
|
|
34
|
+
.source-row:hover { border-color: var(--accent); }
|
|
35
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
36
|
+
.dot-up { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
37
|
+
.dot-down { background: var(--red); box-shadow: 0 0 6px var(--red); }
|
|
38
|
+
.dot-disabled { background: var(--text2); }
|
|
39
|
+
.source-info { flex: 1; min-width: 0; }
|
|
40
|
+
.source-info .name { font-weight: 600; font-size: 14px; }
|
|
41
|
+
.source-info .url { color: var(--text2); font-size: 12px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
42
|
+
.source-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
43
|
+
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; margin-left: 6px; }
|
|
44
|
+
.tag-metrics { color: var(--accent); background: #1f6feb1a; border: 1px solid #1f6feb33; }
|
|
45
|
+
.tag-logs { color: var(--purple); background: #8957e51a; border: 1px solid #8957e533; }
|
|
46
|
+
.tag-type { color: var(--text2); background: var(--surface2); border: 1px solid var(--border); }
|
|
47
|
+
.tag-latency { color: var(--text2); font-size: 11px; }
|
|
48
|
+
.service-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px; }
|
|
49
|
+
.service-row .name { font-weight: 600; font-size: 14px; }
|
|
50
|
+
.btn { padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); transition: all 0.15s; }
|
|
51
|
+
.btn-primary { background: #238636; border-color: #238636; color: #fff; } .btn-primary:hover { background: #2ea043; }
|
|
52
|
+
.btn-ghost { background: none; color: var(--text2); } .btn-ghost:hover { background: var(--surface2); color: var(--text); }
|
|
53
|
+
.btn-danger { background: none; color: var(--red); border-color: transparent; } .btn-danger:hover { background: #f851491a; }
|
|
54
|
+
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
55
|
+
.btn-icon { background: none; border: none; color: var(--text2); cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 16px; } .btn-icon:hover { background: var(--surface2); color: var(--text); }
|
|
56
|
+
.toggle { position: relative; width: 36px; height: 20px; cursor: pointer; }
|
|
57
|
+
.toggle input { display: none; }
|
|
58
|
+
.toggle .slider { position: absolute; inset: 0; background: var(--border); border-radius: 10px; transition: 0.2s; }
|
|
59
|
+
.toggle .slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; background: var(--text); border-radius: 50%; transition: 0.2s; }
|
|
60
|
+
.toggle input:checked + .slider { background: var(--green); }
|
|
61
|
+
.toggle input:checked + .slider::before { transform: translateX(16px); }
|
|
62
|
+
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 100; align-items: center; justify-content: center; }
|
|
63
|
+
.modal-overlay.open { display: flex; }
|
|
64
|
+
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; width: 520px; max-width: 90vw; max-height: 90vh; overflow-y: auto; }
|
|
65
|
+
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
66
|
+
.modal-header h3 { font-size: 16px; font-weight: 600; }
|
|
67
|
+
.modal-body { padding: 20px; }
|
|
68
|
+
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 16px 20px; border-top: 1px solid var(--border); }
|
|
69
|
+
.form-group { margin-bottom: 16px; }
|
|
70
|
+
.form-group label { display: block; font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 6px; }
|
|
71
|
+
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; font-family: inherit; }
|
|
72
|
+
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--accent); }
|
|
73
|
+
.form-group select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238b949e'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; }
|
|
74
|
+
.form-group textarea { resize: vertical; min-height: 80px; font-family: monospace; font-size: 13px; }
|
|
75
|
+
.form-hint { font-size: 11px; color: var(--text2); margin-top: 4px; }
|
|
76
|
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
77
|
+
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
|
78
|
+
.test-result { padding: 10px 14px; border-radius: 6px; font-size: 13px; margin-top: 12px; display: none; }
|
|
79
|
+
.test-result.show { display: block; }
|
|
80
|
+
.test-result.success { background: #238636aa; border: 1px solid var(--green); }
|
|
81
|
+
.test-result.failure { background: #f851491a; border: 1px solid var(--red); color: var(--red); }
|
|
82
|
+
.endpoint-bar { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 16px; font-family: monospace; font-size: 13px; color: var(--accent); margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; }
|
|
83
|
+
.empty { color: var(--text2); text-align: center; padding: 32px; }
|
|
84
|
+
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; }
|
|
85
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
86
|
+
/* Tabs inside settings */
|
|
87
|
+
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
|
|
88
|
+
.tab-btn { background: none; border: none; border-bottom: 2px solid transparent; color: var(--text2); padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
89
|
+
.tab-btn:hover { color: var(--text); }
|
|
90
|
+
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
91
|
+
.tab-content { display: none; } .tab-content.active { display: block; }
|
|
92
|
+
/* Threshold cards */
|
|
93
|
+
.threshold-group { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
|
94
|
+
.threshold-card { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 14px; }
|
|
95
|
+
.threshold-card h4 { font-size: 13px; font-weight: 600; margin-bottom: 10px; }
|
|
96
|
+
/* Metric table */
|
|
97
|
+
.metric-table { width: 100%; border-collapse: collapse; }
|
|
98
|
+
.metric-table th { text-align: left; font-size: 12px; color: var(--text2); font-weight: 500; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
99
|
+
.metric-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
100
|
+
.metric-table .query { font-family: monospace; font-size: 11px; color: var(--text2); max-width: 400px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
101
|
+
.metric-table tr:hover td { background: var(--surface2); }
|
|
102
|
+
.toast { position: fixed; bottom: 24px; right: 24px; background: var(--green); color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 13px; font-weight: 500; opacity: 0; transition: opacity 0.3s; pointer-events: none; z-index: 200; }
|
|
103
|
+
.toast.show { opacity: 1; }
|
|
104
|
+
/* Health cards */
|
|
105
|
+
.health-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; }
|
|
106
|
+
.health-card .hc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
|
107
|
+
.health-card .hc-name { font-size: 16px; font-weight: 600; }
|
|
108
|
+
.health-card .hc-score { font-size: 28px; font-weight: 700; }
|
|
109
|
+
.health-card .hc-score.healthy { color: var(--green); }
|
|
110
|
+
.health-card .hc-score.degraded { color: var(--yellow); }
|
|
111
|
+
.health-card .hc-score.critical { color: var(--red); }
|
|
112
|
+
.health-card .hc-status { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
|
113
|
+
.hc-status.healthy { background: #23863633; color: var(--green); }
|
|
114
|
+
.hc-status.degraded { background: #d2992233; color: var(--yellow); }
|
|
115
|
+
.hc-status.critical { background: #f8514933; color: var(--red); }
|
|
116
|
+
.hc-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
|
|
117
|
+
.hc-metric { background: var(--bg); border-radius: 6px; padding: 8px 10px; }
|
|
118
|
+
.hc-metric .label { font-size: 11px; color: var(--text2); }
|
|
119
|
+
.hc-metric .val { font-size: 15px; font-weight: 600; margin-top: 2px; }
|
|
120
|
+
.hc-anomalies { margin-top: 12px; }
|
|
121
|
+
.hc-anomaly { background: #f851491a; border: 1px solid #f8514933; border-radius: 6px; padding: 8px 10px; margin-top: 6px; font-size: 12px; color: var(--red); }
|
|
122
|
+
.hc-correlation { background: #d2a8ff1a; border: 1px solid #8957e533; border-radius: 6px; padding: 8px 10px; margin-top: 6px; font-size: 12px; color: var(--purple); }
|
|
123
|
+
@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
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<div class="header">
|
|
128
|
+
<h1>Observability MCP</h1>
|
|
129
|
+
<span class="badge badge-ok" id="status-badge">Loading...</span>
|
|
130
|
+
<div class="nav">
|
|
131
|
+
<button class="nav-btn active" data-page="dashboard" onclick="showPage('dashboard')">Dashboard</button>
|
|
132
|
+
<button class="nav-btn" data-page="sources" onclick="showPage('sources')">Sources</button>
|
|
133
|
+
<button class="nav-btn" data-page="services" onclick="showPage('services')">Services</button>
|
|
134
|
+
<button class="nav-btn" data-page="health" onclick="showPage('health')">Health</button>
|
|
135
|
+
<button class="nav-btn" data-page="settings" onclick="showPage('settings')">Settings</button>
|
|
136
|
+
</div>
|
|
137
|
+
<div style="flex:1"></div>
|
|
138
|
+
<button class="btn btn-ghost btn-sm" onclick="refresh()">Refresh</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="container">
|
|
142
|
+
<!-- ===== Dashboard ===== -->
|
|
143
|
+
<div class="page active" id="page-dashboard">
|
|
144
|
+
<div class="stats">
|
|
145
|
+
<div class="stat-card"><div class="value" id="stat-sources">-</div><div class="label">Data Sources</div></div>
|
|
146
|
+
<div class="stat-card"><div class="value" id="stat-sources-up">-</div><div class="label">Sources UP</div></div>
|
|
147
|
+
<div class="stat-card"><div class="value" id="stat-services">-</div><div class="label">Services</div></div>
|
|
148
|
+
<div class="stat-card"><div class="value" id="stat-tools">6</div><div class="label">MCP Tools</div></div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="endpoint-bar">
|
|
151
|
+
<span>MCP Endpoint: <strong id="mcp-url">http://localhost:3000/mcp</strong></span>
|
|
152
|
+
<button class="btn btn-ghost btn-sm" onclick="navigator.clipboard.writeText(document.getElementById('mcp-url').textContent);toast('Copied!')">Copy</button>
|
|
153
|
+
</div>
|
|
154
|
+
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
155
|
+
<div class="card"><div class="card-header"><h2>Sources</h2><button class="btn btn-primary btn-sm" onclick="showPage('sources');openAddModal()">+ Add Source</button></div><div id="dash-sources"><div class="empty">Loading...</div></div></div>
|
|
156
|
+
<div class="card"><div class="card-header"><h2>Services</h2></div><div id="dash-services"><div class="empty">Loading...</div></div></div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- ===== Sources ===== -->
|
|
161
|
+
<div class="page" id="page-sources">
|
|
162
|
+
<div class="card">
|
|
163
|
+
<div class="card-header"><h2>Data Sources</h2><button class="btn btn-primary btn-sm" onclick="openAddModal()">+ Add Source</button></div>
|
|
164
|
+
<div id="sources-list"><div class="empty">Loading...</div></div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- ===== Services ===== -->
|
|
169
|
+
<div class="page" id="page-services">
|
|
170
|
+
<div class="card"><div class="card-header"><h2>Discovered Services</h2></div><div id="services-list"><div class="empty">Loading...</div></div></div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- ===== Health ===== -->
|
|
174
|
+
<div class="page" id="page-health">
|
|
175
|
+
<div id="health-cards" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px;">
|
|
176
|
+
<div class="empty">Loading health data...</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- ===== Settings ===== -->
|
|
181
|
+
<div class="page" id="page-settings">
|
|
182
|
+
<div class="card" style="padding:0;">
|
|
183
|
+
<div class="tabs">
|
|
184
|
+
<button class="tab-btn active" onclick="showTab('general')">General</button>
|
|
185
|
+
<button class="tab-btn" onclick="showTab('health')">Health Scoring</button>
|
|
186
|
+
<button class="tab-btn" onclick="showTab('metrics')">Custom Metrics</button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- General Tab -->
|
|
190
|
+
<div class="tab-content active" id="tab-general" style="padding:20px;">
|
|
191
|
+
<div class="form-row">
|
|
192
|
+
<div class="form-group">
|
|
193
|
+
<label>Check Interval (ms)</label>
|
|
194
|
+
<input type="number" id="set-interval" min="5000" step="1000">
|
|
195
|
+
<div class="form-hint">How often the agent scans for anomalies</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="form-group">
|
|
198
|
+
<label>Default Sensitivity</label>
|
|
199
|
+
<select id="set-sensitivity">
|
|
200
|
+
<option value="low">Low (>3 sigma, major only)</option>
|
|
201
|
+
<option value="medium">Medium (>2 sigma)</option>
|
|
202
|
+
<option value="high">High (>1.5 sigma, subtle)</option>
|
|
203
|
+
</select>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div style="display:flex; gap:8px; justify-content:flex-end;">
|
|
207
|
+
<button class="btn btn-ghost btn-sm" onclick="resetSettings()">Reset to Defaults</button>
|
|
208
|
+
<button class="btn btn-primary btn-sm" onclick="saveSettings()">Save Settings</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Health Scoring Tab -->
|
|
213
|
+
<div class="tab-content" id="tab-health" style="padding:20px;">
|
|
214
|
+
<p style="font-size:13px; color:var(--text2); margin-bottom:16px;">Configure how service health scores are calculated. Weights must sum to 1.0.</p>
|
|
215
|
+
<h3 style="font-size:14px; margin-bottom:12px;">Weights</h3>
|
|
216
|
+
<div class="form-row" style="margin-bottom:20px;">
|
|
217
|
+
<div class="form-group"><label>Error Rate</label><input type="number" id="hw-errorRate" min="0" max="1" step="0.05"></div>
|
|
218
|
+
<div class="form-group"><label>Latency</label><input type="number" id="hw-latency" min="0" max="1" step="0.05"></div>
|
|
219
|
+
<div class="form-group"><label>CPU</label><input type="number" id="hw-cpu" min="0" max="1" step="0.05"></div>
|
|
220
|
+
<div class="form-group"><label>Log Errors</label><input type="number" id="hw-logErrors" min="0" max="1" step="0.05"></div>
|
|
221
|
+
</div>
|
|
222
|
+
<h3 style="font-size:14px; margin-bottom:12px;">Thresholds</h3>
|
|
223
|
+
<div class="threshold-group">
|
|
224
|
+
<div class="threshold-card">
|
|
225
|
+
<h4>CPU (%)</h4>
|
|
226
|
+
<div class="form-row-3">
|
|
227
|
+
<div class="form-group"><label>Good ≤</label><input type="number" id="ht-cpu-good"></div>
|
|
228
|
+
<div class="form-group"><label>Warn ≤</label><input type="number" id="ht-cpu-warn"></div>
|
|
229
|
+
<div class="form-group"><label>Crit ≤</label><input type="number" id="ht-cpu-crit"></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="threshold-card">
|
|
233
|
+
<h4>Error Rate (req/s)</h4>
|
|
234
|
+
<div class="form-row-3">
|
|
235
|
+
<div class="form-group"><label>Good ≤</label><input type="number" id="ht-err-good" step="0.01"></div>
|
|
236
|
+
<div class="form-group"><label>Warn ≤</label><input type="number" id="ht-err-warn" step="0.01"></div>
|
|
237
|
+
<div class="form-group"><label>Crit ≤</label><input type="number" id="ht-err-crit" step="0.01"></div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="threshold-card">
|
|
241
|
+
<h4>Latency P99 (seconds)</h4>
|
|
242
|
+
<div class="form-row-3">
|
|
243
|
+
<div class="form-group"><label>Good ≤</label><input type="number" id="ht-lat-good" step="0.1"></div>
|
|
244
|
+
<div class="form-group"><label>Warn ≤</label><input type="number" id="ht-lat-warn" step="0.1"></div>
|
|
245
|
+
<div class="form-group"><label>Crit ≤</label><input type="number" id="ht-lat-crit" step="0.1"></div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="threshold-card">
|
|
249
|
+
<h4>Log Errors (/min)</h4>
|
|
250
|
+
<div class="form-row-3">
|
|
251
|
+
<div class="form-group"><label>Good ≤</label><input type="number" id="ht-log-good"></div>
|
|
252
|
+
<div class="form-group"><label>Warn ≤</label><input type="number" id="ht-log-warn"></div>
|
|
253
|
+
<div class="form-group"><label>Crit ≤</label><input type="number" id="ht-log-crit"></div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
<h3 style="font-size:14px; margin-bottom:12px;">Status Boundaries</h3>
|
|
258
|
+
<div class="form-row" style="margin-bottom:20px;">
|
|
259
|
+
<div class="form-group"><label>Healthy above score</label><input type="number" id="ht-bound-healthy" min="0" max="100"></div>
|
|
260
|
+
<div class="form-group"><label>Degraded above score</label><input type="number" id="ht-bound-degraded" min="0" max="100"></div>
|
|
261
|
+
</div>
|
|
262
|
+
<div style="display:flex; gap:8px; justify-content:flex-end;">
|
|
263
|
+
<button class="btn btn-ghost btn-sm" onclick="resetHealth()">Reset to Defaults</button>
|
|
264
|
+
<button class="btn btn-primary btn-sm" onclick="saveHealth()">Save Thresholds</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<!-- Source Metrics Tab -->
|
|
269
|
+
<div class="tab-content" id="tab-metrics" style="padding:20px;">
|
|
270
|
+
<p style="font-size:13px; color:var(--text2); margin-bottom:16px;">Each data source has its own metric definitions with backend-specific queries (PromQL, LogQL, etc.). Use <code style="background:var(--bg);padding:1px 5px;border-radius:3px;">{{service}}</code> as placeholder for the service/job name.</p>
|
|
271
|
+
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
|
|
272
|
+
<label style="font-size:13px; color:var(--text2); white-space:nowrap;">Source:</label>
|
|
273
|
+
<select id="metrics-source-select" onchange="loadSourceMetrics(this.value)" style="flex:1; padding:8px 12px; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:14px; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238b949e'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 12px center;"></select>
|
|
274
|
+
<button class="btn btn-primary btn-sm" onclick="openMetricModal()">+ Add Metric</button>
|
|
275
|
+
<button class="btn btn-ghost btn-sm" onclick="resetSourceMetrics()">Reset to Defaults</button>
|
|
276
|
+
</div>
|
|
277
|
+
<table class="metric-table">
|
|
278
|
+
<thead><tr><th>Name</th><th>Unit</th><th>Query</th><th>Description</th><th></th></tr></thead>
|
|
279
|
+
<tbody id="metrics-tbody"><tr><td colspan="5" class="empty">Select a source above</td></tr></tbody>
|
|
280
|
+
</table>
|
|
281
|
+
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:16px;">
|
|
282
|
+
<button class="btn btn-primary btn-sm" onclick="saveSourceMetrics()">Save Metrics</button>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- Source Modal -->
|
|
290
|
+
<div class="modal-overlay" id="source-modal">
|
|
291
|
+
<div class="modal">
|
|
292
|
+
<div class="modal-header"><h3 id="modal-title">Add Source</h3><button class="btn-icon" onclick="closeModal('source-modal')">×</button></div>
|
|
293
|
+
<div class="modal-body">
|
|
294
|
+
<input type="hidden" id="modal-mode" value="add"><input type="hidden" id="modal-original-name" value="">
|
|
295
|
+
<div class="form-group"><label>Name</label><input type="text" id="src-name" placeholder="e.g. prometheus-prod"><div class="form-hint">Unique identifier</div></div>
|
|
296
|
+
<div class="form-group"><label>Type</label><select id="src-type"></select></div>
|
|
297
|
+
<div class="form-group"><label>URL</label><input type="text" id="src-url" placeholder="e.g. http://prometheus:9090"></div>
|
|
298
|
+
<div class="form-group"><label>Authentication</label><select id="src-auth-type" onchange="toggleAuthFields()"><option value="none">None</option><option value="basic">Basic Auth</option><option value="bearer">Bearer Token</option></select></div>
|
|
299
|
+
<div id="auth-basic-fields" style="display:none">
|
|
300
|
+
<div class="form-group"><label>Username</label><input type="text" id="src-auth-username" placeholder="username"></div>
|
|
301
|
+
<div class="form-group"><label>Password</label><input type="password" id="src-auth-password" placeholder="password"></div>
|
|
302
|
+
</div>
|
|
303
|
+
<div id="auth-bearer-fields" style="display:none">
|
|
304
|
+
<div class="form-group"><label>Token</label><input type="password" id="src-auth-token" placeholder="Bearer token"></div>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="form-group" style="display:flex;align-items:center;gap:10px;"><label style="margin:0">Skip TLS Verify</label><label class="toggle"><input type="checkbox" id="src-tls-skip"><span class="slider"></span></label><span class="form-hint" style="margin:0;font-size:11px">Accept any certificate (insecure)</span></div>
|
|
307
|
+
<div class="form-group"><label>CA Certificate Path</label><input type="text" id="src-tls-ca" placeholder="/path/to/ca.pem"><div class="form-hint">Custom CA for self-signed certs (safer than skip verify)</div></div>
|
|
308
|
+
<div class="form-group"><label>Client Certificate Path</label><input type="text" id="src-tls-cert" placeholder="/path/to/client.pem"><div class="form-hint">For mutual TLS (mTLS)</div></div>
|
|
309
|
+
<div class="form-group"><label>Client Key Path</label><input type="text" id="src-tls-key" placeholder="/path/to/client-key.pem"></div>
|
|
310
|
+
<div class="form-group" style="display:flex;align-items:center;gap:10px;"><label style="margin:0">Enabled</label><label class="toggle"><input type="checkbox" id="src-enabled" checked><span class="slider"></span></label></div>
|
|
311
|
+
<div class="test-result" id="test-result"></div>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="modal-footer">
|
|
314
|
+
<button class="btn btn-ghost" id="test-btn" onclick="testConnection()">Test Connection</button>
|
|
315
|
+
<button class="btn btn-ghost" onclick="closeModal('source-modal')">Cancel</button>
|
|
316
|
+
<button class="btn btn-primary" id="save-btn" onclick="saveSource()">Save</button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<!-- Delete Confirm Modal -->
|
|
322
|
+
<div class="modal-overlay" id="delete-modal">
|
|
323
|
+
<div class="modal" style="width:380px;">
|
|
324
|
+
<div class="modal-header"><h3>Delete</h3><button class="btn-icon" onclick="closeModal('delete-modal')">×</button></div>
|
|
325
|
+
<div class="modal-body"><p>Are you sure you want to delete <strong id="delete-name"></strong>?</p></div>
|
|
326
|
+
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('delete-modal')">Cancel</button><button class="btn btn-danger" onclick="confirmDelete()">Delete</button></div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<!-- Metric Modal -->
|
|
331
|
+
<div class="modal-overlay" id="metric-modal">
|
|
332
|
+
<div class="modal">
|
|
333
|
+
<div class="modal-header"><h3 id="metric-modal-title">Add Metric</h3><button class="btn-icon" onclick="closeModal('metric-modal')">×</button></div>
|
|
334
|
+
<div class="modal-body">
|
|
335
|
+
<input type="hidden" id="metric-mode" value="add"><input type="hidden" id="metric-original-name" value="">
|
|
336
|
+
<div class="form-group"><label>Name</label><input type="text" id="met-name" placeholder="e.g. queue_depth"><div class="form-hint">Friendly name used in MCP tool calls</div></div>
|
|
337
|
+
<div class="form-group"><label>Query</label><textarea id="met-query" rows="3" placeholder='e.g. my_queue_depth{job="{{service}}"}'></textarea><div class="form-hint">Backend-specific query. Use {{service}} as the service/job placeholder.</div></div>
|
|
338
|
+
<div class="form-row">
|
|
339
|
+
<div class="form-group"><label>Unit</label><input type="text" id="met-unit" placeholder="e.g. messages, percent, bytes"></div>
|
|
340
|
+
<div class="form-group"><label>Description</label><input type="text" id="met-desc" placeholder="What this metric measures"></div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="modal-footer">
|
|
344
|
+
<button class="btn btn-ghost" onclick="closeModal('metric-modal')">Cancel</button>
|
|
345
|
+
<button class="btn btn-primary" onclick="saveMetric()">Save Metric</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div class="toast" id="toast-el"></div>
|
|
351
|
+
|
|
352
|
+
<script>
|
|
353
|
+
let sourcesData=[], servicesData=[], supportedTypes=[], settings={}, healthThresholds={}, defaults={};
|
|
354
|
+
let deleteTarget=null, deleteType=null;
|
|
355
|
+
// Per-source metrics state
|
|
356
|
+
let selectedMetricsSource='', sourceMetrics=[], sourceMetricDefaults=[];
|
|
357
|
+
|
|
358
|
+
function toast(msg) { const t=document.getElementById('toast-el'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2000); }
|
|
359
|
+
|
|
360
|
+
// --- Nav ---
|
|
361
|
+
function showPage(name) {
|
|
362
|
+
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
|
363
|
+
document.querySelectorAll('.nav-btn').forEach(b=>b.classList.remove('active'));
|
|
364
|
+
document.getElementById('page-'+name).classList.add('active');
|
|
365
|
+
document.querySelector(`.nav-btn[data-page="${name}"]`).classList.add('active');
|
|
366
|
+
if(name==='settings') loadSettingsData();
|
|
367
|
+
if(name==='health') loadHealthData();
|
|
368
|
+
}
|
|
369
|
+
function showTab(name) {
|
|
370
|
+
document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
|
|
371
|
+
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
|
372
|
+
document.getElementById('tab-'+name).classList.add('active');
|
|
373
|
+
event.target.classList.add('active');
|
|
374
|
+
if(name==='metrics') populateMetricsSourceSelect();
|
|
375
|
+
}
|
|
376
|
+
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
|
377
|
+
|
|
378
|
+
// --- Data Loading ---
|
|
379
|
+
async function loadSources() { try { sourcesData=await(await fetch('/api/sources')).json(); renderSources(); updateStats(); } catch(e){} }
|
|
380
|
+
async function loadServices() { try { const d=await(await fetch('/api/services')).json(); servicesData=d.services||[]; renderServices(); updateStats(); } catch(e){} }
|
|
381
|
+
async function loadTypes() { try { supportedTypes=await(await fetch('/api/source-types')).json(); } catch(e){ supportedTypes=['prometheus','loki']; } }
|
|
382
|
+
async function loadSettingsData() {
|
|
383
|
+
try { [settings, healthThresholds, defaults] = await Promise.all([
|
|
384
|
+
(await fetch('/api/settings')).json(), (await fetch('/api/health-thresholds')).json(),
|
|
385
|
+
(await fetch('/api/settings/defaults')).json()
|
|
386
|
+
]); populateSettingsForm(); populateHealthForm(); populateMetricsSourceSelect(); } catch(e){ console.error(e); }
|
|
387
|
+
}
|
|
388
|
+
function updateStats() {
|
|
389
|
+
document.getElementById('stat-sources').textContent=sourcesData.length;
|
|
390
|
+
document.getElementById('stat-sources-up').textContent=sourcesData.filter(s=>s.status==='up').length;
|
|
391
|
+
document.getElementById('stat-services').textContent=servicesData.length;
|
|
392
|
+
const allUp=sourcesData.length>0&&sourcesData.filter(s=>s.enabled).every(s=>s.status==='up');
|
|
393
|
+
const b=document.getElementById('status-badge');
|
|
394
|
+
b.textContent=sourcesData.length===0?'No sources':allUp?'All systems operational':'Issues detected';
|
|
395
|
+
b.className='badge '+(allUp?'badge-ok':'badge-err');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// --- Render Sources ---
|
|
399
|
+
function renderSources() {
|
|
400
|
+
const html = sourcesData.length===0 ? '<div class="empty">No sources configured.</div>' : sourcesData.map(s=>{
|
|
401
|
+
const sc=!s.enabled?'dot-disabled':s.status==='up'?'dot-up':'dot-down';
|
|
402
|
+
return `<div class="source-row"><div class="dot ${sc}"></div><div class="source-info"><div class="name">${esc(s.name)}<span class="tag tag-type">${esc(s.type)}</span>${s.signalType?`<span class="tag tag-${s.signalType}">${s.signalType}</span>`:''}</div><div class="url">${esc(s.url)}</div></div>${s.latencyMs?`<span class="tag-latency">${s.latencyMs}ms</span>`:''}<div class="source-actions"><label class="toggle"><input type="checkbox" ${s.enabled?'checked':''} onchange="toggleSource('${esc(s.name)}')"><span class="slider"></span></label><button class="btn-icon" onclick="openEditModal('${esc(s.name)}')">✎</button><button class="btn-icon" onclick="openDeleteConfirm('source','${esc(s.name)}')">🗑</button></div></div>`;
|
|
403
|
+
}).join('');
|
|
404
|
+
document.getElementById('sources-list').innerHTML=html;
|
|
405
|
+
document.getElementById('dash-sources').innerHTML=html;
|
|
406
|
+
}
|
|
407
|
+
function renderServices() {
|
|
408
|
+
const html = servicesData.length===0 ? '<div class="empty">No services discovered.</div>' : servicesData.map(s=>`<div class="service-row"><div><span class="name">${esc(s.name)}</span>${s.signalTypes.map(t=>`<span class="tag tag-${t}">${t}</span>`).join('')}</div><span style="color:var(--text2);font-size:12px">${s.sources.join(', ')}</span></div>`).join('');
|
|
409
|
+
document.getElementById('services-list').innerHTML=html;
|
|
410
|
+
document.getElementById('dash-services').innerHTML=html;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- TLS helpers ---
|
|
414
|
+
function resetTlsFields() {
|
|
415
|
+
document.getElementById('src-tls-skip').checked=false;
|
|
416
|
+
document.getElementById('src-tls-ca').value='';
|
|
417
|
+
document.getElementById('src-tls-cert').value='';
|
|
418
|
+
document.getElementById('src-tls-key').value='';
|
|
419
|
+
}
|
|
420
|
+
function getTlsFromForm() {
|
|
421
|
+
const skip=document.getElementById('src-tls-skip').checked;
|
|
422
|
+
const ca=document.getElementById('src-tls-ca').value.trim();
|
|
423
|
+
const cert=document.getElementById('src-tls-cert').value.trim();
|
|
424
|
+
const key=document.getElementById('src-tls-key').value.trim();
|
|
425
|
+
if(!skip&&!ca&&!cert) return undefined;
|
|
426
|
+
const tls={};
|
|
427
|
+
if(skip) tls.skipVerify=true;
|
|
428
|
+
if(ca) tls.caCert=ca;
|
|
429
|
+
if(cert) tls.clientCert=cert;
|
|
430
|
+
if(key) tls.clientKey=key;
|
|
431
|
+
return tls;
|
|
432
|
+
}
|
|
433
|
+
function setTlsInForm(tls) {
|
|
434
|
+
resetTlsFields();
|
|
435
|
+
if(!tls) return;
|
|
436
|
+
document.getElementById('src-tls-skip').checked=tls.skipVerify||false;
|
|
437
|
+
document.getElementById('src-tls-ca').value=tls.caCert||'';
|
|
438
|
+
document.getElementById('src-tls-cert').value=tls.clientCert||'';
|
|
439
|
+
document.getElementById('src-tls-key').value=tls.clientKey||'';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- Source CRUD ---
|
|
443
|
+
function toggleAuthFields() {
|
|
444
|
+
const t=document.getElementById('src-auth-type').value;
|
|
445
|
+
document.getElementById('auth-basic-fields').style.display=t==='basic'?'block':'none';
|
|
446
|
+
document.getElementById('auth-bearer-fields').style.display=t==='bearer'?'block':'none';
|
|
447
|
+
}
|
|
448
|
+
function resetAuthFields() {
|
|
449
|
+
document.getElementById('src-auth-type').value='none';
|
|
450
|
+
document.getElementById('src-auth-username').value='';
|
|
451
|
+
document.getElementById('src-auth-password').value='';
|
|
452
|
+
document.getElementById('src-auth-token').value='';
|
|
453
|
+
toggleAuthFields();
|
|
454
|
+
}
|
|
455
|
+
function getAuthFromForm() {
|
|
456
|
+
const t=document.getElementById('src-auth-type').value;
|
|
457
|
+
if(t==='none') return undefined;
|
|
458
|
+
if(t==='basic') return {type:'basic',username:document.getElementById('src-auth-username').value,password:document.getElementById('src-auth-password').value};
|
|
459
|
+
if(t==='bearer') return {type:'bearer',token:document.getElementById('src-auth-token').value};
|
|
460
|
+
}
|
|
461
|
+
function setAuthInForm(auth) {
|
|
462
|
+
if(!auth||auth.type==='none') { resetAuthFields(); return; }
|
|
463
|
+
document.getElementById('src-auth-type').value=auth.type;
|
|
464
|
+
if(auth.type==='basic') { document.getElementById('src-auth-username').value=auth.username||''; document.getElementById('src-auth-password').value=auth.password||''; }
|
|
465
|
+
if(auth.type==='bearer') { document.getElementById('src-auth-token').value=auth.token||''; }
|
|
466
|
+
toggleAuthFields();
|
|
467
|
+
}
|
|
468
|
+
function openAddModal() {
|
|
469
|
+
document.getElementById('modal-title').textContent='Add Source'; document.getElementById('modal-mode').value='add';
|
|
470
|
+
document.getElementById('modal-original-name').value=''; document.getElementById('src-name').value='';
|
|
471
|
+
document.getElementById('src-url').value=''; document.getElementById('src-enabled').checked=true;
|
|
472
|
+
resetTlsFields();
|
|
473
|
+
document.getElementById('src-name').disabled=false; resetAuthFields(); hideTestResult();
|
|
474
|
+
const sel=document.getElementById('src-type'); sel.innerHTML=supportedTypes.map(t=>`<option value="${t}">${t}</option>`).join('');
|
|
475
|
+
document.getElementById('source-modal').classList.add('open');
|
|
476
|
+
}
|
|
477
|
+
function openEditModal(name) {
|
|
478
|
+
const s=sourcesData.find(x=>x.name===name); if(!s)return;
|
|
479
|
+
document.getElementById('modal-title').textContent='Edit Source'; document.getElementById('modal-mode').value='edit';
|
|
480
|
+
document.getElementById('modal-original-name').value=name; document.getElementById('src-name').value=s.name;
|
|
481
|
+
document.getElementById('src-name').disabled=true; document.getElementById('src-url').value=s.url;
|
|
482
|
+
document.getElementById('src-enabled').checked=s.enabled; setTlsInForm(s.tls); setAuthInForm(s.auth); hideTestResult();
|
|
483
|
+
const sel=document.getElementById('src-type'); sel.innerHTML=supportedTypes.map(t=>`<option value="${t}" ${t===s.type?'selected':''}>${t}</option>`).join('');
|
|
484
|
+
document.getElementById('source-modal').classList.add('open');
|
|
485
|
+
}
|
|
486
|
+
async function saveSource() {
|
|
487
|
+
const mode=document.getElementById('modal-mode').value;
|
|
488
|
+
const src={name:document.getElementById('src-name').value.trim(),type:document.getElementById('src-type').value,url:document.getElementById('src-url').value.trim(),enabled:document.getElementById('src-enabled').checked,auth:getAuthFromForm(),tls:getTlsFromForm()};
|
|
489
|
+
if(!src.name||!src.url){alert('Name and URL are required');return;}
|
|
490
|
+
const btn=document.getElementById('save-btn'); btn.disabled=true; btn.textContent='Saving...';
|
|
491
|
+
try {
|
|
492
|
+
let res; if(mode==='add') res=await fetch('/api/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(src)});
|
|
493
|
+
else res=await fetch('/api/sources/'+encodeURIComponent(document.getElementById('modal-original-name').value),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(src)});
|
|
494
|
+
const d=await res.json(); if(!res.ok){alert(d.error);return;} closeModal('source-modal'); toast('Source saved'); await refresh();
|
|
495
|
+
} catch(e){alert(e);} finally{btn.disabled=false;btn.textContent='Save';}
|
|
496
|
+
}
|
|
497
|
+
async function testConnection() {
|
|
498
|
+
const btn=document.getElementById('test-btn'),r=document.getElementById('test-result');
|
|
499
|
+
btn.disabled=true;btn.innerHTML='<span class="spinner"></span> Testing...';r.style.display='none';
|
|
500
|
+
try{const res=await fetch('/api/sources/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:'test',type:document.getElementById('src-type').value,url:document.getElementById('src-url').value,auth:getAuthFromForm(),tls:getTlsFromForm()})});
|
|
501
|
+
const d=await res.json();r.style.display='block';r.className='test-result show '+(d.status==='up'?'success':'failure');r.textContent=d.status==='up'?`Connected (${d.latencyMs}ms)`:`Failed: ${d.message}`;
|
|
502
|
+
}catch(e){r.style.display='block';r.className='test-result show failure';r.textContent='Error: '+e;}
|
|
503
|
+
finally{btn.disabled=false;btn.textContent='Test Connection';}
|
|
504
|
+
}
|
|
505
|
+
function hideTestResult(){const r=document.getElementById('test-result');r.className='test-result';r.style.display='none';}
|
|
506
|
+
async function toggleSource(name){await fetch('/api/sources/'+encodeURIComponent(name)+'/toggle',{method:'PATCH'});toast('Toggled');await refresh();}
|
|
507
|
+
|
|
508
|
+
// --- Delete ---
|
|
509
|
+
function openDeleteConfirm(type,name){deleteType=type;deleteTarget=name;document.getElementById('delete-name').textContent=name;document.getElementById('delete-modal').classList.add('open');}
|
|
510
|
+
async function confirmDelete(){
|
|
511
|
+
if(!deleteTarget)return;
|
|
512
|
+
if(deleteType==='source') await fetch('/api/sources/'+encodeURIComponent(deleteTarget),{method:'DELETE'});
|
|
513
|
+
else if(deleteType==='metric') { sourceMetrics=sourceMetrics.filter(m=>m.name!==deleteTarget); renderMetricsTable(); }
|
|
514
|
+
closeModal('delete-modal'); toast('Deleted');
|
|
515
|
+
if(deleteType==='source') await refresh();
|
|
516
|
+
deleteTarget=null;deleteType=null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// --- Settings: General ---
|
|
520
|
+
function populateSettingsForm() {
|
|
521
|
+
document.getElementById('set-interval').value=settings.checkIntervalMs||30000;
|
|
522
|
+
document.getElementById('set-sensitivity').value=settings.defaultSensitivity||'medium';
|
|
523
|
+
}
|
|
524
|
+
async function saveSettings() {
|
|
525
|
+
const body={checkIntervalMs:parseInt(document.getElementById('set-interval').value),defaultSensitivity:document.getElementById('set-sensitivity').value};
|
|
526
|
+
await fetch('/api/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
527
|
+
toast('Settings saved');
|
|
528
|
+
}
|
|
529
|
+
async function resetSettings() { settings=defaults.settings; populateSettingsForm(); toast('Reset to defaults (save to persist)'); }
|
|
530
|
+
|
|
531
|
+
// --- Settings: Health ---
|
|
532
|
+
function populateHealthForm() {
|
|
533
|
+
const h=healthThresholds; if(!h.weights)return;
|
|
534
|
+
document.getElementById('hw-errorRate').value=h.weights.errorRate;
|
|
535
|
+
document.getElementById('hw-latency').value=h.weights.latency;
|
|
536
|
+
document.getElementById('hw-cpu').value=h.weights.cpu;
|
|
537
|
+
document.getElementById('hw-logErrors').value=h.weights.logErrors;
|
|
538
|
+
document.getElementById('ht-cpu-good').value=h.cpu.good; document.getElementById('ht-cpu-warn').value=h.cpu.warn; document.getElementById('ht-cpu-crit').value=h.cpu.crit;
|
|
539
|
+
document.getElementById('ht-err-good').value=h.errorRate.good; document.getElementById('ht-err-warn').value=h.errorRate.warn; document.getElementById('ht-err-crit').value=h.errorRate.crit;
|
|
540
|
+
document.getElementById('ht-lat-good').value=h.latencyP99.good; document.getElementById('ht-lat-warn').value=h.latencyP99.warn; document.getElementById('ht-lat-crit').value=h.latencyP99.crit;
|
|
541
|
+
document.getElementById('ht-log-good').value=h.logErrors.good; document.getElementById('ht-log-warn').value=h.logErrors.warn; document.getElementById('ht-log-crit').value=h.logErrors.crit;
|
|
542
|
+
document.getElementById('ht-bound-healthy').value=h.statusBoundaries.healthy; document.getElementById('ht-bound-degraded').value=h.statusBoundaries.degraded;
|
|
543
|
+
}
|
|
544
|
+
async function saveHealth() {
|
|
545
|
+
const body={
|
|
546
|
+
weights:{errorRate:parseFloat(document.getElementById('hw-errorRate').value),latency:parseFloat(document.getElementById('hw-latency').value),cpu:parseFloat(document.getElementById('hw-cpu').value),logErrors:parseFloat(document.getElementById('hw-logErrors').value)},
|
|
547
|
+
cpu:{good:parseFloat(document.getElementById('ht-cpu-good').value),warn:parseFloat(document.getElementById('ht-cpu-warn').value),crit:parseFloat(document.getElementById('ht-cpu-crit').value)},
|
|
548
|
+
errorRate:{good:parseFloat(document.getElementById('ht-err-good').value),warn:parseFloat(document.getElementById('ht-err-warn').value),crit:parseFloat(document.getElementById('ht-err-crit').value)},
|
|
549
|
+
latencyP99:{good:parseFloat(document.getElementById('ht-lat-good').value),warn:parseFloat(document.getElementById('ht-lat-warn').value),crit:parseFloat(document.getElementById('ht-lat-crit').value)},
|
|
550
|
+
logErrors:{good:parseFloat(document.getElementById('ht-log-good').value),warn:parseFloat(document.getElementById('ht-log-warn').value),crit:parseFloat(document.getElementById('ht-log-crit').value)},
|
|
551
|
+
statusBoundaries:{healthy:parseFloat(document.getElementById('ht-bound-healthy').value),degraded:parseFloat(document.getElementById('ht-bound-degraded').value)}
|
|
552
|
+
};
|
|
553
|
+
const ws=body.weights; const sum=ws.errorRate+ws.latency+ws.cpu+ws.logErrors;
|
|
554
|
+
if(Math.abs(sum-1)>0.01){alert(`Weights must sum to 1.0 (currently ${sum.toFixed(2)})`);return;}
|
|
555
|
+
await fetch('/api/health-thresholds',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
556
|
+
healthThresholds=body; toast('Health thresholds saved');
|
|
557
|
+
}
|
|
558
|
+
async function resetHealth() { healthThresholds=defaults.healthThresholds; populateHealthForm(); toast('Reset to defaults (save to persist)'); }
|
|
559
|
+
|
|
560
|
+
// --- Settings: Source Metrics ---
|
|
561
|
+
function populateMetricsSourceSelect() {
|
|
562
|
+
const sel=document.getElementById('metrics-source-select');
|
|
563
|
+
const enabledSources=sourcesData.filter(s=>s.enabled);
|
|
564
|
+
if(enabledSources.length===0){sel.innerHTML='<option value="">No sources available</option>';sourceMetrics=[];renderMetricsTable();return;}
|
|
565
|
+
sel.innerHTML=enabledSources.map(s=>`<option value="${esc(s.name)}" ${s.name===selectedMetricsSource?'selected':''}>${esc(s.name)} (${esc(s.type)})</option>`).join('');
|
|
566
|
+
if(!selectedMetricsSource||!enabledSources.find(s=>s.name===selectedMetricsSource)) selectedMetricsSource=enabledSources[0].name;
|
|
567
|
+
sel.value=selectedMetricsSource;
|
|
568
|
+
loadSourceMetrics(selectedMetricsSource);
|
|
569
|
+
}
|
|
570
|
+
async function loadSourceMetrics(name) {
|
|
571
|
+
selectedMetricsSource=name;
|
|
572
|
+
try {
|
|
573
|
+
const d=await(await fetch('/api/sources/'+encodeURIComponent(name)+'/metrics')).json();
|
|
574
|
+
sourceMetrics=d.metrics||[];
|
|
575
|
+
sourceMetricDefaults=d.defaults||[];
|
|
576
|
+
renderMetricsTable();
|
|
577
|
+
} catch(e){ sourceMetrics=[]; sourceMetricDefaults=[]; renderMetricsTable(); }
|
|
578
|
+
}
|
|
579
|
+
function renderMetricsTable() {
|
|
580
|
+
const tbody=document.getElementById('metrics-tbody');
|
|
581
|
+
if(!sourceMetrics||sourceMetrics.length===0){tbody.innerHTML='<tr><td colspan="5" class="empty">No metrics defined for this source.</td></tr>';return;}
|
|
582
|
+
tbody.innerHTML=sourceMetrics.map(m=>`<tr>
|
|
583
|
+
<td><strong>${esc(m.name)}</strong></td>
|
|
584
|
+
<td><span class="tag tag-type">${esc(m.unit||'-')}</span></td>
|
|
585
|
+
<td class="query" title="${esc(m.query)}">${esc(m.query)}</td>
|
|
586
|
+
<td style="color:var(--text2)">${esc(m.description||'')}</td>
|
|
587
|
+
<td style="white-space:nowrap"><button class="btn-icon" onclick="openEditMetric('${esc(m.name)}')">✎</button><button class="btn-icon" onclick="openDeleteConfirm('metric','${esc(m.name)}')">🗑</button></td>
|
|
588
|
+
</tr>`).join('');
|
|
589
|
+
}
|
|
590
|
+
async function saveSourceMetrics() {
|
|
591
|
+
if(!selectedMetricsSource)return;
|
|
592
|
+
await fetch('/api/sources/'+encodeURIComponent(selectedMetricsSource)+'/metrics',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({metrics:sourceMetrics})});
|
|
593
|
+
toast('Metrics saved for '+selectedMetricsSource);
|
|
594
|
+
}
|
|
595
|
+
async function resetSourceMetrics() {
|
|
596
|
+
if(!selectedMetricsSource)return;
|
|
597
|
+
await fetch('/api/sources/'+encodeURIComponent(selectedMetricsSource)+'/metrics',{method:'DELETE'});
|
|
598
|
+
toast('Reset to connector defaults');
|
|
599
|
+
await loadSourceMetrics(selectedMetricsSource);
|
|
600
|
+
}
|
|
601
|
+
function openMetricModal() {
|
|
602
|
+
if(!selectedMetricsSource){alert('Select a source first');return;}
|
|
603
|
+
document.getElementById('metric-modal-title').textContent='Add Metric'; document.getElementById('metric-mode').value='add';
|
|
604
|
+
document.getElementById('metric-original-name').value='';
|
|
605
|
+
document.getElementById('met-name').value=''; document.getElementById('met-name').disabled=false;
|
|
606
|
+
document.getElementById('met-query').value=''; document.getElementById('met-unit').value=''; document.getElementById('met-desc').value='';
|
|
607
|
+
document.getElementById('metric-modal').classList.add('open');
|
|
608
|
+
}
|
|
609
|
+
function openEditMetric(name) {
|
|
610
|
+
const m=sourceMetrics.find(x=>x.name===name); if(!m)return;
|
|
611
|
+
document.getElementById('metric-modal-title').textContent='Edit Metric'; document.getElementById('metric-mode').value='edit';
|
|
612
|
+
document.getElementById('metric-original-name').value=name;
|
|
613
|
+
document.getElementById('met-name').value=m.name; document.getElementById('met-name').disabled=true;
|
|
614
|
+
document.getElementById('met-query').value=m.query; document.getElementById('met-unit').value=m.unit||''; document.getElementById('met-desc').value=m.description||'';
|
|
615
|
+
document.getElementById('metric-modal').classList.add('open');
|
|
616
|
+
}
|
|
617
|
+
function saveMetric() {
|
|
618
|
+
const mode=document.getElementById('metric-mode').value;
|
|
619
|
+
const m={name:document.getElementById('met-name').value.trim(),query:document.getElementById('met-query').value.trim(),unit:document.getElementById('met-unit').value.trim(),description:document.getElementById('met-desc').value.trim()};
|
|
620
|
+
if(!m.name||!m.query){alert('Name and Query are required');return;}
|
|
621
|
+
if(mode==='add'){
|
|
622
|
+
if(sourceMetrics.some(x=>x.name===m.name)){alert('Metric "'+m.name+'" already exists');return;}
|
|
623
|
+
sourceMetrics.push(m);
|
|
624
|
+
} else {
|
|
625
|
+
const idx=sourceMetrics.findIndex(x=>x.name===document.getElementById('metric-original-name').value);
|
|
626
|
+
if(idx>=0) sourceMetrics[idx]=m;
|
|
627
|
+
}
|
|
628
|
+
closeModal('metric-modal'); renderMetricsTable(); toast('Metric updated (save to persist)');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- Health Dashboard ---
|
|
632
|
+
let healthData={};
|
|
633
|
+
let healthInterval=null;
|
|
634
|
+
async function loadHealthData() {
|
|
635
|
+
try {
|
|
636
|
+
healthData=await(await fetch('/api/health')).json();
|
|
637
|
+
renderHealthCards();
|
|
638
|
+
} catch(e){ document.getElementById('health-cards').innerHTML='<div class="empty">Failed to load health data.</div>'; }
|
|
639
|
+
if(!healthInterval) healthInterval=setInterval(()=>{ if(document.getElementById('page-health').classList.contains('active')) loadHealthData(); },15000);
|
|
640
|
+
}
|
|
641
|
+
function renderHealthCards() {
|
|
642
|
+
const el=document.getElementById('health-cards');
|
|
643
|
+
const services=Object.keys(healthData);
|
|
644
|
+
if(services.length===0){el.innerHTML='<div class="empty">No services to show. Wait for metrics to accumulate.</div>';return;}
|
|
645
|
+
el.innerHTML=services.map(name=>{
|
|
646
|
+
const h=healthData[name]; if(h.error) return `<div class="health-card"><div class="hc-header"><span class="hc-name">${esc(name)}</span></div><div class="empty">${esc(h.error)}</div></div>`;
|
|
647
|
+
const s=h.status||'healthy';
|
|
648
|
+
const m=h.signals?.metrics||{};
|
|
649
|
+
const l=h.signals?.logs||{};
|
|
650
|
+
return `<div class="health-card">
|
|
651
|
+
<div class="hc-header">
|
|
652
|
+
<div><span class="hc-name">${esc(name)}</span> <span class="hc-status ${s}">${s}</span></div>
|
|
653
|
+
<span class="hc-score ${s}">${h.score ?? '-'}</span>
|
|
654
|
+
</div>
|
|
655
|
+
<div class="hc-metrics">
|
|
656
|
+
<div class="hc-metric"><div class="label">CPU</div><div class="val">${typeof m.cpu==='number'?m.cpu.toFixed(1)+'%':'-'}</div></div>
|
|
657
|
+
<div class="hc-metric"><div class="label">Memory</div><div class="val">${typeof m.memory==='number'?m.memory.toFixed(0)+' MB':'-'}</div></div>
|
|
658
|
+
<div class="hc-metric"><div class="label">Error Rate</div><div class="val">${typeof m.errorRate==='number'?m.errorRate.toFixed(3)+'/s':'-'}</div></div>
|
|
659
|
+
<div class="hc-metric"><div class="label">Latency P99</div><div class="val">${typeof m.latencyP99==='number'?(m.latencyP99*1000).toFixed(0)+'ms':'-'}</div></div>
|
|
660
|
+
<div class="hc-metric"><div class="label">Log Errors</div><div class="val">${typeof l.errorRate==='number'?l.errorRate:'-'}</div></div>
|
|
661
|
+
<div class="hc-metric"><div class="label">Top Error</div><div class="val" style="font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${l.topErrors&&l.topErrors[0]?esc(l.topErrors[0]):'-'}</div></div>
|
|
662
|
+
</div>
|
|
663
|
+
${(h.anomalies&&h.anomalies.length>0)?'<div class="hc-anomalies">'+h.anomalies.map(a=>`<div class="hc-anomaly">${esc(a.description)}</div>`).join('')+'</div>':''}
|
|
664
|
+
${(h.correlations&&h.correlations.length>0)?h.correlations.map(c=>`<div class="hc-correlation">${esc(c)}</div>`).join(''):''}
|
|
665
|
+
</div>`;
|
|
666
|
+
}).join('');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// --- Utils ---
|
|
670
|
+
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
|
671
|
+
async function refresh(){await Promise.all([loadSources(),loadServices()]);}
|
|
672
|
+
(async()=>{await loadTypes();await refresh();setInterval(refresh,15000);})();
|
|
673
|
+
</script>
|
|
674
|
+
</body>
|
|
675
|
+
</html>
|