groove-dev 0.27.8 → 0.27.12
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/groove-icon.png +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +460 -25
- package/node_modules/@groove-dev/daemon/src/index.js +7 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
- package/node_modules/@groove-dev/daemon/src/process.js +67 -7
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
- package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +14 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
- package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
- package/node_modules/@groove-dev/gui/src/lib/electron.js +25 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
- package/node_modules/@groove-dev/gui/vite.config.js +3 -0
- package/package.json +7 -2
- package/packages/daemon/src/api.js +460 -25
- package/packages/daemon/src/index.js +7 -0
- package/packages/daemon/src/introducer.js +72 -4
- package/packages/daemon/src/journalist.js +66 -11
- package/packages/daemon/src/process.js +67 -7
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/repo-import.js +541 -0
- package/packages/daemon/src/rotator.js +28 -1
- package/packages/daemon/src/supervisor.js +2 -1
- package/packages/daemon/src/tunnel-manager.js +504 -0
- package/packages/daemon/src/validate.js +13 -0
- package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
- package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
- package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
- package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
- package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
- package/packages/gui/src/app.css +14 -0
- package/packages/gui/src/app.jsx +13 -0
- package/packages/gui/src/components/agents/agent-config.jsx +130 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
- package/packages/gui/src/components/layout/app-shell.jsx +7 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/packages/gui/src/components/layout/command-palette.jsx +14 -4
- package/packages/gui/src/components/layout/status-bar.jsx +46 -11
- package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
- package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
- package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
- package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/lib/edition.js +4 -0
- package/packages/gui/src/lib/electron.js +25 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +139 -6
- package/packages/gui/src/views/dashboard.jsx +38 -39
- package/packages/gui/src/views/marketplace.jsx +82 -0
- package/packages/gui/src/views/settings.jsx +66 -0
- package/packages/gui/vite.config.js +3 -0
- package/integrations/FEDERATION_PLAN.md +0 -583
- package/integrations/VOICE_PLAN.md +0 -232
- package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
- package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
- package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
- package/test-slack.mjs +0 -28
- /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { AlertTriangle } from 'lucide-react';
|
|
4
|
+
import { Dialog, DialogContent } from '../ui/dialog';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
|
|
7
|
+
export function RepoNukeDialog({ repo, open, onClose, onConfirm }) {
|
|
8
|
+
const [deleteFiles, setDeleteFiles] = useState(true);
|
|
9
|
+
|
|
10
|
+
if (!repo) return null;
|
|
11
|
+
|
|
12
|
+
const agents = repo.agents?.length || 0;
|
|
13
|
+
const processes = repo.processes?.length || 0;
|
|
14
|
+
const credentials = repo.credentialKeys?.length || 0;
|
|
15
|
+
const fileCount = repo.fileCount || 0;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
|
|
19
|
+
<DialogContent title={`Nuke ${repo.repoName || repo.name}?`} description="Confirm destructive removal of imported repo">
|
|
20
|
+
<div className="px-5 py-4 space-y-4">
|
|
21
|
+
<div className="flex items-start gap-2">
|
|
22
|
+
<AlertTriangle size={16} className="text-danger flex-shrink-0 mt-0.5" />
|
|
23
|
+
<p className="text-sm text-text-1 font-sans">This cannot be undone.</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="space-y-1.5 text-xs text-text-2 font-sans">
|
|
27
|
+
{agents > 0 && <div className="flex items-center gap-2">
|
|
28
|
+
<span className="text-success">✓</span> Kill {agents} agent{agents !== 1 ? 's' : ''}
|
|
29
|
+
</div>}
|
|
30
|
+
{processes > 0 && <div className="flex items-center gap-2">
|
|
31
|
+
<span className="text-success">✓</span> Stop {processes} process{processes !== 1 ? 'es' : ''}
|
|
32
|
+
</div>}
|
|
33
|
+
{credentials > 0 && <div className="flex items-center gap-2">
|
|
34
|
+
<span className="text-success">✓</span> Remove {credentials} credential{credentials !== 1 ? 's' : ''}
|
|
35
|
+
</div>}
|
|
36
|
+
<div className="flex items-center gap-2">
|
|
37
|
+
<span className="text-success">✓</span> Delete team "{repo.teamId || repo.repoName || repo.name}"
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<span className="text-success">✓</span> Clean all .groove state
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
45
|
+
<input
|
|
46
|
+
type="checkbox"
|
|
47
|
+
checked={deleteFiles}
|
|
48
|
+
onChange={(e) => setDeleteFiles(e.target.checked)}
|
|
49
|
+
className="accent-[var(--color-danger)]"
|
|
50
|
+
/>
|
|
51
|
+
<span className="text-xs text-text-1 font-sans">
|
|
52
|
+
Delete repo files{fileCount > 0 ? ` (${fileCount} files)` : ''}
|
|
53
|
+
</span>
|
|
54
|
+
</label>
|
|
55
|
+
|
|
56
|
+
<div className="flex items-center gap-2 pt-1">
|
|
57
|
+
<Button variant="danger" size="sm" onClick={() => onConfirm(deleteFiles)}>
|
|
58
|
+
Nuke Everything
|
|
59
|
+
</Button>
|
|
60
|
+
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
61
|
+
Cancel
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</DialogContent>
|
|
66
|
+
</Dialog>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
3
|
+
import { UpgradeCard } from './upgrade-card';
|
|
4
|
+
|
|
5
|
+
export function ProGate({ feature, description, children }) {
|
|
6
|
+
const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
|
|
7
|
+
const user = useGrooveStore((s) => s.marketplaceUser);
|
|
8
|
+
|
|
9
|
+
if (__GROOVE_EDITION__ !== 'pro') {
|
|
10
|
+
return <UpgradeCard feature={feature} description={description} variant="community" />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!authenticated) {
|
|
14
|
+
return <UpgradeCard feature={feature} description={description} variant="sign-in" />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!user?.subscription?.active) {
|
|
18
|
+
return <UpgradeCard feature={feature} description={description} variant="subscribe" />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return children;
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { Lock, Download, LogIn, Sparkles } from 'lucide-react';
|
|
3
|
+
import { openExternal } from '../../lib/electron';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
|
|
6
|
+
const VARIANTS = {
|
|
7
|
+
community: {
|
|
8
|
+
heading: 'Get Groove Desktop',
|
|
9
|
+
cta: 'Download',
|
|
10
|
+
icon: Download,
|
|
11
|
+
action: () => openExternal('https://groovedev.ai/download'),
|
|
12
|
+
},
|
|
13
|
+
'sign-in': {
|
|
14
|
+
heading: 'Sign in to unlock',
|
|
15
|
+
cta: 'Sign in',
|
|
16
|
+
icon: LogIn,
|
|
17
|
+
action: () => useGrooveStore.getState().marketplaceLogin(),
|
|
18
|
+
},
|
|
19
|
+
subscribe: {
|
|
20
|
+
heading: 'Upgrade to Pro',
|
|
21
|
+
cta: 'Subscribe',
|
|
22
|
+
icon: Sparkles,
|
|
23
|
+
action: () => openExternal('https://groovedev.ai/pro'),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function UpgradeCard({ feature, description, variant = 'community' }) {
|
|
28
|
+
const v = VARIANTS[variant] || VARIANTS.community;
|
|
29
|
+
const CtaIcon = v.icon;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1/50 px-5 py-6 text-center">
|
|
33
|
+
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-purple/10">
|
|
34
|
+
<Lock size={18} className="text-purple" />
|
|
35
|
+
</div>
|
|
36
|
+
<h3 className="text-sm font-semibold text-text-1 font-sans">{v.heading}</h3>
|
|
37
|
+
<p className="mt-1.5 text-2xs text-text-3 font-sans">{feature}</p>
|
|
38
|
+
<p className="mt-1 text-2xs text-text-4 font-sans max-w-xs mx-auto">{description}</p>
|
|
39
|
+
<button
|
|
40
|
+
onClick={v.action}
|
|
41
|
+
className="mt-4 inline-flex items-center gap-1.5 h-7 px-4 rounded-full bg-purple/15 text-purple text-xs font-semibold font-sans hover:bg-purple/25 transition-colors cursor-pointer"
|
|
42
|
+
>
|
|
43
|
+
<CtaIcon size={13} />
|
|
44
|
+
{v.cta}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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 { AnimatePresence, motion } from 'framer-motion';
|
|
6
|
+
import {
|
|
7
|
+
Server, Radio, ExternalLink, Loader2, X, Plus, Settings,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import { StatusDot } from '../ui/status-dot';
|
|
10
|
+
|
|
11
|
+
export function QuickConnect() {
|
|
12
|
+
const open = useGrooveStore((s) => s.quickConnectOpen);
|
|
13
|
+
const toggle = useGrooveStore((s) => s.toggleQuickConnect);
|
|
14
|
+
const savedTunnels = useGrooveStore((s) => s.savedTunnels);
|
|
15
|
+
const [connectingId, setConnectingId] = useState(null);
|
|
16
|
+
|
|
17
|
+
if (!open) return null;
|
|
18
|
+
|
|
19
|
+
async function handleConnect(id) {
|
|
20
|
+
setConnectingId(id);
|
|
21
|
+
try {
|
|
22
|
+
await useGrooveStore.getState().connectTunnel(id);
|
|
23
|
+
toggle();
|
|
24
|
+
} catch {}
|
|
25
|
+
setConnectingId(null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleOpenRemote(server) {
|
|
29
|
+
const port = server.localPort;
|
|
30
|
+
const name = encodeURIComponent(server.name);
|
|
31
|
+
window.open(`http://localhost:${port}?instance=${name}`, '_blank');
|
|
32
|
+
toggle();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" onClick={toggle} />
|
|
38
|
+
|
|
39
|
+
<AnimatePresence>
|
|
40
|
+
<motion.div
|
|
41
|
+
initial={{ opacity: 0, y: -20, scale: 0.96 }}
|
|
42
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
43
|
+
exit={{ opacity: 0, y: -10, scale: 0.98 }}
|
|
44
|
+
transition={{ duration: 0.15 }}
|
|
45
|
+
className="fixed top-[20%] left-1/2 -translate-x-1/2 z-50 w-[400px] bg-surface-1 border border-border rounded-lg shadow-2xl overflow-hidden"
|
|
46
|
+
>
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<Radio size={15} className="text-accent" />
|
|
51
|
+
<span className="text-sm font-semibold text-text-0 font-sans">Quick Connect</span>
|
|
52
|
+
</div>
|
|
53
|
+
<button onClick={toggle} className="p-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors">
|
|
54
|
+
<X size={14} />
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Server list */}
|
|
59
|
+
<div className="overflow-y-auto max-h-[320px] py-1">
|
|
60
|
+
{savedTunnels.length === 0 ? (
|
|
61
|
+
<div className="px-4 py-8 text-center">
|
|
62
|
+
<Server size={24} className="text-text-4 mx-auto mb-2" />
|
|
63
|
+
<p className="text-sm text-text-3 font-sans">No saved servers</p>
|
|
64
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Add one in Settings to get started.</p>
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => {
|
|
67
|
+
toggle();
|
|
68
|
+
useGrooveStore.getState().setActiveView('settings');
|
|
69
|
+
}}
|
|
70
|
+
className="mt-3 inline-flex items-center gap-1.5 text-xs text-accent hover:text-accent/80 font-sans cursor-pointer transition-colors"
|
|
71
|
+
>
|
|
72
|
+
<Settings size={12} /> Go to Settings
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
savedTunnels.map((server) => (
|
|
77
|
+
<button
|
|
78
|
+
key={server.id}
|
|
79
|
+
onClick={() => server.active ? handleOpenRemote(server) : handleConnect(server.id)}
|
|
80
|
+
disabled={connectingId === server.id}
|
|
81
|
+
className={cn(
|
|
82
|
+
'w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors',
|
|
83
|
+
'hover:bg-surface-5',
|
|
84
|
+
connectingId === server.id && 'opacity-60 pointer-events-none',
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<Server size={15} className={server.active ? 'text-success' : 'text-text-4'} />
|
|
88
|
+
<div className="flex-1 min-w-0">
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<span className="text-sm font-medium text-text-0 font-sans truncate">{server.name}</span>
|
|
91
|
+
{server.active && <StatusDot status="running" size="sm" />}
|
|
92
|
+
</div>
|
|
93
|
+
<span className="text-2xs text-text-4 font-mono">{server.user}@{server.host}</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex-shrink-0">
|
|
96
|
+
{connectingId === server.id ? (
|
|
97
|
+
<Loader2 size={14} className="text-text-3 animate-spin" />
|
|
98
|
+
) : server.active ? (
|
|
99
|
+
<span className="flex items-center gap-1 text-2xs text-success font-sans">
|
|
100
|
+
<ExternalLink size={11} /> Open
|
|
101
|
+
</span>
|
|
102
|
+
) : (
|
|
103
|
+
<span className="text-2xs text-text-3 font-sans">Connect</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</button>
|
|
107
|
+
))
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Footer */}
|
|
112
|
+
{savedTunnels.length > 0 && (
|
|
113
|
+
<div className="px-4 py-2 border-t border-border-subtle">
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => {
|
|
116
|
+
toggle();
|
|
117
|
+
useGrooveStore.getState().setActiveView('settings');
|
|
118
|
+
}}
|
|
119
|
+
className="flex items-center gap-1.5 text-2xs text-text-4 hover:text-text-2 font-sans cursor-pointer transition-colors"
|
|
120
|
+
>
|
|
121
|
+
<Plus size={10} /> Manage servers in Settings
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</motion.div>
|
|
126
|
+
</AnimatePresence>
|
|
127
|
+
</>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Badge } from '../ui/badge';
|
|
4
|
+
import { StatusDot } from '../ui/status-dot';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
7
|
+
import { fmtUptime } from '../../lib/format';
|
|
8
|
+
import { cn } from '../../lib/cn';
|
|
9
|
+
import {
|
|
10
|
+
Plug, PlugZap, Pencil, Trash2, Loader2, Check, X, AlertTriangle,
|
|
11
|
+
ExternalLink, Download, Play,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
export function RemoteServerCard({ server, onEdit, onDelete, onConnect, onDisconnect, onTest }) {
|
|
15
|
+
const [testResult, setTestResult] = useState(null);
|
|
16
|
+
const [testLoading, setTestLoading] = useState(false);
|
|
17
|
+
const [connecting, setConnecting] = useState(false);
|
|
18
|
+
const [connectStep, setConnectStep] = useState(null);
|
|
19
|
+
|
|
20
|
+
// Listen for tunnel.status WebSocket events for progress updates
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
function handleWs(e) {
|
|
23
|
+
try {
|
|
24
|
+
const msg = JSON.parse(e.data);
|
|
25
|
+
if (msg.type === 'tunnel.status' && msg.data?.id === server.id) {
|
|
26
|
+
setConnectStep(msg.data.step);
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
const ws = useGrooveStore.getState().ws;
|
|
31
|
+
if (ws) ws.addEventListener('message', handleWs);
|
|
32
|
+
return () => { if (ws) ws.removeEventListener('message', handleWs); };
|
|
33
|
+
}, [server.id]);
|
|
34
|
+
|
|
35
|
+
async function handleTest() {
|
|
36
|
+
setTestLoading(true);
|
|
37
|
+
setTestResult(null);
|
|
38
|
+
try {
|
|
39
|
+
const result = await onTest();
|
|
40
|
+
setTestResult(result);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setTestResult({ error: err.message || 'Test failed' });
|
|
43
|
+
}
|
|
44
|
+
setTestLoading(false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function handleConnect() {
|
|
48
|
+
setConnecting(true);
|
|
49
|
+
setConnectStep(null);
|
|
50
|
+
setTestResult(null);
|
|
51
|
+
try {
|
|
52
|
+
await onConnect();
|
|
53
|
+
setConnectStep(null);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const tr = err?.testResult || err?.body?.testResult;
|
|
56
|
+
if (tr) {
|
|
57
|
+
setTestResult(tr);
|
|
58
|
+
} else {
|
|
59
|
+
setTestResult({ error: err?.body?.error || err?.message || 'Connection failed' });
|
|
60
|
+
}
|
|
61
|
+
setConnectStep(null);
|
|
62
|
+
}
|
|
63
|
+
setConnecting(false);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleDisconnect() {
|
|
67
|
+
setConnecting(true);
|
|
68
|
+
try {
|
|
69
|
+
await onDisconnect();
|
|
70
|
+
} catch {}
|
|
71
|
+
setConnecting(false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleOpenRemote() {
|
|
75
|
+
const port = server.localPort;
|
|
76
|
+
const name = encodeURIComponent(server.name);
|
|
77
|
+
window.open(`http://localhost:${port}?instance=${name}`, '_blank');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const connectLabel = connectStep === 'installing'
|
|
81
|
+
? 'Installing Groove...'
|
|
82
|
+
: connectStep === 'starting'
|
|
83
|
+
? 'Starting daemon...'
|
|
84
|
+
: connecting
|
|
85
|
+
? 'Connecting...'
|
|
86
|
+
: 'Connect';
|
|
87
|
+
|
|
88
|
+
const uptimeSeconds = server.active && server.startedAt
|
|
89
|
+
? Math.floor((Date.now() - new Date(server.startedAt).getTime()) / 1000)
|
|
90
|
+
: 0;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={cn(
|
|
94
|
+
'rounded-lg border bg-surface-2 p-4',
|
|
95
|
+
server.active ? 'border-success/40' : 'border-border-subtle',
|
|
96
|
+
)}>
|
|
97
|
+
{/* Top row: name + status */}
|
|
98
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
99
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">{server.name}</span>
|
|
100
|
+
{server.active ? (
|
|
101
|
+
<Badge variant="success" className="text-2xs gap-1">
|
|
102
|
+
<StatusDot status="running" size="sm" /> Connected
|
|
103
|
+
</Badge>
|
|
104
|
+
) : (
|
|
105
|
+
<Badge variant="default" className="text-2xs">Disconnected</Badge>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Connection string */}
|
|
110
|
+
<div className="text-xs text-text-3 font-mono mb-1">
|
|
111
|
+
{server.user}@{server.host}:{server.port || 22}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* SSH key path */}
|
|
115
|
+
{server.sshKeyPath && (
|
|
116
|
+
<div className="text-2xs text-text-4 font-mono truncate mb-2">
|
|
117
|
+
Key: {server.sshKeyPath}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Active connection stats */}
|
|
122
|
+
{server.active && (
|
|
123
|
+
<div className="flex items-center gap-3 text-2xs text-text-3 font-sans mb-2">
|
|
124
|
+
{uptimeSeconds > 0 && <span>Uptime: {fmtUptime(uptimeSeconds)}</span>}
|
|
125
|
+
{server.latencyMs != null && <span>Latency: {server.latencyMs}ms</span>}
|
|
126
|
+
{server.localPort && <span>Port: {server.localPort}</span>}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Connected instance explanation */}
|
|
131
|
+
{server.active && (
|
|
132
|
+
<div className="text-2xs text-text-4 bg-surface-1 rounded px-2.5 py-1.5 mb-3">
|
|
133
|
+
Separate Groove instance on your remote server. Local teams are not affected.
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Action buttons */}
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
{server.active ? (
|
|
140
|
+
<>
|
|
141
|
+
<Button
|
|
142
|
+
variant="primary"
|
|
143
|
+
size="sm"
|
|
144
|
+
onClick={handleOpenRemote}
|
|
145
|
+
className="h-7 text-2xs gap-1"
|
|
146
|
+
>
|
|
147
|
+
<ExternalLink size={11} />
|
|
148
|
+
Open Remote GUI
|
|
149
|
+
</Button>
|
|
150
|
+
<Button
|
|
151
|
+
variant="ghost"
|
|
152
|
+
size="sm"
|
|
153
|
+
onClick={handleDisconnect}
|
|
154
|
+
disabled={connecting}
|
|
155
|
+
className="h-7 text-2xs text-danger hover:text-danger gap-1"
|
|
156
|
+
>
|
|
157
|
+
<Plug size={11} />
|
|
158
|
+
{connecting ? 'Disconnecting...' : 'Disconnect'}
|
|
159
|
+
</Button>
|
|
160
|
+
</>
|
|
161
|
+
) : (
|
|
162
|
+
<>
|
|
163
|
+
<Button
|
|
164
|
+
variant="primary"
|
|
165
|
+
size="sm"
|
|
166
|
+
onClick={handleConnect}
|
|
167
|
+
disabled={connecting}
|
|
168
|
+
className="h-7 text-2xs gap-1"
|
|
169
|
+
>
|
|
170
|
+
{connecting ? <Loader2 size={11} className="animate-spin" /> : <PlugZap size={11} />}
|
|
171
|
+
{connectLabel}
|
|
172
|
+
</Button>
|
|
173
|
+
<Button
|
|
174
|
+
variant="ghost"
|
|
175
|
+
size="sm"
|
|
176
|
+
onClick={handleTest}
|
|
177
|
+
disabled={testLoading || connecting}
|
|
178
|
+
className="h-7 text-2xs text-text-3 gap-1"
|
|
179
|
+
>
|
|
180
|
+
{testLoading ? <Loader2 size={11} className="animate-spin" /> : <PlugZap size={11} />}
|
|
181
|
+
Test
|
|
182
|
+
</Button>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
<div className="flex-1" />
|
|
186
|
+
{!server.active && (
|
|
187
|
+
<>
|
|
188
|
+
<button
|
|
189
|
+
onClick={() => onEdit(server)}
|
|
190
|
+
className="p-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
|
|
191
|
+
title="Edit"
|
|
192
|
+
>
|
|
193
|
+
<Pencil size={12} />
|
|
194
|
+
</button>
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => onDelete(server.id)}
|
|
197
|
+
className="p-1.5 text-text-4 hover:text-danger cursor-pointer transition-colors"
|
|
198
|
+
title="Delete"
|
|
199
|
+
>
|
|
200
|
+
<Trash2 size={12} />
|
|
201
|
+
</button>
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Inline test result */}
|
|
207
|
+
{testResult && !connecting && (
|
|
208
|
+
<div className={cn(
|
|
209
|
+
'mt-2 px-3 py-2 rounded-md text-2xs font-sans flex items-start gap-2',
|
|
210
|
+
testResult.error
|
|
211
|
+
? 'bg-danger/8 border border-danger/20 text-danger'
|
|
212
|
+
: testResult.reachable
|
|
213
|
+
? 'bg-success/8 border border-success/20 text-success'
|
|
214
|
+
: 'bg-warning/8 border border-warning/20 text-warning',
|
|
215
|
+
)}>
|
|
216
|
+
{testResult.error ? (
|
|
217
|
+
<><X size={11} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
|
|
218
|
+
) : testResult.reachable ? (
|
|
219
|
+
<>
|
|
220
|
+
<Check size={11} className="mt-0.5 flex-shrink-0" />
|
|
221
|
+
<span>
|
|
222
|
+
{testResult.daemonRunning
|
|
223
|
+
? 'Connected. Groove running.'
|
|
224
|
+
: testResult.grooveInstalled
|
|
225
|
+
? 'Connected. Groove installed but stopped.'
|
|
226
|
+
: 'Connected. Groove not installed.'}
|
|
227
|
+
{!testResult.daemonRunning && ' Click Connect to set up automatically.'}
|
|
228
|
+
</span>
|
|
229
|
+
</>
|
|
230
|
+
) : (
|
|
231
|
+
<><AlertTriangle size={11} className="mt-0.5 flex-shrink-0" /> Host unreachable</>
|
|
232
|
+
)}
|
|
233
|
+
<button
|
|
234
|
+
onClick={() => setTestResult(null)}
|
|
235
|
+
className="ml-auto text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0"
|
|
236
|
+
>
|
|
237
|
+
<X size={10} />
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|