@saccolabs/tars 1.8.2 → 1.9.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 (48) hide show
  1. package/context/skills/gws-setup/SKILL.md +71 -0
  2. package/dist/auth/workspace-auth-service.d.ts +10 -0
  3. package/dist/auth/workspace-auth-service.js +78 -0
  4. package/dist/auth/workspace-auth-service.js.map +1 -0
  5. package/dist/cli/commands/setup.js +158 -5
  6. package/dist/cli/commands/setup.js.map +1 -1
  7. package/dist/cli/commands/stop.js +3 -0
  8. package/dist/cli/commands/stop.js.map +1 -1
  9. package/dist/supervisor/dashboard-service.d.ts +12 -0
  10. package/dist/supervisor/dashboard-service.js +109 -0
  11. package/dist/supervisor/dashboard-service.js.map +1 -0
  12. package/dist/supervisor/gemini-engine.js +26 -4
  13. package/dist/supervisor/gemini-engine.js.map +1 -1
  14. package/dist/supervisor/main.js +4 -0
  15. package/dist/supervisor/main.js.map +1 -1
  16. package/package.json +2 -1
  17. package/stock_apps/dashboard/DEPLOY.md +30 -0
  18. package/stock_apps/dashboard/README.md +36 -0
  19. package/stock_apps/dashboard/dash.log +134 -0
  20. package/stock_apps/dashboard/eslint.config.mjs +19 -0
  21. package/stock_apps/dashboard/next.config.ts +12 -0
  22. package/stock_apps/dashboard/package-lock.json +8581 -0
  23. package/stock_apps/dashboard/package.json +42 -0
  24. package/stock_apps/dashboard/postcss.config.mjs +5 -0
  25. package/stock_apps/dashboard/public/file.svg +1 -0
  26. package/stock_apps/dashboard/public/globe.svg +1 -0
  27. package/stock_apps/dashboard/public/next.svg +1 -0
  28. package/stock_apps/dashboard/public/tars-logo.png +0 -0
  29. package/stock_apps/dashboard/public/vercel.svg +1 -0
  30. package/stock_apps/dashboard/public/window.svg +1 -0
  31. package/stock_apps/dashboard/server.js +488 -0
  32. package/stock_apps/dashboard/src/app/globals.css +122 -0
  33. package/stock_apps/dashboard/src/app/icon.png +0 -0
  34. package/stock_apps/dashboard/src/app/layout.tsx +35 -0
  35. package/stock_apps/dashboard/src/app/page.tsx +170 -0
  36. package/stock_apps/dashboard/src/components/FileExplorer.tsx +238 -0
  37. package/stock_apps/dashboard/src/components/IntelligencePanel.tsx +322 -0
  38. package/stock_apps/dashboard/src/components/MetricsPanel.tsx +347 -0
  39. package/stock_apps/dashboard/src/components/SystemActions.tsx +168 -0
  40. package/stock_apps/dashboard/src/context/SocketContext.tsx +62 -0
  41. package/stock_apps/dashboard/src/lib/socket.ts +10 -0
  42. package/stock_apps/dashboard/tsconfig.json +27 -0
  43. package/dist/discord/discord-bot.d.ts +0 -37
  44. package/dist/discord/discord-bot.js +0 -210
  45. package/dist/discord/discord-bot.js.map +0 -1
  46. package/dist/discord/message-formatter.d.ts +0 -95
  47. package/dist/discord/message-formatter.js +0 -482
  48. package/dist/discord/message-formatter.js.map +0 -1
@@ -0,0 +1,322 @@
1
+ 'use client';
2
+ import { useEffect, useState, memo } from 'react';
3
+ import { useSocket } from '@/context/SocketContext';
4
+ import {
5
+ Brain,
6
+ Calendar,
7
+ History,
8
+ Fingerprint,
9
+ Database,
10
+ Sparkles,
11
+ Hash,
12
+ Clock,
13
+ CheckCircle2,
14
+ AlertCircle
15
+ } from 'lucide-react';
16
+ import { motion, AnimatePresence } from 'framer-motion';
17
+
18
+ const SectionHeader = ({ icon: Icon, title, color }: any) => (
19
+ <div className="flex items-center gap-3 mb-3">
20
+ <div
21
+ className={`p-1.5 rounded-lg bg-opacity-10 ${color.replace('text-', 'bg-')} border border-white/5`}
22
+ >
23
+ <Icon size={12} className={color} />
24
+ </div>
25
+ <h3 className="text-[10px] font-black text-white uppercase tracking-[0.2em]">{title}</h3>
26
+ <div className="h-[1px] bg-white/5 flex-1 ml-4"></div>
27
+ </div>
28
+ );
29
+
30
+ const MemoryItem = memo(({ fact, value }: { fact: string; value: any }) => {
31
+ const displayValue =
32
+ typeof value === 'object' && value !== null
33
+ ? value.value || JSON.stringify(value)
34
+ : String(value);
35
+
36
+ return (
37
+ <div className="flex flex-col gap-1 p-1.5 rounded-lg hover:bg-white/[0.03] transition-colors border border-transparent hover:border-white/5 group">
38
+ <div className="flex items-center gap-2">
39
+ <Fingerprint
40
+ size={10}
41
+ className="text-accent-primary opacity-60 group-hover:opacity-100 transition-opacity"
42
+ />
43
+ <span className="text-[9px] font-black text-accent-primary uppercase tracking-tighter">
44
+ {fact.replace(/_/g, ' ')}
45
+ </span>
46
+ </div>
47
+ <span className="text-[11px] text-white font-medium leading-relaxed pl-4 break-words">
48
+ {displayValue}
49
+ </span>
50
+ </div>
51
+ );
52
+ });
53
+
54
+ const TaskItem = memo(({ task }: { task: any }) => {
55
+ const isEnabled = task.enabled;
56
+ return (
57
+ <div className="flex items-center gap-3 p-2.5 rounded-xl bg-white/[0.02] border border-white/5 hover:border-accent-warning/30 transition-all group">
58
+ <div
59
+ className={`p-1.5 rounded-lg ${isEnabled ? 'bg-accent-warning/10' : 'bg-white/5 opacity-40'}`}
60
+ >
61
+ <Calendar size={14} className={isEnabled ? 'text-accent-warning' : 'text-white'} />
62
+ </div>
63
+ <div className="flex-1 flex flex-col min-w-0">
64
+ <span
65
+ className={`text-[10px] font-black truncate uppercase tracking-tight ${isEnabled ? 'text-white' : 'text-white/30'}`}
66
+ >
67
+ {task.title}
68
+ </span>
69
+ <span className="text-[8px] text-white/40 font-mono mt-0.5 truncate italic">
70
+ {task.schedule}
71
+ </span>
72
+ </div>
73
+ <div className="flex flex-col items-end gap-1">
74
+ {task.lastRun && (
75
+ <div className="flex items-center gap-1.5 text-[8px] font-bold text-accent-secondary uppercase">
76
+ <CheckCircle2 size={10} />
77
+ <span className="text-white">Success</span>
78
+ </div>
79
+ )}
80
+ {!isEnabled && (
81
+ <span className="text-[8px] font-black text-white/20 uppercase tracking-widest border border-white/10 px-1.5 py-0.5 rounded">
82
+ Paused
83
+ </span>
84
+ )}
85
+ </div>
86
+ </div>
87
+ );
88
+ });
89
+
90
+ export const CognitiveBuffer = memo(({ data }: { data: any }) => {
91
+ const factsList = data.facts?.facts || data.facts || {};
92
+ return (
93
+ <div className="card bg-[#0c0c0c] border-white/10 flex flex-col h-[400px] p-4">
94
+ <div className="card-header-btop">Cognitive Buffer (Memory)</div>
95
+ <div className="mt-2 flex-1 overflow-y-auto custom-scrollbar pr-2">
96
+ <SectionHeader icon={Brain} title="Long-Term Facts" color="text-accent-primary" />
97
+ <div className="flex flex-col gap-1.5">
98
+ {Object.entries(factsList).map(([key, value]: any) => (
99
+ <MemoryItem key={key} fact={key} value={value} />
100
+ ))}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ );
105
+ });
106
+
107
+ export const JobQueue = memo(({ data }: { data: any }) => {
108
+ return (
109
+ <div className="card bg-[#0c0c0c] border-white/10 flex flex-col h-[400px] p-4">
110
+ <div className="card-header-btop">Autonomous Job Queue</div>
111
+ <div className="mt-2 flex-1 overflow-y-auto custom-scrollbar pr-2">
112
+ <SectionHeader
113
+ icon={Calendar}
114
+ title="Scheduled Actions"
115
+ color="text-accent-warning"
116
+ />
117
+ <div className="flex flex-col gap-2">
118
+ {data.tasks && data.tasks.length > 0 ? (
119
+ data.tasks.map((task: any) => <TaskItem key={task.id} task={task} />)
120
+ ) : (
121
+ <div className="p-12 text-center opacity-20 italic text-[10px] uppercase font-black tracking-widest text-white">
122
+ No Active Tasks
123
+ </div>
124
+ )}
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ });
130
+
131
+ export const SessionIntelligence = memo(({ data }: { data: any }) => {
132
+ return (
133
+ <div className="card bg-[#0c0c0c] border-white/10 flex flex-col h-[400px] p-4 shadow-2xl">
134
+ <div className="card-header-btop text-accent-primary border-accent-primary/30">
135
+ Session Intelligence
136
+ </div>
137
+ <div className="mt-2 flex-1 overflow-y-auto custom-scrollbar pr-2">
138
+ <SectionHeader
139
+ icon={Sparkles}
140
+ title="Current Context"
141
+ color="text-accent-primary"
142
+ />
143
+
144
+ <div className="grid grid-cols-2 gap-2.5 mb-4">
145
+ <div className="bg-white/5 rounded-xl p-2.5 border border-white/5">
146
+ <span className="text-[8px] font-black text-white/40 uppercase block mb-1">
147
+ Interactions
148
+ </span>
149
+ <span className="text-base font-black text-white font-mono leading-none">
150
+ {data.session?.interactionCount || 0}
151
+ </span>
152
+ </div>
153
+ <div className="bg-white/5 rounded-xl p-2.5 border border-white/5">
154
+ <span className="text-[8px] font-black text-white/40 uppercase block mb-1">
155
+ Context Depth
156
+ </span>
157
+ <span className="text-base font-black text-white font-mono leading-none">
158
+ {Math.round((data.session?.totalNetTokens || 0) / 1000)}K
159
+ </span>
160
+ </div>
161
+ </div>
162
+
163
+ <SectionHeader
164
+ icon={Database}
165
+ title="Token Telemetry"
166
+ color="text-accent-secondary"
167
+ />
168
+ <div className="space-y-3 px-1">
169
+ {[
170
+ {
171
+ label: 'Input Tokens',
172
+ value: data.session?.totalInputTokens,
173
+ color: 'bg-accent-primary',
174
+ max: data.session?.totalNetTokens || 1000000
175
+ },
176
+ {
177
+ label: 'Output Tokens',
178
+ value: data.session?.totalOutputTokens,
179
+ color: 'bg-accent-secondary',
180
+ max: (data.session?.totalNetTokens || 1000000) / 10
181
+ },
182
+ {
183
+ label: 'Cached Tokens',
184
+ value: data.session?.totalCachedTokens,
185
+ color: 'bg-accent-warning',
186
+ max: data.session?.totalNetTokens || 1000000
187
+ }
188
+ ].map((stat) => (
189
+ <div key={stat.label} className="space-y-1">
190
+ <div className="flex justify-between items-baseline">
191
+ <span className="text-[8px] font-black text-white/40 uppercase tracking-tighter">
192
+ {stat.label}
193
+ </span>
194
+ <span className="text-[9px] font-bold text-white font-mono">
195
+ {(stat.value || 0).toLocaleString()}
196
+ </span>
197
+ </div>
198
+ <div className="h-1 bg-white/5 rounded-full overflow-hidden">
199
+ <motion.div
200
+ initial={{ width: 0 }}
201
+ animate={{
202
+ width: `${Math.min(100, ((stat.value || 0) / stat.max) * 100)}%`
203
+ }}
204
+ className={`h-full ${stat.color} opacity-60`}
205
+ />
206
+ </div>
207
+ </div>
208
+ ))}
209
+ </div>
210
+
211
+ <div className="mt-8">
212
+ <SectionHeader
213
+ icon={History}
214
+ title="Session Lifecycle"
215
+ color="text-accent-warning"
216
+ />
217
+ </div>
218
+ <div className="grid grid-cols-2 gap-2 mb-4">
219
+ <div className="bg-white/5 rounded-xl p-2.5 border border-white/5">
220
+ <div className="flex items-center gap-1.5 mb-1">
221
+ <Database size={8} className="text-white/40" />
222
+ <span className="text-[8px] font-black text-white/40 uppercase block">
223
+ Total Sessions
224
+ </span>
225
+ </div>
226
+ <span className="text-sm font-black text-white font-mono">
227
+ {data.sessionStats?.total || 0}
228
+ </span>
229
+ </div>
230
+ <div className="bg-white/5 rounded-xl p-2.5 border border-white/5">
231
+ <div className="flex items-center gap-1.5 mb-1">
232
+ <Clock size={8} className="text-white/40" />
233
+ <span className="text-[8px] font-black text-white/40 uppercase block">
234
+ Last Switch
235
+ </span>
236
+ </div>
237
+ <span className="text-[10px] font-bold text-white font-mono leading-tight">
238
+ {data.sessionStats?.lastSwitch
239
+ ? new Date(data.sessionStats.lastSwitch).toLocaleTimeString([], {
240
+ hour: '2-digit',
241
+ minute: '2-digit',
242
+ second: '2-digit'
243
+ })
244
+ : 'N/A'}
245
+ </span>
246
+ </div>
247
+ </div>
248
+
249
+ <div className="space-y-1.5 max-h-[120px] overflow-y-auto custom-scrollbar pr-1">
250
+ {data.sessionStats?.history?.map((s: any, i: number) => (
251
+ <div
252
+ key={i}
253
+ className="flex justify-between items-center p-2 rounded-lg bg-white/[0.02] border border-white/5 hover:bg-white/[0.04] transition-colors group"
254
+ >
255
+ <div className="flex items-center gap-2">
256
+ <div className="w-1 h-1 rounded-full bg-accent-primary opacity-40 group-hover:opacity-100 transition-opacity" />
257
+ <span className="text-[9px] font-mono font-bold text-white/60">
258
+ S_{s.id.slice(0, 6)}
259
+ </span>
260
+ </div>
261
+ <span className="text-[8px] font-mono text-white/30">
262
+ {new Date(s.time).toLocaleDateString()}{' '}
263
+ {new Date(s.time).toLocaleTimeString([], {
264
+ hour: '2-digit',
265
+ minute: '2-digit'
266
+ })}
267
+ </span>
268
+ </div>
269
+ ))}
270
+ </div>
271
+
272
+ <div className="mt-6 p-3 bg-accent-primary/5 border border-accent-primary/20 rounded-xl flex items-center gap-3">
273
+ <div className="p-1.5 bg-accent-primary/20 rounded-lg">
274
+ <Hash size={14} className="text-accent-primary" />
275
+ </div>
276
+ <div className="flex flex-col">
277
+ <span className="text-[8px] font-black text-white/40 uppercase leading-none">
278
+ Session_ID
279
+ </span>
280
+ <span className="text-[9px] font-mono text-white mt-1 break-all">
281
+ {data.session?.sessionId?.split('-')[0]}...
282
+ </span>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ });
289
+
290
+ export function IntelligencePanel() {
291
+ const { socket, subscribe, unsubscribe } = useSocket();
292
+ const [data, setData] = useState<any>(null);
293
+
294
+ useEffect(() => {
295
+ if (!socket) return;
296
+ subscribe('intelligence');
297
+
298
+ const handleInit = (initData: any) => setData(initData);
299
+ const handleUpdate = ({ type, data: updateData }: any) => {
300
+ setData((prev: any) => ({ ...prev, [type]: updateData }));
301
+ };
302
+
303
+ socket.on('intelligence_init', handleInit);
304
+ socket.on('intelligence_update', handleUpdate);
305
+
306
+ return () => {
307
+ unsubscribe('intelligence');
308
+ socket.off('intelligence_init', handleInit);
309
+ socket.off('intelligence_update', handleUpdate);
310
+ };
311
+ }, [socket, subscribe, unsubscribe]);
312
+
313
+ if (!data) return null;
314
+
315
+ return (
316
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
317
+ <CognitiveBuffer data={data} />
318
+ <JobQueue data={data} />
319
+ <SessionIntelligence data={data} />
320
+ </div>
321
+ );
322
+ }
@@ -0,0 +1,347 @@
1
+ 'use client';
2
+ import { useEffect, useState, useRef, memo, useCallback } from 'react';
3
+ import { useSocket } from '@/context/SocketContext';
4
+ import {
5
+ Terminal,
6
+ Cpu,
7
+ MemoryStick as Memory,
8
+ Database,
9
+ Activity,
10
+ Clock,
11
+ Server,
12
+ RefreshCw,
13
+ Zap,
14
+ HardDrive,
15
+ ArrowUpRight,
16
+ ArrowDownLeft
17
+ } from 'lucide-react';
18
+ import { motion } from 'framer-motion';
19
+
20
+ const MetricCard = memo(({ icon: Icon, label, value, unit, color, extra, children }: any) => (
21
+ <div className="card card-with-header relative hover:border-accent-primary/40 transition-all border-white/5 bg-[#0c0c0c] p-4">
22
+ <div className="card-header-btop">{label}</div>
23
+ <div className="flex items-center justify-between mb-4 mt-2">
24
+ <div className="flex items-center gap-3">
25
+ <div
26
+ className={`p-1.5 rounded-lg bg-opacity-10 ${color.replace('text-', 'bg-')} border border-white/5`}
27
+ >
28
+ <Icon size={18} className={color} />
29
+ </div>
30
+ <span className={`text-[14px] font-black font-mono text-white`}>{value}</span>
31
+ <span className={`text-[9px] font-bold opacity-40 text-white uppercase`}>
32
+ {unit}
33
+ </span>
34
+ </div>
35
+ </div>
36
+
37
+ <div className="w-full bg-white/5 h-1.5 rounded-full overflow-hidden relative mb-2">
38
+ <motion.div
39
+ initial={{ width: 0 }}
40
+ animate={{ width: `${Math.min(100, parseFloat(value))}%` }}
41
+ transition={{ duration: 1, ease: 'easeOut' }}
42
+ className={`h-full ${color.replace('text-', 'bg-')} shadow-[0_0:10px_currentColor]`}
43
+ />
44
+ </div>
45
+
46
+ {extra && (
47
+ <div className="flex justify-between items-center text-[9px] font-bold text-white uppercase mt-3 tracking-widest opacity-60">
48
+ {extra}
49
+ </div>
50
+ )}
51
+ {children}
52
+ </div>
53
+ ));
54
+
55
+ export const LogViewer = memo(() => {
56
+ const { socket, subscribe, unsubscribe } = useSocket();
57
+ const [logs, setLogs] = useState<string[]>([]);
58
+ const scrollRef = useRef<HTMLDivElement>(null);
59
+
60
+ useEffect(() => {
61
+ if (!socket) return;
62
+ subscribe('logs');
63
+
64
+ const handleLogsInit = (data: string[]) => setLogs(data);
65
+ const handleLogsUpdate = (newLines: string[]) =>
66
+ setLogs((prev) => [...prev, ...newLines].slice(-300));
67
+
68
+ socket.on('logs_init', handleLogsInit);
69
+ socket.on('logs_update', handleLogsUpdate);
70
+
71
+ return () => {
72
+ unsubscribe('logs');
73
+ socket.off('logs_init', handleLogsInit);
74
+ socket.off('logs_update', handleLogsUpdate);
75
+ };
76
+ }, [socket, subscribe, unsubscribe]);
77
+
78
+ useEffect(() => {
79
+ if (scrollRef.current) {
80
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
81
+ }
82
+ }, [logs]);
83
+
84
+ return (
85
+ <div className="card card-with-header flex flex-col h-[400px] shadow-2xl transition-all border-white/10 bg-black/40 p-0 overflow-visible relative">
86
+ <div className="card-header-btop text-accent-secondary border-accent-secondary/30">
87
+ Supervisor Feed
88
+ </div>
89
+ <div className="flex items-center justify-between p-3 border-b border-white/5 mb-1 bg-[#0a0a0a]/50 mt-4">
90
+ <div className="flex items-center gap-3">
91
+ <span className="text-[8px] text-white/40 uppercase flex items-center gap-2 font-bold tracking-widest">
92
+ <Server size={10} className="text-accent-primary opacity-50" />{' '}
93
+ pm2_supervisor
94
+ <span className="w-1.5 h-1.5 bg-accent-secondary rounded-full animate-pulse shadow-[0_0_8px_#10B981]"></span>
95
+ </span>
96
+ </div>
97
+ </div>
98
+
99
+ <div
100
+ ref={scrollRef}
101
+ className="flex-1 overflow-y-auto p-4 font-mono text-[10px] md:text-[11px] leading-relaxed custom-scrollbar selection:bg-accent-primary selection:text-white rounded-b-2xl"
102
+ >
103
+ {logs.length === 0 ? (
104
+ <div className="flex flex-col items-center justify-center h-full gap-4 opacity-30">
105
+ <RefreshCw size={24} className="animate-spin text-accent-primary" />
106
+ <span className="font-black tracking-[0.3em] uppercase text-[9px] text-white">
107
+ Establishing Stream...
108
+ </span>
109
+ </div>
110
+ ) : (
111
+ logs.map((log, i) => {
112
+ const isError = log.includes('[ERR]');
113
+ const isWarn = log.includes('[WARN]');
114
+
115
+ // Clean up Gemini events for readability
116
+ let displayLog = log;
117
+ if (log.includes('Raw Gemini Event')) {
118
+ try {
119
+ const match = log.match(/\[Turn (\d+)\]: (\{.*\})/);
120
+ if (match) {
121
+ const turn = match[1];
122
+ const data = JSON.parse(match[2]);
123
+ displayLog = `[Turn ${turn}] GEMINI_${data.type.toUpperCase()}: ${typeof data.value === 'string' ? data.value.slice(0, 100) + (data.value.length > 100 ? '...' : '') : JSON.stringify(data.value)}`;
124
+ }
125
+ } catch (e) {}
126
+ }
127
+
128
+ return (
129
+ <div
130
+ key={i}
131
+ className={`flex gap-4 py-1 border-b border-white/[0.03] hover:bg-white/5 transition-all ${isError ? 'text-accent-danger font-bold' : isWarn ? 'text-accent-warning font-bold' : 'text-white/80'}`}
132
+ >
133
+ <span className="opacity-20 shrink-0 w-10 select-none font-black text-right">
134
+ {(i + 1).toString().padStart(3, '0')}
135
+ </span>
136
+ <span className="break-all whitespace-pre-wrap flex-1">
137
+ {displayLog}
138
+ </span>
139
+ </div>
140
+ );
141
+ })
142
+ )}
143
+ </div>
144
+ </div>
145
+ );
146
+ });
147
+
148
+ export function MetricsPanel() {
149
+ const { socket, subscribe, unsubscribe } = useSocket();
150
+ const [metrics, setMetrics] = useState<any>(null);
151
+
152
+ useEffect(() => {
153
+ if (!socket) return;
154
+ subscribe('metrics');
155
+
156
+ const handleMetrics = (data: any) => setMetrics(data);
157
+ socket.on('metrics_update', handleMetrics);
158
+
159
+ return () => {
160
+ unsubscribe('metrics');
161
+ socket.off('metrics_update', handleMetrics);
162
+ };
163
+ }, [socket, subscribe, unsubscribe]);
164
+
165
+ const formatUptime = useCallback((seconds: number) => {
166
+ const d = Math.floor(seconds / (3600 * 24));
167
+ const h = Math.floor((seconds % (3600 * 24)) / 3600);
168
+ const m = Math.floor((seconds % 3600) / 60);
169
+ return `${d}d ${h}h ${m}m`;
170
+ }, []);
171
+
172
+ if (!metrics)
173
+ return (
174
+ <div className="flex flex-col items-center justify-center p-20 gap-6 opacity-30">
175
+ <RefreshCw size={32} className="animate-spin text-accent-primary" />
176
+ <span className="text-white font-black tracking-[0.5em] uppercase text-xs">
177
+ Syncing...
178
+ </span>
179
+ </div>
180
+ );
181
+
182
+ return (
183
+ <div className="flex flex-col gap-8">
184
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
185
+ <MetricCard
186
+ icon={Cpu}
187
+ label="Processor"
188
+ value={metrics.cpu.load}
189
+ unit="%"
190
+ color="text-accent-primary"
191
+ extra={
192
+ <div className="flex items-center justify-between w-full">
193
+ <div className="flex items-center gap-2">
194
+ <Clock size={10} className="opacity-50" />
195
+ <span className="text-white">
196
+ UP: {formatUptime(metrics.uptime)}
197
+ </span>
198
+ </div>
199
+ <span className="text-white font-bold">{metrics.cpu.temp}°C</span>
200
+ </div>
201
+ }
202
+ >
203
+ <div className="grid grid-cols-8 gap-1 mt-3">
204
+ {metrics.cpu.cpus?.map((load: string, i: number) => (
205
+ <div
206
+ key={i}
207
+ className="h-4 bg-white/5 w-full overflow-hidden rounded-sm relative group"
208
+ >
209
+ <div
210
+ className="absolute bottom-0 left-0 right-0 bg-accent-primary opacity-60"
211
+ style={{ height: `${load}%` }}
212
+ />
213
+ </div>
214
+ ))}
215
+ </div>
216
+ </MetricCard>
217
+
218
+ <MetricCard
219
+ icon={Memory}
220
+ label="Memory"
221
+ value={metrics.mem.usage}
222
+ unit="%"
223
+ color="text-accent-primary"
224
+ extra={
225
+ <div className="flex justify-between w-full">
226
+ <span className="text-white font-bold">{metrics.mem.used}G USED</span>
227
+ <span className="text-white opacity-40">{metrics.mem.total}G</span>
228
+ </div>
229
+ }
230
+ >
231
+ <div className="flex flex-col gap-1.5 mt-3">
232
+ <div className="flex justify-between items-center text-[8px] font-black opacity-40 uppercase text-white px-1">
233
+ <span>Cached: {metrics.mem.cached}G</span>
234
+ <span>Swap: {metrics.mem.swapUsed}G</span>
235
+ </div>
236
+ </div>
237
+ </MetricCard>
238
+
239
+ {metrics.gpu && (
240
+ <MetricCard
241
+ icon={Zap}
242
+ label="Graphics (AMD)"
243
+ value={metrics.gpu.usage}
244
+ unit="%"
245
+ color="text-accent-secondary"
246
+ extra={
247
+ <div className="flex justify-between w-full">
248
+ <span className="text-white font-bold">
249
+ {metrics.gpu.memUsed}G VRAM
250
+ </span>
251
+ <span className="text-white opacity-40">{metrics.gpu.temp}°C</span>
252
+ </div>
253
+ }
254
+ >
255
+ <div className="flex flex-col gap-1.5 mt-3">
256
+ <div className="flex justify-between items-center bg-white/[0.03] p-1.5 px-2.5 rounded-lg border border-white/5">
257
+ <span className="text-[8px] font-black opacity-40 uppercase text-white">
258
+ Power
259
+ </span>
260
+ <span className="text-[11px] text-white font-black font-mono">
261
+ {metrics.gpu.power}W
262
+ </span>
263
+ </div>
264
+ <div className="flex justify-between items-center bg-white/[0.03] p-1.5 px-2.5 rounded-lg border border-white/5">
265
+ <span className="text-[8px] font-black opacity-40 uppercase text-white">
266
+ Clock
267
+ </span>
268
+ <span className="text-[11px] text-white font-black font-mono">
269
+ {metrics.gpu.clock}
270
+ </span>
271
+ </div>
272
+ </div>
273
+ </MetricCard>
274
+ )}
275
+
276
+ <MetricCard
277
+ icon={HardDrive}
278
+ label="Storage"
279
+ value={metrics.disks[0]?.use || 0}
280
+ unit="%"
281
+ color="text-accent-warning"
282
+ extra={
283
+ <span className="text-white truncate max-w-full">
284
+ {metrics.disks[0]?.mount}
285
+ </span>
286
+ }
287
+ >
288
+ <div className="flex flex-col gap-1.5 mt-3">
289
+ {metrics.disks.slice(1, 3).map((d: any, i: number) => (
290
+ <div
291
+ key={i}
292
+ className="flex justify-between items-center bg-white/[0.03] p-1.5 px-2.5 rounded-lg border border-white/5"
293
+ >
294
+ <span className="text-[8px] font-black opacity-40 uppercase text-white truncate max-w-[60px]">
295
+ {d.mount}
296
+ </span>
297
+ <span className="text-[10px] text-white font-black font-mono">
298
+ {d.use}%
299
+ </span>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ </MetricCard>
304
+
305
+ <MetricCard
306
+ icon={Activity}
307
+ label="Traffic"
308
+ value={metrics.net[0]?.rx || 0}
309
+ unit=" KB/s"
310
+ color="text-accent-secondary"
311
+ >
312
+ <div className="flex flex-col gap-1.5 mt-3">
313
+ {metrics.net.slice(0, 1).map((n: any, i: number) => (
314
+ <div key={i} className="flex flex-col gap-1">
315
+ <div className="flex justify-between items-center bg-white/[0.03] p-1.5 px-2.5 rounded-lg border border-white/5">
316
+ <div className="flex items-center gap-2">
317
+ <ArrowDownLeft
318
+ size={10}
319
+ className="text-accent-secondary"
320
+ />
321
+ <span className="text-[8px] font-black opacity-40 uppercase text-white">
322
+ {n.iface} In
323
+ </span>
324
+ </div>
325
+ <span className="text-[11px] text-white font-black font-mono">
326
+ {n.rx}
327
+ </span>
328
+ </div>
329
+ <div className="flex justify-between items-center bg-white/[0.03] p-1.5 px-2.5 rounded-lg border border-white/5">
330
+ <div className="flex items-center gap-2">
331
+ <ArrowUpRight size={10} className="text-accent-warning" />
332
+ <span className="text-[8px] font-black opacity-40 uppercase text-white">
333
+ {n.iface} Out
334
+ </span>
335
+ </div>
336
+ <span className="text-[11px] text-white font-black font-mono">
337
+ {n.tx}
338
+ </span>
339
+ </div>
340
+ </div>
341
+ ))}
342
+ </div>
343
+ </MetricCard>
344
+ </div>
345
+ </div>
346
+ );
347
+ }