lobstakit-cloud 1.0.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.
@@ -0,0 +1,755 @@
1
+ /**
2
+ * LobstaKit Cloud — Setup Wizard
3
+ */
4
+
5
+ let currentStep = 1;
6
+ let selectedChannel = 'web';
7
+ let provisionData = null; // Set from /api/provision if available
8
+
9
+ // ─── Provider Definitions ────────────────────────────────────
10
+
11
+ const ANTHROPIC_KEY_TYPES = {
12
+ subscription: {
13
+ placeholder: 'sk-ant-...',
14
+ consoleUrl: 'https://claude.ai/settings',
15
+ consoleName: 'claude.ai/settings',
16
+ },
17
+ api: {
18
+ placeholder: 'sk-ant-api03-...',
19
+ consoleUrl: 'https://console.anthropic.com/settings/keys',
20
+ consoleName: 'console.anthropic.com',
21
+ }
22
+ };
23
+
24
+ const PROVIDERS = {
25
+ anthropic: {
26
+ label: 'Anthropic',
27
+ placeholder: 'sk-ant-...',
28
+ consoleUrl: 'https://claude.ai/settings',
29
+ consoleName: 'claude.ai/settings',
30
+ keyPrefix: 'sk-ant-',
31
+ keyError: 'Anthropic keys start with sk-ant-',
32
+ hint: 'Sonnet is the best balance of speed, capability, and cost.',
33
+ hasKeyType: true,
34
+ models: [
35
+ { value: 'anthropic/claude-sonnet-4-5', label: 'Claude Sonnet 4.5 — Recommended' },
36
+ { value: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5 — Most powerful' },
37
+ { value: 'anthropic/claude-haiku-3-5', label: 'Claude Haiku 3.5 — Budget' },
38
+ ]
39
+ },
40
+ openai: {
41
+ label: 'OpenAI',
42
+ placeholder: 'sk-...',
43
+ consoleUrl: 'https://platform.openai.com/api-keys',
44
+ consoleName: 'platform.openai.com',
45
+ keyPrefix: 'sk-',
46
+ keyError: 'OpenAI keys start with sk-',
47
+ hint: 'GPT-4o is the best balance of speed, capability, and cost.',
48
+ models: [
49
+ { value: 'openai/gpt-4o', label: 'GPT-4o — Recommended' },
50
+ { value: 'openai/gpt-4o-mini', label: 'GPT-4o Mini — Budget' },
51
+ { value: 'openai/o3', label: 'o3 — Reasoning' },
52
+ { value: 'openai/o4-mini', label: 'o4-mini — Fast reasoning' },
53
+ ]
54
+ },
55
+ google: {
56
+ label: 'Google (Gemini)',
57
+ placeholder: 'AIza...',
58
+ consoleUrl: 'https://aistudio.google.com/apikey',
59
+ consoleName: 'aistudio.google.com',
60
+ keyPrefix: null,
61
+ keyError: null,
62
+ hint: 'Gemini 2.5 Pro is the most capable Google model.',
63
+ models: [
64
+ { value: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro — Recommended' },
65
+ { value: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash — Fast' },
66
+ { value: 'google/gemini-2.0-flash', label: 'Gemini 2.0 Flash — Budget' },
67
+ ]
68
+ },
69
+ xai: {
70
+ label: 'xAI (Grok)',
71
+ placeholder: 'xai-...',
72
+ consoleUrl: 'https://console.x.ai/',
73
+ consoleName: 'console.x.ai',
74
+ keyPrefix: 'xai-',
75
+ keyError: 'xAI keys start with xai-',
76
+ hint: 'Grok 3 is xAI\'s most capable model.',
77
+ models: [
78
+ { value: 'xai/grok-3', label: 'Grok 3 — Recommended' },
79
+ { value: 'xai/grok-3-mini', label: 'Grok 3 Mini — Budget' },
80
+ ]
81
+ },
82
+ deepseek: {
83
+ label: 'DeepSeek',
84
+ placeholder: 'sk-...',
85
+ consoleUrl: 'https://platform.deepseek.com/api_keys',
86
+ consoleName: 'platform.deepseek.com',
87
+ keyPrefix: 'sk-',
88
+ keyError: 'DeepSeek keys start with sk-',
89
+ hint: 'DeepSeek V3 is fast and cost-effective.',
90
+ models: [
91
+ { value: 'deepseek/deepseek-chat', label: 'DeepSeek V3 — Recommended' },
92
+ { value: 'deepseek/deepseek-reasoner', label: 'DeepSeek R1 — Reasoning' },
93
+ ]
94
+ },
95
+ mistral: {
96
+ label: 'Mistral',
97
+ placeholder: 'sk-...',
98
+ consoleUrl: 'https://console.mistral.ai/api-keys/',
99
+ consoleName: 'console.mistral.ai',
100
+ keyPrefix: null,
101
+ keyError: null,
102
+ hint: 'Mistral Large is a strong all-round model.',
103
+ models: [
104
+ { value: 'mistral/mistral-large-latest', label: 'Mistral Large — Recommended' },
105
+ { value: 'mistral/mistral-small-latest', label: 'Mistral Small — Budget' },
106
+ ]
107
+ },
108
+ openrouter: {
109
+ label: 'OpenRouter',
110
+ placeholder: 'sk-or-...',
111
+ consoleUrl: 'https://openrouter.ai/keys',
112
+ consoleName: 'openrouter.ai',
113
+ keyPrefix: 'sk-or-',
114
+ keyError: 'OpenRouter keys start with sk-or-',
115
+ hint: 'OpenRouter gives access to all models — see openrouter.ai/models for the full list.',
116
+ models: [
117
+ { value: 'openrouter/claude-opus-4-5', label: 'Claude Opus 4.5 — Default' },
118
+ { value: 'openrouter/gpt-4o', label: 'GPT-4o' },
119
+ { value: 'openrouter/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
120
+ ]
121
+ }
122
+ };
123
+
124
+ // ─── Init ────────────────────────────────────────────────────
125
+
126
+ document.addEventListener('DOMContentLoaded', async () => {
127
+ // Check if setup is already complete — redirect to login or manage
128
+ try {
129
+ const authStatus = await fetch('/api/auth/status').then(r => r.json());
130
+ if (authStatus.setupComplete) {
131
+ // Setup already done — check if authenticated
132
+ const token = localStorage.getItem('lobstakit_token');
133
+ if (token) {
134
+ const tokenCheck = await fetch('/api/auth/status', {
135
+ headers: { 'Authorization': `Bearer ${token}` }
136
+ }).then(r => r.json());
137
+ if (tokenCheck.authenticated) {
138
+ // Allow reconfiguration if explicitly on /setup
139
+ if (window.location.pathname !== '/setup') {
140
+ window.location.href = '/manage.html';
141
+ return;
142
+ }
143
+ // On /setup — user is authenticated, allow reconfigure
144
+ // Hide password setup section (already set)
145
+ const pwSection = document.getElementById('password-setup-section');
146
+ if (pwSection) pwSection.classList.add('hidden');
147
+ } else {
148
+ window.location.href = '/login.html';
149
+ return;
150
+ }
151
+ } else {
152
+ window.location.href = '/login.html';
153
+ return;
154
+ }
155
+ }
156
+ } catch (err) {
157
+ console.error('Failed to check auth status:', err);
158
+ }
159
+
160
+ try {
161
+ const res = await fetch('/api/status');
162
+ const data = await res.json();
163
+
164
+ // Show subdomain in header
165
+ if (data.subdomain) {
166
+ document.getElementById('subdomain-display').textContent = data.subdomain;
167
+ }
168
+
169
+ // If already configured and not explicitly on /setup, redirect to manage
170
+ if (data.configured && window.location.pathname !== '/setup') {
171
+ window.location.href = '/manage';
172
+ }
173
+ } catch (err) {
174
+ console.error('Failed to fetch status:', err);
175
+ }
176
+
177
+ // Fetch provisioning data (email from Stripe checkout via cloud-init)
178
+ try {
179
+ const provRes = await fetch('/api/provision');
180
+ const provData = await provRes.json();
181
+ if (provData.provisioned && provData.email) {
182
+ provisionData = provData;
183
+ const emailEl = document.getElementById('dashboard-email');
184
+ const hintEl = document.getElementById('dashboard-email-hint');
185
+ if (emailEl) {
186
+ emailEl.value = provData.email;
187
+ emailEl.readOnly = true;
188
+ emailEl.classList.add('opacity-70', 'cursor-not-allowed');
189
+ }
190
+ if (hintEl) {
191
+ hintEl.innerHTML = '✓ Email from your LobstaKit account — <span class="text-lobsta-accent cursor-pointer hover:underline" onclick="unlockEmail()">change</span>';
192
+ }
193
+ }
194
+ } catch (err) {
195
+ // No provision data — email field stays editable
196
+ }
197
+ });
198
+
199
+ // ─── Provider Change Handler ─────────────────────────────────
200
+
201
+ function onProviderChange() {
202
+ const providerKey = document.getElementById('provider').value;
203
+ const provider = PROVIDERS[providerKey];
204
+ if (!provider) return;
205
+
206
+ // Show/hide Anthropic key type toggle
207
+ const keyTypeSection = document.getElementById('anthropic-key-type-section');
208
+ if (provider.hasKeyType) {
209
+ keyTypeSection.classList.remove('hidden');
210
+ // Apply current key type settings
211
+ applyKeyType();
212
+ } else {
213
+ keyTypeSection.classList.add('hidden');
214
+ // Use provider defaults
215
+ document.getElementById('api-key').placeholder = provider.placeholder;
216
+ const link = document.getElementById('provider-console-link');
217
+ link.href = provider.consoleUrl;
218
+ link.textContent = provider.consoleName;
219
+ }
220
+
221
+ // Update model dropdown
222
+ const modelSelect = document.getElementById('model');
223
+ modelSelect.innerHTML = '';
224
+ provider.models.forEach(m => {
225
+ const opt = document.createElement('option');
226
+ opt.value = m.value;
227
+ opt.textContent = m.label;
228
+ modelSelect.appendChild(opt);
229
+ });
230
+
231
+ // Update model hint
232
+ document.getElementById('model-hint').textContent = provider.hint;
233
+
234
+ // Clear any validation errors
235
+ clearError('api-key-error');
236
+ }
237
+
238
+ function onKeyTypeChange() {
239
+ // Update selected styling
240
+ const radios = document.querySelectorAll('input[name="anthropic-key-type"]');
241
+ radios.forEach(radio => {
242
+ const label = radio.closest('.key-type-option');
243
+ label.classList.toggle('selected', radio.checked);
244
+ });
245
+
246
+ // Toggle instructions
247
+ const keyType = document.querySelector('input[name="anthropic-key-type"]:checked').value;
248
+ document.getElementById('instructions-subscription').classList.toggle('hidden', keyType !== 'subscription');
249
+ document.getElementById('instructions-api').classList.toggle('hidden', keyType !== 'api');
250
+
251
+ // Update placeholder and link
252
+ applyKeyType();
253
+
254
+ // Clear validation errors
255
+ clearError('api-key-error');
256
+ }
257
+
258
+ function applyKeyType() {
259
+ const keyType = document.querySelector('input[name="anthropic-key-type"]:checked').value;
260
+ const config = ANTHROPIC_KEY_TYPES[keyType];
261
+
262
+ document.getElementById('api-key').placeholder = config.placeholder;
263
+ const link = document.getElementById('provider-console-link');
264
+ link.href = config.consoleUrl;
265
+ link.textContent = config.consoleName;
266
+ }
267
+
268
+ // ─── Channel Selection ───────────────────────────────────────
269
+
270
+ function selectChannel(channel) {
271
+ selectedChannel = channel;
272
+
273
+ // Update card selection styling
274
+ document.querySelectorAll('.channel-card[data-channel]').forEach(card => {
275
+ card.classList.toggle('selected', card.dataset.channel === channel);
276
+ });
277
+
278
+ // Hide all config sections
279
+ document.querySelectorAll('.channel-config').forEach(el => el.classList.add('hidden'));
280
+
281
+ // Show the relevant config section
282
+ const configEl = document.getElementById(`channel-config-${channel}`);
283
+ if (configEl) {
284
+ configEl.classList.remove('hidden');
285
+ }
286
+
287
+ // Clear any validation errors
288
+ clearError('bot-token-error');
289
+ clearError('user-id-error');
290
+ clearError('discord-bot-token-error');
291
+ clearError('discord-server-id-error');
292
+ }
293
+
294
+ // ─── Email Unlock (for provisioned email override) ───────────
295
+
296
+ function unlockEmail() {
297
+ const emailEl = document.getElementById('dashboard-email');
298
+ const hintEl = document.getElementById('dashboard-email-hint');
299
+ if (emailEl) {
300
+ emailEl.readOnly = false;
301
+ emailEl.classList.remove('opacity-70', 'cursor-not-allowed');
302
+ emailEl.focus();
303
+ }
304
+ if (hintEl) {
305
+ hintEl.textContent = 'Enter the email you want to use for login';
306
+ }
307
+ }
308
+
309
+ // ─── Step Navigation ─────────────────────────────────────────
310
+
311
+ function showStep(step) {
312
+ // Hide all steps
313
+ document.querySelectorAll('.step-content').forEach(el => el.classList.add('hidden'));
314
+
315
+ // Update step indicators
316
+ document.querySelectorAll('.step-indicator').forEach(el => {
317
+ el.classList.remove('active', 'completed');
318
+ });
319
+
320
+ // Mark completed steps
321
+ for (let i = 1; i < step; i++) {
322
+ const indicator = document.getElementById(`step-${i}-indicator`);
323
+ if (indicator) {
324
+ indicator.classList.add('completed');
325
+ const numEl = indicator.querySelector('.step-number');
326
+ if (numEl) numEl.textContent = '✓';
327
+ }
328
+ }
329
+
330
+ // Mark current step active
331
+ const currentIndicator = document.getElementById(`step-${step}-indicator`);
332
+ if (currentIndicator) {
333
+ currentIndicator.classList.add('active');
334
+ const numEl = currentIndicator.querySelector('.step-number');
335
+ if (numEl) numEl.textContent = step;
336
+ }
337
+
338
+ // Reset future step numbers
339
+ for (let i = step + 1; i <= 4; i++) {
340
+ const indicator = document.getElementById(`step-${i}-indicator`);
341
+ if (indicator) {
342
+ const numEl = indicator.querySelector('.step-number');
343
+ if (numEl) numEl.textContent = i;
344
+ }
345
+ }
346
+
347
+ // Update connector lines
348
+ for (let i = 1; i <= 3; i++) {
349
+ const line = document.getElementById(`line-${i}`);
350
+ if (line) {
351
+ line.classList.toggle('active', i < step);
352
+ }
353
+ }
354
+
355
+ // Show selected step
356
+ const stepEl = document.getElementById(`step-${step}`);
357
+ if (stepEl) stepEl.classList.remove('hidden');
358
+
359
+ currentStep = step;
360
+
361
+ // Populate summary on step 4
362
+ if (step === 4) {
363
+ populateSummary();
364
+ // Reset launch panels
365
+ document.getElementById('review-panel').classList.remove('hidden');
366
+ document.getElementById('launch-progress').classList.add('hidden');
367
+ document.getElementById('launch-success').classList.add('hidden');
368
+ document.getElementById('launch-error').classList.add('hidden');
369
+ }
370
+
371
+ // Scroll to top
372
+ window.scrollTo({ top: 0, behavior: 'smooth' });
373
+ }
374
+
375
+ // ─── Validation ──────────────────────────────────────────────
376
+
377
+ function showError(id, message) {
378
+ const el = document.getElementById(id);
379
+ if (el) {
380
+ el.textContent = message;
381
+ el.classList.remove('hidden');
382
+ }
383
+ }
384
+
385
+ function clearError(id) {
386
+ const el = document.getElementById(id);
387
+ if (el) {
388
+ el.textContent = '';
389
+ el.classList.add('hidden');
390
+ }
391
+ }
392
+
393
+ function validateStep2() {
394
+ const apiKey = document.getElementById('api-key').value.trim();
395
+ const providerKey = document.getElementById('provider').value;
396
+ const provider = PROVIDERS[providerKey];
397
+ let valid = true;
398
+
399
+ clearError('api-key-error');
400
+
401
+ if (!apiKey) {
402
+ showError('api-key-error', 'API key is required');
403
+ valid = false;
404
+ } else if (provider && provider.keyPrefix && !apiKey.startsWith(provider.keyPrefix)) {
405
+ showError('api-key-error', `Invalid key — ${provider.keyError}`);
406
+ valid = false;
407
+ }
408
+
409
+ if (valid) showStep(3);
410
+ }
411
+
412
+ function validateStep3() {
413
+ let valid = true;
414
+
415
+ // Clear all channel errors
416
+ clearError('bot-token-error');
417
+ clearError('user-id-error');
418
+ clearError('discord-bot-token-error');
419
+ clearError('discord-server-id-error');
420
+
421
+ if (selectedChannel === 'web') {
422
+ // Web chat: always valid, no fields needed
423
+ valid = true;
424
+ } else if (selectedChannel === 'telegram') {
425
+ const botToken = document.getElementById('bot-token').value.trim();
426
+ const userId = document.getElementById('user-id').value.trim();
427
+
428
+ if (!botToken) {
429
+ showError('bot-token-error', 'Bot token is required');
430
+ valid = false;
431
+ } else if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
432
+ showError('bot-token-error', 'Invalid format — should look like 123456789:ABCdef...');
433
+ valid = false;
434
+ }
435
+
436
+ if (!userId) {
437
+ showError('user-id-error', 'User ID is required');
438
+ valid = false;
439
+ } else if (!/^\d+$/.test(userId)) {
440
+ showError('user-id-error', 'User ID should be a number');
441
+ valid = false;
442
+ }
443
+ } else if (selectedChannel === 'discord') {
444
+ const discordToken = document.getElementById('discord-bot-token').value.trim();
445
+ const serverId = document.getElementById('discord-server-id').value.trim();
446
+
447
+ if (!discordToken) {
448
+ showError('discord-bot-token-error', 'Bot token is required');
449
+ valid = false;
450
+ }
451
+
452
+ if (!serverId) {
453
+ showError('discord-server-id-error', 'Server ID is required');
454
+ valid = false;
455
+ } else if (!/^\d+$/.test(serverId)) {
456
+ showError('discord-server-id-error', 'Server ID should be a number');
457
+ valid = false;
458
+ }
459
+ }
460
+
461
+ if (valid) showStep(4);
462
+ }
463
+
464
+ // ─── Summary ─────────────────────────────────────────────────
465
+
466
+ function maskKey(key) {
467
+ if (!key || key.length < 12) return '••••••••';
468
+ return key.substring(0, 7) + '•••' + key.substring(key.length - 4);
469
+ }
470
+
471
+ function getModelLabel(value) {
472
+ for (const pKey of Object.keys(PROVIDERS)) {
473
+ for (const m of PROVIDERS[pKey].models) {
474
+ if (m.value === value) {
475
+ // Strip the suffix like " — Recommended"
476
+ return m.label.split(' — ')[0];
477
+ }
478
+ }
479
+ }
480
+ return value;
481
+ }
482
+
483
+ function getProviderLabel(providerKey) {
484
+ const provider = PROVIDERS[providerKey];
485
+ return provider ? provider.label : providerKey;
486
+ }
487
+
488
+ function getChannelLabel(channel) {
489
+ const labels = {
490
+ web: '🌐 Web Chat',
491
+ telegram: '📱 Telegram',
492
+ discord: '🎮 Discord'
493
+ };
494
+ return labels[channel] || channel;
495
+ }
496
+
497
+ function populateSummary() {
498
+ const providerKey = document.getElementById('provider').value;
499
+ const apiKey = document.getElementById('api-key').value.trim();
500
+ const model = document.getElementById('model').value;
501
+ const privateMemory = document.getElementById('private-memory-toggle')?.checked ?? true;
502
+
503
+ document.getElementById('summary-provider').textContent = getProviderLabel(providerKey);
504
+ document.getElementById('summary-api-key').textContent = maskKey(apiKey);
505
+ document.getElementById('summary-model').textContent = getModelLabel(model);
506
+ document.getElementById('summary-channel').textContent = getChannelLabel(selectedChannel);
507
+
508
+ const memoryEl = document.getElementById('summary-memory');
509
+ if (memoryEl) {
510
+ memoryEl.textContent = privateMemory ? '🔒 Private (local)' : '☁️ Cloud';
511
+ }
512
+
513
+ // Channel-specific detail row
514
+ const detailRow = document.getElementById('summary-channel-detail-row');
515
+ const detailLabel = document.getElementById('summary-channel-detail-label');
516
+ const detailValue = document.getElementById('summary-channel-detail-value');
517
+
518
+ if (selectedChannel === 'telegram') {
519
+ const botToken = document.getElementById('bot-token').value.trim();
520
+ const userId = document.getElementById('user-id').value.trim();
521
+ detailRow.classList.remove('hidden');
522
+ detailLabel.textContent = 'Bot Token / User ID';
523
+ detailValue.textContent = maskKey(botToken) + ' / ' + userId;
524
+ } else if (selectedChannel === 'discord') {
525
+ const discordToken = document.getElementById('discord-bot-token').value.trim();
526
+ const serverId = document.getElementById('discord-server-id').value.trim();
527
+ detailRow.classList.remove('hidden');
528
+ detailLabel.textContent = 'Bot Token / Server ID';
529
+ detailValue.textContent = maskKey(discordToken) + ' / ' + serverId;
530
+ } else {
531
+ detailRow.classList.add('hidden');
532
+ }
533
+
534
+ // Email summary row
535
+ const emailRow = document.getElementById('summary-email-row');
536
+ const emailEl = document.getElementById('dashboard-email');
537
+ if (emailRow && emailEl && emailEl.value.trim()) {
538
+ document.getElementById('summary-email').textContent = emailEl.value.trim();
539
+ emailRow.classList.remove('hidden');
540
+ } else if (emailRow) {
541
+ emailRow.classList.add('hidden');
542
+ }
543
+ }
544
+
545
+ // ─── Launch ──────────────────────────────────────────────────
546
+
547
+ function setProgressStep(step, status, icon) {
548
+ const iconEl = document.getElementById(`progress-${step}-icon`);
549
+ const statusEl = document.getElementById(`progress-${step}-status`);
550
+
551
+ if (iconEl) {
552
+ const iconMap = {
553
+ pending: '○',
554
+ running: '⏳',
555
+ success: '✓',
556
+ error: '✗'
557
+ };
558
+ const colorMap = {
559
+ pending: 'text-lobsta-muted',
560
+ running: 'text-lobsta-warning',
561
+ success: 'text-green-400',
562
+ error: 'text-red-400'
563
+ };
564
+ iconEl.textContent = iconMap[icon] || icon;
565
+ iconEl.className = colorMap[icon] || '';
566
+ }
567
+ if (statusEl) {
568
+ statusEl.textContent = status;
569
+ const colorMap = {
570
+ pending: 'text-sm text-lobsta-muted',
571
+ running: 'text-sm text-lobsta-warning',
572
+ success: 'text-sm text-green-400',
573
+ error: 'text-sm text-red-400'
574
+ };
575
+ statusEl.className = colorMap[icon] || 'text-sm text-lobsta-muted';
576
+ }
577
+ }
578
+
579
+ async function launchBot() {
580
+ const launchBtn = document.getElementById('launch-btn');
581
+
582
+ // Validate dashboard account (only on first setup — password fields exist)
583
+ const emailEl = document.getElementById('dashboard-email');
584
+ const passwordEl = document.getElementById('dashboard-password');
585
+ const confirmEl = document.getElementById('dashboard-password-confirm');
586
+ const passwordErrorEl = document.getElementById('password-error');
587
+
588
+ if (passwordEl && confirmEl && !passwordEl.closest('#password-setup-section')?.classList.contains('hidden')) {
589
+ const email = emailEl ? emailEl.value.trim() : '';
590
+ const password = passwordEl.value;
591
+ const confirm = confirmEl.value;
592
+
593
+ if (passwordErrorEl) passwordErrorEl.classList.add('hidden');
594
+
595
+ if (!email || !email.includes('@')) {
596
+ if (passwordErrorEl) {
597
+ passwordErrorEl.textContent = 'Please enter a valid email address';
598
+ passwordErrorEl.classList.remove('hidden');
599
+ }
600
+ return;
601
+ }
602
+
603
+ if (!password || password.length < 6) {
604
+ if (passwordErrorEl) {
605
+ passwordErrorEl.textContent = 'Password must be at least 6 characters';
606
+ passwordErrorEl.classList.remove('hidden');
607
+ }
608
+ return;
609
+ }
610
+ if (password !== confirm) {
611
+ if (passwordErrorEl) {
612
+ passwordErrorEl.textContent = 'Passwords do not match';
613
+ passwordErrorEl.classList.remove('hidden');
614
+ }
615
+ return;
616
+ }
617
+ }
618
+
619
+ // Gather data
620
+ const privateMemory = document.getElementById('private-memory-toggle')?.checked ?? true;
621
+ const payload = {
622
+ provider: document.getElementById('provider').value,
623
+ apiKey: document.getElementById('api-key').value.trim(),
624
+ model: document.getElementById('model').value,
625
+ channel: selectedChannel,
626
+ privateMemory: privateMemory
627
+ };
628
+
629
+ // Add channel-specific fields
630
+ if (selectedChannel === 'telegram') {
631
+ payload.telegramBotToken = document.getElementById('bot-token').value.trim();
632
+ payload.telegramUserId = document.getElementById('user-id').value.trim();
633
+ } else if (selectedChannel === 'discord') {
634
+ payload.discordBotToken = document.getElementById('discord-bot-token').value.trim();
635
+ payload.discordServerId = document.getElementById('discord-server-id').value.trim();
636
+ }
637
+
638
+ // Show progress, hide review
639
+ document.getElementById('review-panel').classList.add('hidden');
640
+ document.getElementById('launch-progress').classList.remove('hidden');
641
+ document.getElementById('launch-success').classList.add('hidden');
642
+ document.getElementById('launch-error').classList.add('hidden');
643
+
644
+ // Step 0: Set dashboard account (if password fields exist — first setup)
645
+ if (passwordEl && passwordEl.value) {
646
+ try {
647
+ const email = emailEl ? emailEl.value.trim() : '';
648
+ const authRes = await fetch('/api/auth/setup', {
649
+ method: 'POST',
650
+ headers: { 'Content-Type': 'application/json' },
651
+ body: JSON.stringify({ email, password: passwordEl.value })
652
+ });
653
+ const authData = await authRes.json();
654
+ if (authData.status === 'ok') {
655
+ localStorage.setItem('lobstakit_token', authData.token);
656
+ }
657
+ // If password was already set (reconfigure), that's fine — continue
658
+ } catch (e) {
659
+ console.error('Auth setup error:', e);
660
+ // Non-fatal — continue with gateway setup
661
+ }
662
+ }
663
+
664
+ // Build auth headers for subsequent calls
665
+ const token = localStorage.getItem('lobstakit_token');
666
+ const authH = token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
667
+
668
+ // Step 1: Write Config
669
+ setProgressStep('config', 'Writing...', 'running');
670
+
671
+ try {
672
+ const res = await fetch('/api/setup', {
673
+ method: 'POST',
674
+ headers: authH,
675
+ body: JSON.stringify(payload)
676
+ });
677
+
678
+ const data = await res.json();
679
+
680
+ if (!res.ok) {
681
+ throw new Error(data.error || 'Setup failed');
682
+ }
683
+
684
+ setProgressStep('config', 'Done', 'success');
685
+
686
+ // Step 2: Start Gateway
687
+ setProgressStep('gateway', 'Starting...', 'running');
688
+
689
+ // The API already restarts the gateway, so we just wait a moment
690
+ await sleep(2000);
691
+
692
+ setProgressStep('gateway', 'Done', 'success');
693
+
694
+ // Step 3: Health Check
695
+ setProgressStep('health', 'Checking...', 'running');
696
+
697
+ await sleep(1000);
698
+
699
+ try {
700
+ const gwHeaders = localStorage.getItem('lobstakit_token') ? { 'Authorization': `Bearer ${localStorage.getItem('lobstakit_token')}` } : {};
701
+ const statusRes = await fetch('/api/gateway-status', { headers: gwHeaders });
702
+ const statusData = await statusRes.json();
703
+
704
+ if (statusData.running) {
705
+ setProgressStep('health', 'Healthy', 'success');
706
+ } else {
707
+ setProgressStep('health', 'Starting up...', 'running');
708
+ }
709
+ } catch (e) {
710
+ setProgressStep('health', 'Pending', 'running');
711
+ }
712
+
713
+ // Show success
714
+ await sleep(500);
715
+ document.getElementById('launch-progress').classList.add('hidden');
716
+ document.getElementById('launch-success').classList.remove('hidden');
717
+
718
+ // Update success panel based on channel
719
+ const successMsg = document.getElementById('success-message');
720
+ const successLink = document.getElementById('success-channel-link');
721
+ if (selectedChannel === 'telegram') {
722
+ successMsg.textContent = 'Open Telegram and send a message to your bot. It\'s ready to chat.';
723
+ successLink.href = 'https://t.me/';
724
+ successLink.textContent = 'Open Telegram ↗';
725
+ successLink.classList.remove('hidden');
726
+ } else if (selectedChannel === 'discord') {
727
+ successMsg.textContent = 'Your Discord bot is online! Send a message in your server.';
728
+ successLink.href = 'https://discord.com/channels/@me';
729
+ successLink.textContent = 'Open Discord ↗';
730
+ successLink.classList.remove('hidden');
731
+ } else {
732
+ successMsg.textContent = 'Your gateway is configured and running. Chat from your dashboard!';
733
+ successLink.classList.add('hidden');
734
+ }
735
+
736
+ } catch (err) {
737
+ setProgressStep('config', 'Failed', 'error');
738
+
739
+ // Show error
740
+ document.getElementById('launch-progress').classList.add('hidden');
741
+ document.getElementById('launch-error').classList.remove('hidden');
742
+ document.getElementById('launch-error-message').textContent = err.message;
743
+ }
744
+ }
745
+
746
+ function retryLaunch() {
747
+ // Reset and show review panel
748
+ document.getElementById('launch-error').classList.add('hidden');
749
+ document.getElementById('review-panel').classList.remove('hidden');
750
+
751
+ // Reset progress indicators
752
+ setProgressStep('config', 'Pending', 'pending');
753
+ setProgressStep('gateway', 'Pending', 'pending');
754
+ setProgressStep('health', 'Pending', 'pending');
755
+ }