@cybermem/dashboard 0.1.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/.dockerignore +11 -0
- package/.eslintrc.json +3 -0
- package/Dockerfile +48 -0
- package/app/api/audit-logs/route.ts +60 -0
- package/app/api/metrics/route.ts +141 -0
- package/app/api/prometheus/route.ts +65 -0
- package/app/api/settings/regenerate/route.ts +20 -0
- package/app/api/settings/route.ts +25 -0
- package/app/api/system/restart/route.ts +18 -0
- package/app/globals.css +148 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +150 -0
- package/components/dashboard/audit-log-table.tsx +195 -0
- package/components/dashboard/chart-card.tsx +196 -0
- package/components/dashboard/charts-section.tsx +16 -0
- package/components/dashboard/header.tsx +82 -0
- package/components/dashboard/login-modal.tsx +87 -0
- package/components/dashboard/mcp-config-modal.tsx +397 -0
- package/components/dashboard/metric-card.tsx +23 -0
- package/components/dashboard/metrics-chart.tsx +134 -0
- package/components/dashboard/metrics-grid.tsx +136 -0
- package/components/dashboard/settings-modal.tsx +345 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/calendar.tsx +213 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +135 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +104 -0
- package/components/ui/field.tsx +244 -0
- package/components/ui/form.tsx +167 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +169 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/item.tsx +193 -0
- package/components/ui/kbd.tsx +28 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +166 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +56 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +185 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/sonner.tsx +25 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/toast.tsx +129 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.tsx +19 -0
- package/components/ui/use-toast.ts +191 -0
- package/components.json +21 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +191 -0
- package/lib/data/dashboard-context.tsx +75 -0
- package/lib/data/demo-strategy.ts +110 -0
- package/lib/data/production-strategy.ts +152 -0
- package/lib/data/types.ts +52 -0
- package/lib/prometheus/client.ts +58 -0
- package/lib/prometheus/index.ts +6 -0
- package/lib/prometheus/metrics.ts +234 -0
- package/lib/prometheus/sparklines.ts +71 -0
- package/lib/prometheus/timeseries.ts +305 -0
- package/lib/prometheus/utils.ts +176 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +36 -0
- package/package.json +91 -0
- package/postcss.config.mjs +8 -0
- package/public/clients.json +165 -0
- package/public/favicon-dark.svg +1 -0
- package/public/favicon-light.svg +1 -0
- package/public/icons/antigravity.png +0 -0
- package/public/icons/chatgpt.png +0 -0
- package/public/icons/claude-code.png +0 -0
- package/public/icons/claude.png +0 -0
- package/public/icons/codex.png +0 -0
- package/public/icons/cursor.png +0 -0
- package/public/icons/gemini.png +0 -0
- package/public/icons/images.jpeg +0 -0
- package/public/icons/mcp.png +0 -0
- package/public/icons/mono.png +0 -0
- package/public/icons/perplexity.png +0 -0
- package/public/icons/vscode.png +0 -0
- package/public/icons/warp.png +0 -0
- package/public/icons/windsurf.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +7 -0
- package/public/manifest.json +21 -0
- package/public/site.webmanifest +21 -0
- package/public/web-app-manifest-192x192.png +0 -0
- package/public/web-app-manifest-512x512.png +0 -0
- package/shared.env +0 -0
- package/styles/globals.css +125 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { queryRange } from './client'
|
|
2
|
+
import { getAllActiveClients } from './metrics'
|
|
3
|
+
import { chooseStep, formatRangeSeriesByClient, parseDuration } from './utils'
|
|
4
|
+
|
|
5
|
+
export async function getRequestsTimeSeries(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number | string }>> {
|
|
6
|
+
const now = Math.floor(Date.now() / 1000)
|
|
7
|
+
const start = now - parseDuration(duration)
|
|
8
|
+
const step = chooseStep(duration)
|
|
9
|
+
const stepSeconds = parseDuration(step)
|
|
10
|
+
|
|
11
|
+
// Use sum (Cumulative) to show total requests over time
|
|
12
|
+
const result = await queryRange(
|
|
13
|
+
`sum by (client_name) (openmemory_requests_total)`,
|
|
14
|
+
start,
|
|
15
|
+
now,
|
|
16
|
+
step
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getRequestsTimeSeriesByMethod(method: string, duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number | string }>> {
|
|
23
|
+
const now = Math.floor(Date.now() / 1000)
|
|
24
|
+
const start = now - parseDuration(duration)
|
|
25
|
+
const step = chooseStep(duration)
|
|
26
|
+
const stepSeconds = parseDuration(step)
|
|
27
|
+
|
|
28
|
+
const result = await queryRange(
|
|
29
|
+
`sum by (client_name) (openmemory_requests_total{operation="${method}"})`,
|
|
30
|
+
start,
|
|
31
|
+
now,
|
|
32
|
+
step
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getResponseTimeTimeSeries(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number | string }>> {
|
|
39
|
+
const now = Math.floor(Date.now() / 1000)
|
|
40
|
+
const start = now - parseDuration(duration)
|
|
41
|
+
|
|
42
|
+
const result = await queryRange(
|
|
43
|
+
'sum by (client_name) (rate(openmemory_request_duration_seconds_sum[5m])) / sum by (client_name) (rate(openmemory_request_duration_seconds_count[5m]))',
|
|
44
|
+
start,
|
|
45
|
+
now,
|
|
46
|
+
'1m'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const timeSeries: Array<{ time: number, [client: string]: number }> = []
|
|
50
|
+
const timeMap = new Map<number, Record<string, number>>()
|
|
51
|
+
|
|
52
|
+
result.data.result.forEach((series) => {
|
|
53
|
+
const clientId = series.metric.client_name || 'unknown'
|
|
54
|
+
series.values?.forEach(([timestamp, value]) => {
|
|
55
|
+
if (!timeMap.has(timestamp)) {
|
|
56
|
+
timeMap.set(timestamp, {})
|
|
57
|
+
}
|
|
58
|
+
timeMap.get(timestamp)![clientId] = parseFloat(value) * 1000 // convert to ms
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
Array.from(timeMap.entries())
|
|
63
|
+
.sort((a, b) => a[0] - b[0])
|
|
64
|
+
.forEach(([timestamp, clients]) => {
|
|
65
|
+
timeSeries.push({
|
|
66
|
+
time: timestamp,
|
|
67
|
+
...clients
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return timeSeries
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getSuccessRateTimeSeries(duration: string = '1h'): Promise<Array<{ time: number, value: number }>> {
|
|
75
|
+
const now = Math.floor(Date.now() / 1000)
|
|
76
|
+
const start = now - parseDuration(duration)
|
|
77
|
+
|
|
78
|
+
const step = chooseStep(duration)
|
|
79
|
+
|
|
80
|
+
const result = await queryRange(
|
|
81
|
+
'sum(rate(openmemory_requests_total{status=~"2.."}[5m])) / sum(rate(openmemory_requests_total[5m]))',
|
|
82
|
+
start,
|
|
83
|
+
now,
|
|
84
|
+
step
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const series = result.data.result[0]
|
|
88
|
+
const values = series?.values || []
|
|
89
|
+
|
|
90
|
+
// Fill gaps
|
|
91
|
+
const timeSeries: Array<{ time: number, value: number }> = []
|
|
92
|
+
const valueMap = new Map<number, number>()
|
|
93
|
+
values.forEach(([t, v]) => valueMap.set(t, parseFloat(v) * 100))
|
|
94
|
+
|
|
95
|
+
const stepSeconds = parseDuration(chooseStep(duration))
|
|
96
|
+
for (let t = start; t <= now; t += stepSeconds) {
|
|
97
|
+
// Find closest value
|
|
98
|
+
let val = 100 // Default to 100% if no data
|
|
99
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
100
|
+
if (valueMap.has(t + offset)) {
|
|
101
|
+
val = valueMap.get(t + offset)!
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
timeSeries.push({ time: t, value: val })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return timeSeries
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getSuccessRateTimeSeriesByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number | string }>> {
|
|
112
|
+
const now = Math.floor(Date.now() / 1000)
|
|
113
|
+
const start = now - parseDuration(duration)
|
|
114
|
+
|
|
115
|
+
// Formula: (requests - errors) / requests * 100
|
|
116
|
+
// Use raw counters instead of rate() to avoid 0% when no new requests
|
|
117
|
+
const step = chooseStep(duration)
|
|
118
|
+
const stepSeconds = parseDuration(step)
|
|
119
|
+
const [allClients, totalResult, errorResult] = await Promise.all([
|
|
120
|
+
getAllActiveClients(duration),
|
|
121
|
+
queryRange(
|
|
122
|
+
`sum by (client_name) (openmemory_requests_total)`,
|
|
123
|
+
start,
|
|
124
|
+
now,
|
|
125
|
+
step
|
|
126
|
+
),
|
|
127
|
+
queryRange(
|
|
128
|
+
`sum by (client_name) (openmemory_errors_total)`,
|
|
129
|
+
start,
|
|
130
|
+
now,
|
|
131
|
+
step
|
|
132
|
+
)
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const timeMap = new Map<number, Record<string, { total: number, errors: number }>>()
|
|
136
|
+
|
|
137
|
+
// Initialize all timestamps
|
|
138
|
+
for (let t = start; t <= now; t += stepSeconds) {
|
|
139
|
+
timeMap.set(t, {})
|
|
140
|
+
allClients.forEach(client => {
|
|
141
|
+
timeMap.get(t)![client] = { total: 0, errors: 0 }
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Collect totals
|
|
146
|
+
totalResult.data.result.forEach((series) => {
|
|
147
|
+
const clientId = series.metric.client_name || 'unknown'
|
|
148
|
+
|
|
149
|
+
series.values?.forEach(([timestamp, value]) => {
|
|
150
|
+
// Find closest timestamp
|
|
151
|
+
let targetTs = timestamp
|
|
152
|
+
if (!timeMap.has(timestamp)) {
|
|
153
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
154
|
+
if (timeMap.has(timestamp + offset)) {
|
|
155
|
+
targetTs = timestamp + offset
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (timeMap.has(targetTs)) {
|
|
162
|
+
const clients = timeMap.get(targetTs)!
|
|
163
|
+
if (!clients[clientId]) clients[clientId] = { total: 0, errors: 0 }
|
|
164
|
+
clients[clientId].total = parseFloat(value)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Collect errors
|
|
170
|
+
errorResult.data.result.forEach((series) => {
|
|
171
|
+
const clientId = series.metric.client_name || 'unknown'
|
|
172
|
+
|
|
173
|
+
series.values?.forEach(([timestamp, value]) => {
|
|
174
|
+
// Find closest timestamp
|
|
175
|
+
let targetTs = timestamp
|
|
176
|
+
if (!timeMap.has(timestamp)) {
|
|
177
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
178
|
+
if (timeMap.has(timestamp + offset)) {
|
|
179
|
+
targetTs = timestamp + offset
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (timeMap.has(targetTs)) {
|
|
186
|
+
const clients = timeMap.get(targetTs)!
|
|
187
|
+
if (!clients[clientId]) clients[clientId] = { total: 0, errors: 0 }
|
|
188
|
+
clients[clientId].errors = parseFloat(value)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Calculate success rate percentages: (total - errors) / total * 100
|
|
194
|
+
const timeSeries: Array<{ time: number, [client: string]: number }> = []
|
|
195
|
+
Array.from(timeMap.entries())
|
|
196
|
+
.sort((a, b) => a[0] - b[0])
|
|
197
|
+
.forEach(([timestamp, clients]) => {
|
|
198
|
+
const dataPoint: any = {
|
|
199
|
+
time: timestamp
|
|
200
|
+
}
|
|
201
|
+
Object.keys(clients).forEach((client) => {
|
|
202
|
+
const { total, errors } = clients[client]
|
|
203
|
+
dataPoint[client] = total > 0 ? ((total - errors) / total) * 100 : 100
|
|
204
|
+
})
|
|
205
|
+
timeSeries.push(dataPoint)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return timeSeries
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// CRUD operation time series
|
|
212
|
+
export async function getCreatesByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number }>> {
|
|
213
|
+
const now = Math.floor(Date.now() / 1000)
|
|
214
|
+
const start = now - parseDuration(duration)
|
|
215
|
+
const step = chooseStep(duration)
|
|
216
|
+
const stepSeconds = parseDuration(step)
|
|
217
|
+
|
|
218
|
+
const [allClients, result] = await Promise.all([
|
|
219
|
+
getAllActiveClients(duration),
|
|
220
|
+
queryRange(
|
|
221
|
+
`sum by (client_name) (increase(openmemory_requests_total{operation="create"}[${step}]))`,
|
|
222
|
+
start,
|
|
223
|
+
now,
|
|
224
|
+
step
|
|
225
|
+
)
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds, allClients, 'client_name', false, true)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function getReadsByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number }>> {
|
|
232
|
+
const now = Math.floor(Date.now() / 1000)
|
|
233
|
+
const start = now - parseDuration(duration)
|
|
234
|
+
const step = chooseStep(duration)
|
|
235
|
+
const stepSeconds = parseDuration(step)
|
|
236
|
+
|
|
237
|
+
const [allClients, result] = await Promise.all([
|
|
238
|
+
getAllActiveClients(duration),
|
|
239
|
+
queryRange(
|
|
240
|
+
`sum by (client_name) (increase(openmemory_requests_total{operation="read"}[${step}]))`,
|
|
241
|
+
start,
|
|
242
|
+
now,
|
|
243
|
+
step
|
|
244
|
+
)
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds, allClients, 'client_name', false, true)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function getUpdatesByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number }>> {
|
|
251
|
+
const now = Math.floor(Date.now() / 1000)
|
|
252
|
+
const start = now - parseDuration(duration)
|
|
253
|
+
const step = chooseStep(duration)
|
|
254
|
+
const stepSeconds = parseDuration(step)
|
|
255
|
+
|
|
256
|
+
const [allClients, result] = await Promise.all([
|
|
257
|
+
getAllActiveClients(duration),
|
|
258
|
+
queryRange(
|
|
259
|
+
`sum by (client_name) (increase(openmemory_requests_total{operation="update"}[${step}]))`,
|
|
260
|
+
start,
|
|
261
|
+
now,
|
|
262
|
+
step
|
|
263
|
+
)
|
|
264
|
+
])
|
|
265
|
+
|
|
266
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds, allClients, 'client_name', false, true)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function getDeletesByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number }>> {
|
|
270
|
+
const now = Math.floor(Date.now() / 1000)
|
|
271
|
+
const start = now - parseDuration(duration)
|
|
272
|
+
const step = chooseStep(duration)
|
|
273
|
+
const stepSeconds = parseDuration(step)
|
|
274
|
+
|
|
275
|
+
const [allClients, result] = await Promise.all([
|
|
276
|
+
getAllActiveClients(duration),
|
|
277
|
+
queryRange(
|
|
278
|
+
`sum by (client_name) (increase(openmemory_requests_total{operation="delete"}[${step}]))`,
|
|
279
|
+
start,
|
|
280
|
+
now,
|
|
281
|
+
step
|
|
282
|
+
)
|
|
283
|
+
])
|
|
284
|
+
|
|
285
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds, allClients, 'client_name', false, true)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function getErrorsByClient(duration: string = '1h'): Promise<Array<{ time: number, [client: string]: number }>> {
|
|
289
|
+
const now = Math.floor(Date.now() / 1000)
|
|
290
|
+
const start = now - parseDuration(duration)
|
|
291
|
+
const step = chooseStep(duration)
|
|
292
|
+
const stepSeconds = parseDuration(step)
|
|
293
|
+
|
|
294
|
+
const [allClients, result] = await Promise.all([
|
|
295
|
+
getAllActiveClients(duration),
|
|
296
|
+
queryRange(
|
|
297
|
+
`sum by (client_name) (increase(openmemory_errors_total[${step}]))`,
|
|
298
|
+
start,
|
|
299
|
+
now,
|
|
300
|
+
step
|
|
301
|
+
)
|
|
302
|
+
])
|
|
303
|
+
|
|
304
|
+
return formatRangeSeriesByClient(result, start, now, stepSeconds, allClients, 'client_name', false, true)
|
|
305
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { PrometheusQueryResult } from './client'
|
|
2
|
+
|
|
3
|
+
export function parseDuration(duration: string): number {
|
|
4
|
+
const match = duration.match(/^(\d+)([smhdwMYy])$/)
|
|
5
|
+
if (!match) return 3600
|
|
6
|
+
|
|
7
|
+
const [, amount, unit] = match
|
|
8
|
+
const multipliers: Record<string, number> = {
|
|
9
|
+
s: 1,
|
|
10
|
+
m: 60,
|
|
11
|
+
h: 3600,
|
|
12
|
+
d: 86400,
|
|
13
|
+
w: 604800, // 7 days
|
|
14
|
+
M: 2592000, // 30 days (approximation)
|
|
15
|
+
Y: 31536000, // 365 days
|
|
16
|
+
y: 31536000
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return parseInt(amount) * (multipliers[unit] || 3600)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function chooseStep(duration: string): string {
|
|
23
|
+
const seconds = parseDuration(duration)
|
|
24
|
+
if (seconds <= 3600) return '4m' // 1h -> 4m (15 pts). More reliable than 2m.
|
|
25
|
+
if (seconds <= 6 * 3600) return '5m' // 6h -> 5m (72 pts)
|
|
26
|
+
if (seconds <= 24 * 3600) return '15m' // 24h -> 15m (96 pts)
|
|
27
|
+
if (seconds <= 7 * 86400) return '1h' // 7d -> 1h (168 pts)
|
|
28
|
+
return '4h' // >7d -> 4h (e.g. 90d -> 540 pts)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// PromQL doesn't support M/Y, map them to days so query_range stays valid
|
|
32
|
+
export function toPromDuration(duration: string): string {
|
|
33
|
+
const match = duration.match(/^(\d+)([smhdwMyY])$/)
|
|
34
|
+
if (!match) return duration
|
|
35
|
+
const amount = parseInt(match[1])
|
|
36
|
+
const unit = match[2]
|
|
37
|
+
if (unit === 'M') return `${amount * 30}d`
|
|
38
|
+
if (unit === 'Y' || unit === 'y') return `${amount * 365}d`
|
|
39
|
+
return `${amount}${unit}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Improved version that handles the map lookup correctly
|
|
43
|
+
export function fillSparklineData(values: Array<[number, string]>, start: number, end: number, step: number, defaultValue: number = 0): number[] {
|
|
44
|
+
const map = new Map<number, number>()
|
|
45
|
+
values.forEach(([t, v]) => map.set(t, parseFloat(v)))
|
|
46
|
+
|
|
47
|
+
const result: number[] = []
|
|
48
|
+
|
|
49
|
+
for (let t = start; t <= end; t += step) {
|
|
50
|
+
// Allow for small jitter (1s)
|
|
51
|
+
let val = defaultValue
|
|
52
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
53
|
+
if (map.has(t + offset)) {
|
|
54
|
+
val = map.get(t + offset)!
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
result.push(val)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Format range query results (already aggregated) into series by client
|
|
64
|
+
export function formatRangeSeriesByClient(
|
|
65
|
+
result: PrometheusQueryResult,
|
|
66
|
+
start: number,
|
|
67
|
+
end: number,
|
|
68
|
+
stepSeconds: number,
|
|
69
|
+
allClients?: string[],
|
|
70
|
+
labelKey: string = 'client_name',
|
|
71
|
+
isCumulative: boolean = true,
|
|
72
|
+
integrate: boolean = false
|
|
73
|
+
): Array<{ time: number, [client: string]: number }> {
|
|
74
|
+
const timeMap = new Map<number, Record<string, number>>()
|
|
75
|
+
|
|
76
|
+
// Initialize all timestamps with 0s
|
|
77
|
+
for (let t = start; t <= end; t += stepSeconds) {
|
|
78
|
+
timeMap.set(t, {})
|
|
79
|
+
if (allClients) {
|
|
80
|
+
allClients.forEach(client => {
|
|
81
|
+
timeMap.get(t)![client] = 0
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
result.data.result.forEach((series) => {
|
|
87
|
+
const clientName = series.metric[labelKey] || 'unknown'
|
|
88
|
+
const values = series.values || []
|
|
89
|
+
values.forEach(([timestamp, value]) => {
|
|
90
|
+
// Find closest timestamp in our map (to handle slight jitter)
|
|
91
|
+
let targetTs = timestamp
|
|
92
|
+
if (!timeMap.has(timestamp)) {
|
|
93
|
+
// Try to find within 1 second
|
|
94
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
95
|
+
if (timeMap.has(timestamp + offset)) {
|
|
96
|
+
targetTs = timestamp + offset
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (timeMap.has(targetTs)) {
|
|
103
|
+
timeMap.get(targetTs)![clientName] = parseFloat(value)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Fill in zeros for all clients at all timestamps
|
|
109
|
+
if (allClients && allClients.length > 0) {
|
|
110
|
+
// Iterate over all timestamps in the map
|
|
111
|
+
Array.from(timeMap.keys()).forEach((timestamp) => {
|
|
112
|
+
const timestampData = timeMap.get(timestamp)!
|
|
113
|
+
allClients.forEach((client) => {
|
|
114
|
+
if (!(client in timestampData)) {
|
|
115
|
+
timestampData[client] = 0
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sortedSeries = Array.from(timeMap.entries())
|
|
122
|
+
.sort((a, b) => a[0] - b[0])
|
|
123
|
+
.map(([timestamp, clients]) => ({
|
|
124
|
+
time: timestamp,
|
|
125
|
+
...clients
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
// "Tare" logic: Subtract the initial value (at t=0 of the graph) from all subsequent values
|
|
129
|
+
// This ensures the graph starts at 0 and shows cumulative growth relative to the start of the period.
|
|
130
|
+
if (isCumulative && sortedSeries.length > 0) {
|
|
131
|
+
const initialValues: Record<string, number> = {}
|
|
132
|
+
// Initialize with the values from the first point
|
|
133
|
+
const firstPoint = sortedSeries[0] as Record<string, any>
|
|
134
|
+
Object.keys(firstPoint).forEach(key => {
|
|
135
|
+
if (key !== 'time') {
|
|
136
|
+
initialValues[key] = firstPoint[key] as number
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Subtract initial values from all points
|
|
141
|
+
return sortedSeries.map(point => {
|
|
142
|
+
const p = point as Record<string, any>
|
|
143
|
+
const newPoint: any = { time: p.time }
|
|
144
|
+
Object.keys(p).forEach(key => {
|
|
145
|
+
if (key !== 'time') {
|
|
146
|
+
const rawValue = p[key] as number
|
|
147
|
+
const startValue = initialValues[key] || 0
|
|
148
|
+
// Ensure we don't go below zero (in case of resets)
|
|
149
|
+
newPoint[key] = Math.max(0, rawValue - startValue)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
return newPoint
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Integration logic: Accumulate values over time
|
|
157
|
+
if (integrate && sortedSeries.length > 0) {
|
|
158
|
+
const runningTotals: Record<string, number> = {}
|
|
159
|
+
|
|
160
|
+
return sortedSeries.map(point => {
|
|
161
|
+
const p = point as Record<string, any>
|
|
162
|
+
const newPoint: any = { time: p.time }
|
|
163
|
+
|
|
164
|
+
Object.keys(p).forEach(key => {
|
|
165
|
+
if (key !== 'time') {
|
|
166
|
+
const delta = p[key] as number
|
|
167
|
+
runningTotals[key] = (runningTotals[key] || 0) + delta
|
|
168
|
+
newPoint[key] = Math.round(runningTotals[key])
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
return newPoint
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return sortedSeries
|
|
176
|
+
}
|
package/lib/utils.ts
ADDED
package/next.config.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
/** @type {import('next').NextConfig} */
|
|
3
|
+
const nextConfig = {
|
|
4
|
+
typescript: {
|
|
5
|
+
ignoreBuildErrors: true,
|
|
6
|
+
},
|
|
7
|
+
eslint: {
|
|
8
|
+
ignoreDuringBuilds: true,
|
|
9
|
+
},
|
|
10
|
+
images: {
|
|
11
|
+
unoptimized: true,
|
|
12
|
+
},
|
|
13
|
+
output: 'standalone',
|
|
14
|
+
transpilePackages: ['recharts'],
|
|
15
|
+
serverExternalPackages: ['dockerode', 'ssh2'],
|
|
16
|
+
experimental: {
|
|
17
|
+
},
|
|
18
|
+
webpack: (config) => {
|
|
19
|
+
config.externals = [...(config.externals || []), 'ssh2', 'dockerode'];
|
|
20
|
+
return config;
|
|
21
|
+
},
|
|
22
|
+
async rewrites() {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
source: '/docs',
|
|
26
|
+
destination: '/docs/index.html',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
source: '/docs/:slug',
|
|
30
|
+
destination: '/docs/:slug.html',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default nextConfig
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cybermem/dashboard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "next build --webpack",
|
|
9
|
+
"dev": "next dev",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"start": "next start"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@headlessui/react": "^2.2.9",
|
|
15
|
+
"@hookform/resolvers": "^3.10.0",
|
|
16
|
+
"@radix-ui/react-accordion": "1.2.2",
|
|
17
|
+
"@radix-ui/react-alert-dialog": "1.1.4",
|
|
18
|
+
"@radix-ui/react-aspect-ratio": "1.1.1",
|
|
19
|
+
"@radix-ui/react-avatar": "1.1.2",
|
|
20
|
+
"@radix-ui/react-checkbox": "1.1.3",
|
|
21
|
+
"@radix-ui/react-collapsible": "1.1.2",
|
|
22
|
+
"@radix-ui/react-context-menu": "2.2.4",
|
|
23
|
+
"@radix-ui/react-dialog": "1.1.4",
|
|
24
|
+
"@radix-ui/react-dropdown-menu": "2.1.4",
|
|
25
|
+
"@radix-ui/react-hover-card": "1.1.4",
|
|
26
|
+
"@radix-ui/react-label": "2.1.1",
|
|
27
|
+
"@radix-ui/react-menubar": "1.1.4",
|
|
28
|
+
"@radix-ui/react-navigation-menu": "1.2.3",
|
|
29
|
+
"@radix-ui/react-popover": "1.1.4",
|
|
30
|
+
"@radix-ui/react-progress": "1.1.1",
|
|
31
|
+
"@radix-ui/react-radio-group": "1.2.2",
|
|
32
|
+
"@radix-ui/react-scroll-area": "1.2.2",
|
|
33
|
+
"@radix-ui/react-select": "2.1.4",
|
|
34
|
+
"@radix-ui/react-separator": "1.1.1",
|
|
35
|
+
"@radix-ui/react-slider": "1.2.2",
|
|
36
|
+
"@radix-ui/react-slot": "1.1.1",
|
|
37
|
+
"@radix-ui/react-switch": "1.1.2",
|
|
38
|
+
"@radix-ui/react-tabs": "1.1.2",
|
|
39
|
+
"@radix-ui/react-toast": "1.2.4",
|
|
40
|
+
"@radix-ui/react-toggle": "1.1.1",
|
|
41
|
+
"@radix-ui/react-toggle-group": "1.1.1",
|
|
42
|
+
"@radix-ui/react-tooltip": "1.1.6",
|
|
43
|
+
"@types/mdx": "^2.0.13",
|
|
44
|
+
"@vercel/analytics": "1.3.1",
|
|
45
|
+
"autoprefixer": "^10.4.20",
|
|
46
|
+
"class-variance-authority": "^0.7.1",
|
|
47
|
+
"clsx": "^2.1.1",
|
|
48
|
+
"cmdk": "1.0.4",
|
|
49
|
+
"d3-format": "^3.1.0",
|
|
50
|
+
"d3-path": "^3.1.0",
|
|
51
|
+
"d3-scale": "^4.0.2",
|
|
52
|
+
"d3-shape": "^3.2.0",
|
|
53
|
+
"d3-time": "^3.1.0",
|
|
54
|
+
"d3-time-format": "^4.1.0",
|
|
55
|
+
"date-fns": "4.1.0",
|
|
56
|
+
"dockerode": "^4.0.9",
|
|
57
|
+
"echarts": "^5.5.0",
|
|
58
|
+
"echarts-for-react": "^3.0.0",
|
|
59
|
+
"embla-carousel-react": "8.5.1",
|
|
60
|
+
"input-otp": "1.4.1",
|
|
61
|
+
"lucide-react": "^0.454.0",
|
|
62
|
+
"next": "16.0.10",
|
|
63
|
+
"next-themes": "^0.4.6",
|
|
64
|
+
"react": "19.2.0",
|
|
65
|
+
"react-day-picker": "9.8.0",
|
|
66
|
+
"react-dom": "19.2.0",
|
|
67
|
+
"react-hook-form": "^7.60.0",
|
|
68
|
+
"react-resizable-panels": "^2.1.7",
|
|
69
|
+
"recharts": "2.15.4",
|
|
70
|
+
"recharts-scale": "^0.4.5",
|
|
71
|
+
"sonner": "^1.7.4",
|
|
72
|
+
"ssh2": "^1.17.0",
|
|
73
|
+
"tailwind-merge": "^3.3.1",
|
|
74
|
+
"tailwindcss-animate": "^1.0.7",
|
|
75
|
+
"vaul": "^1.1.2",
|
|
76
|
+
"zod": "3.25.76"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@tailwindcss/postcss": "^4.1.9",
|
|
80
|
+
"@types/dockerode": "^3.3.47",
|
|
81
|
+
"@types/node": "^22",
|
|
82
|
+
"@types/react": "^19",
|
|
83
|
+
"@types/react-dom": "^19",
|
|
84
|
+
"eslint": "^8.57.0",
|
|
85
|
+
"eslint-config-next": "14.2.3",
|
|
86
|
+
"postcss": "^8.5",
|
|
87
|
+
"tailwindcss": "^4.1.9",
|
|
88
|
+
"tw-animate-css": "1.3.3",
|
|
89
|
+
"typescript": "^5"
|
|
90
|
+
}
|
|
91
|
+
}
|