groove-dev 0.27.68 → 0.27.70
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/CLAUDE.md +7 -0
- 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 +137 -3
- package/node_modules/@groove-dev/gui/dist/assets/{index-Cz4tj733.js → index-D5BpdcWS.js} +1738 -1738
- package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +1 -0
- 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 +209 -124
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +12 -1
- package/node_modules/@groove-dev/gui/src/components/network/identity-bar.jsx +4 -40
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +600 -0
- package/node_modules/@groove-dev/gui/src/components/network/token-waterfall.jsx +111 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +60 -0
- package/node_modules/@groove-dev/gui/src/views/network.jsx +6 -0
- 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 +137 -3
- package/packages/gui/dist/assets/{index-Cz4tj733.js → index-D5BpdcWS.js} +1738 -1738
- package/packages/gui/dist/assets/index-oQ0ejlfH.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/network/activity-chart.jsx +209 -124
- package/packages/gui/src/components/network/compute-header.jsx +12 -1
- package/packages/gui/src/components/network/identity-bar.jsx +4 -40
- package/packages/gui/src/components/network/performance-dashboard.jsx +600 -0
- package/packages/gui/src/components/network/token-waterfall.jsx +111 -0
- package/packages/gui/src/stores/groove.js +60 -0
- package/packages/gui/src/views/network.jsx +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-YeunozTU.css +0 -1
- package/packages/gui/dist/assets/index-YeunozTU.css +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useMemo, memo } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { HEX } from '../../lib/theme-hex';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { Badge } from '../ui/badge';
|
|
7
|
+
|
|
8
|
+
const PHASE_COLORS = {
|
|
9
|
+
serialize_ms: HEX.info,
|
|
10
|
+
send_ms: HEX.accent,
|
|
11
|
+
wait_ms: HEX.text3,
|
|
12
|
+
forward_ms: HEX.success,
|
|
13
|
+
queue_ms: HEX.warning,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PHASE_LABELS = {
|
|
17
|
+
serialize_ms: 'Serialize',
|
|
18
|
+
send_ms: 'Send',
|
|
19
|
+
wait_ms: 'Wait',
|
|
20
|
+
forward_ms: 'Forward',
|
|
21
|
+
queue_ms: 'Queue',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function shortAddr(addr) {
|
|
25
|
+
if (!addr || typeof addr !== 'string') return '\u2014';
|
|
26
|
+
if (addr.length < 14) return addr;
|
|
27
|
+
return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const TokenWaterfall = memo(function TokenWaterfall() {
|
|
31
|
+
const timing = useGrooveStore((s) => s.networkTokenTiming);
|
|
32
|
+
|
|
33
|
+
const stages = useMemo(() => {
|
|
34
|
+
if (!timing?.stages || !Array.isArray(timing.stages)) return [];
|
|
35
|
+
return timing.stages;
|
|
36
|
+
}, [timing]);
|
|
37
|
+
|
|
38
|
+
if (!stages.length) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex items-center justify-center py-6">
|
|
41
|
+
<span className="text-xs font-mono text-text-3">Waiting for token timing data\u2026</span>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const maxRtt = Math.max(...stages.map((s) => s.rtt_ms || 1), 1);
|
|
47
|
+
const phases = Object.keys(PHASE_COLORS);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col gap-0.5">
|
|
51
|
+
<div className="flex items-center gap-3 px-3 py-1.5 flex-wrap">
|
|
52
|
+
{phases.map((p) => (
|
|
53
|
+
<div key={p} className="flex items-center gap-1">
|
|
54
|
+
<span className="w-2 h-2 rounded-sm flex-shrink-0" style={{ background: PHASE_COLORS[p] }} />
|
|
55
|
+
<span className="text-2xs font-mono text-text-3">{PHASE_LABELS[p]}</span>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{stages.map((stage, i) => {
|
|
61
|
+
const rtt = stage.rtt_ms || 1;
|
|
62
|
+
const tel = stage.node_telemetry;
|
|
63
|
+
const gpuModel = tel?.gpu_model;
|
|
64
|
+
const device = tel?.device;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div key={i} className="flex items-center gap-2 px-3 py-1">
|
|
68
|
+
<div className="w-[110px] flex-shrink-0 flex flex-col gap-0.5">
|
|
69
|
+
<div className="flex items-center gap-1.5">
|
|
70
|
+
<span className="text-2xs font-mono text-text-2 truncate">{shortAddr(stage.node)}</span>
|
|
71
|
+
<Badge variant={stage.via === 'p2p' ? 'success' : 'warning'} className="text-2xs px-1 py-0 leading-tight">
|
|
72
|
+
{stage.via || '?'}
|
|
73
|
+
</Badge>
|
|
74
|
+
</div>
|
|
75
|
+
{gpuModel && (
|
|
76
|
+
<div className="flex items-center gap-1">
|
|
77
|
+
{device && (
|
|
78
|
+
<span className={cn(
|
|
79
|
+
'text-2xs font-mono px-1 py-0 rounded leading-tight',
|
|
80
|
+
device === 'cuda' ? 'bg-info/20 text-info' : 'bg-purple/20 text-purple',
|
|
81
|
+
)}>
|
|
82
|
+
{device}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
<span className="text-2xs font-mono text-text-4 truncate">{gpuModel}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex-1 min-w-0 h-4 bg-surface-2 rounded-sm overflow-hidden relative flex">
|
|
90
|
+
{phases.map((p) => {
|
|
91
|
+
const ms = stage[p] || 0;
|
|
92
|
+
const pct = (ms / maxRtt) * 100;
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
key={p}
|
|
96
|
+
className="h-full flex-shrink-0 transition-all"
|
|
97
|
+
style={{ width: `${pct}%`, background: PHASE_COLORS[p] }}
|
|
98
|
+
title={`${PHASE_LABELS[p]}: ${ms.toFixed(1)}ms`}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums w-[52px] text-right flex-shrink-0">
|
|
104
|
+
{rtt.toFixed(1)}ms
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -106,6 +106,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
106
106
|
networkUpdateProgress: { updating: false, step: null, message: null, percent: 0, error: null },
|
|
107
107
|
networkCompute: { totalRamMb: 0, totalVramMb: 0, totalCpuCores: 0, totalBandwidthMbps: 0, activeNodes: 0, totalNodes: 0, avgLoad: 0 },
|
|
108
108
|
networkSnapshots: [],
|
|
109
|
+
networkTokenTiming: null,
|
|
110
|
+
networkBenchmarks: [],
|
|
111
|
+
networkTraces: [],
|
|
112
|
+
networkPerfSnapshots: [],
|
|
113
|
+
networkNodeTelemetry: {},
|
|
109
114
|
networkWallet: { connected: false, address: null, balance: '0.00', token: 'GROOVE', chain: 'base-l2' },
|
|
110
115
|
networkEarnings: { today: 0, thisWeek: 0, allTime: 0, history: [] },
|
|
111
116
|
|
|
@@ -901,6 +906,33 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
901
906
|
}
|
|
902
907
|
break;
|
|
903
908
|
}
|
|
909
|
+
|
|
910
|
+
case 'network:token:timing': {
|
|
911
|
+
const { __proto__: _a, constructor: _b, prototype: _c, ...td } = msg.data || {};
|
|
912
|
+
const updates = {
|
|
913
|
+
networkTokenTiming: td,
|
|
914
|
+
networkPerfSnapshots: [...get().networkPerfSnapshots, { t: Date.now(), tps: td.tps || 0 }].slice(-100),
|
|
915
|
+
};
|
|
916
|
+
if (Array.isArray(td.stages)) {
|
|
917
|
+
const telMap = { ...get().networkNodeTelemetry };
|
|
918
|
+
const unsafe = new Set(['__proto__', 'constructor', 'prototype']);
|
|
919
|
+
for (const stage of td.stages) {
|
|
920
|
+
const nid = stage.node_telemetry?.node_id;
|
|
921
|
+
if (nid && typeof nid === 'string' && !unsafe.has(nid)) {
|
|
922
|
+
telMap[nid] = { ...stage.node_telemetry, forward_ms: stage.forward_ms, updatedAt: Date.now() };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
updates.networkNodeTelemetry = telMap;
|
|
926
|
+
}
|
|
927
|
+
set(updates);
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
case 'network:timing:summary': {
|
|
932
|
+
const { __proto__: _a, constructor: _b, prototype: _c, ...sd } = msg.data || {};
|
|
933
|
+
set((s) => ({ networkBenchmarks: [...s.networkBenchmarks, sd].slice(-100) }));
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
904
936
|
}
|
|
905
937
|
};
|
|
906
938
|
|
|
@@ -2279,6 +2311,34 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2279
2311
|
return get().networkEarnings;
|
|
2280
2312
|
},
|
|
2281
2313
|
|
|
2314
|
+
async fetchNetworkBenchmarks() {
|
|
2315
|
+
try {
|
|
2316
|
+
const data = await api.get('/network/benchmarks');
|
|
2317
|
+
if (Array.isArray(data)) set({ networkBenchmarks: data.slice(-100) });
|
|
2318
|
+
return data;
|
|
2319
|
+
} catch { return null; }
|
|
2320
|
+
},
|
|
2321
|
+
|
|
2322
|
+
async fetchNetworkTraces() {
|
|
2323
|
+
try {
|
|
2324
|
+
const data = await api.get('/network/traces');
|
|
2325
|
+
if (Array.isArray(data)) set({ networkTraces: data });
|
|
2326
|
+
return data;
|
|
2327
|
+
} catch { return null; }
|
|
2328
|
+
},
|
|
2329
|
+
|
|
2330
|
+
async fetchNetworkTrace(filename) {
|
|
2331
|
+
try {
|
|
2332
|
+
return await api.get(`/network/traces/${encodeURIComponent(filename)}`);
|
|
2333
|
+
} catch { return null; }
|
|
2334
|
+
},
|
|
2335
|
+
|
|
2336
|
+
async fetchLiveTrace(offset = 0) {
|
|
2337
|
+
try {
|
|
2338
|
+
return await api.get(`/network/traces/live?offset=${offset}`);
|
|
2339
|
+
} catch { return null; }
|
|
2340
|
+
},
|
|
2341
|
+
|
|
2282
2342
|
async renameFile(oldPath, newPath) {
|
|
2283
2343
|
try {
|
|
2284
2344
|
await api.post('/files/rename', { oldPath, newPath });
|
|
@@ -17,6 +17,7 @@ import { NodeCard } from '../components/network/node-card';
|
|
|
17
17
|
import { EarningsCard } from '../components/network/earnings-card';
|
|
18
18
|
import { FleetTable } from '../components/network/fleet-table';
|
|
19
19
|
import { WalletView } from '../components/network/wallet-view';
|
|
20
|
+
import { PerformanceDashboard } from '../components/network/performance-dashboard';
|
|
20
21
|
import { HEX, hexAlpha } from '../lib/theme-hex';
|
|
21
22
|
import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle, Zap } from 'lucide-react';
|
|
22
23
|
|
|
@@ -483,6 +484,7 @@ export default function NetworkView() {
|
|
|
483
484
|
<TabsList className="bg-surface-0 border-b border-border-subtle px-4 flex-shrink-0">
|
|
484
485
|
<TabsTrigger value="overview" className="text-xs">Overview</TabsTrigger>
|
|
485
486
|
<TabsTrigger value="fleet" className="text-xs">Fleet</TabsTrigger>
|
|
487
|
+
<TabsTrigger value="performance" className="text-xs flex items-center gap-1"><Zap size={10} />Performance</TabsTrigger>
|
|
486
488
|
<TabsTrigger value="wallet" className="text-xs">Wallet</TabsTrigger>
|
|
487
489
|
</TabsList>
|
|
488
490
|
|
|
@@ -494,6 +496,10 @@ export default function NetworkView() {
|
|
|
494
496
|
<FleetTable />
|
|
495
497
|
</TabsContent>
|
|
496
498
|
|
|
499
|
+
<TabsContent value="performance" className="flex-1 min-h-0 overflow-hidden bg-surface-1">
|
|
500
|
+
<PerformanceDashboard active={activeTab === 'performance'} />
|
|
501
|
+
</TabsContent>
|
|
502
|
+
|
|
497
503
|
<TabsContent value="wallet" className="flex-1 min-h-0 overflow-hidden">
|
|
498
504
|
<WalletView />
|
|
499
505
|
</TabsContent>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.70",
|
|
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)",
|
|
@@ -8,6 +8,7 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSy
|
|
|
8
8
|
import { spawn, execFile, execFileSync } from 'child_process';
|
|
9
9
|
import { createHash, randomUUID } from 'crypto';
|
|
10
10
|
import { hostname, networkInterfaces, homedir } from 'os';
|
|
11
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
12
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
12
13
|
import { listProviders, getProvider } from './providers/index.js';
|
|
13
14
|
import { OllamaProvider } from './providers/ollama.js';
|
|
@@ -4299,14 +4300,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4299
4300
|
hardware: getLocalHardware(),
|
|
4300
4301
|
startedAt: Date.now(),
|
|
4301
4302
|
events: [],
|
|
4303
|
+
lastTokenTiming: null,
|
|
4302
4304
|
};
|
|
4305
|
+
if (!daemon.networkBenchmarks) daemon.networkBenchmarks = [];
|
|
4303
4306
|
|
|
4304
4307
|
pushNodeEvent('starting', { pid: proc.pid, signal, device });
|
|
4305
4308
|
broadcastNodeStatus();
|
|
4306
4309
|
|
|
4307
4310
|
let stderrBuf = '';
|
|
4311
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
4308
4312
|
proc.stderr.on('data', (chunk) => {
|
|
4309
|
-
stderrBuf +=
|
|
4313
|
+
stderrBuf += stderrDecoder.write(chunk);
|
|
4310
4314
|
let idx;
|
|
4311
4315
|
while ((idx = stderrBuf.indexOf('\n')) !== -1) {
|
|
4312
4316
|
const line = stderrBuf.slice(0, idx).trim();
|
|
@@ -4367,14 +4371,49 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4367
4371
|
if (entry.capabilities || entry.hardware) {
|
|
4368
4372
|
daemon.networkNode.hardware = normalizeHardware(entry.capabilities || entry.hardware); changed = true;
|
|
4369
4373
|
}
|
|
4374
|
+
if (entry.type === 'token') {
|
|
4375
|
+
const timing = {
|
|
4376
|
+
token_ms: entry.token_ms, pipeline_ms: entry.pipeline_ms,
|
|
4377
|
+
prefill_ms: entry.prefill_ms, logits_deser_ms: entry.logits_deser_ms,
|
|
4378
|
+
sample_ms: entry.sample_ms, decode_ms: entry.decode_ms,
|
|
4379
|
+
tps: entry.tps, ttft_ms: entry.ttft_ms, is_prefill: entry.is_prefill,
|
|
4380
|
+
tokens_generated: entry.tokens_generated,
|
|
4381
|
+
stages: Array.isArray(entry.stages) ? entry.stages : [],
|
|
4382
|
+
};
|
|
4383
|
+
daemon.networkNode.lastTokenTiming = timing;
|
|
4384
|
+
daemon.broadcast({ type: 'network:token:timing', data: timing });
|
|
4385
|
+
}
|
|
4386
|
+
if (entry.type === 'timing') {
|
|
4387
|
+
const summary = {
|
|
4388
|
+
ttft_ms: entry.ttft_ms, tps: entry.tps,
|
|
4389
|
+
tokens_generated: entry.tokens_generated,
|
|
4390
|
+
total_network_ms: entry.total_network_ms,
|
|
4391
|
+
total_compute_ms: entry.total_compute_ms,
|
|
4392
|
+
p2p_sends: entry.p2p_sends, relay_sends: entry.relay_sends,
|
|
4393
|
+
stage_0_avg_ms: entry.stage_0_avg_ms, stage_0_count: entry.stage_0_count,
|
|
4394
|
+
stage_1_avg_ms: entry.stage_1_avg_ms, stage_1_count: entry.stage_1_count,
|
|
4395
|
+
t: Date.now(),
|
|
4396
|
+
};
|
|
4397
|
+
if (!daemon.networkBenchmarks) daemon.networkBenchmarks = [];
|
|
4398
|
+
daemon.networkBenchmarks.push(summary);
|
|
4399
|
+
if (daemon.networkBenchmarks.length > 100) daemon.networkBenchmarks.shift();
|
|
4400
|
+
daemon.broadcast({ type: 'network:timing:summary', data: summary });
|
|
4401
|
+
}
|
|
4370
4402
|
pushNodeEvent(msg || 'log', entry);
|
|
4371
4403
|
if (changed) broadcastNodeStatus();
|
|
4372
4404
|
}
|
|
4373
4405
|
});
|
|
4374
4406
|
|
|
4407
|
+
let stdoutBuf = '';
|
|
4408
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
4375
4409
|
proc.stdout.on('data', (chunk) => {
|
|
4376
|
-
|
|
4377
|
-
|
|
4410
|
+
stdoutBuf += stdoutDecoder.write(chunk);
|
|
4411
|
+
let idx;
|
|
4412
|
+
while ((idx = stdoutBuf.indexOf('\n')) !== -1) {
|
|
4413
|
+
const line = stdoutBuf.slice(0, idx).trim();
|
|
4414
|
+
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
4415
|
+
if (line) pushNodeEvent('stdout', { line });
|
|
4416
|
+
}
|
|
4378
4417
|
});
|
|
4379
4418
|
|
|
4380
4419
|
proc.on('error', (err) => {
|
|
@@ -4384,6 +4423,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4384
4423
|
});
|
|
4385
4424
|
|
|
4386
4425
|
proc.on('exit', (code, signal) => {
|
|
4426
|
+
const trailing = stdoutDecoder.end();
|
|
4427
|
+
if (trailing) stdoutBuf += trailing;
|
|
4428
|
+
if (stdoutBuf.trim()) pushNodeEvent('stdout', { line: stdoutBuf.trim() });
|
|
4429
|
+
const trailingErr = stderrDecoder.end();
|
|
4430
|
+
if (trailingErr) stderrBuf += trailingErr;
|
|
4387
4431
|
daemon.networkNode.active = false;
|
|
4388
4432
|
daemon.networkNode.status = 'stopped';
|
|
4389
4433
|
daemon.networkNode.pid = null;
|
|
@@ -4409,6 +4453,96 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4409
4453
|
res.json({ stopping: true });
|
|
4410
4454
|
});
|
|
4411
4455
|
|
|
4456
|
+
app.get('/api/network/benchmarks', networkGate, (req, res) => {
|
|
4457
|
+
res.json(daemon.networkBenchmarks || []);
|
|
4458
|
+
});
|
|
4459
|
+
|
|
4460
|
+
app.get('/api/network/timing', networkGate, (req, res) => {
|
|
4461
|
+
res.json({
|
|
4462
|
+
current: daemon.networkNode?.lastTokenTiming || null,
|
|
4463
|
+
benchmarkCount: (daemon.networkBenchmarks || []).length,
|
|
4464
|
+
});
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
app.get('/api/network/traces', networkGate, (req, res) => {
|
|
4468
|
+
const tracesDir = resolve(homedir(), '.groove', 'traces');
|
|
4469
|
+
if (!existsSync(tracesDir)) return res.json([]);
|
|
4470
|
+
try {
|
|
4471
|
+
const files = readdirSync(tracesDir)
|
|
4472
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
4473
|
+
.map((f) => {
|
|
4474
|
+
const st = statSync(resolve(tracesDir, f));
|
|
4475
|
+
return { filename: f, size: st.size, mtime: st.mtimeMs };
|
|
4476
|
+
})
|
|
4477
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
4478
|
+
res.json(files);
|
|
4479
|
+
} catch { res.json([]); }
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4482
|
+
app.get('/api/network/traces/live', networkGate, (req, res) => {
|
|
4483
|
+
const tracesDir = resolve(homedir(), '.groove', 'traces');
|
|
4484
|
+
if (!existsSync(tracesDir)) {
|
|
4485
|
+
return res.json({ lines: [], nextOffset: 0, filename: null, active: false });
|
|
4486
|
+
}
|
|
4487
|
+
try {
|
|
4488
|
+
const files = readdirSync(tracesDir)
|
|
4489
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
4490
|
+
.map((f) => {
|
|
4491
|
+
const st = statSync(resolve(tracesDir, f));
|
|
4492
|
+
return { filename: f, mtime: st.mtimeMs };
|
|
4493
|
+
})
|
|
4494
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
4495
|
+
if (files.length === 0) {
|
|
4496
|
+
return res.json({ lines: [], nextOffset: 0, filename: null, active: false });
|
|
4497
|
+
}
|
|
4498
|
+
const newest = files[0];
|
|
4499
|
+
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
|
|
4500
|
+
const filePath = resolve(tracesDir, newest.filename);
|
|
4501
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
4502
|
+
const allLines = raw.split('\n').filter(Boolean);
|
|
4503
|
+
const sliced = allLines.slice(offset);
|
|
4504
|
+
const parsed = [];
|
|
4505
|
+
for (const line of sliced) {
|
|
4506
|
+
try { parsed.push(JSON.parse(line)); } catch { /* skip malformed */ }
|
|
4507
|
+
}
|
|
4508
|
+
const active = !!(daemon.networkNode?.active && (daemon.networkNode.sessions || 0) > 0);
|
|
4509
|
+
res.json({
|
|
4510
|
+
lines: parsed,
|
|
4511
|
+
nextOffset: offset + sliced.length,
|
|
4512
|
+
filename: newest.filename,
|
|
4513
|
+
active,
|
|
4514
|
+
});
|
|
4515
|
+
} catch {
|
|
4516
|
+
res.json({ lines: [], nextOffset: 0, filename: null, active: false });
|
|
4517
|
+
}
|
|
4518
|
+
});
|
|
4519
|
+
|
|
4520
|
+
app.get('/api/network/traces/:filename', networkGate, (req, res) => {
|
|
4521
|
+
const { filename } = req.params;
|
|
4522
|
+
if (!filename || /[/\\]/.test(filename) || !filename.endsWith('.jsonl')) {
|
|
4523
|
+
return res.status(400).json({ error: 'Invalid filename' });
|
|
4524
|
+
}
|
|
4525
|
+
const tracesDir = resolve(homedir(), '.groove', 'traces');
|
|
4526
|
+
const filePath = resolve(tracesDir, filename);
|
|
4527
|
+
if (!filePath.startsWith(tracesDir + sep)) {
|
|
4528
|
+
return res.status(400).json({ error: 'Invalid filename' });
|
|
4529
|
+
}
|
|
4530
|
+
if (!existsSync(filePath)) {
|
|
4531
|
+
return res.status(404).json({ error: 'Trace file not found' });
|
|
4532
|
+
}
|
|
4533
|
+
try {
|
|
4534
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
4535
|
+
const lines = raw.split('\n').filter(Boolean).slice(0, 5000);
|
|
4536
|
+
const entries = [];
|
|
4537
|
+
for (const line of lines) {
|
|
4538
|
+
try { entries.push(JSON.parse(line)); } catch { /* skip malformed lines */ }
|
|
4539
|
+
}
|
|
4540
|
+
res.json(entries);
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
res.status(500).json({ error: `Failed to read trace: ${err.message}` });
|
|
4543
|
+
}
|
|
4544
|
+
});
|
|
4545
|
+
|
|
4412
4546
|
function isAllowedSignalHost(host) {
|
|
4413
4547
|
const h = (host || '').replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '').toLowerCase();
|
|
4414
4548
|
return h === 'signal.groovedev.ai' || h.endsWith('.groovedev.ai');
|