groove-dev 0.17.0 → 0.17.2

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-C5k-qSwi.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CEf7nLM2.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BhjOFLBc.css">
9
9
  </head>
10
10
  <body>
@@ -75,16 +75,31 @@ function sortIntegrations(items, sortBy) {
75
75
  }
76
76
  }
77
77
 
78
- // -- Credential Setup Modal --
79
- function CredentialModal({ integration, onSave, onClose }) {
78
+ // -- Credential Setup Modal (Guided Wizard) --
79
+ function CredentialModal({ integration, onClose }) {
80
80
  const [values, setValues] = useState({});
81
81
  const [saving, setSaving] = useState(false);
82
82
  const [saved, setSaved] = useState({});
83
+ const [oauthStatus, setOauthStatus] = useState(null); // null, 'checking', 'not-configured', 'ready', 'connecting'
84
+ const [googleClientId, setGoogleClientId] = useState('');
85
+ const [googleClientSecret, setGoogleClientSecret] = useState('');
86
+ const [showGoogleSetup, setShowGoogleSetup] = useState(false);
87
+
88
+ useEffect(() => {
89
+ if (integration?.authType === 'oauth-google') {
90
+ setOauthStatus('checking');
91
+ fetch('/api/integrations/google-oauth/status')
92
+ .then((r) => r.json())
93
+ .then((data) => setOauthStatus(data.configured ? 'ready' : 'not-configured'))
94
+ .catch(() => setOauthStatus('not-configured'));
95
+ }
96
+ }, [integration]);
83
97
 
84
98
  if (!integration) return null;
85
99
 
86
- const envKeys = integration.envKeys || [];
87
- if (envKeys.length === 0) return null;
100
+ const isOAuth = integration.authType === 'oauth-google';
101
+ const envKeys = (integration.envKeys || []).filter((ek) => !ek.hidden);
102
+ const setupSteps = integration.setupSteps || [];
88
103
 
89
104
  async function handleSave(key) {
90
105
  if (!values[key]) return;
@@ -103,54 +118,256 @@ function CredentialModal({ integration, onSave, onClose }) {
103
118
  setSaving(false);
104
119
  }
105
120
 
121
+ async function handleGoogleSetup() {
122
+ if (!googleClientId || !googleClientSecret) return;
123
+ setSaving(true);
124
+ try {
125
+ await fetch('/api/integrations/google-oauth/setup', {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ clientId: googleClientId, clientSecret: googleClientSecret }),
129
+ });
130
+ setOauthStatus('ready');
131
+ setShowGoogleSetup(false);
132
+ } catch { /* ignore */ }
133
+ setSaving(false);
134
+ }
135
+
136
+ async function handleOAuthConnect() {
137
+ setOauthStatus('connecting');
138
+ try {
139
+ const res = await fetch(`/api/integrations/${integration.id}/oauth/start`, { method: 'POST' });
140
+ const data = await res.json();
141
+ if (data.url) {
142
+ window.open(data.url, '_blank', 'width=600,height=700');
143
+ // Poll for completion
144
+ const poll = setInterval(async () => {
145
+ try {
146
+ const statusRes = await fetch(`/api/integrations/${integration.id}/status`);
147
+ const status = await statusRes.json();
148
+ if (status.configured) {
149
+ clearInterval(poll);
150
+ setOauthStatus('ready');
151
+ onClose();
152
+ }
153
+ } catch { /* ignore */ }
154
+ }, 2000);
155
+ // Stop polling after 5 minutes
156
+ setTimeout(() => clearInterval(poll), 300000);
157
+ }
158
+ } catch {
159
+ setOauthStatus('ready');
160
+ }
161
+ }
162
+
106
163
  return (
107
164
  <div style={modal.overlay} onClick={onClose}>
108
165
  <div style={modal.container} onClick={(e) => e.stopPropagation()}>
109
166
  <div style={modal.topBar}>
110
167
  <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-bright)' }}>
111
- Configure {integration.name}
168
+ Connect {integration.name}
112
169
  </span>
113
170
  <button onClick={onClose} style={modal.closeBtn}>&times;</button>
114
171
  </div>
115
172
 
116
173
  <div style={{ padding: '16px 0' }}>
117
- <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 16, lineHeight: 1.5 }}>
118
- Enter the credentials required for this integration. Values are encrypted and stored locally.
119
- </div>
120
-
121
- {envKeys.map((ek) => (
122
- <div key={ek.key} style={{ marginBottom: 14 }}>
123
- <label style={modal.label}>
124
- {ek.label || ek.key}
125
- {ek.required && <span style={{ color: 'var(--red)', marginLeft: 4 }}>*</span>}
126
- {saved[ek.key] && (
127
- <span style={{ color: 'var(--green)', marginLeft: 8, fontSize: 10, fontWeight: 500 }}>
128
- {'\u2713'} saved
174
+ {/* Setup guide steps */}
175
+ {setupSteps.length > 0 && (
176
+ <div style={{ marginBottom: 20 }}>
177
+ <div style={{
178
+ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)',
179
+ textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10,
180
+ }}>
181
+ Setup Guide
182
+ </div>
183
+ {setupSteps.map((step, i) => (
184
+ <div key={i} style={{
185
+ display: 'flex', gap: 10, marginBottom: 8,
186
+ fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5,
187
+ }}>
188
+ <span style={{
189
+ width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
190
+ background: 'var(--bg-active)', color: 'var(--accent)',
191
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
192
+ fontSize: 10, fontWeight: 700,
193
+ }}>
194
+ {i + 1}
129
195
  </span>
130
- )}
131
- </label>
132
- <div style={{ display: 'flex', gap: 6 }}>
133
- <input
134
- type="password"
135
- value={values[ek.key] || ''}
136
- placeholder={ek.placeholder || ek.key}
137
- onChange={(e) => setValues((prev) => ({ ...prev, [ek.key]: e.target.value }))}
138
- onKeyDown={(e) => e.key === 'Enter' && handleSave(ek.key)}
139
- style={modal.input}
140
- />
196
+ <span>{step}</span>
197
+ </div>
198
+ ))}
199
+ </div>
200
+ )}
201
+
202
+ {/* Open setup page button for API key integrations */}
203
+ {integration.setupUrl && !isOAuth && (
204
+ <a
205
+ href={integration.setupUrl}
206
+ target="_blank"
207
+ rel="noopener noreferrer"
208
+ style={{
209
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
210
+ padding: '10px 16px', marginBottom: 16,
211
+ background: 'var(--bg-active)', color: 'var(--accent)',
212
+ border: '1px solid var(--accent)', borderRadius: 6,
213
+ fontSize: 12, fontWeight: 600, textDecoration: 'none',
214
+ cursor: 'pointer',
215
+ }}
216
+ >
217
+ Open {integration.name} Settings {'\u2197'}
218
+ </a>
219
+ )}
220
+
221
+ {/* OAuth flow for Google integrations */}
222
+ {isOAuth && (
223
+ <div style={{ marginBottom: 16 }}>
224
+ {oauthStatus === 'checking' && (
225
+ <div style={{ fontSize: 11, color: 'var(--text-muted)', textAlign: 'center', padding: 16 }}>
226
+ Checking Google OAuth setup...
227
+ </div>
228
+ )}
229
+
230
+ {oauthStatus === 'not-configured' && !showGoogleSetup && (
231
+ <div style={{
232
+ padding: 16, borderRadius: 8,
233
+ background: 'rgba(229, 192, 123, 0.06)', border: '1px solid var(--amber)',
234
+ }}>
235
+ <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--amber)', marginBottom: 8 }}>
236
+ One-time Google setup needed
237
+ </div>
238
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, marginBottom: 12 }}>
239
+ To connect Google services, you need a Google Cloud project with OAuth credentials.
240
+ This is a one-time setup that works for Gmail, Calendar, and Drive.
241
+ </div>
242
+ <a
243
+ href="https://console.cloud.google.com/apis/credentials"
244
+ target="_blank"
245
+ rel="noopener noreferrer"
246
+ style={{
247
+ display: 'inline-flex', alignItems: 'center', gap: 4,
248
+ fontSize: 11, color: 'var(--accent)', textDecoration: 'none',
249
+ marginBottom: 12,
250
+ }}
251
+ >
252
+ Open Google Cloud Console {'\u2197'}
253
+ </a>
254
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.6, marginBottom: 12 }}>
255
+ 1. Create a project (or select existing){'\n'}
256
+ 2. Configure OAuth consent screen (External, add your email as test user){'\n'}
257
+ 3. Create OAuth Client ID (Desktop app){'\n'}
258
+ 4. Copy the Client ID and Client Secret below
259
+ </div>
260
+ <button
261
+ onClick={() => setShowGoogleSetup(true)}
262
+ style={{ ...modal.saveBtn, width: '100%' }}
263
+ >
264
+ I have my Client ID and Secret
265
+ </button>
266
+ </div>
267
+ )}
268
+
269
+ {oauthStatus === 'not-configured' && showGoogleSetup && (
270
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
271
+ <div>
272
+ <label style={modal.label}>Google OAuth Client ID</label>
273
+ <input
274
+ value={googleClientId}
275
+ onChange={(e) => setGoogleClientId(e.target.value)}
276
+ placeholder="123456789.apps.googleusercontent.com"
277
+ style={modal.input}
278
+ />
279
+ </div>
280
+ <div>
281
+ <label style={modal.label}>Google OAuth Client Secret</label>
282
+ <input
283
+ type="password"
284
+ value={googleClientSecret}
285
+ onChange={(e) => setGoogleClientSecret(e.target.value)}
286
+ placeholder="GOCSPX-..."
287
+ style={modal.input}
288
+ />
289
+ </div>
290
+ <button
291
+ onClick={handleGoogleSetup}
292
+ disabled={saving || !googleClientId || !googleClientSecret}
293
+ style={{
294
+ ...modal.saveBtn, width: '100%',
295
+ opacity: saving || !googleClientId || !googleClientSecret ? 0.4 : 1,
296
+ }}
297
+ >
298
+ {saving ? 'Saving...' : 'Save Google OAuth Credentials'}
299
+ </button>
300
+ <div style={{ fontSize: 9, color: 'var(--text-muted)', textAlign: 'center' }}>
301
+ Stored encrypted, only on this machine. One-time setup for all Google integrations.
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
+ {(oauthStatus === 'ready' || oauthStatus === 'connecting') && (
141
307
  <button
142
- onClick={() => handleSave(ek.key)}
143
- disabled={saving || !values[ek.key]}
308
+ onClick={handleOAuthConnect}
309
+ disabled={oauthStatus === 'connecting'}
144
310
  style={{
145
- ...modal.saveBtn,
146
- opacity: saving || !values[ek.key] ? 0.4 : 1,
311
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
312
+ width: '100%', padding: '12px 16px',
313
+ background: oauthStatus === 'connecting' ? 'var(--bg-active)' : '#4285f4',
314
+ color: '#fff', border: 'none', borderRadius: 6,
315
+ fontSize: 13, fontWeight: 600, cursor: 'pointer',
316
+ fontFamily: 'var(--font)',
147
317
  }}
148
318
  >
149
- Save
319
+ {oauthStatus === 'connecting'
320
+ ? 'Waiting for authorization...'
321
+ : `Connect with Google`}
150
322
  </button>
323
+ )}
324
+ </div>
325
+ )}
326
+
327
+ {/* API key inputs (only show non-hidden keys) */}
328
+ {envKeys.length > 0 && (
329
+ <div>
330
+ <div style={{
331
+ fontSize: 11, color: 'var(--text-dim)', marginBottom: 12, lineHeight: 1.5,
332
+ }}>
333
+ Paste your credentials below. Values are encrypted and stored locally on this machine only.
151
334
  </div>
335
+
336
+ {envKeys.map((ek) => (
337
+ <div key={ek.key} style={{ marginBottom: 14 }}>
338
+ <label style={modal.label}>
339
+ {ek.label || ek.key}
340
+ {ek.required && <span style={{ color: 'var(--red)', marginLeft: 4 }}>*</span>}
341
+ {saved[ek.key] && (
342
+ <span style={{ color: 'var(--green)', marginLeft: 8, fontSize: 10, fontWeight: 500 }}>
343
+ {'\u2713'} saved
344
+ </span>
345
+ )}
346
+ </label>
347
+ <div style={{ display: 'flex', gap: 6 }}>
348
+ <input
349
+ type="password"
350
+ value={values[ek.key] || ''}
351
+ placeholder={ek.placeholder || ek.key}
352
+ onChange={(e) => setValues((prev) => ({ ...prev, [ek.key]: e.target.value }))}
353
+ onKeyDown={(e) => e.key === 'Enter' && handleSave(ek.key)}
354
+ style={modal.input}
355
+ />
356
+ <button
357
+ onClick={() => handleSave(ek.key)}
358
+ disabled={saving || !values[ek.key]}
359
+ style={{
360
+ ...modal.saveBtn,
361
+ opacity: saving || !values[ek.key] ? 0.4 : 1,
362
+ }}
363
+ >
364
+ Save
365
+ </button>
366
+ </div>
367
+ </div>
368
+ ))}
152
369
  </div>
153
- ))}
370
+ )}
154
371
  </div>
155
372
  </div>
156
373
  </div>