@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.
Files changed (123) hide show
  1. package/.dockerignore +11 -0
  2. package/.eslintrc.json +3 -0
  3. package/Dockerfile +48 -0
  4. package/app/api/audit-logs/route.ts +60 -0
  5. package/app/api/metrics/route.ts +141 -0
  6. package/app/api/prometheus/route.ts +65 -0
  7. package/app/api/settings/regenerate/route.ts +20 -0
  8. package/app/api/settings/route.ts +25 -0
  9. package/app/api/system/restart/route.ts +18 -0
  10. package/app/globals.css +148 -0
  11. package/app/layout.tsx +37 -0
  12. package/app/page.tsx +150 -0
  13. package/components/dashboard/audit-log-table.tsx +195 -0
  14. package/components/dashboard/chart-card.tsx +196 -0
  15. package/components/dashboard/charts-section.tsx +16 -0
  16. package/components/dashboard/header.tsx +82 -0
  17. package/components/dashboard/login-modal.tsx +87 -0
  18. package/components/dashboard/mcp-config-modal.tsx +397 -0
  19. package/components/dashboard/metric-card.tsx +23 -0
  20. package/components/dashboard/metrics-chart.tsx +134 -0
  21. package/components/dashboard/metrics-grid.tsx +136 -0
  22. package/components/dashboard/settings-modal.tsx +345 -0
  23. package/components/theme-provider.tsx +11 -0
  24. package/components/ui/accordion.tsx +66 -0
  25. package/components/ui/alert-dialog.tsx +157 -0
  26. package/components/ui/alert.tsx +66 -0
  27. package/components/ui/aspect-ratio.tsx +11 -0
  28. package/components/ui/avatar.tsx +53 -0
  29. package/components/ui/badge.tsx +46 -0
  30. package/components/ui/breadcrumb.tsx +109 -0
  31. package/components/ui/button-group.tsx +83 -0
  32. package/components/ui/button.tsx +60 -0
  33. package/components/ui/calendar.tsx +213 -0
  34. package/components/ui/card.tsx +92 -0
  35. package/components/ui/carousel.tsx +241 -0
  36. package/components/ui/chart.tsx +353 -0
  37. package/components/ui/checkbox.tsx +32 -0
  38. package/components/ui/collapsible.tsx +33 -0
  39. package/components/ui/command.tsx +184 -0
  40. package/components/ui/context-menu.tsx +252 -0
  41. package/components/ui/dialog.tsx +143 -0
  42. package/components/ui/drawer.tsx +135 -0
  43. package/components/ui/dropdown-menu.tsx +257 -0
  44. package/components/ui/empty.tsx +104 -0
  45. package/components/ui/field.tsx +244 -0
  46. package/components/ui/form.tsx +167 -0
  47. package/components/ui/hover-card.tsx +44 -0
  48. package/components/ui/input-group.tsx +169 -0
  49. package/components/ui/input-otp.tsx +77 -0
  50. package/components/ui/input.tsx +21 -0
  51. package/components/ui/item.tsx +193 -0
  52. package/components/ui/kbd.tsx +28 -0
  53. package/components/ui/label.tsx +24 -0
  54. package/components/ui/menubar.tsx +276 -0
  55. package/components/ui/navigation-menu.tsx +166 -0
  56. package/components/ui/pagination.tsx +127 -0
  57. package/components/ui/popover.tsx +48 -0
  58. package/components/ui/progress.tsx +31 -0
  59. package/components/ui/radio-group.tsx +45 -0
  60. package/components/ui/resizable.tsx +56 -0
  61. package/components/ui/scroll-area.tsx +58 -0
  62. package/components/ui/select.tsx +185 -0
  63. package/components/ui/separator.tsx +28 -0
  64. package/components/ui/sheet.tsx +139 -0
  65. package/components/ui/sidebar.tsx +726 -0
  66. package/components/ui/skeleton.tsx +13 -0
  67. package/components/ui/slider.tsx +63 -0
  68. package/components/ui/sonner.tsx +25 -0
  69. package/components/ui/spinner.tsx +16 -0
  70. package/components/ui/switch.tsx +31 -0
  71. package/components/ui/table.tsx +116 -0
  72. package/components/ui/tabs.tsx +66 -0
  73. package/components/ui/textarea.tsx +18 -0
  74. package/components/ui/toast.tsx +129 -0
  75. package/components/ui/toaster.tsx +35 -0
  76. package/components/ui/toggle-group.tsx +73 -0
  77. package/components/ui/toggle.tsx +47 -0
  78. package/components/ui/tooltip.tsx +61 -0
  79. package/components/ui/use-mobile.tsx +19 -0
  80. package/components/ui/use-toast.ts +191 -0
  81. package/components.json +21 -0
  82. package/hooks/use-mobile.ts +19 -0
  83. package/hooks/use-toast.ts +191 -0
  84. package/lib/data/dashboard-context.tsx +75 -0
  85. package/lib/data/demo-strategy.ts +110 -0
  86. package/lib/data/production-strategy.ts +152 -0
  87. package/lib/data/types.ts +52 -0
  88. package/lib/prometheus/client.ts +58 -0
  89. package/lib/prometheus/index.ts +6 -0
  90. package/lib/prometheus/metrics.ts +234 -0
  91. package/lib/prometheus/sparklines.ts +71 -0
  92. package/lib/prometheus/timeseries.ts +305 -0
  93. package/lib/prometheus/utils.ts +176 -0
  94. package/lib/utils.ts +6 -0
  95. package/next.config.mjs +36 -0
  96. package/package.json +91 -0
  97. package/postcss.config.mjs +8 -0
  98. package/public/clients.json +165 -0
  99. package/public/favicon-dark.svg +1 -0
  100. package/public/favicon-light.svg +1 -0
  101. package/public/icons/antigravity.png +0 -0
  102. package/public/icons/chatgpt.png +0 -0
  103. package/public/icons/claude-code.png +0 -0
  104. package/public/icons/claude.png +0 -0
  105. package/public/icons/codex.png +0 -0
  106. package/public/icons/cursor.png +0 -0
  107. package/public/icons/gemini.png +0 -0
  108. package/public/icons/images.jpeg +0 -0
  109. package/public/icons/mcp.png +0 -0
  110. package/public/icons/mono.png +0 -0
  111. package/public/icons/perplexity.png +0 -0
  112. package/public/icons/vscode.png +0 -0
  113. package/public/icons/warp.png +0 -0
  114. package/public/icons/windsurf.png +0 -0
  115. package/public/logo.png +0 -0
  116. package/public/logo.svg +7 -0
  117. package/public/manifest.json +21 -0
  118. package/public/site.webmanifest +21 -0
  119. package/public/web-app-manifest-192x192.png +0 -0
  120. package/public/web-app-manifest-512x512.png +0 -0
  121. package/shared.env +0 -0
  122. package/styles/globals.css +125 -0
  123. 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
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ '@tailwindcss/postcss': {},
5
+ },
6
+ }
7
+
8
+ export default config