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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +126 -7
- package/node_modules/@groove-dev/daemon/src/conversations.js +2 -5
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +1 -0
- package/{packages/gui/dist/assets/index-X58BAjGp.js → node_modules/@groove-dev/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +49 -24
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +9 -36
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/activity-stream.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +166 -0
- package/node_modules/@groove-dev/gui/src/components/network/fleet-table.jsx +190 -0
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +135 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -4
- package/node_modules/@groove-dev/gui/src/views/network.jsx +128 -55
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +126 -7
- package/packages/daemon/src/conversations.js +2 -5
- package/packages/daemon/src/providers/groove-network.js +1 -1
- package/packages/gui/dist/assets/index-BrfCzrxJ.css +1 -0
- package/{node_modules/@groove-dev/gui/dist/assets/index-X58BAjGp.js → packages/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/chat/chat-header.jsx +1 -1
- package/packages/gui/src/components/chat/chat-input.jsx +1 -1
- package/packages/gui/src/components/chat/chat-messages.jsx +49 -24
- package/packages/gui/src/components/chat/chat-view.jsx +9 -36
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -2
- package/packages/gui/src/components/network/activity-stream.jsx +105 -0
- package/packages/gui/src/components/network/compute-header.jsx +166 -0
- package/packages/gui/src/components/network/fleet-table.jsx +190 -0
- package/packages/gui/src/components/network/network-health.jsx +135 -0
- package/packages/gui/src/components/network/node-toggle.jsx +1 -1
- package/packages/gui/src/stores/groove.js +57 -4
- package/packages/gui/src/views/network.jsx +128 -55
- package/ai-chat/CHAT_MASTER_PLAN.md +0 -184
- package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +0 -1
- 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-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
<div className="
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
}
|