groove-dev 0.27.44 → 0.27.45

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/default/groovedev-beta-auth-endpoint.md +166 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +619 -0
  5. package/node_modules/@groove-dev/daemon/src/firstrun.js +11 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +28 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -1
  8. package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +114 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-BoIbnaqa.js +8607 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/app.jsx +3 -0
  15. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +7 -2
  16. package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +164 -0
  17. package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +66 -0
  18. package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +172 -0
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +190 -0
  20. package/node_modules/@groove-dev/gui/src/views/network.jsx +227 -0
  21. package/node_modules/@groove-dev/gui/src/views/settings.jsx +88 -1
  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 +619 -0
  26. package/packages/daemon/src/firstrun.js +11 -0
  27. package/packages/daemon/src/index.js +28 -0
  28. package/packages/daemon/src/providers/claude-code.js +1 -1
  29. package/packages/daemon/src/providers/groove-network.js +114 -0
  30. package/packages/daemon/src/providers/index.js +2 -0
  31. package/packages/gui/dist/assets/index-BoIbnaqa.js +8607 -0
  32. package/packages/gui/dist/assets/index-CyVj0fHl.css +1 -0
  33. package/packages/gui/dist/index.html +2 -2
  34. package/packages/gui/package.json +1 -1
  35. package/packages/gui/src/app.jsx +3 -0
  36. package/packages/gui/src/components/layout/activity-bar.jsx +7 -2
  37. package/packages/gui/src/components/network/network-status.jsx +164 -0
  38. package/packages/gui/src/components/network/node-details.jsx +66 -0
  39. package/packages/gui/src/components/network/node-toggle.jsx +172 -0
  40. package/packages/gui/src/stores/groove.js +190 -0
  41. package/packages/gui/src/views/network.jsx +227 -0
  42. package/packages/gui/src/views/settings.jsx +88 -1
  43. package/node_modules/@groove-dev/gui/dist/assets/index-B5Uor698.js +0 -8607
  44. package/node_modules/@groove-dev/gui/dist/assets/index-VGmIZurO.css +0 -1
  45. package/packages/gui/dist/assets/index-B5Uor698.js +0 -8607
  46. package/packages/gui/dist/assets/index-VGmIZurO.css +0 -1
@@ -0,0 +1,172 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { Badge } from '../ui/badge';
6
+ import { StatusDot } from '../ui/status-dot';
7
+ import { Tooltip } from '../ui/tooltip';
8
+ import { Copy, Check, Cpu } from 'lucide-react';
9
+
10
+ function shortAddress(addr) {
11
+ if (!addr || typeof addr !== 'string') return '—';
12
+ if (addr.length < 14) return addr;
13
+ return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
14
+ }
15
+
16
+ function nodeStatus(node) {
17
+ if (node.active && node.status === 'connected') return 'running';
18
+ if (node.status === 'connecting') return 'starting';
19
+ return 'crashed';
20
+ }
21
+
22
+ function Toggle({ value, onChange, disabled }) {
23
+ return (
24
+ <button
25
+ onClick={() => !disabled && onChange(!value)}
26
+ disabled={disabled}
27
+ className={cn(
28
+ 'relative inline-flex h-7 w-12 rounded-full p-0.5 transition-colors',
29
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
30
+ value ? 'bg-accent' : 'bg-surface-5',
31
+ )}
32
+ >
33
+ <span
34
+ className={cn(
35
+ 'inline-block h-6 w-6 rounded-full bg-white shadow-sm transition-transform',
36
+ value ? 'translate-x-5' : 'translate-x-0',
37
+ )}
38
+ />
39
+ </button>
40
+ );
41
+ }
42
+
43
+ export function NodeToggle() {
44
+ const node = useGrooveStore((s) => s.networkNode);
45
+ const startNetworkNode = useGrooveStore((s) => s.startNetworkNode);
46
+ const stopNetworkNode = useGrooveStore((s) => s.stopNetworkNode);
47
+ const [pending, setPending] = useState(false);
48
+ const [copied, setCopied] = useState(false);
49
+
50
+ async function handleToggle(next) {
51
+ setPending(true);
52
+ try {
53
+ if (next) await startNetworkNode();
54
+ else await stopNetworkNode();
55
+ } catch { /* toasted in store */ }
56
+ setPending(false);
57
+ }
58
+
59
+ async function handleCopy() {
60
+ if (!node.nodeId) return;
61
+ try {
62
+ await navigator.clipboard.writeText(node.nodeId);
63
+ setCopied(true);
64
+ setTimeout(() => setCopied(false), 1500);
65
+ } catch { /* clipboard blocked */ }
66
+ }
67
+
68
+ const active = !!node.active;
69
+ const hardware = node.hardware || {};
70
+ const layersLabel = Array.isArray(node.layers)
71
+ ? `layers ${node.layers[0]}-${node.layers[1]}`
72
+ : (node.layers && node.layers.start != null)
73
+ ? `layers ${node.layers.start}-${node.layers.end}`
74
+ : 'unassigned';
75
+ const modelLabel = node.model || 'No model assigned';
76
+ const memPct = Number.isFinite(node.memoryPct) ? node.memoryPct : null;
77
+
78
+ return (
79
+ <div className="rounded-lg border border-border bg-surface-1 overflow-hidden">
80
+ {/* Hero toggle */}
81
+ <div className="flex items-center gap-4 px-5 py-4 border-b border-border-subtle">
82
+ <div className="flex-1 min-w-0">
83
+ <div className="text-sm font-semibold text-text-0 font-sans">Lend Compute</div>
84
+ <div className="text-2xs text-text-3 font-sans mt-0.5">
85
+ {active ? 'Contributing to the Groove network' : 'Your machine can contribute to the Groove network'}
86
+ </div>
87
+ </div>
88
+ <Toggle value={active} onChange={handleToggle} disabled={pending} />
89
+ </div>
90
+
91
+ {/* Body */}
92
+ <div className="px-5 py-4 space-y-4">
93
+ {!active ? (
94
+ <div>
95
+ <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2">Detected hardware</div>
96
+ <div className="grid grid-cols-3 gap-2">
97
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
98
+ <div className="text-2xs text-text-4 font-sans">Device</div>
99
+ <div className="text-xs font-mono text-text-1 truncate">{hardware.device || 'auto'}</div>
100
+ </div>
101
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
102
+ <div className="text-2xs text-text-4 font-sans">Memory</div>
103
+ <div className="text-xs font-mono text-text-1 truncate">{hardware.memory || '—'}</div>
104
+ </div>
105
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
106
+ <div className="text-2xs text-text-4 font-sans">GPU</div>
107
+ <div className="text-xs font-mono text-text-1 truncate">{hardware.gpu || 'None'}</div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ ) : (
112
+ <>
113
+ {/* Status row */}
114
+ <div className="flex items-center gap-3">
115
+ <StatusDot status={nodeStatus(node)} />
116
+ <div className="text-xs font-sans text-text-1 capitalize">{node.status || 'disconnected'}</div>
117
+ <div className="flex-1" />
118
+ <Badge variant="accent" className="font-mono">
119
+ <Cpu size={9} /> {modelLabel} · {layersLabel}
120
+ </Badge>
121
+ </div>
122
+
123
+ {/* Identity */}
124
+ <div>
125
+ <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">Node Identity</div>
126
+ <div className="flex items-center gap-2">
127
+ <code className="flex-1 h-8 px-2.5 flex items-center bg-surface-0 border border-border-subtle rounded-md text-xs font-mono text-text-1">
128
+ {shortAddress(node.nodeId)}
129
+ </code>
130
+ <Tooltip content={copied ? 'Copied' : 'Copy full address'} side="top">
131
+ <button
132
+ onClick={handleCopy}
133
+ className="h-8 w-8 inline-flex items-center justify-center rounded-md border border-border-subtle bg-surface-0 text-text-3 hover:text-accent hover:border-accent/40 cursor-pointer transition-colors"
134
+ >
135
+ {copied ? <Check size={12} /> : <Copy size={12} />}
136
+ </button>
137
+ </Tooltip>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Sessions + memory */}
142
+ <div className="grid grid-cols-2 gap-2">
143
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
144
+ <div className="text-2xs text-text-4 font-sans uppercase tracking-wider">Active Sessions</div>
145
+ <div className="text-lg font-mono text-text-0 tabular-nums leading-tight">{node.sessions || 0}</div>
146
+ </div>
147
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
148
+ <div className="text-2xs text-text-4 font-sans uppercase tracking-wider mb-1">Memory</div>
149
+ {memPct != null ? (
150
+ <>
151
+ <div className="h-1.5 w-full rounded-full bg-surface-3 overflow-hidden">
152
+ <div
153
+ className={cn(
154
+ 'h-full rounded-full transition-all',
155
+ memPct >= 90 ? 'bg-danger' : memPct >= 70 ? 'bg-warning' : 'bg-accent',
156
+ )}
157
+ style={{ width: `${Math.min(100, Math.max(0, memPct))}%` }}
158
+ />
159
+ </div>
160
+ <div className="mt-1 text-2xs font-mono text-text-3 tabular-nums">{Math.round(memPct)}%</div>
161
+ </>
162
+ ) : (
163
+ <div className="text-xs font-mono text-text-3">—</div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ </>
168
+ )}
169
+ </div>
170
+ </div>
171
+ );
172
+ }
@@ -86,6 +86,14 @@ export const useGrooveStore = create((set, get) => ({
86
86
  // ── Journalist ────────────────────────────────────────────
87
87
  journalistStatus: null, // { cycleCount, lastCycleTime, history, lastSynthesis }
88
88
 
89
+ // ── Network (Early Access) ────────────────────────────────
90
+ networkUnlocked: false,
91
+ networkInstalled: false,
92
+ networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
93
+ networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
94
+ networkStatus: { nodes: [], coverage: 0, totalLayers: 0, models: [], activeSessions: 0 },
95
+ networkEvents: [],
96
+
89
97
  // ── Marketplace Auth ───────────────────────────────────────
90
98
  marketplaceUser: null, // { id, displayName, avatar, ... } or null
91
99
  marketplaceAuthenticated: false,
@@ -155,6 +163,8 @@ export const useGrooveStore = create((set, get) => ({
155
163
  get().fetchApprovals();
156
164
  get().checkMarketplaceAuth();
157
165
  get().fetchTunnels();
166
+ get().fetchBetaStatus();
167
+ get().fetchNetworkInstallStatus();
158
168
  if (!get().onboardingComplete) get().fetchOnboardingStatus();
159
169
  if (window.groove?.auth?.onSubscriptionStatus) {
160
170
  window.groove.auth.onSubscriptionStatus((data) => {
@@ -593,6 +603,59 @@ export const useGrooveStore = create((set, get) => ({
593
603
  set({ marketplaceAuthenticated: false, marketplaceUser: null });
594
604
  get().addToast('warning', 'Session expired', 'Please sign in again');
595
605
  break;
606
+
607
+ case 'network:node:status':
608
+ set({ networkNode: { ...get().networkNode, ...(msg.data || {}) } });
609
+ break;
610
+
611
+ case 'network:node:event': {
612
+ const ev = msg.data || {};
613
+ set((s) => ({
614
+ networkEvents: [...s.networkEvents, { ...ev, timestamp: ev.timestamp || Date.now() }].slice(-100),
615
+ }));
616
+ break;
617
+ }
618
+
619
+ case 'network:status':
620
+ set({ networkStatus: { ...get().networkStatus, ...(msg.data || {}) } });
621
+ break;
622
+
623
+ case 'network:install:progress': {
624
+ const { step, message, percent } = msg.data || {};
625
+ if (step === 'done') {
626
+ set({
627
+ networkInstalled: true,
628
+ networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
629
+ });
630
+ get().addToast('success', 'Network package installed');
631
+ } else if (step === 'error') {
632
+ set({
633
+ networkInstallProgress: {
634
+ installing: false,
635
+ step: 'error',
636
+ message: message || 'Install failed',
637
+ percent: 0,
638
+ error: message || 'Install failed',
639
+ },
640
+ });
641
+ } else {
642
+ set({
643
+ networkInstallProgress: {
644
+ installing: true,
645
+ step: step || 'progress',
646
+ message: message || '',
647
+ percent: Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : get().networkInstallProgress.percent,
648
+ error: null,
649
+ },
650
+ });
651
+ }
652
+ break;
653
+ }
654
+
655
+ case 'config:updated':
656
+ get().fetchBetaStatus();
657
+ get().fetchNetworkInstallStatus();
658
+ break;
596
659
  }
597
660
  };
598
661
 
@@ -1589,6 +1652,133 @@ export const useGrooveStore = create((set, get) => ({
1589
1652
  }
1590
1653
  },
1591
1654
 
1655
+ // ── Network (Early Access) ────────────────────────────────
1656
+
1657
+ async fetchBetaStatus() {
1658
+ try {
1659
+ const data = await api.get('/beta/status');
1660
+ set({ networkUnlocked: !!data?.unlocked });
1661
+ } catch { /* endpoint may not exist yet */ }
1662
+ },
1663
+
1664
+ async activateBeta(code) {
1665
+ const data = await api.post('/beta/activate', { code });
1666
+ if (!data?.unlocked) {
1667
+ throw new Error(data?.message || 'Invalid invite code');
1668
+ }
1669
+ set({ networkUnlocked: true });
1670
+ return data;
1671
+ },
1672
+
1673
+ async deactivateBeta() {
1674
+ try {
1675
+ await api.post('/beta/deactivate');
1676
+ set({
1677
+ networkUnlocked: false,
1678
+ activeView: get().activeView === 'network' ? 'agents' : get().activeView,
1679
+ });
1680
+ } catch (err) {
1681
+ get().addToast('error', 'Deactivate failed', err.message);
1682
+ throw err;
1683
+ }
1684
+ },
1685
+
1686
+ async fetchNetworkNodeStatus() {
1687
+ try {
1688
+ const data = await api.get('/network/node/status');
1689
+ const update = { networkNode: { ...get().networkNode, ...(data || {}) } };
1690
+ if (data && typeof data.installed === 'boolean') {
1691
+ update.networkInstalled = data.installed;
1692
+ }
1693
+ set(update);
1694
+ return data;
1695
+ } catch { return null; }
1696
+ },
1697
+
1698
+ async fetchNetworkInstallStatus() {
1699
+ try {
1700
+ const data = await api.get('/network/install/status');
1701
+ if (data && typeof data.installed === 'boolean') {
1702
+ set({ networkInstalled: data.installed });
1703
+ }
1704
+ return data;
1705
+ } catch { return null; }
1706
+ },
1707
+
1708
+ async installNetworkPackage() {
1709
+ set({
1710
+ networkInstallProgress: {
1711
+ installing: true,
1712
+ step: 'starting',
1713
+ message: 'Starting install…',
1714
+ percent: 0,
1715
+ error: null,
1716
+ },
1717
+ });
1718
+ try {
1719
+ await api.post('/network/install');
1720
+ } catch (err) {
1721
+ set({
1722
+ networkInstallProgress: {
1723
+ installing: false,
1724
+ step: 'error',
1725
+ message: err.message,
1726
+ percent: 0,
1727
+ error: err.message,
1728
+ },
1729
+ });
1730
+ get().addToast('error', 'Install failed', err.message);
1731
+ }
1732
+ },
1733
+
1734
+ async uninstallNetworkPackage() {
1735
+ try {
1736
+ await api.post('/network/uninstall');
1737
+ set({
1738
+ networkInstalled: false,
1739
+ networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
1740
+ networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
1741
+ });
1742
+ get().addToast('success', 'Network package uninstalled');
1743
+ } catch (err) {
1744
+ get().addToast('error', 'Uninstall failed', err.message);
1745
+ throw err;
1746
+ }
1747
+ },
1748
+
1749
+ async fetchNetworkStatus() {
1750
+ try {
1751
+ const data = await api.get('/network/status');
1752
+ set({ networkStatus: { ...get().networkStatus, ...(data || {}) } });
1753
+ return data;
1754
+ } catch { return null; }
1755
+ },
1756
+
1757
+ async startNetworkNode() {
1758
+ set({ networkNode: { ...get().networkNode, status: 'connecting' } });
1759
+ try {
1760
+ const data = await api.post('/network/node/start');
1761
+ set({ networkNode: { ...get().networkNode, active: true, ...(data || {}) } });
1762
+ get().addToast('success', 'Node started', 'Connecting to the Groove network');
1763
+ return data;
1764
+ } catch (err) {
1765
+ set({ networkNode: { ...get().networkNode, status: 'disconnected', active: false } });
1766
+ get().addToast('error', 'Node start failed', err.message);
1767
+ throw err;
1768
+ }
1769
+ },
1770
+
1771
+ async stopNetworkNode() {
1772
+ try {
1773
+ await api.post('/network/node/stop');
1774
+ set({ networkNode: { ...get().networkNode, active: false, status: 'disconnected' } });
1775
+ get().addToast('info', 'Node stopped');
1776
+ } catch (err) {
1777
+ get().addToast('error', 'Node stop failed', err.message);
1778
+ throw err;
1779
+ }
1780
+ },
1781
+
1592
1782
  async renameFile(oldPath, newPath) {
1593
1783
  try {
1594
1784
  await api.post('/files/rename', { oldPath, newPath });
@@ -0,0 +1,227 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useEffect, useState } from 'react';
3
+ import { useGrooveStore } from '../stores/groove';
4
+ import { ScrollArea } from '../components/ui/scroll-area';
5
+ import { StatusDot } from '../components/ui/status-dot';
6
+ import { Badge } from '../components/ui/badge';
7
+ import { Button } from '../components/ui/button';
8
+ import { Dialog, DialogContent, DialogTrigger } from '../components/ui/dialog';
9
+ import { NodeToggle } from '../components/network/node-toggle';
10
+ import { NodeDetails } from '../components/network/node-details';
11
+ import { NetworkStatus } from '../components/network/network-status';
12
+ import { Globe, Download, Check, AlertCircle, Loader2, Trash2 } from 'lucide-react';
13
+
14
+ const REQUIREMENTS = [
15
+ 'Python 3.10 or higher',
16
+ '~2 GB disk space for model shards',
17
+ '8 GB+ RAM recommended',
18
+ ];
19
+
20
+ function InstallProgress({ progress }) {
21
+ const percent = Math.max(0, Math.min(100, Number.isFinite(progress.percent) ? progress.percent : 0));
22
+ return (
23
+ <div className="w-full flex flex-col gap-3">
24
+ <div className="h-2 w-full rounded-full bg-surface-3 overflow-hidden">
25
+ <div
26
+ className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
27
+ style={{ width: `${percent}%` }}
28
+ />
29
+ </div>
30
+ <div className="flex items-center justify-between text-2xs font-mono text-text-3 tabular-nums">
31
+ <div className="flex items-center gap-2 text-text-2 font-sans">
32
+ <Loader2 size={12} className="animate-spin text-accent" />
33
+ <span className="truncate">{progress.message || 'Installing…'}</span>
34
+ </div>
35
+ <span>{percent}%</span>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ function InstallError({ message, onRetry }) {
42
+ return (
43
+ <div className="w-full flex flex-col gap-3">
44
+ <div className="rounded-md border border-danger/40 bg-danger/10 px-4 py-3 flex items-start gap-2.5 text-left">
45
+ <AlertCircle size={14} className="text-danger flex-shrink-0 mt-0.5" />
46
+ <div className="flex-1 min-w-0">
47
+ <div className="text-xs font-semibold text-danger font-sans mb-0.5">Install failed</div>
48
+ <div className="text-xs text-text-1 font-sans break-words">{message}</div>
49
+ </div>
50
+ </div>
51
+ <Button variant="primary" size="lg" onClick={onRetry} className="w-full">
52
+ <Download size={14} />
53
+ Retry Install
54
+ </Button>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function InstallGate() {
60
+ const installNetworkPackage = useGrooveStore((s) => s.installNetworkPackage);
61
+ const progress = useGrooveStore((s) => s.networkInstallProgress);
62
+ const { installing, error } = progress;
63
+
64
+ return (
65
+ <div className="flex flex-col items-center justify-center min-h-full px-6 py-12">
66
+ <div className="w-full max-w-md flex flex-col items-center text-center">
67
+ <div className="mb-5 rounded-full bg-surface-2 border border-border-subtle p-5">
68
+ <Globe size={48} className="text-text-3" strokeWidth={1.25} />
69
+ </div>
70
+ <h3 className="text-base font-semibold text-text-0 font-sans mb-2">
71
+ Install Groove Network
72
+ </h3>
73
+ <p className="text-sm text-text-2 font-sans leading-relaxed mb-6">
74
+ The network package enables decentralized LLM inference. Contribute your compute power or run models across the Groove network.
75
+ </p>
76
+
77
+ <div className="w-full rounded-md border border-border-subtle bg-surface-1 px-4 py-3 mb-6">
78
+ <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2 text-left">
79
+ Requirements
80
+ </div>
81
+ <ul className="flex flex-col gap-1.5">
82
+ {REQUIREMENTS.map((req) => (
83
+ <li key={req} className="flex items-center gap-2 text-xs font-sans text-text-1 text-left">
84
+ <Check size={12} className="text-accent flex-shrink-0" />
85
+ <span>{req}</span>
86
+ </li>
87
+ ))}
88
+ </ul>
89
+ </div>
90
+
91
+ {installing ? (
92
+ <InstallProgress progress={progress} />
93
+ ) : error ? (
94
+ <InstallError message={error} onRetry={() => installNetworkPackage()} />
95
+ ) : (
96
+ <>
97
+ <Button
98
+ variant="primary"
99
+ size="lg"
100
+ onClick={() => installNetworkPackage()}
101
+ className="w-full"
102
+ >
103
+ <Download size={14} />
104
+ Install Network Package
105
+ </Button>
106
+ <p className="text-2xs font-sans text-text-3 mt-3">
107
+ This will download and set up the Groove Network runtime (~500 MB)
108
+ </p>
109
+ </>
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function UninstallButton() {
117
+ const [open, setOpen] = useState(false);
118
+ const uninstallNetworkPackage = useGrooveStore((s) => s.uninstallNetworkPackage);
119
+ const [busy, setBusy] = useState(false);
120
+
121
+ const confirm = async () => {
122
+ setBusy(true);
123
+ try {
124
+ await uninstallNetworkPackage();
125
+ setOpen(false);
126
+ } catch { /* toast already shown */ }
127
+ finally { setBusy(false); }
128
+ };
129
+
130
+ return (
131
+ <Dialog open={open} onOpenChange={setOpen}>
132
+ <DialogTrigger asChild>
133
+ <button
134
+ type="button"
135
+ className="inline-flex items-center gap-1.5 text-2xs font-sans text-text-3 hover:text-danger transition-colors"
136
+ >
137
+ <Trash2 size={11} />
138
+ Uninstall Network Package
139
+ </button>
140
+ </DialogTrigger>
141
+ <DialogContent title="Uninstall Network Package" description="Confirm uninstall">
142
+ <div className="px-5 py-4 flex flex-col gap-3">
143
+ <p className="text-sm text-text-1 font-sans leading-relaxed">
144
+ This will stop your node and remove the network package from <span className="font-mono text-text-2">~/.groove/network</span>.
145
+ </p>
146
+ <p className="text-xs text-text-3 font-sans leading-relaxed">
147
+ Your identity (<span className="font-mono">~/.groove/node_key.json</span>) will be preserved — you can reinstall later without losing your wallet.
148
+ </p>
149
+ </div>
150
+ <div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border-subtle bg-surface-0">
151
+ <Button variant="ghost" size="sm" onClick={() => setOpen(false)} disabled={busy}>Cancel</Button>
152
+ <Button variant="danger" size="sm" onClick={confirm} disabled={busy}>
153
+ {busy ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
154
+ Uninstall
155
+ </Button>
156
+ </div>
157
+ </DialogContent>
158
+ </Dialog>
159
+ );
160
+ }
161
+
162
+ export default function NetworkView() {
163
+ const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
164
+ const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
165
+ const node = useGrooveStore((s) => s.networkNode);
166
+ const installed = useGrooveStore((s) => s.networkInstalled);
167
+
168
+ useEffect(() => {
169
+ fetchNetworkNodeStatus();
170
+ if (installed) {
171
+ fetchNetworkStatus();
172
+ const interval = setInterval(() => { fetchNetworkStatus(); }, 10000);
173
+ return () => clearInterval(interval);
174
+ }
175
+ }, [fetchNetworkNodeStatus, fetchNetworkStatus, installed]);
176
+
177
+ return (
178
+ <div className="flex flex-col h-full">
179
+ {/* Header */}
180
+ <div className="flex items-center gap-3 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
181
+ <Globe size={14} className="text-accent" />
182
+ <h2 className="text-sm font-semibold text-text-0 font-sans">Groove Network</h2>
183
+ <Badge variant="purple">Early Access</Badge>
184
+ <div className="flex-1" />
185
+ {installed && (
186
+ <>
187
+ <UninstallButton />
188
+ <div className="flex items-center gap-1.5 text-2xs font-sans text-text-3">
189
+ <StatusDot status={node.active ? 'running' : 'crashed'} size="sm" />
190
+ {node.active ? 'Contributing' : 'Idle'}
191
+ </div>
192
+ </>
193
+ )}
194
+ </div>
195
+
196
+ {/* Body */}
197
+ <ScrollArea className="flex-1">
198
+ {!installed ? (
199
+ <InstallGate />
200
+ ) : (
201
+ <div className="p-4 grid grid-cols-1 xl:grid-cols-2 gap-4">
202
+ {/* Left column — node operator */}
203
+ <div className="flex flex-col gap-3 min-w-0">
204
+ <div>
205
+ <div className="flex items-center gap-2 mb-2 px-0.5">
206
+ <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Node Operator</span>
207
+ <div className="flex-1 h-px bg-border-subtle" />
208
+ </div>
209
+ <NodeToggle />
210
+ </div>
211
+ <NodeDetails />
212
+ </div>
213
+
214
+ {/* Right column — network status */}
215
+ <div className="flex flex-col gap-3 min-w-0">
216
+ <div className="flex items-center gap-2 px-0.5">
217
+ <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Network Status</span>
218
+ <div className="flex-1 h-px bg-border-subtle" />
219
+ </div>
220
+ <NetworkStatus />
221
+ </div>
222
+ </div>
223
+ )}
224
+ </ScrollArea>
225
+ </div>
226
+ );
227
+ }