groove-dev 0.27.59 → 0.27.60
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 +4 -4
- package/node_modules/@groove-dev/daemon/src/conversations.js +3 -4
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/index.js +16 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DD6taBMp.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BycOlqLx.js → index-DcnRqlqB.js} +1741 -1741
- 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/network/activity-chart.jsx +245 -0
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -0
- package/node_modules/@groove-dev/gui/src/views/network.jsx +99 -17
- 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 +4 -4
- package/packages/daemon/src/conversations.js +3 -4
- package/packages/daemon/src/providers/groove-network.js +1 -1
- package/packages/daemon/src/providers/index.js +16 -1
- package/packages/gui/dist/assets/index-DD6taBMp.css +1 -0
- package/packages/gui/dist/assets/{index-BycOlqLx.js → index-DcnRqlqB.js} +1741 -1741
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/network/activity-chart.jsx +245 -0
- package/packages/gui/src/components/network/compute-header.jsx +1 -1
- package/packages/gui/src/components/network/network-health.jsx +1 -1
- package/packages/gui/src/components/network/network-status.jsx +5 -5
- package/packages/gui/src/components/network/node-details.jsx +1 -1
- package/packages/gui/src/stores/groove.js +39 -0
- package/packages/gui/src/views/network.jsx +99 -17
- package/default/fix-beta-endpoint-deployment.md +0 -68
- package/default/groovedev-beta-auth-endpoint.md +0 -166
- package/default/security-review-prompt.md +0 -98
- package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +0 -1
- package/packages/gui/dist/assets/index-BrfCzrxJ.css +0 -1
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DcnRqlqB.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DD6taBMp.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useRef, useEffect, useState, useCallback, useMemo, memo } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { HEX, hexAlpha } from '../../lib/theme-hex';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
6
|
+
|
|
7
|
+
function shortAddr(addr) {
|
|
8
|
+
if (!addr || typeof addr !== 'string') return '—';
|
|
9
|
+
if (addr.length < 14) return addr;
|
|
10
|
+
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ActivityChart = memo(function ActivityChart() {
|
|
14
|
+
const snapshots = useGrooveStore((s) => s.networkSnapshots);
|
|
15
|
+
const nodes = useGrooveStore((s) => s.networkStatus.nodes || []);
|
|
16
|
+
const ownNodeId = useGrooveStore((s) => s.networkNode.nodeId);
|
|
17
|
+
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const canvasRef = useRef(null);
|
|
20
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
21
|
+
const [hover, setHover] = useState(null);
|
|
22
|
+
|
|
23
|
+
const { width, height } = size;
|
|
24
|
+
const pad = { top: 28, right: 12, bottom: 8, left: 12 };
|
|
25
|
+
const w = Math.max(width - pad.left - pad.right, 0);
|
|
26
|
+
const h = Math.max(height - pad.top - pad.bottom, 0);
|
|
27
|
+
|
|
28
|
+
const chartData = useMemo(() => {
|
|
29
|
+
if (!snapshots || snapshots.length < 2) return [];
|
|
30
|
+
return snapshots;
|
|
31
|
+
}, [snapshots]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const el = containerRef.current;
|
|
35
|
+
if (!el) return;
|
|
36
|
+
const observer = new ResizeObserver((entries) => {
|
|
37
|
+
const { width: cw, height: ch } = entries[0].contentRect;
|
|
38
|
+
if (cw > 0 && ch > 0) setSize({ width: Math.floor(cw), height: Math.floor(ch) });
|
|
39
|
+
});
|
|
40
|
+
observer.observe(el);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const onMouseMove = useCallback((e) => {
|
|
45
|
+
const canvas = canvasRef.current;
|
|
46
|
+
if (!canvas || !chartData.length || w <= 0) return;
|
|
47
|
+
const rect = canvas.getBoundingClientRect();
|
|
48
|
+
const x = e.clientX - rect.left - pad.left;
|
|
49
|
+
if (x < 0 || x > w) { setHover(null); return; }
|
|
50
|
+
const index = Math.round((x / w) * (chartData.length - 1));
|
|
51
|
+
setHover({ x: pad.left + (index / Math.max(chartData.length - 1, 1)) * w, index });
|
|
52
|
+
}, [chartData, w, pad.left]);
|
|
53
|
+
|
|
54
|
+
const onMouseLeave = useCallback(() => setHover(null), []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const canvas = canvasRef.current;
|
|
58
|
+
if (!canvas || !chartData.length || width <= 0 || height <= 0 || w <= 0 || h <= 0) return;
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
const dpr = window.devicePixelRatio || 1;
|
|
61
|
+
|
|
62
|
+
canvas.width = width * dpr;
|
|
63
|
+
canvas.height = height * dpr;
|
|
64
|
+
ctx.scale(dpr, dpr);
|
|
65
|
+
ctx.clearRect(0, 0, width, height);
|
|
66
|
+
|
|
67
|
+
const globalSessions = chartData.map((d) => d.globalSessions);
|
|
68
|
+
const mySessions = chartData.map((d) => d.mySessions);
|
|
69
|
+
const maxVal = Math.max(...globalSessions, ...mySessions, 1);
|
|
70
|
+
|
|
71
|
+
const xAt = (i) => pad.left + (i / Math.max(chartData.length - 1, 1)) * w;
|
|
72
|
+
const yAt = (v) => pad.top + h - (v / maxVal) * h;
|
|
73
|
+
|
|
74
|
+
// Horizontal grid lines
|
|
75
|
+
ctx.setLineDash([2, 4]);
|
|
76
|
+
ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
|
|
77
|
+
ctx.lineWidth = 1;
|
|
78
|
+
for (let i = 1; i <= 3; i++) {
|
|
79
|
+
const y = pad.top + (h / 4) * i;
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.moveTo(pad.left, y);
|
|
82
|
+
ctx.lineTo(pad.left + w, y);
|
|
83
|
+
ctx.stroke();
|
|
84
|
+
}
|
|
85
|
+
ctx.setLineDash([]);
|
|
86
|
+
|
|
87
|
+
// Y-axis labels
|
|
88
|
+
ctx.font = "9px 'JetBrains Mono Variable', monospace";
|
|
89
|
+
ctx.textAlign = 'left';
|
|
90
|
+
ctx.fillStyle = hexAlpha(HEX.text3, 0.5);
|
|
91
|
+
ctx.fillText(String(maxVal), pad.left + 4, pad.top + 10);
|
|
92
|
+
ctx.fillText(String(Math.round(maxVal / 2)), pad.left + 4, pad.top + h / 2 + 4);
|
|
93
|
+
|
|
94
|
+
// Network line — gradient fill
|
|
95
|
+
ctx.beginPath();
|
|
96
|
+
ctx.moveTo(pad.left, pad.top + h);
|
|
97
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
98
|
+
ctx.lineTo(xAt(i), yAt(globalSessions[i]));
|
|
99
|
+
}
|
|
100
|
+
ctx.lineTo(xAt(chartData.length - 1), pad.top + h);
|
|
101
|
+
ctx.closePath();
|
|
102
|
+
const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + h);
|
|
103
|
+
grad.addColorStop(0, hexAlpha(HEX.purple, 0.2));
|
|
104
|
+
grad.addColorStop(0.7, hexAlpha(HEX.purple, 0.04));
|
|
105
|
+
grad.addColorStop(1, hexAlpha(HEX.purple, 0));
|
|
106
|
+
ctx.fillStyle = grad;
|
|
107
|
+
ctx.fill();
|
|
108
|
+
|
|
109
|
+
// Network line — stroke
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.strokeStyle = HEX.purple;
|
|
112
|
+
ctx.lineWidth = 1.5;
|
|
113
|
+
ctx.lineJoin = 'round';
|
|
114
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
115
|
+
const x = xAt(i);
|
|
116
|
+
const y = yAt(globalSessions[i]);
|
|
117
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
118
|
+
}
|
|
119
|
+
ctx.stroke();
|
|
120
|
+
|
|
121
|
+
// Your Node line — stroke only
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.strokeStyle = HEX.accent;
|
|
124
|
+
ctx.lineWidth = 1.5;
|
|
125
|
+
ctx.lineJoin = 'round';
|
|
126
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
127
|
+
const x = xAt(i);
|
|
128
|
+
const y = yAt(mySessions[i]);
|
|
129
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
130
|
+
}
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
|
|
133
|
+
// Inline legend (top-right)
|
|
134
|
+
ctx.font = "9px 'Inter Variable', sans-serif";
|
|
135
|
+
ctx.textAlign = 'right';
|
|
136
|
+
let rx = width - pad.right - 4;
|
|
137
|
+
const ly = 14;
|
|
138
|
+
|
|
139
|
+
ctx.fillStyle = HEX.accent;
|
|
140
|
+
ctx.fillText('Your Node', rx, ly);
|
|
141
|
+
rx -= ctx.measureText('Your Node').width + 4;
|
|
142
|
+
ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
|
|
143
|
+
rx -= 14;
|
|
144
|
+
|
|
145
|
+
ctx.fillStyle = HEX.purple;
|
|
146
|
+
ctx.fillText('Network', rx, ly);
|
|
147
|
+
rx -= ctx.measureText('Network').width + 4;
|
|
148
|
+
ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
|
|
149
|
+
|
|
150
|
+
// Hover
|
|
151
|
+
if (hover && hover.index >= 0 && hover.index < chartData.length) {
|
|
152
|
+
const hx = hover.x;
|
|
153
|
+
const d = chartData[hover.index];
|
|
154
|
+
|
|
155
|
+
// Crosshair
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
ctx.moveTo(hx, pad.top);
|
|
158
|
+
ctx.lineTo(hx, pad.top + h);
|
|
159
|
+
ctx.strokeStyle = hexAlpha(HEX.text1, 0.15);
|
|
160
|
+
ctx.lineWidth = 1;
|
|
161
|
+
ctx.stroke();
|
|
162
|
+
|
|
163
|
+
// Dots
|
|
164
|
+
ctx.beginPath(); ctx.arc(hx, yAt(d.globalSessions), 3, 0, Math.PI * 2);
|
|
165
|
+
ctx.fillStyle = HEX.purple; ctx.fill();
|
|
166
|
+
ctx.beginPath(); ctx.arc(hx, yAt(d.mySessions), 3, 0, Math.PI * 2);
|
|
167
|
+
ctx.fillStyle = HEX.accent; ctx.fill();
|
|
168
|
+
|
|
169
|
+
// Tooltip
|
|
170
|
+
const lines = [
|
|
171
|
+
{ label: 'Network', value: String(d.globalSessions), color: HEX.purple },
|
|
172
|
+
{ label: 'Your Node', value: String(d.mySessions), color: HEX.accent },
|
|
173
|
+
{ label: 'Nodes', value: String(d.nodeCount), color: HEX.text2 },
|
|
174
|
+
];
|
|
175
|
+
const tooltipW = 104;
|
|
176
|
+
const tooltipH = lines.length * 16 + 12;
|
|
177
|
+
let tx = hx + 12;
|
|
178
|
+
if (tx + tooltipW > width - 8) tx = hx - tooltipW - 12;
|
|
179
|
+
const ty = Math.max(pad.top, yAt(d.globalSessions) - tooltipH / 2);
|
|
180
|
+
|
|
181
|
+
ctx.fillStyle = hexAlpha(HEX.surface0, 0.92);
|
|
182
|
+
ctx.beginPath(); ctx.roundRect(tx, ty, tooltipW, tooltipH, 4); ctx.fill();
|
|
183
|
+
ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
|
|
184
|
+
ctx.lineWidth = 1; ctx.stroke();
|
|
185
|
+
|
|
186
|
+
ctx.textAlign = 'left';
|
|
187
|
+
lines.forEach((line, i) => {
|
|
188
|
+
const rowY = ty + 14 + i * 16;
|
|
189
|
+
ctx.beginPath(); ctx.arc(tx + 8, rowY - 3, 2, 0, Math.PI * 2);
|
|
190
|
+
ctx.fillStyle = line.color; ctx.fill();
|
|
191
|
+
ctx.font = "8px 'Inter Variable', sans-serif";
|
|
192
|
+
ctx.fillStyle = HEX.text3; ctx.fillText(line.label, tx + 14, rowY);
|
|
193
|
+
ctx.font = "9px 'JetBrains Mono Variable', monospace";
|
|
194
|
+
ctx.fillStyle = HEX.text0; ctx.textAlign = 'right';
|
|
195
|
+
ctx.fillText(line.value, tx + tooltipW - 8, rowY);
|
|
196
|
+
ctx.textAlign = 'left';
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}, [chartData, width, height, hover, w, h, pad]);
|
|
200
|
+
|
|
201
|
+
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="flex flex-col h-full">
|
|
205
|
+
<div className="px-3 pt-2.5 pb-1 flex-shrink-0 flex items-center justify-between">
|
|
206
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Network Activity</span>
|
|
207
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums">{activeNodes.length} nodes</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="relative flex-1 min-h-0">
|
|
211
|
+
{chartData.length < 2 ? (
|
|
212
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
213
|
+
<span className="text-xs font-mono text-text-3">Collecting network data…</span>
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<div ref={containerRef} className="absolute inset-0">
|
|
217
|
+
{width > 0 && height > 0 && (
|
|
218
|
+
<canvas
|
|
219
|
+
ref={canvasRef}
|
|
220
|
+
style={{ width, height }}
|
|
221
|
+
className="block cursor-crosshair"
|
|
222
|
+
onMouseMove={onMouseMove}
|
|
223
|
+
onMouseLeave={onMouseLeave}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="px-3 py-1.5 border-t border-border-subtle flex items-center gap-2 flex-shrink-0 font-mono text-2xs" style={{ background: hexAlpha(HEX.accent, 0.04) }}>
|
|
231
|
+
<span className="text-text-3">{activeNodes.length} node{activeNodes.length !== 1 ? 's' : ''} online</span>
|
|
232
|
+
{activeNodes.map((n) => {
|
|
233
|
+
const id = n.node_id || n.nodeId || '';
|
|
234
|
+
const isSelf = ownNodeId && id === ownNodeId;
|
|
235
|
+
const layers = Array.isArray(n.layers) ? `${n.layers[0]}-${n.layers[1]}` : '';
|
|
236
|
+
return (
|
|
237
|
+
<span key={id} className={isSelf ? 'text-accent' : 'text-text-2'}>
|
|
238
|
+
{shortAddr(id)}{isSelf ? ' (You' : '('}{layers ? ` · ${layers}` : ''})
|
|
239
|
+
</span>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
});
|
|
@@ -121,7 +121,7 @@ export const ComputeHeader = memo(function ComputeHeader() {
|
|
|
121
121
|
const loadColor = compute.avgLoad > 2.0 ? HEX.danger : compute.avgLoad > 1.0 ? HEX.warning : HEX.success;
|
|
122
122
|
const activeModel = models.length > 0
|
|
123
123
|
? (typeof models[0] === 'string' ? models[0] : models[0].name)
|
|
124
|
-
: '
|
|
124
|
+
: 'Qwen/Qwen3-4B';
|
|
125
125
|
|
|
126
126
|
const kpis = [
|
|
127
127
|
{ label: 'RAM', value: `${fmtMbToGb(compute.totalRamMb)} GB`, color: HEX.accent, hint: 'Total RAM across all network nodes.' },
|
|
@@ -22,7 +22,7 @@ export const NetworkHealth = memo(function NetworkHealth() {
|
|
|
22
22
|
const node = useGrooveStore((s) => s.networkNode);
|
|
23
23
|
|
|
24
24
|
const nodes = Array.isArray(status.nodes) ? status.nodes : [];
|
|
25
|
-
const totalLayers = status.totalLayers ||
|
|
25
|
+
const totalLayers = status.totalLayers || 36;
|
|
26
26
|
const covered = status.coverage || 0;
|
|
27
27
|
const coverage = coverageState(covered, totalLayers);
|
|
28
28
|
const coveragePct = totalLayers ? Math.min(100, (covered / totalLayers) * 100) : 0;
|
|
@@ -24,7 +24,7 @@ function coverageState(covered, total) {
|
|
|
24
24
|
|
|
25
25
|
function KpiTile({ icon: Icon, label, value, sub }) {
|
|
26
26
|
return (
|
|
27
|
-
<div className="rounded-
|
|
27
|
+
<div className="rounded-sm border border-border bg-surface-1 px-3 py-2.5 flex items-center gap-2.5 min-w-0">
|
|
28
28
|
<div className="w-8 h-8 rounded-md bg-accent/10 text-accent flex items-center justify-center flex-shrink-0">
|
|
29
29
|
<Icon size={14} />
|
|
30
30
|
</div>
|
|
@@ -53,7 +53,7 @@ export function NetworkStatus() {
|
|
|
53
53
|
return (
|
|
54
54
|
<div className="flex flex-col gap-3">
|
|
55
55
|
{/* Signal connection indicator */}
|
|
56
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-
|
|
56
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-sm border border-border bg-surface-1">
|
|
57
57
|
<StatusDot status={signalReachable ? 'running' : 'crashed'} size="sm" />
|
|
58
58
|
<span className="text-2xs font-sans text-text-3">Signal:</span>
|
|
59
59
|
<span className="text-2xs font-mono text-text-1">signal.groovedev.ai</span>
|
|
@@ -71,7 +71,7 @@ export function NetworkStatus() {
|
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
{/* Coverage bar */}
|
|
74
|
-
<div className="rounded-
|
|
74
|
+
<div className="rounded-sm border border-border bg-surface-1 px-4 py-3">
|
|
75
75
|
<div className="flex items-center justify-between mb-2">
|
|
76
76
|
<span className="text-xs font-semibold text-text-1 font-sans">Layer Coverage</span>
|
|
77
77
|
<Badge variant={coverage.tone}>{coverage.label}</Badge>
|
|
@@ -86,7 +86,7 @@ export function NetworkStatus() {
|
|
|
86
86
|
</div>
|
|
87
87
|
|
|
88
88
|
{/* Models */}
|
|
89
|
-
<div className="rounded-
|
|
89
|
+
<div className="rounded-sm border border-border bg-surface-1 overflow-hidden">
|
|
90
90
|
<div className="px-4 py-2.5 border-b border-border-subtle">
|
|
91
91
|
<span className="text-xs font-semibold text-text-1 font-sans">Models</span>
|
|
92
92
|
</div>
|
|
@@ -111,7 +111,7 @@ export function NetworkStatus() {
|
|
|
111
111
|
</div>
|
|
112
112
|
|
|
113
113
|
{/* Nodes table */}
|
|
114
|
-
<div className="rounded-
|
|
114
|
+
<div className="rounded-sm border border-border bg-surface-1 overflow-hidden">
|
|
115
115
|
<button
|
|
116
116
|
onClick={() => setNodesOpen((v) => !v)}
|
|
117
117
|
className="w-full flex items-center gap-2 px-4 py-2.5 border-b border-border-subtle cursor-pointer hover:bg-surface-2/40 transition-colors"
|
|
@@ -29,7 +29,7 @@ export function NodeDetails() {
|
|
|
29
29
|
const events = useGrooveStore((s) => s.networkEvents);
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
|
-
<div className="flex flex-col rounded-
|
|
32
|
+
<div className="flex flex-col rounded-sm border border-border bg-surface-1 overflow-hidden min-h-0">
|
|
33
33
|
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border-subtle">
|
|
34
34
|
<Activity size={12} className="text-text-3" />
|
|
35
35
|
<span className="text-xs font-semibold text-text-1 font-sans">Node Activity</span>
|
|
@@ -104,6 +104,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
104
104
|
networkVersion: { installed: null, latest: null, updateAvailable: false },
|
|
105
105
|
networkUpdateProgress: { updating: false, step: null, message: null, percent: 0, error: null },
|
|
106
106
|
networkCompute: { totalRamMb: 0, totalVramMb: 0, totalCpuCores: 0, totalBandwidthMbps: 0, activeNodes: 0, totalNodes: 0, avgLoad: 0 },
|
|
107
|
+
networkSnapshots: [],
|
|
107
108
|
|
|
108
109
|
// ── Marketplace Auth ───────────────────────────────────────
|
|
109
110
|
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
@@ -712,6 +713,24 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
712
713
|
};
|
|
713
714
|
}
|
|
714
715
|
set(nsUpdate);
|
|
716
|
+
|
|
717
|
+
// Push snapshot for activity chart
|
|
718
|
+
const wsNodes = nsData.nodes || [];
|
|
719
|
+
const wsOwnId = get().networkNode.nodeId;
|
|
720
|
+
const wsOwn = wsOwnId ? wsNodes.find((n) => (n.node_id || n.nodeId) === wsOwnId) : null;
|
|
721
|
+
const wsActive = wsNodes.filter((n) => n.status === 'active');
|
|
722
|
+
const wsSnap = {
|
|
723
|
+
t: Date.now(),
|
|
724
|
+
globalSessions: nsData.activeSessions || 0,
|
|
725
|
+
mySessions: wsOwn?.active_sessions ?? wsOwn?.sessions ?? 0,
|
|
726
|
+
nodeCount: wsActive.length,
|
|
727
|
+
avgLoad: wsActive.length > 0 ? wsActive.reduce((s, n) => s + (n.load || 0), 0) / wsActive.length : 0,
|
|
728
|
+
myLoad: wsOwn?.load ?? 0,
|
|
729
|
+
};
|
|
730
|
+
let wsSnapshots = [...get().networkSnapshots, wsSnap];
|
|
731
|
+
if (wsSnapshots.length > 100) wsSnapshots = wsSnapshots.slice(-100);
|
|
732
|
+
set({ networkSnapshots: wsSnapshots });
|
|
733
|
+
|
|
715
734
|
break;
|
|
716
735
|
}
|
|
717
736
|
|
|
@@ -2106,6 +2125,26 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2106
2125
|
};
|
|
2107
2126
|
}
|
|
2108
2127
|
set(update);
|
|
2128
|
+
|
|
2129
|
+
// Push snapshot for activity chart
|
|
2130
|
+
if (data) {
|
|
2131
|
+
const ownId = get().networkNode.nodeId;
|
|
2132
|
+
const nodes = data.nodes || [];
|
|
2133
|
+
const ownNode = ownId ? nodes.find((n) => (n.node_id || n.nodeId) === ownId) : null;
|
|
2134
|
+
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
2135
|
+
const snap = {
|
|
2136
|
+
t: Date.now(),
|
|
2137
|
+
globalSessions: data.activeSessions || 0,
|
|
2138
|
+
mySessions: ownNode?.active_sessions ?? ownNode?.sessions ?? 0,
|
|
2139
|
+
nodeCount: activeNodes.length,
|
|
2140
|
+
avgLoad: activeNodes.length > 0 ? activeNodes.reduce((s, n) => s + (n.load || 0), 0) / activeNodes.length : 0,
|
|
2141
|
+
myLoad: ownNode?.load ?? 0,
|
|
2142
|
+
};
|
|
2143
|
+
let snapshots = [...get().networkSnapshots, snap];
|
|
2144
|
+
if (snapshots.length > 100) snapshots = snapshots.slice(-100);
|
|
2145
|
+
set({ networkSnapshots: snapshots });
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2109
2148
|
return data;
|
|
2110
2149
|
} catch {
|
|
2111
2150
|
set({ networkStatusReachable: false });
|
|
@@ -9,11 +9,11 @@ import { ScrollArea } from '../components/ui/scroll-area';
|
|
|
9
9
|
import { cn } from '../lib/cn';
|
|
10
10
|
import { NodeToggle } from '../components/network/node-toggle';
|
|
11
11
|
import { ComputeHeader } from '../components/network/compute-header';
|
|
12
|
-
import {
|
|
12
|
+
import { ActivityChart } from '../components/network/activity-chart';
|
|
13
13
|
import { ActivityStream } from '../components/network/activity-stream';
|
|
14
14
|
import { NetworkHealth } from '../components/network/network-health';
|
|
15
|
-
import { HEX } from '../lib/theme-hex';
|
|
16
|
-
import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle } from 'lucide-react';
|
|
15
|
+
import { HEX, hexAlpha } from '../lib/theme-hex';
|
|
16
|
+
import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle, Zap } from 'lucide-react';
|
|
17
17
|
|
|
18
18
|
const REQUIREMENTS = [
|
|
19
19
|
'Python 3.10 or higher',
|
|
@@ -312,6 +312,99 @@ function NetworkHeader() {
|
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
function IdleHero() {
|
|
316
|
+
const node = useGrooveStore((s) => s.networkNode);
|
|
317
|
+
const networkStatus = useGrooveStore((s) => s.networkStatus);
|
|
318
|
+
const startNetworkNode = useGrooveStore((s) => s.startNetworkNode);
|
|
319
|
+
const [pending, setPending] = useState(false);
|
|
320
|
+
|
|
321
|
+
const hardware = node.hardware || {};
|
|
322
|
+
const activeNodes = (networkStatus.nodes || []).filter((n) => n.status === 'active').length;
|
|
323
|
+
const activeSessions = networkStatus.activeSessions || 0;
|
|
324
|
+
|
|
325
|
+
async function handleStart() {
|
|
326
|
+
setPending(true);
|
|
327
|
+
try { await startNetworkNode(); }
|
|
328
|
+
catch { /* toasted in store */ }
|
|
329
|
+
setPending(false);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div className="flex flex-col h-full">
|
|
334
|
+
<NetworkHeader />
|
|
335
|
+
<div
|
|
336
|
+
className="flex-1 flex items-center justify-center bg-surface-0"
|
|
337
|
+
style={{ background: `radial-gradient(ellipse at 50% 40%, ${hexAlpha(HEX.accent, 0.06)} 0%, transparent 70%) ${HEX.surface0}` }}
|
|
338
|
+
>
|
|
339
|
+
<div className="max-w-lg w-full px-6 flex flex-col items-center text-center">
|
|
340
|
+
<div
|
|
341
|
+
className="w-24 h-24 rounded-full flex items-center justify-center mb-6 mx-auto"
|
|
342
|
+
style={{
|
|
343
|
+
background: hexAlpha(HEX.accent, 0.08),
|
|
344
|
+
border: `1px solid ${hexAlpha(HEX.accent, 0.15)}`,
|
|
345
|
+
boxShadow: `0 0 40px ${hexAlpha(HEX.accent, 0.1)}`,
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<Globe size={56} className="text-accent" strokeWidth={1.25} />
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<h2 className="text-xl font-semibold text-text-0 font-sans text-center">
|
|
352
|
+
Join the Groove Network
|
|
353
|
+
</h2>
|
|
354
|
+
|
|
355
|
+
<p className="text-sm text-text-2 font-sans text-center leading-relaxed mt-2 max-w-sm mx-auto">
|
|
356
|
+
Contribute your compute to power decentralized AI inference. Your hardware runs model shards alongside other nodes in the network.
|
|
357
|
+
</p>
|
|
358
|
+
|
|
359
|
+
<div className="mt-8 w-full">
|
|
360
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-widest mb-2 text-left">Your Hardware</div>
|
|
361
|
+
<div className="grid grid-cols-3 gap-2">
|
|
362
|
+
<div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
|
|
363
|
+
<div className="text-2xs font-mono text-text-4 uppercase tracking-wider">Device</div>
|
|
364
|
+
<div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.device || 'auto'}</div>
|
|
365
|
+
</div>
|
|
366
|
+
<div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
|
|
367
|
+
<div className="text-2xs font-mono text-text-4 uppercase tracking-wider">Memory</div>
|
|
368
|
+
<div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.memory || '—'}</div>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
|
|
371
|
+
<div className="text-2xs font-mono text-text-4 uppercase tracking-wider">GPU</div>
|
|
372
|
+
<div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.gpu || 'None'}</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div className="mt-6 w-full max-w-sm mx-auto">
|
|
378
|
+
<Button variant="primary" size="lg" onClick={handleStart} disabled={pending} className="w-full">
|
|
379
|
+
{pending ? (
|
|
380
|
+
<><Loader2 size={14} className="animate-spin" /> Connecting…</>
|
|
381
|
+
) : (
|
|
382
|
+
<><Zap size={14} /> Start Contributing</>
|
|
383
|
+
)}
|
|
384
|
+
</Button>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div className="mt-4 text-center text-2xs font-mono text-text-3">
|
|
388
|
+
{activeNodes > 0 || activeSessions > 0 ? (
|
|
389
|
+
<span>
|
|
390
|
+
<span className="text-accent">{activeNodes}</span> node{activeNodes !== 1 ? 's' : ''} online · {activeSessions} active session{activeSessions !== 1 ? 's' : ''}
|
|
391
|
+
</span>
|
|
392
|
+
) : (
|
|
393
|
+
<span>Checking network status…</span>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{node.nodeId && (
|
|
398
|
+
<div className="mt-6 text-center text-2xs font-mono text-text-4">
|
|
399
|
+
Node identity: {node.nodeId.length > 14 ? `${node.nodeId.slice(0, 6)}…${node.nodeId.slice(-4)}` : node.nodeId}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
315
408
|
export default function NetworkView() {
|
|
316
409
|
const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
|
|
317
410
|
const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
|
|
@@ -341,18 +434,7 @@ export default function NetworkView() {
|
|
|
341
434
|
}
|
|
342
435
|
|
|
343
436
|
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 />
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
</ScrollArea>
|
|
354
|
-
</div>
|
|
355
|
-
);
|
|
437
|
+
return <IdleHero />;
|
|
356
438
|
}
|
|
357
439
|
|
|
358
440
|
return (
|
|
@@ -361,10 +443,10 @@ export default function NetworkView() {
|
|
|
361
443
|
|
|
362
444
|
<ComputeHeader />
|
|
363
445
|
|
|
364
|
-
<div className="flex-1 min-h-0 flex flex-col" style={{ background:
|
|
446
|
+
<div className="flex-1 min-h-0 flex flex-col" style={{ background: HEX.surface3, gap: '1px' }}>
|
|
365
447
|
<div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '3fr 1.5fr', gap: '0 1px' }}>
|
|
366
448
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
|
|
367
|
-
<
|
|
449
|
+
<ActivityChart />
|
|
368
450
|
</div>
|
|
369
451
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
|
|
370
452
|
<NetworkHealth />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.60",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -4233,7 +4233,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4233
4233
|
}
|
|
4234
4234
|
|
|
4235
4235
|
const signalFlag = supportsSignalFlag(cfg.version) ? '--signal' : '--relay';
|
|
4236
|
-
const model = cfg.model || '
|
|
4236
|
+
const model = cfg.model || 'Qwen/Qwen3-4B';
|
|
4237
4237
|
const args = [
|
|
4238
4238
|
'-m', 'src.node.server',
|
|
4239
4239
|
signalFlag, signal,
|
|
@@ -4484,7 +4484,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4484
4484
|
models,
|
|
4485
4485
|
compute: data.compute || null,
|
|
4486
4486
|
coverage: data.covered_layers ?? primaryModel.covered_layers ?? data.coverage ?? 0,
|
|
4487
|
-
totalLayers: data.total_layers ?? primaryModel.total_layers ?? data.totalLayers ??
|
|
4487
|
+
totalLayers: data.total_layers ?? primaryModel.total_layers ?? data.totalLayers ?? 36,
|
|
4488
4488
|
activeSessions: data.active_sessions ?? data.activeSessions ?? 0,
|
|
4489
4489
|
totalNodes: data.total_nodes ?? data.totalNodes ?? (Array.isArray(data.nodes) ? data.nodes.length : 0),
|
|
4490
4490
|
});
|
|
@@ -4529,10 +4529,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4529
4529
|
} : null;
|
|
4530
4530
|
res.json({
|
|
4531
4531
|
nodes: selfNode,
|
|
4532
|
-
models: ['
|
|
4532
|
+
models: ['Qwen/Qwen3-4B'],
|
|
4533
4533
|
compute: localCompute,
|
|
4534
4534
|
coverage,
|
|
4535
|
-
totalLayers:
|
|
4535
|
+
totalLayers: 36,
|
|
4536
4536
|
activeSessions: node.sessions || 0,
|
|
4537
4537
|
totalNodes: selfNode.length,
|
|
4538
4538
|
});
|
|
@@ -5,7 +5,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { spawn as cpSpawn } from 'child_process';
|
|
8
|
-
import { getProvider, getInstalledProviders } from './providers/index.js';
|
|
8
|
+
import { getProvider, getInstalledProviders, isProviderInstalled } from './providers/index.js';
|
|
9
9
|
|
|
10
10
|
export class ConversationManager {
|
|
11
11
|
constructor(daemon) {
|
|
@@ -280,10 +280,9 @@ export class ConversationManager {
|
|
|
280
280
|
let provider = getProvider(conv.provider);
|
|
281
281
|
let modelId = conv.model;
|
|
282
282
|
|
|
283
|
-
if (!provider || !provider
|
|
283
|
+
if (!provider || !isProviderInstalled(conv.provider)) {
|
|
284
284
|
const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
|
|
285
|
-
const
|
|
286
|
-
const fallbackId = priority.find((p) => installed.some((i) => i.id === p));
|
|
285
|
+
const fallbackId = priority.find((p) => isProviderInstalled(p));
|
|
287
286
|
if (!fallbackId) throw new Error('No provider available for chat');
|
|
288
287
|
provider = getProvider(fallbackId);
|
|
289
288
|
modelId = null;
|
|
@@ -79,7 +79,7 @@ export class GrooveNetworkProvider extends Provider {
|
|
|
79
79
|
static isOneShot = true;
|
|
80
80
|
|
|
81
81
|
static models = [
|
|
82
|
-
{ id: '
|
|
82
|
+
{ id: 'Qwen/Qwen3-4B', name: 'Qwen 3 4B (Network)', context: 32768 },
|
|
83
83
|
];
|
|
84
84
|
|
|
85
85
|
static isInstalled() {
|