groove-dev 0.26.38 → 0.27.0
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/CHANGELOG.md +59 -0
- package/CLAUDE.md +24 -19
- package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
- package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
- package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
- package/node_modules/@groove-dev/daemon/src/api.js +346 -22
- package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
- package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
- package/node_modules/@groove-dev/daemon/src/index.js +28 -4
- package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
- package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
- package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
- package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
- package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +141 -9
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
- package/node_modules/@groove-dev/daemon/src/router.js +43 -0
- package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
- package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
- package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
- package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
- package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
- package/package.json +2 -8
- package/packages/cli/bin/groove.js +2 -0
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/nuke.js +16 -4
- package/packages/cli/src/commands/stop.js +17 -2
- package/packages/daemon/integrations-registry.json +681 -75
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/adaptive.js +23 -25
- package/packages/daemon/src/api.js +346 -22
- package/packages/daemon/src/classifier.js +53 -6
- package/packages/daemon/src/firstrun.js +14 -1
- package/packages/daemon/src/gateways/manager.js +2 -2
- package/packages/daemon/src/index.js +28 -4
- package/packages/daemon/src/integrations.js +215 -14
- package/packages/daemon/src/introducer.js +84 -11
- package/packages/daemon/src/journalist.js +43 -1
- package/packages/daemon/src/lockmanager.js +60 -0
- package/packages/daemon/src/mcp-manager.js +270 -0
- package/packages/daemon/src/memory.js +370 -0
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +141 -9
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/rotator.js +334 -31
- package/packages/daemon/src/router.js +43 -0
- package/packages/daemon/src/tokentracker.js +70 -18
- package/packages/daemon/src/validate.js +5 -13
- package/packages/daemon/templates/groove-slides.cjs +306 -0
- package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -4
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
- package/packages/gui/src/components/agents/agent-config.jsx +22 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
- package/packages/gui/src/components/agents/agent-node.jsx +132 -90
- package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/app-shell.jsx +24 -19
- package/packages/gui/src/components/layout/command-palette.jsx +2 -2
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/packages/gui/src/lib/format.js +0 -6
- package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/packages/gui/src/stores/groove.js +59 -9
- package/packages/gui/src/views/agents.jsx +84 -10
- package/packages/gui/src/views/dashboard.jsx +24 -21
- package/packages/gui/src/views/marketplace.jsx +153 -85
- package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
- package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
- package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
- package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
- package/node_modules/@radix-ui/react-popover/README.md +0 -3
- package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
- package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
- package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-popover/package.json +0 -82
- package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
- package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
- package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
- package/node_modules/@radix-ui/react-separator/package.json +0 -69
- package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
3
|
import { Dialog, DialogContent } from '../ui/dialog';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
5
|
import { Input } from '../ui/input';
|
|
@@ -8,7 +8,7 @@ import { api } from '../../lib/api';
|
|
|
8
8
|
import { useToast } from '../../lib/hooks/use-toast';
|
|
9
9
|
import {
|
|
10
10
|
Check, CheckCircle, ExternalLink, Loader2, Eye, EyeOff,
|
|
11
|
-
Key, Shield, Trash2, ChevronRight, X,
|
|
11
|
+
Key, Shield, Trash2, ChevronRight, X, Copy, RefreshCw,
|
|
12
12
|
} from 'lucide-react';
|
|
13
13
|
|
|
14
14
|
// Reuse integration logos from marketplace-card
|
|
@@ -19,6 +19,9 @@ const INTEGRATION_LOGOS = {
|
|
|
19
19
|
gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
|
|
20
20
|
'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
|
|
21
21
|
'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
|
|
22
|
+
'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
|
|
23
|
+
'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
|
|
24
|
+
'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
|
|
22
25
|
'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
|
|
23
26
|
postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
|
|
24
27
|
notion: 'https://cdn.simpleicons.org/notion/white',
|
|
@@ -233,13 +236,25 @@ function OverviewStep({ item, status, installing, onInstall, onUninstall, onNext
|
|
|
233
236
|
);
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
// ── Which APIs each Google integration needs ───────────
|
|
240
|
+
const GOOGLE_API_NAMES = {
|
|
241
|
+
gmail: 'Gmail API',
|
|
242
|
+
'google-calendar': 'Google Calendar API',
|
|
243
|
+
'google-drive': 'Google Drive API',
|
|
244
|
+
'google-docs': 'Google Docs API',
|
|
245
|
+
'google-sheets': 'Google Sheets API',
|
|
246
|
+
'google-slides': 'Google Slides API',
|
|
247
|
+
};
|
|
248
|
+
|
|
236
249
|
// ── Google OAuth Setup (shared by google-autoauth + oauth-google) ──
|
|
237
|
-
function GoogleOAuthSetup({ onConfigured }) {
|
|
250
|
+
function GoogleOAuthSetup({ integrationId, onConfigured }) {
|
|
238
251
|
const toast = useToast();
|
|
239
252
|
const [clientId, setClientId] = useState('');
|
|
240
253
|
const [clientSecret, setClientSecret] = useState('');
|
|
241
254
|
const [saving, setSaving] = useState(false);
|
|
242
255
|
|
|
256
|
+
const apiName = GOOGLE_API_NAMES[integrationId] || 'the relevant Google API';
|
|
257
|
+
|
|
243
258
|
async function handleSave() {
|
|
244
259
|
if (!clientId.trim() || !clientSecret.trim()) return;
|
|
245
260
|
setSaving(true);
|
|
@@ -256,23 +271,75 @@ function GoogleOAuthSetup({ onConfigured }) {
|
|
|
256
271
|
setSaving(false);
|
|
257
272
|
}
|
|
258
273
|
|
|
274
|
+
const redirectUri = 'http://localhost:31415/api/integrations/oauth/callback';
|
|
275
|
+
|
|
276
|
+
const steps = [
|
|
277
|
+
{ text: 'Go to the Google Cloud Console and sign in with your Google account',
|
|
278
|
+
link: { url: 'https://console.cloud.google.com', label: 'Open Google Cloud Console' } },
|
|
279
|
+
{ text: 'Create a new project (or select an existing one). Any name is fine — this is just a container for your credentials.' },
|
|
280
|
+
{ text: <>Enable the <strong>{apiName}</strong> — search for it in the API Library and click <strong>Enable</strong></>,
|
|
281
|
+
link: { url: 'https://console.cloud.google.com/apis/library', label: 'Open API Library' } },
|
|
282
|
+
{ text: <>Go to <strong>Credentials</strong> and click <strong>Create Credentials</strong> → <strong>OAuth client ID</strong></>,
|
|
283
|
+
link: { url: 'https://console.cloud.google.com/apis/credentials', label: 'Open Credentials page' } },
|
|
284
|
+
{ text: <>If prompted to configure the consent screen, choose <strong>External</strong>, fill in an app name (e.g. "Groove"), your email, and save. You can skip optional fields.</> },
|
|
285
|
+
{ text: <>For Application type, choose <strong>Web application</strong>. Give it any name.</> },
|
|
286
|
+
{ text: <>Under <strong>Authorized redirect URIs</strong>, click <strong>Add URI</strong> and paste this exact URL:</>,
|
|
287
|
+
copyable: redirectUri },
|
|
288
|
+
{ text: <>Click <strong>Create</strong>, then copy the <strong>Client ID</strong> and <strong>Client Secret</strong> and paste them below.</> },
|
|
289
|
+
];
|
|
290
|
+
|
|
259
291
|
return (
|
|
260
292
|
<div className="space-y-4">
|
|
261
|
-
<div className="bg-
|
|
262
|
-
<
|
|
263
|
-
<
|
|
264
|
-
|
|
293
|
+
<div className="bg-surface-2 rounded-md px-4 py-3 space-y-3">
|
|
294
|
+
<span className="text-xs font-semibold text-text-1 font-sans">How to get your Google credentials</span>
|
|
295
|
+
<ol className="space-y-2.5">
|
|
296
|
+
{steps.map((step, i) => (
|
|
297
|
+
<li key={i} className="flex gap-2.5 text-xs text-text-2 font-sans leading-relaxed">
|
|
298
|
+
<span className="text-accent font-mono font-bold flex-shrink-0 w-4 text-right">{i + 1}.</span>
|
|
299
|
+
<div className="min-w-0">
|
|
300
|
+
<span>{step.text}</span>
|
|
301
|
+
{step.link && (
|
|
302
|
+
<a
|
|
303
|
+
href={step.link.url}
|
|
304
|
+
target="_blank"
|
|
305
|
+
rel="noopener noreferrer"
|
|
306
|
+
className="flex items-center gap-1 text-2xs text-accent font-sans hover:underline mt-0.5"
|
|
307
|
+
>
|
|
308
|
+
<ExternalLink size={9} />
|
|
309
|
+
{step.link.label}
|
|
310
|
+
</a>
|
|
311
|
+
)}
|
|
312
|
+
{step.copyable && (
|
|
313
|
+
<div className="mt-1.5 flex items-center gap-1.5">
|
|
314
|
+
<code className="flex-1 min-w-0 text-2xs font-mono text-accent bg-surface-4 px-2.5 py-1.5 rounded select-all break-all">
|
|
315
|
+
{step.copyable}
|
|
316
|
+
</code>
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={() => { navigator.clipboard.writeText(step.copyable); }}
|
|
320
|
+
className="flex-shrink-0 p-1.5 rounded text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
321
|
+
title="Copy to clipboard"
|
|
322
|
+
>
|
|
323
|
+
<Copy size={12} />
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
</li>
|
|
329
|
+
))}
|
|
330
|
+
</ol>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div className="bg-accent/8 border border-accent/15 rounded-md px-4 py-2.5">
|
|
334
|
+
<p className="text-2xs text-text-2 font-sans leading-relaxed">
|
|
335
|
+
<strong className="text-text-1">One-time setup</strong> — these same credentials work for
|
|
336
|
+
Gmail, Calendar, Drive, Docs, Sheets, and Slides. You only need to do this once.
|
|
337
|
+
For each integration, just enable the matching API in your Google Cloud project.
|
|
265
338
|
</p>
|
|
266
|
-
<a
|
|
267
|
-
href="https://console.cloud.google.com/apis/credentials"
|
|
268
|
-
target="_blank"
|
|
269
|
-
rel="noopener noreferrer"
|
|
270
|
-
className="inline-flex items-center gap-1 text-2xs text-accent font-sans hover:underline mt-1.5"
|
|
271
|
-
>
|
|
272
|
-
<ExternalLink size={10} />
|
|
273
|
-
console.cloud.google.com
|
|
274
|
-
</a>
|
|
275
339
|
</div>
|
|
340
|
+
|
|
341
|
+
<div className="h-px bg-border-subtle" />
|
|
342
|
+
|
|
276
343
|
<div className="space-y-3">
|
|
277
344
|
<div className="space-y-1.5">
|
|
278
345
|
<label className="text-xs font-medium text-text-2 font-sans flex items-center gap-1.5">
|
|
@@ -318,17 +385,6 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
318
385
|
}
|
|
319
386
|
}, [needsGoogleOAuth]);
|
|
320
387
|
|
|
321
|
-
async function handleGoogleAutoAuth() {
|
|
322
|
-
setAuthenticating(true);
|
|
323
|
-
try {
|
|
324
|
-
await api.post(`/integrations/${item.id}/authenticate`);
|
|
325
|
-
toast.success('Browser opened — complete sign-in there');
|
|
326
|
-
} catch (err) {
|
|
327
|
-
toast.error('Auth failed', err.message);
|
|
328
|
-
}
|
|
329
|
-
setAuthenticating(false);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
388
|
async function handleOAuthStart() {
|
|
333
389
|
setAuthenticating(true);
|
|
334
390
|
try {
|
|
@@ -338,7 +394,7 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
338
394
|
toast.success('Browser opened — complete sign-in there');
|
|
339
395
|
}
|
|
340
396
|
} catch (err) {
|
|
341
|
-
toast.error('
|
|
397
|
+
toast.error('Sign-in failed', err.message);
|
|
342
398
|
}
|
|
343
399
|
setAuthenticating(false);
|
|
344
400
|
}
|
|
@@ -359,8 +415,8 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
359
415
|
</div>
|
|
360
416
|
</div>
|
|
361
417
|
|
|
362
|
-
{/* Setup steps */}
|
|
363
|
-
{item.setupSteps?.length > 0 && (
|
|
418
|
+
{/* Setup steps — hide when Google OAuth setup guide is showing (it has its own steps) */}
|
|
419
|
+
{item.setupSteps?.length > 0 && !(needsGoogleOAuth && googleOAuthReady === false) && (
|
|
364
420
|
<div className="bg-surface-2 rounded-md px-4 py-3 space-y-2">
|
|
365
421
|
<span className="text-xs font-semibold text-text-1 font-sans">Setup guide</span>
|
|
366
422
|
<ol className="space-y-1.5">
|
|
@@ -385,7 +441,7 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
385
441
|
</div>
|
|
386
442
|
)}
|
|
387
443
|
|
|
388
|
-
<div className="h-px bg-border-subtle" />
|
|
444
|
+
{!(needsGoogleOAuth && googleOAuthReady === false) && <div className="h-px bg-border-subtle" />}
|
|
389
445
|
|
|
390
446
|
{/* Auth type specific UI */}
|
|
391
447
|
{authType === 'api-key' && (
|
|
@@ -402,15 +458,15 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
402
458
|
)}
|
|
403
459
|
|
|
404
460
|
{needsGoogleOAuth && googleOAuthReady === false && (
|
|
405
|
-
<GoogleOAuthSetup onConfigured={() => setGoogleOAuthReady(true)} />
|
|
461
|
+
<GoogleOAuthSetup integrationId={item.id} onConfigured={() => setGoogleOAuthReady(true)} />
|
|
406
462
|
)}
|
|
407
463
|
|
|
408
|
-
{
|
|
464
|
+
{needsGoogleOAuth && googleOAuthReady && (
|
|
409
465
|
<div className="space-y-3">
|
|
410
466
|
<Button
|
|
411
467
|
variant="primary"
|
|
412
468
|
size="lg"
|
|
413
|
-
onClick={
|
|
469
|
+
onClick={handleOAuthStart}
|
|
414
470
|
disabled={authenticating}
|
|
415
471
|
className="w-full gap-2"
|
|
416
472
|
>
|
|
@@ -427,35 +483,15 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
|
427
483
|
)}
|
|
428
484
|
</Button>
|
|
429
485
|
<p className="text-2xs text-text-4 font-sans text-center">
|
|
430
|
-
A browser window will open
|
|
486
|
+
A browser window will open — sign in and allow access to your {item.name}
|
|
431
487
|
</p>
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<div className="space-y-3">
|
|
437
|
-
<Button
|
|
438
|
-
variant="primary"
|
|
439
|
-
size="lg"
|
|
440
|
-
onClick={handleOAuthStart}
|
|
441
|
-
disabled={authenticating}
|
|
442
|
-
className="w-full gap-2"
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
onClick={() => setGoogleOAuthReady(false)}
|
|
491
|
+
className="w-full text-2xs text-text-4 font-sans hover:text-text-2 transition-colors cursor-pointer py-1"
|
|
443
492
|
>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
<Loader2 size={14} className="animate-spin" />
|
|
447
|
-
Connecting...
|
|
448
|
-
</>
|
|
449
|
-
) : (
|
|
450
|
-
<>
|
|
451
|
-
<img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
|
|
452
|
-
Connect with Google
|
|
453
|
-
</>
|
|
454
|
-
)}
|
|
455
|
-
</Button>
|
|
456
|
-
<p className="text-2xs text-text-4 font-sans text-center">
|
|
457
|
-
Authorize Groove to access your {item.name}
|
|
458
|
-
</p>
|
|
493
|
+
Reconfigure Google OAuth credentials
|
|
494
|
+
</button>
|
|
459
495
|
</div>
|
|
460
496
|
)}
|
|
461
497
|
|
|
@@ -601,3 +637,297 @@ export function IntegrationWizard({ integration, open, onClose }) {
|
|
|
601
637
|
</Dialog>
|
|
602
638
|
);
|
|
603
639
|
}
|
|
640
|
+
|
|
641
|
+
// ── Google Workspace Wizard ────────────────────────────
|
|
642
|
+
const GOOGLE_IDS = ['gmail', 'google-calendar', 'google-drive', 'google-docs', 'google-sheets', 'google-slides'];
|
|
643
|
+
|
|
644
|
+
function ServiceRow({ item, status, onInstall, onUninstall, busy }) {
|
|
645
|
+
const installed = status?.installed;
|
|
646
|
+
const authenticated = status?.authenticated;
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<div className="flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-2 border border-border-subtle">
|
|
650
|
+
<IntegrationIcon item={item} size={32} />
|
|
651
|
+
<div className="flex-1 min-w-0">
|
|
652
|
+
<div className="text-xs font-semibold text-text-0 font-sans">{item.name}</div>
|
|
653
|
+
<div className="text-2xs text-text-3 font-sans truncate">{item.description}</div>
|
|
654
|
+
</div>
|
|
655
|
+
{installed && authenticated && !status?.needsReauth && (
|
|
656
|
+
<Badge variant="success" className="text-2xs flex-shrink-0 gap-1">
|
|
657
|
+
<Check size={8} /> Ready
|
|
658
|
+
</Badge>
|
|
659
|
+
)}
|
|
660
|
+
{installed && authenticated && status?.needsReauth && (
|
|
661
|
+
<Badge variant="warning" className="text-2xs flex-shrink-0 gap-1">
|
|
662
|
+
<RefreshCw size={8} /> Update
|
|
663
|
+
</Badge>
|
|
664
|
+
)}
|
|
665
|
+
{installed && !authenticated && (
|
|
666
|
+
<Badge variant="warning" className="text-2xs flex-shrink-0">Needs sign-in</Badge>
|
|
667
|
+
)}
|
|
668
|
+
<Button
|
|
669
|
+
variant={installed ? 'ghost' : 'primary'}
|
|
670
|
+
size="sm"
|
|
671
|
+
onClick={() => installed ? onUninstall(item.id) : onInstall(item.id)}
|
|
672
|
+
disabled={busy === item.id}
|
|
673
|
+
className={installed ? 'text-text-3 hover:text-danger' : ''}
|
|
674
|
+
>
|
|
675
|
+
{busy === item.id ? (
|
|
676
|
+
<Loader2 size={12} className="animate-spin" />
|
|
677
|
+
) : installed ? (
|
|
678
|
+
<Trash2 size={12} />
|
|
679
|
+
) : (
|
|
680
|
+
'Install'
|
|
681
|
+
)}
|
|
682
|
+
</Button>
|
|
683
|
+
</div>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function GoogleWorkspaceWizard({ integrations, open, onClose }) {
|
|
688
|
+
const toast = useToast();
|
|
689
|
+
const [googleOAuthReady, setGoogleOAuthReady] = useState(null);
|
|
690
|
+
const [statuses, setStatuses] = useState({});
|
|
691
|
+
const [busy, setBusy] = useState(null);
|
|
692
|
+
const [authenticating, setAuthenticating] = useState(false);
|
|
693
|
+
const [loading, setLoading] = useState(true);
|
|
694
|
+
const pollRef = useRef(null);
|
|
695
|
+
|
|
696
|
+
const googleItems = integrations.filter((i) => GOOGLE_IDS.includes(i.id));
|
|
697
|
+
|
|
698
|
+
const fetchStatuses = useCallback(async () => {
|
|
699
|
+
const results = {};
|
|
700
|
+
await Promise.all(googleItems.map(async (item) => {
|
|
701
|
+
try {
|
|
702
|
+
results[item.id] = await api.get(`/integrations/${item.id}/status`);
|
|
703
|
+
} catch {
|
|
704
|
+
results[item.id] = null;
|
|
705
|
+
}
|
|
706
|
+
}));
|
|
707
|
+
setStatuses(results);
|
|
708
|
+
setLoading(false);
|
|
709
|
+
}, [googleItems.map((i) => i.id).join(',')]);
|
|
710
|
+
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (open) {
|
|
713
|
+
setLoading(true);
|
|
714
|
+
api.get('/integrations/google-oauth/status')
|
|
715
|
+
.then((d) => setGoogleOAuthReady(d.configured))
|
|
716
|
+
.catch(() => setGoogleOAuthReady(false));
|
|
717
|
+
fetchStatuses();
|
|
718
|
+
}
|
|
719
|
+
return () => { if (pollRef.current) clearInterval(pollRef.current); };
|
|
720
|
+
}, [open]);
|
|
721
|
+
|
|
722
|
+
const installedIds = Object.entries(statuses).filter(([, s]) => s?.installed).map(([id]) => id);
|
|
723
|
+
const allAuthenticated = installedIds.length > 0 && installedIds.every((id) => statuses[id]?.authenticated);
|
|
724
|
+
const needsAuth = installedIds.some((id) => !statuses[id]?.authenticated);
|
|
725
|
+
const needsReauth = allAuthenticated && installedIds.some((id) => statuses[id]?.needsReauth);
|
|
726
|
+
|
|
727
|
+
async function handleInstall(id) {
|
|
728
|
+
setBusy(id);
|
|
729
|
+
try {
|
|
730
|
+
await api.post(`/integrations/${id}/install`);
|
|
731
|
+
toast.success(`${googleItems.find((i) => i.id === id)?.name} installed`);
|
|
732
|
+
await fetchStatuses();
|
|
733
|
+
} catch (err) {
|
|
734
|
+
toast.error('Install failed', err.message);
|
|
735
|
+
}
|
|
736
|
+
setBusy(null);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function handleUninstall(id) {
|
|
740
|
+
setBusy(id);
|
|
741
|
+
try {
|
|
742
|
+
await api.delete(`/integrations/${id}`);
|
|
743
|
+
toast.success(`${googleItems.find((i) => i.id === id)?.name} removed`);
|
|
744
|
+
await fetchStatuses();
|
|
745
|
+
} catch (err) {
|
|
746
|
+
toast.error('Uninstall failed', err.message);
|
|
747
|
+
}
|
|
748
|
+
setBusy(null);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function handleConnect() {
|
|
752
|
+
if (!installedIds.length) {
|
|
753
|
+
toast.error('Install at least one service first');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
setAuthenticating(true);
|
|
757
|
+
try {
|
|
758
|
+
const data = await api.post('/integrations/google-workspace/oauth/start', { integrationIds: installedIds });
|
|
759
|
+
if (data.url) {
|
|
760
|
+
window.open(data.url, '_blank', 'noopener');
|
|
761
|
+
toast.success('Browser opened — complete sign-in there');
|
|
762
|
+
// Poll for auth completion
|
|
763
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
764
|
+
pollRef.current = setInterval(async () => {
|
|
765
|
+
await fetchStatuses();
|
|
766
|
+
}, 3000);
|
|
767
|
+
// Stop polling after 3 minutes
|
|
768
|
+
setTimeout(() => { if (pollRef.current) clearInterval(pollRef.current); }, 180_000);
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
toast.error('Sign-in failed', err.message);
|
|
772
|
+
}
|
|
773
|
+
setAuthenticating(false);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Stop polling once all installed services are authenticated
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (allAuthenticated && pollRef.current) {
|
|
779
|
+
clearInterval(pollRef.current);
|
|
780
|
+
pollRef.current = null;
|
|
781
|
+
}
|
|
782
|
+
}, [allAuthenticated]);
|
|
783
|
+
|
|
784
|
+
if (!open) return null;
|
|
785
|
+
|
|
786
|
+
return (
|
|
787
|
+
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
|
|
788
|
+
<DialogContent
|
|
789
|
+
title="Google Workspace"
|
|
790
|
+
description="Connect your Google services"
|
|
791
|
+
className="max-w-md"
|
|
792
|
+
>
|
|
793
|
+
<div className="px-5 py-5 space-y-5">
|
|
794
|
+
{/* Header */}
|
|
795
|
+
<div className="flex items-center gap-3">
|
|
796
|
+
<div className="w-11 h-11 rounded-lg bg-surface-4 flex items-center justify-center flex-shrink-0">
|
|
797
|
+
<img src="https://cdn.simpleicons.org/google/white" alt="Google" className="w-6 h-6" />
|
|
798
|
+
</div>
|
|
799
|
+
<div>
|
|
800
|
+
<h2 className="text-sm font-bold text-text-0 font-sans">Google Workspace</h2>
|
|
801
|
+
<p className="text-2xs text-text-3 font-sans">
|
|
802
|
+
One set of credentials for all Google services
|
|
803
|
+
</p>
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
{/* OAuth Setup */}
|
|
808
|
+
{googleOAuthReady === false && (
|
|
809
|
+
<GoogleOAuthSetup integrationId="gmail" onConfigured={() => setGoogleOAuthReady(true)} />
|
|
810
|
+
)}
|
|
811
|
+
|
|
812
|
+
{googleOAuthReady === null && (
|
|
813
|
+
<div className="flex justify-center py-3">
|
|
814
|
+
<Loader2 size={16} className="animate-spin text-text-4" />
|
|
815
|
+
</div>
|
|
816
|
+
)}
|
|
817
|
+
|
|
818
|
+
{/* Services list */}
|
|
819
|
+
{googleOAuthReady && (
|
|
820
|
+
<>
|
|
821
|
+
<div className="space-y-1.5">
|
|
822
|
+
<span className="text-xs font-semibold text-text-2 font-sans">Services</span>
|
|
823
|
+
<div className="space-y-1.5">
|
|
824
|
+
{loading ? (
|
|
825
|
+
Array.from({ length: 4 }).map((_, i) => (
|
|
826
|
+
<div key={i} className="h-14 rounded-md bg-surface-2 animate-pulse" />
|
|
827
|
+
))
|
|
828
|
+
) : (
|
|
829
|
+
googleItems.map((item) => (
|
|
830
|
+
<ServiceRow
|
|
831
|
+
key={item.id}
|
|
832
|
+
item={item}
|
|
833
|
+
status={statuses[item.id]}
|
|
834
|
+
onInstall={handleInstall}
|
|
835
|
+
onUninstall={handleUninstall}
|
|
836
|
+
busy={busy}
|
|
837
|
+
/>
|
|
838
|
+
))
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div className="h-px bg-border-subtle" />
|
|
844
|
+
|
|
845
|
+
{/* Connect button */}
|
|
846
|
+
{installedIds.length > 0 && !allAuthenticated && (
|
|
847
|
+
<div className="space-y-3">
|
|
848
|
+
<Button
|
|
849
|
+
variant="primary"
|
|
850
|
+
size="lg"
|
|
851
|
+
onClick={handleConnect}
|
|
852
|
+
disabled={authenticating}
|
|
853
|
+
className="w-full gap-2"
|
|
854
|
+
>
|
|
855
|
+
{authenticating ? (
|
|
856
|
+
<><Loader2 size={14} className="animate-spin" /> Opening browser...</>
|
|
857
|
+
) : (
|
|
858
|
+
<>
|
|
859
|
+
<img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
|
|
860
|
+
Sign in with Google
|
|
861
|
+
</>
|
|
862
|
+
)}
|
|
863
|
+
</Button>
|
|
864
|
+
<p className="text-2xs text-text-4 font-sans text-center">
|
|
865
|
+
Connects {installedIds.length} service{installedIds.length !== 1 ? 's' : ''} with one sign-in
|
|
866
|
+
</p>
|
|
867
|
+
</div>
|
|
868
|
+
)}
|
|
869
|
+
|
|
870
|
+
{allAuthenticated && installedIds.length > 0 && (
|
|
871
|
+
<div className="flex flex-col items-center text-center gap-2 py-2">
|
|
872
|
+
<div className="w-10 h-10 rounded-full bg-success/15 flex items-center justify-center">
|
|
873
|
+
<CheckCircle size={20} className="text-success" />
|
|
874
|
+
</div>
|
|
875
|
+
<p className="text-sm font-medium text-success font-sans">
|
|
876
|
+
All services connected
|
|
877
|
+
</p>
|
|
878
|
+
<p className="text-2xs text-text-3 font-sans">
|
|
879
|
+
Your agents can now use these Google integrations.
|
|
880
|
+
</p>
|
|
881
|
+
{needsReauth ? (
|
|
882
|
+
<div className="w-full space-y-2 pt-2">
|
|
883
|
+
<p className="text-2xs text-warning font-sans">
|
|
884
|
+
New permissions available — re-authenticate to enable all features.
|
|
885
|
+
</p>
|
|
886
|
+
<Button
|
|
887
|
+
variant="secondary"
|
|
888
|
+
size="sm"
|
|
889
|
+
onClick={handleConnect}
|
|
890
|
+
disabled={authenticating}
|
|
891
|
+
className="w-full gap-2"
|
|
892
|
+
>
|
|
893
|
+
{authenticating ? (
|
|
894
|
+
<><Loader2 size={12} className="animate-spin" /> Opening browser...</>
|
|
895
|
+
) : (
|
|
896
|
+
<><RefreshCw size={12} /> Re-authenticate</>
|
|
897
|
+
)}
|
|
898
|
+
</Button>
|
|
899
|
+
</div>
|
|
900
|
+
) : (
|
|
901
|
+
<button
|
|
902
|
+
onClick={handleConnect}
|
|
903
|
+
disabled={authenticating}
|
|
904
|
+
className="text-2xs text-text-4 hover:text-text-2 font-sans underline underline-offset-2 transition-colors mt-1"
|
|
905
|
+
>
|
|
906
|
+
{authenticating ? 'Opening browser...' : 'Re-authenticate'}
|
|
907
|
+
</button>
|
|
908
|
+
)}
|
|
909
|
+
</div>
|
|
910
|
+
)}
|
|
911
|
+
|
|
912
|
+
{installedIds.length === 0 && !loading && (
|
|
913
|
+
<p className="text-xs text-text-4 font-sans text-center py-2">
|
|
914
|
+
Install at least one service above, then connect with Google.
|
|
915
|
+
</p>
|
|
916
|
+
)}
|
|
917
|
+
</>
|
|
918
|
+
)}
|
|
919
|
+
|
|
920
|
+
{/* Close */}
|
|
921
|
+
<Button
|
|
922
|
+
variant="secondary"
|
|
923
|
+
size="lg"
|
|
924
|
+
onClick={onClose}
|
|
925
|
+
className="w-full"
|
|
926
|
+
>
|
|
927
|
+
{allAuthenticated && installedIds.length > 0 ? 'Done' : 'Close'}
|
|
928
|
+
</Button>
|
|
929
|
+
</div>
|
|
930
|
+
</DialogContent>
|
|
931
|
+
</Dialog>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
@@ -5,20 +5,40 @@ import { Badge } from '../ui/badge';
|
|
|
5
5
|
import { fmtNum } from '../../lib/format';
|
|
6
6
|
|
|
7
7
|
// Well-known integration logos via CDN (simple-icons on cdn.simpleicons.org)
|
|
8
|
-
const INTEGRATION_LOGOS = {
|
|
9
|
-
|
|
8
|
+
export const INTEGRATION_LOGOS = {
|
|
9
|
+
'google-workspace': 'https://cdn.simpleicons.org/google/white',
|
|
10
10
|
github: 'https://cdn.simpleicons.org/github/white',
|
|
11
11
|
stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
|
|
12
12
|
gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
|
|
13
13
|
'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
|
|
14
14
|
'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
|
|
15
|
+
'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
|
|
16
|
+
'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
|
|
17
|
+
'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
|
|
15
18
|
'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
|
|
16
19
|
postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
|
|
17
20
|
notion: 'https://cdn.simpleicons.org/notion/white',
|
|
18
|
-
discord: 'https://cdn.simpleicons.org/discord/5865F2',
|
|
19
21
|
linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
|
|
20
22
|
'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
|
|
21
23
|
'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
|
|
24
|
+
sentry: 'https://cdn.simpleicons.org/sentry/362D59',
|
|
25
|
+
elevenlabs: 'https://cdn.simpleicons.org/elevenlabs/white',
|
|
26
|
+
hubspot: 'https://cdn.simpleicons.org/hubspot/FF7A59',
|
|
27
|
+
jira: 'https://cdn.simpleicons.org/jira/0052CC',
|
|
28
|
+
sendgrid: 'https://cdn.simpleicons.org/sendgrid/1A82E2',
|
|
29
|
+
resend: 'https://cdn.simpleicons.org/resend/white',
|
|
30
|
+
replicate: 'https://cdn.simpleicons.org/replicate/white',
|
|
31
|
+
vercel: 'https://cdn.simpleicons.org/vercel/white',
|
|
32
|
+
supabase: 'https://cdn.simpleicons.org/supabase/3FCF8E',
|
|
33
|
+
mixpanel: 'https://cdn.simpleicons.org/mixpanel/7856FF',
|
|
34
|
+
datadog: 'https://cdn.simpleicons.org/datadog/632CA6',
|
|
35
|
+
airtable: 'https://cdn.simpleicons.org/airtable/18BFFF',
|
|
36
|
+
zendesk: 'https://cdn.simpleicons.org/zendesk/03363D',
|
|
37
|
+
intercom: 'https://cdn.simpleicons.org/intercom/6AFDEF',
|
|
38
|
+
twilio: 'https://cdn.simpleicons.org/twilio/F22F46',
|
|
39
|
+
telnyx: 'https://cdn.simpleicons.org/telnyx/00C08B',
|
|
40
|
+
aws: 'https://cdn.simpleicons.org/amazonaws/FF9900',
|
|
41
|
+
plaid: 'https://cdn.simpleicons.org/plaid/white',
|
|
22
42
|
};
|
|
23
43
|
|
|
24
44
|
function ItemIcon({ item, size = 40 }) {
|
|
@@ -62,7 +82,7 @@ function ItemIcon({ item, size = 40 }) {
|
|
|
62
82
|
);
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
export function MarketplaceCard({ item, onClick, className }) {
|
|
85
|
+
export function MarketplaceCard({ item, onClick, className, statusBadge }) {
|
|
66
86
|
const installed = item.installed;
|
|
67
87
|
|
|
68
88
|
return (
|
|
@@ -107,9 +127,11 @@ export function MarketplaceCard({ item, onClick, className }) {
|
|
|
107
127
|
</span>
|
|
108
128
|
)}
|
|
109
129
|
<span className="flex-1" />
|
|
110
|
-
{installed && (
|
|
111
|
-
<Badge variant="accent" className="text-2xs">
|
|
112
|
-
|
|
130
|
+
{statusBadge || (installed && (
|
|
131
|
+
<Badge variant="accent" className="text-2xs">
|
|
132
|
+
{item._installedCount ? `${item._installedCount} active` : 'Installed'}
|
|
133
|
+
</Badge>
|
|
134
|
+
))}
|
|
113
135
|
</div>
|
|
114
136
|
</button>
|
|
115
137
|
);
|
|
@@ -42,9 +42,3 @@ export function timeAgo(ts) {
|
|
|
42
42
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
43
43
|
return `${Math.floor(diff / 86400)}d ago`;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
export function formatDuration(ms) {
|
|
47
|
-
if (!ms) return '0s';
|
|
48
|
-
const s = Math.floor(ms / 1000);
|
|
49
|
-
return fmtUptime(s);
|
|
50
|
-
}
|