groove-dev 0.27.57 → 0.27.59

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 (46) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +126 -7
  4. package/node_modules/@groove-dev/daemon/src/conversations.js +2 -5
  5. package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
  6. package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +1 -0
  7. package/{packages/gui/dist/assets/index-X58BAjGp.js → node_modules/@groove-dev/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
  8. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  9. package/node_modules/@groove-dev/gui/package.json +1 -1
  10. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +1 -1
  11. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +49 -24
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +9 -36
  14. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -2
  15. package/node_modules/@groove-dev/gui/src/components/network/activity-stream.jsx +105 -0
  16. package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +166 -0
  17. package/node_modules/@groove-dev/gui/src/components/network/fleet-table.jsx +190 -0
  18. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +135 -0
  19. package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +1 -1
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -4
  21. package/node_modules/@groove-dev/gui/src/views/network.jsx +128 -55
  22. package/package.json +1 -1
  23. package/packages/cli/package.json +1 -1
  24. package/packages/daemon/package.json +1 -1
  25. package/packages/daemon/src/api.js +126 -7
  26. package/packages/daemon/src/conversations.js +2 -5
  27. package/packages/daemon/src/providers/groove-network.js +1 -1
  28. package/packages/gui/dist/assets/index-BrfCzrxJ.css +1 -0
  29. package/{node_modules/@groove-dev/gui/dist/assets/index-X58BAjGp.js → packages/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
  30. package/packages/gui/dist/index.html +2 -2
  31. package/packages/gui/package.json +1 -1
  32. package/packages/gui/src/components/chat/chat-header.jsx +1 -1
  33. package/packages/gui/src/components/chat/chat-input.jsx +1 -1
  34. package/packages/gui/src/components/chat/chat-messages.jsx +49 -24
  35. package/packages/gui/src/components/chat/chat-view.jsx +9 -36
  36. package/packages/gui/src/components/layout/activity-bar.jsx +2 -2
  37. package/packages/gui/src/components/network/activity-stream.jsx +105 -0
  38. package/packages/gui/src/components/network/compute-header.jsx +166 -0
  39. package/packages/gui/src/components/network/fleet-table.jsx +190 -0
  40. package/packages/gui/src/components/network/network-health.jsx +135 -0
  41. package/packages/gui/src/components/network/node-toggle.jsx +1 -1
  42. package/packages/gui/src/stores/groove.js +57 -4
  43. package/packages/gui/src/views/network.jsx +128 -55
  44. package/ai-chat/CHAT_MASTER_PLAN.md +0 -184
  45. package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +0 -1
  46. package/packages/gui/dist/assets/index-C5WTeZO4.css +0 -1
@@ -0,0 +1,190 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo, useState, useMemo } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { HEX } from '../../lib/theme-hex';
6
+ import { ScrollArea } from '../ui/scroll-area';
7
+ import { StatusDot } from '../ui/status-dot';
8
+ import { Badge } from '../ui/badge';
9
+ import { ArrowUp, ArrowDown } from 'lucide-react';
10
+
11
+ function shortAddr(addr) {
12
+ if (!addr || typeof addr !== 'string') return '\u2014';
13
+ if (addr.length < 14) return addr;
14
+ return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
15
+ }
16
+
17
+ function fmtMb(mb) {
18
+ if (!mb && mb !== 0) return '\u2014';
19
+ if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
20
+ return `${Math.round(mb)} MB`;
21
+ }
22
+
23
+ function statusMap(s) {
24
+ if (s === 'active') return 'running';
25
+ if (s === 'connecting') return 'starting';
26
+ return 'crashed';
27
+ }
28
+
29
+ function fmtUptime(secs) {
30
+ if (!secs && secs !== 0) return '\u2014';
31
+ const d = Math.floor(secs / 86400);
32
+ const h = Math.floor((secs % 86400) / 3600);
33
+ const m = Math.floor((secs % 3600) / 60);
34
+ if (d > 0) return `${d}d ${h}h`;
35
+ if (h > 0) return `${h}h ${m}m`;
36
+ return `${m}m`;
37
+ }
38
+
39
+ const COLUMNS = [
40
+ { key: 'status', label: 'STATUS', w: 'w-[52px]' },
41
+ { key: 'node', label: 'NODE', w: 'flex-1 min-w-[90px]' },
42
+ { key: 'device', label: 'DEVICE', w: 'w-[72px]' },
43
+ { key: 'gpu', label: 'GPU', w: 'w-[90px]' },
44
+ { key: 'ram', label: 'RAM', w: 'w-[88px]', numeric: true },
45
+ { key: 'vram', label: 'VRAM', w: 'w-[88px]', numeric: true },
46
+ { key: 'cpu', label: 'CPU', w: 'w-[42px]', numeric: true },
47
+ { key: 'gpuUtil', label: 'GPU%', w: 'w-[48px]', numeric: true },
48
+ { key: 'layers', label: 'LAYERS', w: 'w-[60px]' },
49
+ { key: 'sessions', label: 'SESS', w: 'w-[42px]', numeric: true },
50
+ { key: 'uptime', label: 'UPTIME', w: 'w-[60px]' },
51
+ ];
52
+
53
+ function getSortValue(node, key) {
54
+ switch (key) {
55
+ case 'status': return node.status === 'active' ? 0 : node.status === 'connecting' ? 1 : 2;
56
+ case 'node': return (node.node_id || node.nodeId || '').toLowerCase();
57
+ case 'device': return (node.device || '').toLowerCase();
58
+ case 'gpu': return (node.gpu_model || '').toLowerCase();
59
+ case 'ram': return node.ram_mb || 0;
60
+ case 'vram': return node.vram_mb || 0;
61
+ case 'cpu': return node.cpu_cores || 0;
62
+ case 'layers': return Array.isArray(node.layers) ? node.layers[0] : 0;
63
+ case 'gpuUtil': return node.gpu_utilization_pct ?? -1;
64
+ case 'sessions': return node.sessions || node.active_sessions || 0;
65
+ case 'uptime': return node.uptime_seconds || 0;
66
+ default: return 0;
67
+ }
68
+ }
69
+
70
+ export const FleetTable = memo(function FleetTable() {
71
+ const nodes = useGrooveStore((s) => s.networkStatus.nodes || []);
72
+ const ownNodeId = useGrooveStore((s) => s.networkNode.nodeId);
73
+ const [sortKey, setSortKey] = useState('status');
74
+ const [sortAsc, setSortAsc] = useState(true);
75
+
76
+ function handleSort(key) {
77
+ if (sortKey === key) setSortAsc((v) => !v);
78
+ else { setSortKey(key); setSortAsc(true); }
79
+ }
80
+
81
+ const sorted = useMemo(() => {
82
+ const list = [...nodes];
83
+ list.sort((a, b) => {
84
+ const va = getSortValue(a, sortKey);
85
+ const vb = getSortValue(b, sortKey);
86
+ if (va < vb) return sortAsc ? -1 : 1;
87
+ if (va > vb) return sortAsc ? 1 : -1;
88
+ return 0;
89
+ });
90
+ return list;
91
+ }, [nodes, sortKey, sortAsc]);
92
+
93
+ return (
94
+ <div className="flex flex-col h-full">
95
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0 flex items-center justify-between">
96
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Node Fleet</span>
97
+ <span className="text-2xs font-mono text-text-3 tabular-nums">{nodes.length} nodes</span>
98
+ </div>
99
+
100
+ <div className="flex items-center gap-0 px-3 py-1 flex-shrink-0" style={{ background: 'rgba(51,175,188,0.04)' }}>
101
+ {COLUMNS.map((col) => (
102
+ <button
103
+ key={col.key}
104
+ onClick={() => handleSort(col.key)}
105
+ className={cn(
106
+ 'flex items-center gap-0.5 text-2xs font-mono text-text-4 uppercase tracking-wider cursor-pointer hover:text-text-2 transition-colors',
107
+ col.w,
108
+ col.numeric && 'justify-end',
109
+ )}
110
+ >
111
+ {col.label}
112
+ {sortKey === col.key && (
113
+ sortAsc ? <ArrowUp size={8} /> : <ArrowDown size={8} />
114
+ )}
115
+ </button>
116
+ ))}
117
+ </div>
118
+
119
+ <ScrollArea className="flex-1 min-h-0">
120
+ {sorted.length === 0 ? (
121
+ <div className="px-3 py-6 text-2xs font-mono text-text-4 text-center">No nodes online</div>
122
+ ) : (
123
+ <div>
124
+ {sorted.map((n, i) => {
125
+ const id = n.node_id || n.nodeId || '';
126
+ const isSelf = ownNodeId && id && id === ownNodeId;
127
+ const layersLabel = Array.isArray(n.layers) ? `${n.layers[0]}-${n.layers[1]}` : n.layers || '\u2014';
128
+ const isActive = n.status === 'active' || n.status === 'connecting';
129
+ const gpuPct = n.gpu_utilization_pct;
130
+ const gpuClr = gpuPct == null ? undefined : gpuPct > 80 ? HEX.danger : gpuPct > 50 ? HEX.warning : HEX.success;
131
+ const ramPct = n.ram_pct || (n.ram_mb > 0 && n.ram_used_mb != null ? (n.ram_used_mb / n.ram_mb) * 100 : null);
132
+ const vramPct = n.vram_mb > 0 && n.vram_used_mb != null ? (n.vram_used_mb / n.vram_mb) * 100 : null;
133
+
134
+ return (
135
+ <div
136
+ key={id || i}
137
+ className={cn(
138
+ 'flex items-center gap-0 px-3 py-2 text-xs font-mono transition-colors',
139
+ isSelf ? 'bg-[rgba(51,175,188,0.06)]' : 'hover:bg-[rgba(51,175,188,0.06)]',
140
+ )}
141
+ >
142
+ <div className={cn('flex items-center gap-1.5', COLUMNS[0].w)}>
143
+ <span className="relative flex-shrink-0 w-[6px] h-[6px]">
144
+ <span className="absolute inset-0 rounded-sm" style={{ background: isActive ? HEX.success : HEX.text4 }} />
145
+ {isActive && (
146
+ <span
147
+ className="absolute inset-[-2px] rounded-sm"
148
+ style={{ background: HEX.success, opacity: 0.15, animation: 'node-pulse-bar 2s ease-in-out infinite' }}
149
+ />
150
+ )}
151
+ </span>
152
+ </div>
153
+ <div className={cn('truncate text-text-1', COLUMNS[1].w)}>
154
+ {shortAddr(id)}
155
+ {isSelf && <Badge variant="accent" className="ml-1 text-2xs">You</Badge>}
156
+ </div>
157
+ <div className={cn('truncate text-text-2', COLUMNS[2].w)}>{n.device || '\u2014'}</div>
158
+ <div className={cn('truncate text-text-2', COLUMNS[3].w)}>{n.gpu_model || '\u2014'}</div>
159
+ <div className={cn('text-right tabular-nums', COLUMNS[4].w)}>
160
+ <div className="text-text-2">{n.ram_used_mb != null ? `${fmtMb(n.ram_used_mb)}/${fmtMb(n.ram_mb)}` : fmtMb(n.ram_mb)}</div>
161
+ {ramPct != null && (
162
+ <div className="h-0.5 rounded-sm mt-0.5 overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
163
+ <div className="h-full rounded-sm" style={{ width: `${Math.min(100, ramPct)}%`, background: ramPct > 90 ? HEX.danger : ramPct > 70 ? HEX.warning : HEX.accent }} />
164
+ </div>
165
+ )}
166
+ </div>
167
+ <div className={cn('text-right tabular-nums', COLUMNS[5].w)}>
168
+ <div className="text-text-2">{n.vram_used_mb != null ? `${fmtMb(n.vram_used_mb)}/${fmtMb(n.vram_mb)}` : fmtMb(n.vram_mb)}</div>
169
+ {vramPct != null && (
170
+ <div className="h-0.5 rounded-sm mt-0.5 overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
171
+ <div className="h-full rounded-sm" style={{ width: `${Math.min(100, vramPct)}%`, background: vramPct > 90 ? HEX.danger : vramPct > 70 ? HEX.warning : HEX.info }} />
172
+ </div>
173
+ )}
174
+ </div>
175
+ <div className={cn('text-right text-text-2 tabular-nums', COLUMNS[6].w)}>{n.cpu_cores || '\u2014'}</div>
176
+ <div className={cn('text-right tabular-nums', COLUMNS[7].w)} style={{ color: gpuClr }}>
177
+ {gpuPct != null ? `${Math.round(gpuPct)}%` : '\u2014'}
178
+ </div>
179
+ <div className={cn('text-text-2', COLUMNS[8].w)}>{layersLabel}</div>
180
+ <div className={cn('text-right text-text-2 tabular-nums', COLUMNS[9].w)}>{n.sessions ?? n.active_sessions ?? 0}</div>
181
+ <div className={cn('text-text-2', COLUMNS[10].w)}>{fmtUptime(n.uptime_seconds)}</div>
182
+ </div>
183
+ );
184
+ })}
185
+ </div>
186
+ )}
187
+ </ScrollArea>
188
+ </div>
189
+ );
190
+ });
@@ -0,0 +1,135 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { HEX, hexAlpha } from '../../lib/theme-hex';
6
+ import { fmtNum } from '../../lib/format';
7
+ import { StatusDot } from '../ui/status-dot';
8
+ import { Badge } from '../ui/badge';
9
+ import { ScrollArea } from '../ui/scroll-area';
10
+
11
+ function coverageState(covered, total) {
12
+ if (!total) return { color: HEX.danger, label: 'Insufficient' };
13
+ const pct = covered / total;
14
+ if (pct >= 1) return { color: HEX.success, label: 'Full coverage' };
15
+ if (pct >= 0.5) return { color: HEX.warning, label: 'Partial' };
16
+ return { color: HEX.danger, label: 'Insufficient' };
17
+ }
18
+
19
+ export const NetworkHealth = memo(function NetworkHealth() {
20
+ const status = useGrooveStore((s) => s.networkStatus);
21
+ const signalReachable = useGrooveStore((s) => s.networkStatusReachable);
22
+ const node = useGrooveStore((s) => s.networkNode);
23
+
24
+ const nodes = Array.isArray(status.nodes) ? status.nodes : [];
25
+ const totalLayers = status.totalLayers || 34;
26
+ const covered = status.coverage || 0;
27
+ const coverage = coverageState(covered, totalLayers);
28
+ const coveragePct = totalLayers ? Math.min(100, (covered / totalLayers) * 100) : 0;
29
+ const models = Array.isArray(status.models) ? status.models : [];
30
+
31
+ return (
32
+ <div className="flex flex-col h-full">
33
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
34
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Network Health</span>
35
+ </div>
36
+
37
+ <ScrollArea className="flex-1 min-h-0">
38
+ <div className="px-3 py-2 space-y-3">
39
+ {/* Signal connection */}
40
+ <div className="bg-surface-0 rounded p-2.5">
41
+ <div className="flex items-center gap-2">
42
+ <span className="relative flex-shrink-0 w-[6px] h-[6px]">
43
+ <span className="absolute inset-0 rounded-sm" style={{ background: signalReachable ? HEX.success : HEX.danger }} />
44
+ {signalReachable && (
45
+ <span
46
+ className="absolute inset-[-2px] rounded-sm"
47
+ style={{ background: HEX.success, opacity: 0.15, animation: 'node-pulse-bar 2s ease-in-out infinite' }}
48
+ />
49
+ )}
50
+ </span>
51
+ <span className="text-2xs font-mono text-text-3">Signal</span>
52
+ <span className="text-2xs font-mono text-text-1">signal.groovedev.ai</span>
53
+ <div className="flex-1" />
54
+ <span className="text-2xs font-mono" style={{ color: signalReachable ? HEX.success : HEX.danger }}>
55
+ {signalReachable ? 'Connected' : 'Unreachable'}
56
+ </span>
57
+ </div>
58
+ </div>
59
+
60
+ {/* Coverage */}
61
+ <div className="bg-surface-0 rounded p-2.5">
62
+ <div className="flex items-center justify-between mb-1.5">
63
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Layer Coverage</span>
64
+ <span className="text-2xs font-mono tabular-nums" style={{ color: coverage.color }}>
65
+ {covered}/{totalLayers}
66
+ </span>
67
+ </div>
68
+ <div className="h-0.5 rounded-sm overflow-hidden" style={{ background: hexAlpha(HEX.accent, 0.08) }}>
69
+ <div
70
+ className="h-full rounded-sm transition-all duration-700"
71
+ style={{ width: `${coveragePct}%`, background: coverage.color }}
72
+ />
73
+ </div>
74
+ <div className="mt-1 text-2xs font-mono text-text-4">{coverage.label}</div>
75
+ </div>
76
+
77
+ {/* Session throughput */}
78
+ <div className="bg-surface-0 rounded p-2.5">
79
+ <div className="flex items-center justify-between">
80
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Sessions</span>
81
+ <span className="text-base font-mono font-semibold text-text-0 tabular-nums leading-none">
82
+ {fmtNum(status.activeSessions || 0)}
83
+ </span>
84
+ </div>
85
+ <div className="text-2xs font-mono text-text-4 mt-0.5">Active streams</div>
86
+ </div>
87
+
88
+ {/* Uptime */}
89
+ {node.active && (
90
+ <div className="bg-surface-0 rounded p-2.5">
91
+ <div className="flex items-center justify-between">
92
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Node Status</span>
93
+ <span className="text-2xs font-mono capitalize" style={{ color: node.status === 'connected' ? HEX.success : HEX.warning }}>
94
+ {node.status || 'disconnected'}
95
+ </span>
96
+ </div>
97
+ {node.sessions > 0 && (
98
+ <div className="flex items-center justify-between mt-1.5">
99
+ <span className="text-2xs font-mono text-text-4">Local Sessions</span>
100
+ <span className="text-xs font-mono text-text-1 tabular-nums">{node.sessions}</span>
101
+ </div>
102
+ )}
103
+ </div>
104
+ )}
105
+
106
+ {/* Models */}
107
+ <div>
108
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1.5">Models</div>
109
+ {models.length === 0 ? (
110
+ <div className="text-2xs font-mono text-text-4">No models available</div>
111
+ ) : (
112
+ <div className="space-y-1">
113
+ {models.map((m, i) => {
114
+ const name = typeof m === 'string' ? m : m.name;
115
+ const available = typeof m === 'string' ? true : !!m.available;
116
+ return (
117
+ <div key={i} className="flex items-center gap-2 bg-surface-0 rounded px-2.5 py-1.5">
118
+ <span className="relative flex-shrink-0 w-[5px] h-[5px]">
119
+ <span className="absolute inset-0 rounded-sm" style={{ background: available ? HEX.success : HEX.text4 }} />
120
+ </span>
121
+ <span className="text-xs font-mono text-text-1 truncate flex-1">{name}</span>
122
+ <span className="text-2xs font-mono" style={{ color: available ? HEX.success : HEX.text3 }}>
123
+ {available ? 'Online' : 'Offline'}
124
+ </span>
125
+ </div>
126
+ );
127
+ })}
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ </ScrollArea>
133
+ </div>
134
+ );
135
+ });
@@ -76,7 +76,7 @@ export function NodeToggle() {
76
76
  const memPct = Number.isFinite(node.memoryPct) ? node.memoryPct : null;
77
77
 
78
78
  return (
79
- <div className="rounded-lg border border-border bg-surface-1 overflow-hidden">
79
+ <div className="rounded-sm border border-border bg-surface-1 overflow-hidden">
80
80
  {/* Hero toggle */}
81
81
  <div className="flex items-center gap-4 px-5 py-4 border-b border-border-subtle">
82
82
  <div className="flex-1 min-w-0">
@@ -103,6 +103,7 @@ export const useGrooveStore = create((set, get) => ({
103
103
  networkEvents: [],
104
104
  networkVersion: { installed: null, latest: null, updateAvailable: false },
105
105
  networkUpdateProgress: { updating: false, step: null, message: null, percent: 0, error: null },
106
+ networkCompute: { totalRamMb: 0, totalVramMb: 0, totalCpuCores: 0, totalBandwidthMbps: 0, activeNodes: 0, totalNodes: 0, avgLoad: 0 },
106
107
 
107
108
  // ── Marketplace Auth ───────────────────────────────────────
108
109
  marketplaceUser: null, // { id, displayName, avatar, ... } or null
@@ -683,9 +684,36 @@ export const useGrooveStore = create((set, get) => ({
683
684
  break;
684
685
  }
685
686
 
686
- case 'network:status':
687
- set({ networkStatus: { ...get().networkStatus, ...(msg.data || {}) } });
687
+ case 'network:status': {
688
+ const nsData = msg.data || {};
689
+ const nsUpdate = { networkStatus: { ...get().networkStatus, ...nsData } };
690
+ if (nsData.compute) {
691
+ const c = nsData.compute;
692
+ nsUpdate.networkCompute = {
693
+ totalRamMb: c.totalRamMb ?? c.total_ram_mb ?? 0,
694
+ totalVramMb: c.totalVramMb ?? c.total_vram_mb ?? 0,
695
+ totalCpuCores: c.totalCpuCores ?? c.total_cpu_cores ?? 0,
696
+ totalBandwidthMbps: c.totalBandwidthMbps ?? c.total_bandwidth_mbps ?? 0,
697
+ activeNodes: c.activeNodes ?? c.active_nodes ?? 0,
698
+ totalNodes: c.totalNodes ?? c.total_nodes ?? 0,
699
+ avgLoad: c.avgLoad ?? c.avg_load ?? 0,
700
+ };
701
+ } else if (Array.isArray(nsData.nodes) && nsData.nodes.length > 0) {
702
+ const wsNodes = nsData.nodes;
703
+ const wsActive = wsNodes.filter((n) => n.status === 'active');
704
+ nsUpdate.networkCompute = {
705
+ totalRamMb: wsNodes.reduce((s, n) => s + (n.ram_mb || 0), 0),
706
+ totalVramMb: wsNodes.reduce((s, n) => s + (n.vram_mb || 0), 0),
707
+ totalCpuCores: wsNodes.reduce((s, n) => s + (n.cpu_cores || 0), 0),
708
+ totalBandwidthMbps: wsNodes.reduce((s, n) => s + (n.bandwidth_mbps || 0), 0),
709
+ activeNodes: wsActive.length,
710
+ totalNodes: wsNodes.length,
711
+ avgLoad: wsActive.length > 0 ? wsActive.reduce((s, n) => s + (n.load || 0), 0) / wsActive.length : 0,
712
+ };
713
+ }
714
+ set(nsUpdate);
688
715
  break;
716
+ }
689
717
 
690
718
  case 'network:install:progress': {
691
719
  const { step, message, percent } = msg.data || {};
@@ -2049,10 +2077,35 @@ export const useGrooveStore = create((set, get) => ({
2049
2077
  async fetchNetworkStatus() {
2050
2078
  try {
2051
2079
  const data = await api.get('/network/status');
2052
- set({
2080
+ const update = {
2053
2081
  networkStatus: { ...get().networkStatus, ...(data || {}) },
2054
2082
  networkStatusReachable: true,
2055
- });
2083
+ };
2084
+ if (data?.compute) {
2085
+ const c = data.compute;
2086
+ update.networkCompute = {
2087
+ totalRamMb: c.totalRamMb ?? c.total_ram_mb ?? 0,
2088
+ totalVramMb: c.totalVramMb ?? c.total_vram_mb ?? 0,
2089
+ totalCpuCores: c.totalCpuCores ?? c.total_cpu_cores ?? 0,
2090
+ totalBandwidthMbps: c.totalBandwidthMbps ?? c.total_bandwidth_mbps ?? 0,
2091
+ activeNodes: c.activeNodes ?? c.active_nodes ?? 0,
2092
+ totalNodes: c.totalNodes ?? c.total_nodes ?? 0,
2093
+ avgLoad: c.avgLoad ?? c.avg_load ?? 0,
2094
+ };
2095
+ } else if (Array.isArray(data?.nodes) && data.nodes.length > 0) {
2096
+ const nodes = data.nodes;
2097
+ const active = nodes.filter((n) => n.status === 'active');
2098
+ update.networkCompute = {
2099
+ totalRamMb: nodes.reduce((s, n) => s + (n.ram_mb || 0), 0),
2100
+ totalVramMb: nodes.reduce((s, n) => s + (n.vram_mb || 0), 0),
2101
+ totalCpuCores: nodes.reduce((s, n) => s + (n.cpu_cores || 0), 0),
2102
+ totalBandwidthMbps: nodes.reduce((s, n) => s + (n.bandwidth_mbps || 0), 0),
2103
+ activeNodes: active.length,
2104
+ totalNodes: nodes.length,
2105
+ avgLoad: active.length > 0 ? active.reduce((s, n) => s + (n.load || 0), 0) / active.length : 0,
2106
+ };
2107
+ }
2108
+ set(update);
2056
2109
  return data;
2057
2110
  } catch {
2058
2111
  set({ networkStatusReachable: false });
@@ -1,14 +1,18 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useEffect, useState } from 'react';
3
3
  import { useGrooveStore } from '../stores/groove';
4
- import { ScrollArea } from '../components/ui/scroll-area';
5
4
  import { StatusDot } from '../components/ui/status-dot';
6
5
  import { Badge } from '../components/ui/badge';
7
6
  import { Button } from '../components/ui/button';
8
7
  import { Dialog, DialogContent, DialogTrigger } from '../components/ui/dialog';
8
+ import { ScrollArea } from '../components/ui/scroll-area';
9
+ import { cn } from '../lib/cn';
9
10
  import { NodeToggle } from '../components/network/node-toggle';
10
- import { NodeDetails } from '../components/network/node-details';
11
- import { NetworkStatus } from '../components/network/network-status';
11
+ import { ComputeHeader } from '../components/network/compute-header';
12
+ import { FleetTable } from '../components/network/fleet-table';
13
+ import { ActivityStream } from '../components/network/activity-stream';
14
+ import { NetworkHealth } from '../components/network/network-health';
15
+ import { HEX } from '../lib/theme-hex';
12
16
  import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle } from 'lucide-react';
13
17
 
14
18
  const REQUIREMENTS = [
@@ -30,7 +34,7 @@ function InstallProgress({ progress }) {
30
34
  <div className="flex items-center justify-between text-2xs font-mono text-text-3 tabular-nums">
31
35
  <div className="flex items-center gap-2 text-text-2 font-sans">
32
36
  <Loader2 size={12} className="animate-spin text-accent" />
33
- <span className="truncate">{progress.message || 'Installing'}</span>
37
+ <span className="truncate">{progress.message || 'Installing\u2026'}</span>
34
38
  </div>
35
39
  <span>{percent}%</span>
36
40
  </div>
@@ -119,7 +123,7 @@ function UpdateProgress({ progress }) {
119
123
  <div className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border-subtle bg-surface-0">
120
124
  <Loader2 size={11} className="animate-spin text-accent flex-shrink-0" />
121
125
  <div className="flex flex-col min-w-0">
122
- <span className="text-2xs font-sans text-text-1 truncate">{progress.message || 'Updating'}</span>
126
+ <span className="text-2xs font-sans text-text-1 truncate">{progress.message || 'Updating\u2026'}</span>
123
127
  <div className="h-1 w-32 rounded-full bg-surface-3 overflow-hidden mt-0.5">
124
128
  <div
125
129
  className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
@@ -208,7 +212,7 @@ function UninstallButton() {
208
212
  className="inline-flex items-center gap-1.5 text-2xs font-sans text-text-3 hover:text-danger transition-colors"
209
213
  >
210
214
  <Trash2 size={11} />
211
- Uninstall Network Package
215
+ Uninstall
212
216
  </button>
213
217
  </DialogTrigger>
214
218
  <DialogContent title="Uninstall Network Package" description="Confirm uninstall">
@@ -232,13 +236,88 @@ function UninstallButton() {
232
236
  );
233
237
  }
234
238
 
239
+ function InlineToggle({ value, onChange, disabled }) {
240
+ return (
241
+ <button
242
+ onClick={() => !disabled && onChange(!value)}
243
+ disabled={disabled}
244
+ className={cn(
245
+ 'relative inline-flex h-5 w-9 rounded-full p-0.5 transition-colors flex-shrink-0',
246
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
247
+ value ? 'bg-accent' : 'bg-surface-5',
248
+ )}
249
+ >
250
+ <span
251
+ className={cn(
252
+ 'inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform',
253
+ value ? 'translate-x-4' : 'translate-x-0',
254
+ )}
255
+ />
256
+ </button>
257
+ );
258
+ }
259
+
260
+ function NetworkHeader() {
261
+ const node = useGrooveStore((s) => s.networkNode);
262
+ const installed = useGrooveStore((s) => s.networkInstalled);
263
+ const version = useGrooveStore((s) => s.networkVersion);
264
+ const signalReachable = useGrooveStore((s) => s.networkStatusReachable);
265
+ const startNetworkNode = useGrooveStore((s) => s.startNetworkNode);
266
+ const stopNetworkNode = useGrooveStore((s) => s.stopNetworkNode);
267
+ const [pending, setPending] = useState(false);
268
+
269
+ async function handleToggle(next) {
270
+ setPending(true);
271
+ try {
272
+ if (next) await startNetworkNode();
273
+ else await stopNetworkNode();
274
+ } catch { /* toasted in store */ }
275
+ setPending(false);
276
+ }
277
+
278
+ return (
279
+ <div className="flex items-center gap-4 px-4 py-2 bg-surface-1 border-b border-border flex-shrink-0">
280
+ <h2 className="text-xs font-semibold text-text-0 font-sans tracking-wide uppercase">Network Command Center</h2>
281
+
282
+ {installed && version.installed && (
283
+ <>
284
+ <span className="text-text-4">/</span>
285
+ <span className="text-xs font-mono text-text-2 tabular-nums">v{String(version.installed).replace(/^v/, '')}</span>
286
+ </>
287
+ )}
288
+
289
+ {installed && <UpdateButton />}
290
+
291
+ <div className="flex-1" />
292
+
293
+ {installed && (
294
+ <div className="flex items-center gap-3.5 text-xs font-mono text-text-2">
295
+ <span className="flex items-center gap-1.5">
296
+ <InlineToggle value={!!node.active} onChange={handleToggle} disabled={pending} />
297
+ <span className="text-text-3">{node.active ? 'Contributing' : 'Idle'}</span>
298
+ </span>
299
+ <span className="flex items-center gap-1.5">
300
+ <span className="relative flex-shrink-0 w-[5px] h-[5px]">
301
+ <span className="absolute inset-0 rounded-sm" style={{ background: signalReachable ? HEX.accent : HEX.danger }} />
302
+ </span>
303
+ <span className="text-text-3">Signal</span>
304
+ </span>
305
+ </div>
306
+ )}
307
+
308
+ {installed && <UninstallButton />}
309
+
310
+ <StatusDot status={installed && node.active ? 'running' : installed ? 'stopped' : 'crashed'} size="sm" />
311
+ </div>
312
+ );
313
+ }
314
+
235
315
  export default function NetworkView() {
236
316
  const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
237
317
  const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
238
318
  const checkNetworkUpdate = useGrooveStore((s) => s.checkNetworkUpdate);
239
- const node = useGrooveStore((s) => s.networkNode);
240
319
  const installed = useGrooveStore((s) => s.networkInstalled);
241
- const version = useGrooveStore((s) => s.networkVersion);
320
+ const nodeActive = useGrooveStore((s) => s.networkNode.active);
242
321
 
243
322
  useEffect(() => {
244
323
  fetchNetworkNodeStatus();
@@ -250,58 +329,52 @@ export default function NetworkView() {
250
329
  }
251
330
  }, [fetchNetworkNodeStatus, fetchNetworkStatus, checkNetworkUpdate, installed]);
252
331
 
253
- return (
254
- <div className="flex flex-col h-full">
255
- {/* Header */}
256
- <div className="flex items-center gap-3 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
257
- <Globe size={14} className="text-accent" />
258
- <h2 className="text-sm font-semibold text-text-0 font-sans">Groove Network</h2>
259
- <Badge variant="purple">Early Access</Badge>
260
- {installed && version.installed && (
261
- <span className="text-2xs font-mono text-text-3 tabular-nums">v{String(version.installed).replace(/^v/, '')}</span>
262
- )}
263
- {installed && <UpdateButton />}
264
- <div className="flex-1" />
265
- {installed && (
266
- <>
267
- <UninstallButton />
268
- <div className="flex items-center gap-1.5 text-2xs font-sans text-text-3">
269
- <StatusDot status={node.active ? 'running' : 'crashed'} size="sm" />
270
- {node.active ? 'Contributing' : 'Idle'}
271
- </div>
272
- </>
273
- )}
332
+ if (!installed) {
333
+ return (
334
+ <div className="flex flex-col h-full">
335
+ <NetworkHeader />
336
+ <ScrollArea className="flex-1">
337
+ <InstallGate />
338
+ </ScrollArea>
274
339
  </div>
340
+ );
341
+ }
275
342
 
276
- {/* Body */}
277
- <ScrollArea className="flex-1">
278
- {!installed ? (
279
- <InstallGate />
280
- ) : (
281
- <div className="p-4 grid grid-cols-1 xl:grid-cols-2 gap-4">
282
- {/* Left column — node operator */}
283
- <div className="flex flex-col gap-3 min-w-0">
284
- <div>
285
- <div className="flex items-center gap-2 mb-2 px-0.5">
286
- <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Node Operator</span>
287
- <div className="flex-1 h-px bg-border-subtle" />
288
- </div>
289
- <NodeToggle />
290
- </div>
291
- <NodeDetails />
343
+ if (!nodeActive) {
344
+ return (
345
+ <div className="flex flex-col h-full">
346
+ <NetworkHeader />
347
+ <ScrollArea className="flex-1">
348
+ <div className="flex items-center justify-center min-h-full px-6 py-12">
349
+ <div className="w-full max-w-md">
350
+ <NodeToggle />
292
351
  </div>
352
+ </div>
353
+ </ScrollArea>
354
+ </div>
355
+ );
356
+ }
293
357
 
294
- {/* Right column — network status */}
295
- <div className="flex flex-col gap-3 min-w-0">
296
- <div className="flex items-center gap-2 px-0.5">
297
- <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Network Status</span>
298
- <div className="flex-1 h-px bg-border-subtle" />
299
- </div>
300
- <NetworkStatus />
301
- </div>
358
+ return (
359
+ <div className="flex flex-col h-full">
360
+ <NetworkHeader />
361
+
362
+ <ComputeHeader />
363
+
364
+ <div className="flex-1 min-h-0 flex flex-col" style={{ background: '#282c34', gap: '1px' }}>
365
+ <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '3fr 1.5fr', gap: '0 1px' }}>
366
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
367
+ <FleetTable />
302
368
  </div>
303
- )}
304
- </ScrollArea>
369
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
370
+ <NetworkHealth />
371
+ </div>
372
+ </div>
373
+
374
+ <div className="min-h-0 flex-[0.6] bg-surface-1">
375
+ <ActivityStream />
376
+ </div>
377
+ </div>
305
378
  </div>
306
379
  );
307
380
  }