@gravito/zenith 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/ARCHITECTURE.md +88 -0
  2. package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
  3. package/DEMO.md +156 -0
  4. package/DEPLOYMENT.md +157 -0
  5. package/DOCS_INTERNAL.md +73 -0
  6. package/Dockerfile +46 -0
  7. package/Dockerfile.demo-worker +29 -0
  8. package/EVOLUTION_BLUEPRINT.md +112 -0
  9. package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
  10. package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
  11. package/QUICK_TEST_GUIDE.md +72 -0
  12. package/README.md +33 -0
  13. package/ROADMAP.md +85 -0
  14. package/TESTING_BATCH_OPERATIONS.md +252 -0
  15. package/bin/flux-console.ts +2 -0
  16. package/dist/bin.js +108196 -0
  17. package/dist/client/assets/index-DGYEwTDL.css +1 -0
  18. package/dist/client/assets/index-oyTdySX0.js +421 -0
  19. package/dist/client/index.html +13 -0
  20. package/dist/server/index.js +108191 -0
  21. package/docker-compose.yml +40 -0
  22. package/docs/integrations/LARAVEL.md +207 -0
  23. package/package.json +50 -0
  24. package/postcss.config.js +6 -0
  25. package/scripts/flood-logs.ts +21 -0
  26. package/scripts/seed.ts +213 -0
  27. package/scripts/verify-throttle.ts +45 -0
  28. package/scripts/worker.ts +123 -0
  29. package/src/bin.ts +6 -0
  30. package/src/client/App.tsx +70 -0
  31. package/src/client/Layout.tsx +644 -0
  32. package/src/client/Sidebar.tsx +102 -0
  33. package/src/client/ThroughputChart.tsx +135 -0
  34. package/src/client/WorkerStatus.tsx +170 -0
  35. package/src/client/components/ConfirmDialog.tsx +103 -0
  36. package/src/client/components/JobInspector.tsx +524 -0
  37. package/src/client/components/LogArchiveModal.tsx +383 -0
  38. package/src/client/components/NotificationBell.tsx +203 -0
  39. package/src/client/components/Toaster.tsx +80 -0
  40. package/src/client/components/UserProfileDropdown.tsx +177 -0
  41. package/src/client/contexts/AuthContext.tsx +93 -0
  42. package/src/client/contexts/NotificationContext.tsx +103 -0
  43. package/src/client/index.css +174 -0
  44. package/src/client/index.html +12 -0
  45. package/src/client/main.tsx +15 -0
  46. package/src/client/pages/LoginPage.tsx +153 -0
  47. package/src/client/pages/MetricsPage.tsx +408 -0
  48. package/src/client/pages/OverviewPage.tsx +511 -0
  49. package/src/client/pages/QueuesPage.tsx +372 -0
  50. package/src/client/pages/SchedulesPage.tsx +531 -0
  51. package/src/client/pages/SettingsPage.tsx +449 -0
  52. package/src/client/pages/WorkersPage.tsx +316 -0
  53. package/src/client/pages/index.ts +7 -0
  54. package/src/client/utils.ts +6 -0
  55. package/src/server/index.ts +556 -0
  56. package/src/server/middleware/auth.ts +127 -0
  57. package/src/server/services/AlertService.ts +160 -0
  58. package/src/server/services/QueueService.ts +828 -0
  59. package/tailwind.config.js +73 -0
  60. package/tests/placeholder.test.ts +7 -0
  61. package/tsconfig.json +38 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.ts +27 -0
@@ -0,0 +1,177 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { AnimatePresence, motion } from 'framer-motion'
3
+ import { Activity, ChevronRight, Clock, LogOut, Server, Settings, Shield, User } from 'lucide-react'
4
+ import { useEffect, useRef, useState } from 'react'
5
+ import { useNavigate } from 'react-router-dom'
6
+ import { useAuth } from '../contexts/AuthContext'
7
+
8
+ export function UserProfileDropdown() {
9
+ const [isOpen, setIsOpen] = useState(false)
10
+ const { isAuthEnabled, logout } = useAuth()
11
+ const navigate = useNavigate()
12
+ const dropdownRef = useRef<HTMLDivElement>(null)
13
+
14
+ const { data: systemStatus } = useQuery<any>({
15
+ queryKey: ['system-status'],
16
+ queryFn: () => fetch('/api/system/status').then((res) => res.json()),
17
+ refetchInterval: 30000,
18
+ })
19
+
20
+ // Close dropdown when clicking outside
21
+ useEffect(() => {
22
+ const handleClickOutside = (event: MouseEvent) => {
23
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
24
+ setIsOpen(false)
25
+ }
26
+ }
27
+
28
+ if (isOpen) {
29
+ document.addEventListener('mousedown', handleClickOutside)
30
+ }
31
+
32
+ return () => {
33
+ document.removeEventListener('mousedown', handleClickOutside)
34
+ }
35
+ }, [isOpen])
36
+
37
+ const handleLogout = async () => {
38
+ await logout()
39
+ setIsOpen(false)
40
+ }
41
+
42
+ const handleNavigate = (path: string) => {
43
+ navigate(path)
44
+ setIsOpen(false)
45
+ }
46
+
47
+ const formatUptime = (seconds: number) => {
48
+ const hours = Math.floor(seconds / 3600)
49
+ const minutes = Math.floor((seconds % 3600) / 60)
50
+ if (hours > 0) {
51
+ return `${hours}h ${minutes}m`
52
+ }
53
+ return `${minutes}m`
54
+ }
55
+
56
+ return (
57
+ <div className="relative" ref={dropdownRef}>
58
+ {/* Trigger */}
59
+ <button
60
+ type="button"
61
+ className="flex items-center gap-3 cursor-pointer group outline-none"
62
+ onClick={() => setIsOpen(!isOpen)}
63
+ >
64
+ <div className="text-right hidden sm:block">
65
+ <p className="text-sm font-black tracking-tight group-hover:text-primary transition-colors">
66
+ Admin User
67
+ </p>
68
+ <p className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-widest">
69
+ Architect
70
+ </p>
71
+ </div>
72
+ <div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-primary to-indigo-600 flex items-center justify-center text-primary-foreground font-black shadow-lg shadow-primary/20 group-hover:scale-105 transition-transform">
73
+ <User size={20} />
74
+ </div>
75
+ </button>
76
+
77
+ {/* Dropdown */}
78
+ <AnimatePresence>
79
+ {isOpen && (
80
+ <motion.div
81
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
82
+ animate={{ opacity: 1, y: 0, scale: 1 }}
83
+ exit={{ opacity: 0, y: 10, scale: 0.95 }}
84
+ transition={{ duration: 0.2 }}
85
+ className="absolute right-0 top-full mt-2 w-72 bg-card border rounded-2xl shadow-2xl overflow-hidden z-50"
86
+ >
87
+ {/* User Info Header */}
88
+ <div className="p-5 bg-gradient-to-br from-primary/10 to-indigo-500/10 border-b">
89
+ <div className="flex items-center gap-4">
90
+ <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-indigo-600 flex items-center justify-center text-primary-foreground shadow-lg">
91
+ <User size={28} />
92
+ </div>
93
+ <div>
94
+ <h3 className="font-bold text-lg">Admin User</h3>
95
+ <p className="text-xs text-muted-foreground">System Administrator</p>
96
+ <div className="flex items-center gap-1 mt-1">
97
+ <Shield size={10} className="text-green-500" />
98
+ <span className="text-[9px] font-bold text-green-500 uppercase tracking-widest">
99
+ Full Access
100
+ </span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ {/* System Status */}
107
+ <div className="p-4 bg-muted/20 border-b">
108
+ <p className="text-[9px] font-black text-muted-foreground/50 uppercase tracking-widest mb-3">
109
+ System Status
110
+ </p>
111
+ <div className="grid grid-cols-2 gap-3">
112
+ <div className="flex items-center gap-2">
113
+ <Activity size={12} className="text-green-500" />
114
+ <span className="text-[10px] font-bold">Online</span>
115
+ </div>
116
+ <div className="flex items-center gap-2">
117
+ <Clock size={12} className="text-muted-foreground" />
118
+ <span className="text-[10px] font-bold">
119
+ {systemStatus?.uptime ? formatUptime(systemStatus.uptime) : '...'}
120
+ </span>
121
+ </div>
122
+ <div className="flex items-center gap-2">
123
+ <Server size={12} className="text-muted-foreground" />
124
+ <span className="text-[10px] font-bold">{systemStatus?.node || '...'}</span>
125
+ </div>
126
+ <div className="flex items-center gap-2">
127
+ <span className="w-3 h-3 text-[10px] font-black">🔥</span>
128
+ <span className="text-[10px] font-bold">
129
+ {systemStatus?.memory?.rss || '...'}
130
+ </span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ {/* Menu Items */}
136
+ <div className="p-2">
137
+ <button
138
+ type="button"
139
+ onClick={() => handleNavigate('/settings')}
140
+ className="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-muted/50 transition-colors group"
141
+ >
142
+ <div className="flex items-center gap-3">
143
+ <Settings
144
+ size={18}
145
+ className="text-muted-foreground group-hover:text-foreground transition-colors"
146
+ />
147
+ <span className="text-sm font-semibold">Settings</span>
148
+ </div>
149
+ <ChevronRight size={16} className="text-muted-foreground/50" />
150
+ </button>
151
+
152
+ {isAuthEnabled && (
153
+ <button
154
+ type="button"
155
+ onClick={handleLogout}
156
+ className="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-red-500/10 transition-colors group"
157
+ >
158
+ <div className="flex items-center gap-3">
159
+ <LogOut size={18} className="text-red-500" />
160
+ <span className="text-sm font-semibold text-red-500">Logout</span>
161
+ </div>
162
+ </button>
163
+ )}
164
+ </div>
165
+
166
+ {/* Footer */}
167
+ <div className="px-4 py-3 bg-muted/10 border-t">
168
+ <p className="text-[9px] text-center text-muted-foreground/50">
169
+ Flux Console v0.1.0-alpha.1
170
+ </p>
171
+ </div>
172
+ </motion.div>
173
+ )}
174
+ </AnimatePresence>
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,93 @@
1
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react'
2
+
3
+ interface AuthContextType {
4
+ isAuthenticated: boolean
5
+ isAuthEnabled: boolean
6
+ isLoading: boolean
7
+ login: (password: string) => Promise<{ success: boolean; error?: string }>
8
+ logout: () => Promise<void>
9
+ checkAuth: () => Promise<void>
10
+ }
11
+
12
+ const AuthContext = createContext<AuthContextType | null>(null)
13
+
14
+ export function useAuth() {
15
+ const context = useContext(AuthContext)
16
+ if (!context) {
17
+ throw new Error('useAuth must be used within an AuthProvider')
18
+ }
19
+ return context
20
+ }
21
+
22
+ interface AuthProviderProps {
23
+ children: ReactNode
24
+ }
25
+
26
+ export function AuthProvider({ children }: AuthProviderProps) {
27
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
28
+ const [isAuthEnabled, setIsAuthEnabled] = useState(false)
29
+ const [isLoading, setIsLoading] = useState(true)
30
+
31
+ const checkAuth = useCallback(async () => {
32
+ try {
33
+ const res = await fetch('/api/auth/status')
34
+ const data = await res.json()
35
+ setIsAuthEnabled(data.enabled)
36
+ setIsAuthenticated(data.authenticated)
37
+ } catch (err) {
38
+ console.error('Failed to check auth status:', err)
39
+ setIsAuthenticated(false)
40
+ } finally {
41
+ setIsLoading(false)
42
+ }
43
+ }, [])
44
+
45
+ const login = async (password: string): Promise<{ success: boolean; error?: string }> => {
46
+ try {
47
+ const res = await fetch('/api/auth/login', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ password }),
51
+ })
52
+ const data = await res.json()
53
+
54
+ if (data.success) {
55
+ setIsAuthenticated(true)
56
+ return { success: true }
57
+ }
58
+
59
+ return { success: false, error: data.error || 'Login failed' }
60
+ } catch (_err) {
61
+ return { success: false, error: 'Network error' }
62
+ }
63
+ }
64
+
65
+ const logout = async () => {
66
+ try {
67
+ await fetch('/api/auth/logout', { method: 'POST' })
68
+ } catch (err) {
69
+ console.error('Logout error:', err)
70
+ } finally {
71
+ setIsAuthenticated(false)
72
+ }
73
+ }
74
+
75
+ useEffect(() => {
76
+ checkAuth()
77
+ }, [checkAuth])
78
+
79
+ return (
80
+ <AuthContext.Provider
81
+ value={{
82
+ isAuthenticated,
83
+ isAuthEnabled,
84
+ isLoading,
85
+ login,
86
+ logout,
87
+ checkAuth,
88
+ }}
89
+ >
90
+ {children}
91
+ </AuthContext.Provider>
92
+ )
93
+ }
@@ -0,0 +1,103 @@
1
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react'
2
+
3
+ export interface Notification {
4
+ id: string
5
+ type: 'info' | 'success' | 'warning' | 'error'
6
+ title: string
7
+ message: string
8
+ timestamp: number
9
+ read: boolean
10
+ source?: string
11
+ }
12
+
13
+ interface NotificationContextType {
14
+ notifications: Notification[]
15
+ unreadCount: number
16
+ addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void
17
+ markAsRead: (id: string) => void
18
+ markAllAsRead: () => void
19
+ clearAll: () => void
20
+ removeNotification: (id: string) => void
21
+ }
22
+
23
+ const NotificationContext = createContext<NotificationContextType | null>(null)
24
+
25
+ export function useNotifications() {
26
+ const context = useContext(NotificationContext)
27
+ if (!context) {
28
+ throw new Error('useNotifications must be used within a NotificationProvider')
29
+ }
30
+ return context
31
+ }
32
+
33
+ interface NotificationProviderProps {
34
+ children: ReactNode
35
+ }
36
+
37
+ export function NotificationProvider({ children }: NotificationProviderProps) {
38
+ const [notifications, setNotifications] = useState<Notification[]>([])
39
+
40
+ const unreadCount = notifications.filter((n) => !n.read).length
41
+
42
+ const addNotification = useCallback(
43
+ (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => {
44
+ const newNotification: Notification = {
45
+ ...notification,
46
+ id: crypto.randomUUID(),
47
+ timestamp: Date.now(),
48
+ read: false,
49
+ }
50
+ setNotifications((prev) => [newNotification, ...prev].slice(0, 50)) // Keep max 50 notifications
51
+ },
52
+ []
53
+ )
54
+
55
+ const markAsRead = useCallback((id: string) => {
56
+ setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)))
57
+ }, [])
58
+
59
+ const markAllAsRead = useCallback(() => {
60
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
61
+ }, [])
62
+
63
+ const clearAll = useCallback(() => {
64
+ setNotifications([])
65
+ }, [])
66
+
67
+ const removeNotification = useCallback((id: string) => {
68
+ setNotifications((prev) => prev.filter((n) => n.id !== id))
69
+ }, [])
70
+
71
+ // Listen to the global log update event dispatched by Layout's SSE stream
72
+ useEffect(() => {
73
+ const handler = (e: any) => {
74
+ const log = e.detail
75
+ if (log.level === 'error' || log.level === 'warn') {
76
+ addNotification({
77
+ type: log.level === 'error' ? 'error' : 'warning',
78
+ title: log.level === 'error' ? 'Job Failed' : 'Warning',
79
+ message: log.message || 'An event occurred',
80
+ source: log.queue || log.source,
81
+ })
82
+ }
83
+ }
84
+ window.addEventListener('flux-log-update', handler)
85
+ return () => window.removeEventListener('flux-log-update', handler)
86
+ }, [addNotification])
87
+
88
+ return (
89
+ <NotificationContext.Provider
90
+ value={{
91
+ notifications,
92
+ unreadCount,
93
+ addNotification,
94
+ markAsRead,
95
+ markAllAsRead,
96
+ clearAll,
97
+ removeNotification,
98
+ }}
99
+ >
100
+ {children}
101
+ </NotificationContext.Provider>
102
+ )
103
+ }
@@ -0,0 +1,174 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 224 71.4% 4.1%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 224 71.4% 4.1%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 224 71.4% 4.1%;
13
+ --primary: 238.7 83.5% 66.7%;
14
+ /* Indigo 500 */
15
+ --primary-foreground: 210 20% 98%;
16
+ --secondary: 220 14.3% 95.9%;
17
+ --secondary-foreground: 238.7 83.5% 66.7%;
18
+ --muted: 220 14.3% 95.9%;
19
+ --muted-foreground: 220 8.9% 46.1%;
20
+ --accent: 220 14.3% 95.9%;
21
+ --accent-foreground: 238.7 83.5% 66.7%;
22
+ --destructive: 0 84.2% 60.2%;
23
+ --destructive-foreground: 210 20% 98%;
24
+ --border: 220 13% 91%;
25
+ --input: 220 13% 91%;
26
+ --ring: 238.7 83.5% 66.7%;
27
+ --radius: 1rem;
28
+ }
29
+
30
+ .dark {
31
+ --background: 222 47% 2%;
32
+ --foreground: 213 31% 91%;
33
+ --card: 222 47% 6%;
34
+ --card-foreground: 213 31% 91%;
35
+ --popover: 222 47% 4%;
36
+ --popover-foreground: 213 31% 91%;
37
+ --primary: 238.7 83.5% 66.7%;
38
+ --primary-foreground: 222 47% 4%;
39
+ --secondary: 222 47% 10%;
40
+ --secondary-foreground: 213 31% 91%;
41
+ --muted: 222 47% 8%;
42
+ --muted-foreground: 215.4 16.3% 56.9%;
43
+ --accent: 222 47% 12%;
44
+ --accent-foreground: 213 31% 91%;
45
+ --destructive: 0 62.8% 30.6%;
46
+ --destructive-foreground: 210 20% 98%;
47
+ --border: 222 47% 14%;
48
+ --input: 222 47% 12%;
49
+ --ring: 238.7 83.5% 66.7%;
50
+ }
51
+ }
52
+
53
+ @layer base {
54
+ * {
55
+ @apply border-border;
56
+ }
57
+
58
+ body {
59
+ @apply bg-background text-foreground antialiased;
60
+ font-feature-settings: "ss01", "ss02", "cv01", "cv02", "cv03";
61
+ }
62
+
63
+ .dark body {
64
+ background-image: radial-gradient(at 50% 0%, hsla(238, 83%, 66%, 0.05) 0%, transparent 50%),
65
+ radial-gradient(at 100% 100%, hsla(238, 83%, 66%, 0.02) 0%, transparent 50%);
66
+ background-attachment: fixed;
67
+ }
68
+ }
69
+
70
+ @layer utilities {
71
+ .scrollbar-thin::-webkit-scrollbar {
72
+ width: 6px;
73
+ height: 6px;
74
+ }
75
+
76
+ .scrollbar-thin::-webkit-scrollbar-track {
77
+ background: transparent;
78
+ }
79
+
80
+ .scrollbar-thin::-webkit-scrollbar-thumb {
81
+ @apply bg-muted-foreground/20 rounded-full hover:bg-muted-foreground/40 transition-colors;
82
+ }
83
+
84
+ .glass {
85
+ @apply bg-card/60 backdrop-blur-xl border-white/10;
86
+ }
87
+
88
+ .scanline {
89
+ position: relative;
90
+ overflow: hidden;
91
+ }
92
+
93
+ .scanline::after {
94
+ content: " ";
95
+ display: block;
96
+ position: absolute;
97
+ top: 0;
98
+ left: 0;
99
+ right: 0;
100
+ bottom: 0;
101
+ background: linear-gradient(to bottom,
102
+ transparent 50%,
103
+ rgba(var(--primary), 0.02) 50%);
104
+ background-size: 100% 4px;
105
+ z-index: 2;
106
+ pointer-events: none;
107
+ animation: scanline-move 10s linear infinite;
108
+ }
109
+
110
+ @keyframes scanline-move {
111
+ 0% {
112
+ background-position: 0 0;
113
+ }
114
+
115
+ 100% {
116
+ background-position: 0 100%;
117
+ }
118
+ }
119
+
120
+ .glow-pulse {
121
+ animation: glow-pulse 2.5s ease-in-out infinite;
122
+ }
123
+
124
+ @keyframes glow-pulse {
125
+
126
+ 0%,
127
+ 100% {
128
+ opacity: 0.3;
129
+ transform: scale(1);
130
+ filter: blur(2px);
131
+ }
132
+
133
+ 50% {
134
+ opacity: 0.7;
135
+ transform: scale(1.5);
136
+ filter: blur(4px);
137
+ }
138
+ }
139
+
140
+ .card-premium {
141
+ @apply bg-card border border-border/50 shadow-sm transition-all duration-300;
142
+ }
143
+
144
+ .dark .card-premium {
145
+ background-color: hsla(222, 47%, 6%, 0.6);
146
+ backdrop-filter: blur(12px);
147
+ border-color: rgba(255, 255, 255, 0.05);
148
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
149
+ }
150
+
151
+ .dark .card-premium:hover {
152
+ border-color: hsla(238, 83%, 66%, 0.2);
153
+ box-shadow: 0 8px 30px rgba(99, 102, 241, 0.05);
154
+ }
155
+ }
156
+
157
+ .animate-in {
158
+ animation-duration: 400ms;
159
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
160
+ fill-mode: forwards;
161
+ }
162
+
163
+ @keyframes toast-progress {
164
+ from {
165
+ transform: scaleX(1);
166
+ }
167
+ to {
168
+ transform: scaleX(0);
169
+ }
170
+ }
171
+
172
+ .animate-toast-progress {
173
+ animation: toast-progress 5s linear forwards;
174
+ }
@@ -0,0 +1,12 @@
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>Gravito Zenith</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,15 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import React from 'react'
3
+ import ReactDOM from 'react-dom/client'
4
+ import App from './App'
5
+ import './index.css'
6
+
7
+ const queryClient = new QueryClient()
8
+
9
+ ReactDOM.createRoot(document.getElementById('root')!).render(
10
+ <React.StrictMode>
11
+ <QueryClientProvider client={queryClient}>
12
+ <App />
13
+ </QueryClientProvider>
14
+ </React.StrictMode>
15
+ )