@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,61 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return (
25
+ <TooltipProvider>
26
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
27
+ </TooltipProvider>
28
+ )
29
+ }
30
+
31
+ function TooltipTrigger({
32
+ ...props
33
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
34
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
35
+ }
36
+
37
+ function TooltipContent({
38
+ className,
39
+ sideOffset = 0,
40
+ children,
41
+ ...props
42
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
43
+ return (
44
+ <TooltipPrimitive.Portal>
45
+ <TooltipPrimitive.Content
46
+ data-slot="tooltip-content"
47
+ sideOffset={sideOffset}
48
+ className={cn(
49
+ 'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
50
+ className,
51
+ )}
52
+ {...props}
53
+ >
54
+ {children}
55
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
56
+ </TooltipPrimitive.Content>
57
+ </TooltipPrimitive.Portal>
58
+ )
59
+ }
60
+
61
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener('change', onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener('change', onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,191 @@
1
+ 'use client'
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from 'react'
5
+
6
+ import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
7
+
8
+ const TOAST_LIMIT = 1
9
+ const TOAST_REMOVE_DELAY = 1000000
10
+
11
+ type ToasterToast = ToastProps & {
12
+ id: string
13
+ title?: React.ReactNode
14
+ description?: React.ReactNode
15
+ action?: ToastActionElement
16
+ }
17
+
18
+ const actionTypes = {
19
+ ADD_TOAST: 'ADD_TOAST',
20
+ UPDATE_TOAST: 'UPDATE_TOAST',
21
+ DISMISS_TOAST: 'DISMISS_TOAST',
22
+ REMOVE_TOAST: 'REMOVE_TOAST',
23
+ } as const
24
+
25
+ let count = 0
26
+
27
+ function genId() {
28
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
29
+ return count.toString()
30
+ }
31
+
32
+ type ActionType = typeof actionTypes
33
+
34
+ type Action =
35
+ | {
36
+ type: ActionType['ADD_TOAST']
37
+ toast: ToasterToast
38
+ }
39
+ | {
40
+ type: ActionType['UPDATE_TOAST']
41
+ toast: Partial<ToasterToast>
42
+ }
43
+ | {
44
+ type: ActionType['DISMISS_TOAST']
45
+ toastId?: ToasterToast['id']
46
+ }
47
+ | {
48
+ type: ActionType['REMOVE_TOAST']
49
+ toastId?: ToasterToast['id']
50
+ }
51
+
52
+ interface State {
53
+ toasts: ToasterToast[]
54
+ }
55
+
56
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
57
+
58
+ const addToRemoveQueue = (toastId: string) => {
59
+ if (toastTimeouts.has(toastId)) {
60
+ return
61
+ }
62
+
63
+ const timeout = setTimeout(() => {
64
+ toastTimeouts.delete(toastId)
65
+ dispatch({
66
+ type: 'REMOVE_TOAST',
67
+ toastId: toastId,
68
+ })
69
+ }, TOAST_REMOVE_DELAY)
70
+
71
+ toastTimeouts.set(toastId, timeout)
72
+ }
73
+
74
+ export const reducer = (state: State, action: Action): State => {
75
+ switch (action.type) {
76
+ case 'ADD_TOAST':
77
+ return {
78
+ ...state,
79
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80
+ }
81
+
82
+ case 'UPDATE_TOAST':
83
+ return {
84
+ ...state,
85
+ toasts: state.toasts.map((t) =>
86
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
87
+ ),
88
+ }
89
+
90
+ case 'DISMISS_TOAST': {
91
+ const { toastId } = action
92
+
93
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
94
+ // but I'll keep it here for simplicity
95
+ if (toastId) {
96
+ addToRemoveQueue(toastId)
97
+ } else {
98
+ state.toasts.forEach((toast) => {
99
+ addToRemoveQueue(toast.id)
100
+ })
101
+ }
102
+
103
+ return {
104
+ ...state,
105
+ toasts: state.toasts.map((t) =>
106
+ t.id === toastId || toastId === undefined
107
+ ? {
108
+ ...t,
109
+ open: false,
110
+ }
111
+ : t,
112
+ ),
113
+ }
114
+ }
115
+ case 'REMOVE_TOAST':
116
+ if (action.toastId === undefined) {
117
+ return {
118
+ ...state,
119
+ toasts: [],
120
+ }
121
+ }
122
+ return {
123
+ ...state,
124
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
125
+ }
126
+ }
127
+ }
128
+
129
+ const listeners: Array<(state: State) => void> = []
130
+
131
+ let memoryState: State = { toasts: [] }
132
+
133
+ function dispatch(action: Action) {
134
+ memoryState = reducer(memoryState, action)
135
+ listeners.forEach((listener) => {
136
+ listener(memoryState)
137
+ })
138
+ }
139
+
140
+ type Toast = Omit<ToasterToast, 'id'>
141
+
142
+ function toast({ ...props }: Toast) {
143
+ const id = genId()
144
+
145
+ const update = (props: ToasterToast) =>
146
+ dispatch({
147
+ type: 'UPDATE_TOAST',
148
+ toast: { ...props, id },
149
+ })
150
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
151
+
152
+ dispatch({
153
+ type: 'ADD_TOAST',
154
+ toast: {
155
+ ...props,
156
+ id,
157
+ open: true,
158
+ onOpenChange: (open) => {
159
+ if (!open) dismiss()
160
+ },
161
+ },
162
+ })
163
+
164
+ return {
165
+ id: id,
166
+ dismiss,
167
+ update,
168
+ }
169
+ }
170
+
171
+ function useToast() {
172
+ const [state, setState] = React.useState<State>(memoryState)
173
+
174
+ React.useEffect(() => {
175
+ listeners.push(setState)
176
+ return () => {
177
+ const index = listeners.indexOf(setState)
178
+ if (index > -1) {
179
+ listeners.splice(index, 1)
180
+ }
181
+ }
182
+ }, [state])
183
+
184
+ return {
185
+ ...state,
186
+ toast,
187
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
188
+ }
189
+ }
190
+
191
+ export { useToast, toast }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener('change', onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener('change', onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,191 @@
1
+ 'use client'
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from 'react'
5
+
6
+ import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
7
+
8
+ const TOAST_LIMIT = 1
9
+ const TOAST_REMOVE_DELAY = 1000000
10
+
11
+ type ToasterToast = ToastProps & {
12
+ id: string
13
+ title?: React.ReactNode
14
+ description?: React.ReactNode
15
+ action?: ToastActionElement
16
+ }
17
+
18
+ const actionTypes = {
19
+ ADD_TOAST: 'ADD_TOAST',
20
+ UPDATE_TOAST: 'UPDATE_TOAST',
21
+ DISMISS_TOAST: 'DISMISS_TOAST',
22
+ REMOVE_TOAST: 'REMOVE_TOAST',
23
+ } as const
24
+
25
+ let count = 0
26
+
27
+ function genId() {
28
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
29
+ return count.toString()
30
+ }
31
+
32
+ type ActionType = typeof actionTypes
33
+
34
+ type Action =
35
+ | {
36
+ type: ActionType['ADD_TOAST']
37
+ toast: ToasterToast
38
+ }
39
+ | {
40
+ type: ActionType['UPDATE_TOAST']
41
+ toast: Partial<ToasterToast>
42
+ }
43
+ | {
44
+ type: ActionType['DISMISS_TOAST']
45
+ toastId?: ToasterToast['id']
46
+ }
47
+ | {
48
+ type: ActionType['REMOVE_TOAST']
49
+ toastId?: ToasterToast['id']
50
+ }
51
+
52
+ interface State {
53
+ toasts: ToasterToast[]
54
+ }
55
+
56
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
57
+
58
+ const addToRemoveQueue = (toastId: string) => {
59
+ if (toastTimeouts.has(toastId)) {
60
+ return
61
+ }
62
+
63
+ const timeout = setTimeout(() => {
64
+ toastTimeouts.delete(toastId)
65
+ dispatch({
66
+ type: 'REMOVE_TOAST',
67
+ toastId: toastId,
68
+ })
69
+ }, TOAST_REMOVE_DELAY)
70
+
71
+ toastTimeouts.set(toastId, timeout)
72
+ }
73
+
74
+ export const reducer = (state: State, action: Action): State => {
75
+ switch (action.type) {
76
+ case 'ADD_TOAST':
77
+ return {
78
+ ...state,
79
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80
+ }
81
+
82
+ case 'UPDATE_TOAST':
83
+ return {
84
+ ...state,
85
+ toasts: state.toasts.map((t) =>
86
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
87
+ ),
88
+ }
89
+
90
+ case 'DISMISS_TOAST': {
91
+ const { toastId } = action
92
+
93
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
94
+ // but I'll keep it here for simplicity
95
+ if (toastId) {
96
+ addToRemoveQueue(toastId)
97
+ } else {
98
+ state.toasts.forEach((toast) => {
99
+ addToRemoveQueue(toast.id)
100
+ })
101
+ }
102
+
103
+ return {
104
+ ...state,
105
+ toasts: state.toasts.map((t) =>
106
+ t.id === toastId || toastId === undefined
107
+ ? {
108
+ ...t,
109
+ open: false,
110
+ }
111
+ : t,
112
+ ),
113
+ }
114
+ }
115
+ case 'REMOVE_TOAST':
116
+ if (action.toastId === undefined) {
117
+ return {
118
+ ...state,
119
+ toasts: [],
120
+ }
121
+ }
122
+ return {
123
+ ...state,
124
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
125
+ }
126
+ }
127
+ }
128
+
129
+ const listeners: Array<(state: State) => void> = []
130
+
131
+ let memoryState: State = { toasts: [] }
132
+
133
+ function dispatch(action: Action) {
134
+ memoryState = reducer(memoryState, action)
135
+ listeners.forEach((listener) => {
136
+ listener(memoryState)
137
+ })
138
+ }
139
+
140
+ type Toast = Omit<ToasterToast, 'id'>
141
+
142
+ function toast({ ...props }: Toast) {
143
+ const id = genId()
144
+
145
+ const update = (props: ToasterToast) =>
146
+ dispatch({
147
+ type: 'UPDATE_TOAST',
148
+ toast: { ...props, id },
149
+ })
150
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
151
+
152
+ dispatch({
153
+ type: 'ADD_TOAST',
154
+ toast: {
155
+ ...props,
156
+ id,
157
+ open: true,
158
+ onOpenChange: (open) => {
159
+ if (!open) dismiss()
160
+ },
161
+ },
162
+ })
163
+
164
+ return {
165
+ id: id,
166
+ dismiss,
167
+ update,
168
+ }
169
+ }
170
+
171
+ function useToast() {
172
+ const [state, setState] = React.useState<State>(memoryState)
173
+
174
+ React.useEffect(() => {
175
+ listeners.push(setState)
176
+ return () => {
177
+ const index = listeners.indexOf(setState)
178
+ if (index > -1) {
179
+ listeners.splice(index, 1)
180
+ }
181
+ }
182
+ }, [state])
183
+
184
+ return {
185
+ ...state,
186
+ toast,
187
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
188
+ }
189
+ }
190
+
191
+ export { useToast, toast }
@@ -0,0 +1,75 @@
1
+
2
+ "use client"
3
+
4
+ import React, { createContext, useContext, useEffect, useState } from "react"
5
+ import { DemoDataSource } from "./demo-strategy"
6
+ import { ProductionDataSource } from "./production-strategy"
7
+ import { DataSourceStrategy } from "./types"
8
+
9
+ interface ClientConfig {
10
+ id: string
11
+ name: string
12
+ match: string
13
+ color: string
14
+ icon: string | null
15
+ description: string
16
+ steps: string[]
17
+ configType: string
18
+ }
19
+
20
+ interface DashboardContextType {
21
+ strategy: DataSourceStrategy
22
+ isDemo: boolean
23
+ toggleDemo: () => void
24
+ refreshSignal: number
25
+ clientConfigs: ClientConfig[]
26
+ }
27
+
28
+ const DashboardContext = createContext<DashboardContextType | undefined>(undefined)
29
+
30
+ export function DashboardProvider({ children }: { children: React.ReactNode }) {
31
+ const [isDemo, setIsDemo] = useState(false)
32
+ const [strategy, setStrategy] = useState<DataSourceStrategy>(new ProductionDataSource())
33
+ const [refreshSignal, setRefreshSignal] = useState(0)
34
+ const [clientConfigs, setClientConfigs] = useState<ClientConfig[]>([])
35
+
36
+ // Load configuration on mount
37
+ useEffect(() => {
38
+ // Load client config
39
+ fetch("/clients.json")
40
+ .then(res => res.json())
41
+ .then(data => setClientConfigs(data))
42
+ .catch(err => console.error("Failed to load client configs:", err))
43
+ }, [])
44
+
45
+ const toggleDemo = () => {
46
+ const newState = !isDemo
47
+ setIsDemo(newState)
48
+ setStrategy(newState ? new DemoDataSource() : new ProductionDataSource())
49
+ setRefreshSignal(prev => prev + 1)
50
+ }
51
+
52
+ // Refresh data periodically (centralized trigger)
53
+ useEffect(() => {
54
+ if (isDemo) return // No auto-refresh in Demo Mode (static data)
55
+
56
+ const interval = setInterval(() => {
57
+ setRefreshSignal(prev => prev + 1)
58
+ }, 5000)
59
+ return () => clearInterval(interval)
60
+ }, [isDemo])
61
+
62
+ return (
63
+ <DashboardContext.Provider value={{ strategy, isDemo, toggleDemo, refreshSignal, clientConfigs }}>
64
+ {children}
65
+ </DashboardContext.Provider>
66
+ )
67
+ }
68
+
69
+ export function useDashboard() {
70
+ const context = useContext(DashboardContext)
71
+ if (context === undefined) {
72
+ throw new Error("useDashboard must be used within a DashboardProvider")
73
+ }
74
+ return context
75
+ }
@@ -0,0 +1,110 @@
1
+
2
+ import { DashboardData, DataSourceStrategy, TimeSeriesData } from "./types"
3
+
4
+ export class DemoDataSource implements DataSourceStrategy {
5
+ async fetchGlobalStats(): Promise<DashboardData> {
6
+ // 1. Generate full time-series history for sparklines
7
+ const generateSeries = (start: number, count: number, variance: number) => {
8
+ const series = [start]
9
+ for (let i = 1; i < count; i++) {
10
+ const change = (Math.random() - 0.5) * variance
11
+ series.push(Math.max(0, series[i - 1] + change))
12
+ }
13
+ return series
14
+ }
15
+
16
+ const exactData = {
17
+ memory: generateSeries(12000, 20, 500),
18
+ clients: generateSeries(40, 20, 2),
19
+ success: generateSeries(98, 20, 0.5).map((v) => Math.min(100, v)),
20
+ requests: generateSeries(85000, 20, 1000),
21
+ }
22
+
23
+ // 2. Generate stable, rich Audit Log history
24
+ const mockClients = ["Antigravity", "Claude", "Cursor", "ChatGPT", "Copilot"]
25
+ const mockOps = ["Create", "Read", "Update", "Delete"]
26
+ const demoLogs = Array.from({ length: 50 }).map((_, i) => ({
27
+ id: i,
28
+ date: new Date(Date.now() - i * 1000 * 60 * 5),
29
+ client: mockClients[i % mockClients.length],
30
+ operation: mockOps[i % mockOps.length],
31
+ status: i % 10 === 0 ? "Error" : "Success",
32
+ description:
33
+ i % 10 === 0 ? "Rate limit exceeded" : "Operation completed successfully",
34
+ timestamp: Date.now() - i * 1000 * 60 * 5,
35
+ }))
36
+
37
+ return {
38
+ stats: {
39
+ memoryRecords: Math.round(exactData.memory[exactData.memory.length - 1]),
40
+ totalClients: Math.round(exactData.clients[exactData.clients.length - 1]),
41
+ successRate: Number(
42
+ exactData.success[exactData.success.length - 1].toFixed(1)
43
+ ),
44
+ totalRequests: Math.round(
45
+ exactData.requests[exactData.requests.length - 1]
46
+ ),
47
+ topWriter: { name: "Antigravity", count: 4200 },
48
+ topReader: { name: "Claude", count: 3100 },
49
+ lastWriter: { name: "VS Code", timestamp: Date.now() - 1000 * 60 * 2 },
50
+ lastReader: { name: "Cursor", timestamp: Date.now() - 1000 * 30 },
51
+ },
52
+ trends: {
53
+ memory: {
54
+ change: "+450",
55
+ trend: "up",
56
+ hasData: true,
57
+ data: exactData.memory,
58
+ },
59
+ clients: {
60
+ change: "+5",
61
+ trend: "up",
62
+ hasData: true,
63
+ data: exactData.clients,
64
+ },
65
+ success: {
66
+ change: "+0.5%",
67
+ trend: "up",
68
+ hasData: true,
69
+ data: exactData.success,
70
+ },
71
+ requests: {
72
+ change: "+1.2k",
73
+ trend: "up",
74
+ hasData: true,
75
+ data: exactData.requests,
76
+ },
77
+ },
78
+ logs: demoLogs,
79
+ }
80
+ }
81
+
82
+ async getChartData(period: string): Promise<TimeSeriesData> {
83
+ const clients = ["Antigravity", "Claude", "Cursor", "ChatGPT"]
84
+ const now = Math.floor(Date.now() / 1000)
85
+ // Generate 20 points
86
+ const points = 20
87
+ const interval = 300 // 5 mins
88
+
89
+ const generateSeries = () => {
90
+ return Array.from({ length: points }).map((_, i) => {
91
+ const time = now - (points - 1 - i) * interval
92
+ const point: any = { time }
93
+ clients.forEach(c => {
94
+ // Random value between 0 and 10
95
+ point[c] = Math.floor(Math.random() * 10)
96
+ })
97
+ return point
98
+ })
99
+ }
100
+
101
+ return {
102
+ creates: generateSeries(),
103
+ reads: generateSeries(),
104
+ updates: generateSeries(),
105
+ deletes: generateSeries(),
106
+ // Metadata is now handled globally via clients.json, so we don't return partial overrides here
107
+ metadata: {}
108
+ }
109
+ }
110
+ }