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.
- package/bin/lobstakit.js +2 -0
- package/lib/config.js +176 -0
- package/lib/gateway.js +104 -0
- package/lib/proxy.js +33 -0
- package/package.json +16 -0
- package/public/css/styles.css +579 -0
- package/public/index.html +507 -0
- package/public/js/app.js +198 -0
- package/public/js/login.js +93 -0
- package/public/js/manage.js +1274 -0
- package/public/js/setup.js +755 -0
- package/public/login.html +73 -0
- package/public/manage.html +734 -0
- package/server.js +1357 -0
|
@@ -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
|
+
}
|