filepilot-enterprise-vault 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,511 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>FilePilot Vault Setup Wizard</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ font-family: 'Outfit', sans-serif;
12
+ background: radial-gradient(circle at top right, #002e3d, #00121a 70%);
13
+ color: #cbd5e1;
14
+ }
15
+ .glass-dark {
16
+ background: rgba(15, 23, 42, 0.45);
17
+ backdrop-filter: blur(16px);
18
+ -webkit-backdrop-filter: blur(16px);
19
+ border: 1px solid rgba(255, 255, 255, 0.08);
20
+ }
21
+ .step-node.active {
22
+ background: #00b857;
23
+ color: #ffffff;
24
+ box-shadow: 0 0 15px rgba(0, 184, 87, 0.4);
25
+ border-color: #00b857;
26
+ }
27
+ .step-node.completed {
28
+ background: #1e293b;
29
+ color: #00b857;
30
+ border-color: #00b857;
31
+ }
32
+ .pulse-green {
33
+ animation: pulse 2s infinite;
34
+ }
35
+ @keyframes pulse {
36
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 184, 87, 0.4); }
37
+ 50% { box-shadow: 0 0 0 10px rgba(0, 184, 87, 0); }
38
+ }
39
+ input, select {
40
+ background-color: rgba(30, 41, 59, 0.7) !important;
41
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
42
+ color: #f8fafc !important;
43
+ transition: all 0.2s ease-in-out;
44
+ }
45
+ input:focus, select:focus {
46
+ outline: none !important;
47
+ border-color: #00b857 !important;
48
+ box-shadow: 0 0 0 2px rgba(0, 184, 87, 0.2) !important;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body class="min-h-screen flex flex-col justify-between py-12 px-4 selection:bg-[#00b857]/30 selection:text-white">
53
+
54
+ <!-- Setup Header -->
55
+ <header class="max-w-md w-full mx-auto text-center space-y-3 mb-6">
56
+ <div class="inline-flex p-3 rounded-full bg-[#00b857]/10 border border-[#00b857]/20 text-[#00b857] mb-2">
57
+ <svg class="w-9 h-9" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
59
+ </svg>
60
+ </div>
61
+ <h1 class="text-2xl font-extrabold tracking-tight text-white uppercase font-sans">FilePilot Enterprise</h1>
62
+ <p class="text-xs text-slate-400">Welcome to the 3-Step Setup and Installation Wizard</p>
63
+ </header>
64
+
65
+ <!-- Progress Bar -->
66
+ <div class="max-w-xl w-full mx-auto mb-8 px-6">
67
+ <div class="flex items-center justify-between relative">
68
+ <!-- Line Behind -->
69
+ <div class="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-0.5 bg-slate-800 -z-10"></div>
70
+ <div id="progress-indicator" class="absolute left-0 top-1/2 -translate-y-1/2 h-0.5 bg-[#00b857] transition-all duration-300 -z-10" style="width: 0%;"></div>
71
+
72
+ <!-- Step 1 Node -->
73
+ <div class="flex flex-col items-center gap-1.5">
74
+ <div id="node-1" class="step-node active w-8 h-8 rounded-full border-2 border-slate-700 flex items-center justify-center text-xs font-bold font-mono transition-all duration-200">1</div>
75
+ <span class="text-[9px] uppercase tracking-wider font-bold text-slate-400">Database</span>
76
+ </div>
77
+
78
+ <!-- Step 2 Node -->
79
+ <div class="flex flex-col items-center gap-1.5">
80
+ <div id="node-2" class="step-node w-8 h-8 rounded-full border-2 border-slate-700 bg-[#00121a] flex items-center justify-center text-xs font-bold font-mono transition-all duration-200">2</div>
81
+ <span class="text-[9px] uppercase tracking-wider font-bold text-slate-400">Admin Account</span>
82
+ </div>
83
+
84
+ <!-- Step 3 Node -->
85
+ <div class="flex flex-col items-center gap-1.5">
86
+ <div id="node-3" class="step-node w-8 h-8 rounded-full border-2 border-slate-700 bg-[#00121a] flex items-center justify-center text-xs font-bold font-mono transition-all duration-200">3</div>
87
+ <span class="text-[9px] uppercase tracking-wider font-bold text-slate-400">Encryption Keys</span>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Wizard Card -->
93
+ <main class="max-w-xl w-full mx-auto glass-dark rounded-2xl p-8 shadow-2xl space-y-6 flex-grow flex flex-col justify-between">
94
+
95
+ <div id="step-content" class="flex-grow">
96
+ <!-- Content injected by JavaScript -->
97
+ </div>
98
+
99
+ <!-- Navigation Buttons -->
100
+ <div class="flex items-center justify-between pt-6 border-t border-slate-800/80 mt-6">
101
+ <button id="btn-back" onclick="prevStep()" class="px-5 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer border-none disabled:opacity-0 disabled:pointer-events-none" disabled>
102
+ Back
103
+ </button>
104
+
105
+ <div class="flex items-center gap-3">
106
+ <button id="btn-test-db" onclick="testDatabaseConnection()" class="hidden px-5 py-2 rounded-lg bg-[#1e293b] border border-[#00b857]/30 hover:border-[#00b857]/60 text-[#00b857] text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer">
107
+ Test Connection
108
+ </button>
109
+ <button id="btn-next" onclick="nextStep()" class="px-6 py-2.5 bg-[#00b857] hover:bg-[#009b46] text-white text-xs font-bold uppercase tracking-wider transition-all shadow-md shadow-[#00b857]/15 rounded-lg cursor-pointer border-none">
110
+ Next Step
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </main>
115
+
116
+ <!-- Footer Info -->
117
+ <footer class="text-center text-[10px] text-slate-500 max-w-md mx-auto mt-8 leading-relaxed">
118
+ FilePilot Corporate Provisioner & Configuration Engine.<br>
119
+ Ensure connection properties are correct. Master encryption keys cannot be changed once active.
120
+ </footer>
121
+
122
+ <!-- Custom Dialog Modal Overlay (Dark Theme) -->
123
+ <div id="custom-dialog-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-sm hidden animate-fade-in">
124
+ <div class="bg-[#001721] max-w-sm w-full rounded-2xl p-6 space-y-4 shadow-2xl border border-slate-800 transform scale-95 transition-transform duration-200" id="custom-dialog-box">
125
+ <div class="flex items-start gap-3.5">
126
+ <div id="custom-dialog-icon" class="p-2 rounded-full shrink-0"></div>
127
+ <div class="space-y-1">
128
+ <h3 id="custom-dialog-title" class="font-extrabold text-xs uppercase tracking-wider text-white">Notification</h3>
129
+ <p id="custom-dialog-message" class="text-[11px] text-slate-400 leading-normal font-sans"></p>
130
+ </div>
131
+ </div>
132
+ <div class="flex items-center justify-end gap-2.5 pt-2" id="custom-dialog-buttons"></div>
133
+ </div>
134
+ </div>
135
+
136
+ <script>
137
+ function customDialog({ title, message, type = 'info', confirmText = 'Confirm' }) {
138
+ return new Promise((resolve) => {
139
+ const modal = document.getElementById('custom-dialog-modal');
140
+ const box = document.getElementById('custom-dialog-box');
141
+ const titleEl = document.getElementById('custom-dialog-title');
142
+ const msgEl = document.getElementById('custom-dialog-message');
143
+ const iconEl = document.getElementById('custom-dialog-icon');
144
+ const btnsEl = document.getElementById('custom-dialog-buttons');
145
+
146
+ titleEl.textContent = title;
147
+ msgEl.textContent = message;
148
+
149
+ if (type === 'warning' || type === 'danger') {
150
+ iconEl.className = 'p-2 rounded-full shrink-0 bg-amber-500/10 text-amber-400 border border-amber-500/20';
151
+ iconEl.innerHTML = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>`;
152
+ } else if (type === 'success') {
153
+ iconEl.className = 'p-2 rounded-full shrink-0 bg-emerald-500/10 text-emerald-400 border border-emerald-500/20';
154
+ iconEl.innerHTML = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`;
155
+ } else {
156
+ iconEl.className = 'p-2 rounded-full shrink-0 bg-blue-500/10 text-blue-400 border border-blue-500/20';
157
+ iconEl.innerHTML = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`;
158
+ }
159
+
160
+ btnsEl.innerHTML = '';
161
+ const confirmBtn = document.createElement('button');
162
+ confirmBtn.className = 'px-4 py-2 bg-[#00b857] hover:bg-[#009b46] text-white rounded-lg font-bold text-xs cursor-pointer transition-all border-none';
163
+ confirmBtn.textContent = confirmText;
164
+ confirmBtn.onclick = () => {
165
+ modal.classList.add('hidden');
166
+ box.classList.add('scale-95');
167
+ resolve(true);
168
+ };
169
+ btnsEl.appendChild(confirmBtn);
170
+
171
+ modal.classList.remove('hidden');
172
+ setTimeout(() => {
173
+ box.classList.remove('scale-95');
174
+ }, 10);
175
+ });
176
+ }
177
+
178
+ function showAlert(message, type = 'info', title = 'System Alert') {
179
+ return customDialog({ title, message, type, confirmText: 'OK' });
180
+ }
181
+ let currentStep = 1;
182
+
183
+ // Installation Data State
184
+ const setupData = {
185
+ dbDialect: 'sqlite',
186
+ dbHost: 'localhost',
187
+ dbPort: 3306,
188
+ dbUsername: 'root',
189
+ dbPassword: '',
190
+ dbName: 'filepilotenterprise',
191
+ dbStorage: './vault.db',
192
+ adminEmail: '',
193
+ adminPassword: '',
194
+ keyMode: 'generate',
195
+ customKey: ''
196
+ };
197
+
198
+ // Render step view
199
+ function renderStep() {
200
+ const contentDiv = document.getElementById('step-content');
201
+ const testDbBtn = document.getElementById('btn-test-db');
202
+ const nextBtn = document.getElementById('btn-next');
203
+ const backBtn = document.getElementById('btn-back');
204
+
205
+ // Update Navigation Nodes
206
+ for (let i = 1; i <= 3; i++) {
207
+ const node = document.getElementById(`node-${i}`);
208
+ node.className = 'step-node w-8 h-8 rounded-full border-2 flex items-center justify-center text-xs font-bold font-mono transition-all duration-200 ';
209
+ if (i === currentStep) {
210
+ node.className += 'active';
211
+ } else if (i < currentStep) {
212
+ node.className += 'completed';
213
+ node.innerHTML = '✓';
214
+ } else {
215
+ node.className += 'bg-[#00121a] border-slate-700 text-slate-500';
216
+ node.innerHTML = i;
217
+ }
218
+ }
219
+
220
+ // Update progress bar length
221
+ const progressPercent = ((currentStep - 1) / 2) * 100;
222
+ document.getElementById('progress-indicator').style.width = `${progressPercent}%`;
223
+
224
+ // Render Forms
225
+ if (currentStep === 1) {
226
+ backBtn.disabled = true;
227
+ nextBtn.textContent = 'Next Step';
228
+ testDbBtn.classList.remove('hidden');
229
+
230
+ contentDiv.innerHTML = `
231
+ <div class="space-y-5">
232
+ <div>
233
+ <h2 class="text-lg font-bold text-white tracking-wide">Configure Database</h2>
234
+ <p class="text-xs text-slate-400 mt-1">Select your preferred database engine. Out-of-the-box SQLite is fully supported and recommended for immediate launch.</p>
235
+ </div>
236
+
237
+ <div class="grid grid-cols-2 gap-4">
238
+ <div class="col-span-2 space-y-1.5">
239
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Database Dialect</label>
240
+ <select id="db-dialect" onchange="updateDbDialect(this.value)" class="w-full rounded-lg px-3 py-2 text-xs">
241
+ <option value="sqlite" ${setupData.dbDialect === 'sqlite' ? 'selected' : ''}>Embedded SQLite (Recommended)</option>
242
+ <option value="mysql" ${setupData.dbDialect === 'mysql' ? 'selected' : ''}>MySQL Server</option>
243
+ <option value="postgres" ${setupData.dbDialect === 'postgres' ? 'selected' : ''}>PostgreSQL Server</option>
244
+ </select>
245
+ </div>
246
+ </div>
247
+
248
+ <div id="dialect-options-container" class="space-y-4 pt-2">
249
+ <!-- Dynamically rendered inputs based on dialect -->
250
+ </div>
251
+ </div>
252
+ `;
253
+ renderDialectOptions();
254
+ } else if (currentStep === 2) {
255
+ backBtn.disabled = false;
256
+ nextBtn.textContent = 'Next Step';
257
+ testDbBtn.classList.add('hidden');
258
+
259
+ contentDiv.innerHTML = `
260
+ <div class="space-y-5">
261
+ <div>
262
+ <h2 class="text-lg font-bold text-white tracking-wide">Create Admin Account</h2>
263
+ <p class="text-xs text-slate-400 mt-1">Establish the administrator account. This user will manage provisioned keys, synchronization profiles, and security groups.</p>
264
+ </div>
265
+
266
+ <div class="space-y-4">
267
+ <div class="space-y-1.5">
268
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Administrator Username</label>
269
+ <input type="text" value="admin" disabled class="w-full rounded-lg px-3 py-2 text-xs opacity-60 cursor-not-allowed">
270
+ </div>
271
+
272
+ <div class="space-y-1.5">
273
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Administrator Email</label>
274
+ <input type="email" id="admin-email" placeholder="admin@domain.com" required oninput="setupData.adminEmail = this.value" value="${setupData.adminEmail}" class="w-full rounded-lg px-3 py-2 text-xs">
275
+ </div>
276
+
277
+ <div class="space-y-1.5">
278
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Console Password</label>
279
+ <input type="password" id="admin-password" placeholder="••••••••••••" required oninput="setupData.adminPassword = this.value" value="${setupData.adminPassword}" class="w-full rounded-lg px-3 py-2 text-xs font-mono">
280
+ </div>
281
+ </div>
282
+ </div>
283
+ `;
284
+ } else if (currentStep === 3) {
285
+ backBtn.disabled = false;
286
+ nextBtn.textContent = 'Finalize Installation';
287
+ testDbBtn.classList.add('hidden');
288
+
289
+ if (!setupData.customKey) {
290
+ const arr = new Uint8Array(32);
291
+ window.crypto.getRandomValues(arr);
292
+ setupData.customKey = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
293
+ }
294
+
295
+ contentDiv.innerHTML = `
296
+ <div class="space-y-5">
297
+ <div>
298
+ <h2 class="text-lg font-bold text-white tracking-wide">Master Vault Encryption Key</h2>
299
+ <p class="text-xs text-slate-400 mt-1">The vault encrypts remote SSH/SFTP connection profiles at rest. Specify how the master AES key should be initialized.</p>
300
+ </div>
301
+
302
+ <div class="space-y-4">
303
+ <div class="flex items-center gap-3 p-3.5 bg-slate-900/60 rounded-xl border border-slate-800">
304
+ <input type="radio" name="key-mode" id="key-gen" class="w-4 h-4 text-[#00b857] border-slate-700 bg-slate-800 focus:ring-0 focus:ring-offset-0" ${setupData.keyMode === 'generate' ? 'checked' : ''} onchange="updateKeyMode('generate')">
305
+ <label for="key-gen" class="cursor-pointer">
306
+ <span class="block text-xs font-semibold text-white">Generate AES-256 Key Automatically</span>
307
+ <span class="block text-[10px] text-slate-400 mt-0.5">Generates a highly secure key. You must configure it as the VAULT_MASTER_KEY environment variable.</span>
308
+ </label>
309
+ </div>
310
+
311
+ <div class="flex items-center gap-3 p-3.5 bg-slate-900/60 rounded-xl border border-slate-800">
312
+ <input type="radio" name="key-mode" id="key-custom" class="w-4 h-4 text-[#00b857] border-slate-700 bg-slate-800 focus:ring-0 focus:ring-offset-0" ${setupData.keyMode === 'custom' ? 'checked' : ''} onchange="updateKeyMode('custom')">
313
+ <label for="key-custom" class="cursor-pointer">
314
+ <span class="block text-xs font-semibold text-white">Provide Custom Key File Content</span>
315
+ <span class="block text-[10px] text-slate-400 mt-0.5">Use your own Base64-encoded/plaintext key buffer.</span>
316
+ </label>
317
+ </div>
318
+
319
+ <div id="key-display-container" class="space-y-2 p-3.5 bg-slate-950/70 border border-slate-800 rounded-xl">
320
+ <label id="key-label" class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Master Encryption Key</label>
321
+ <div class="flex gap-2">
322
+ <textarea id="master-key-value" class="flex-1 h-12 rounded-lg px-3 py-2 text-xs font-mono bg-slate-900/50 border border-slate-800 text-slate-200 focus:outline-none select-all" oninput="setupData.customKey = this.value">${setupData.customKey}</textarea>
323
+ <button type="button" onclick="copyMasterKey()" class="px-3.5 bg-[#00b857] hover:bg-[#009b46] text-[10px] font-bold uppercase text-white rounded-lg transition-all cursor-pointer border-none shadow-md shadow-[#00b857]/10">
324
+ Copy
325
+ </button>
326
+ </div>
327
+ <p id="key-hint" class="text-[10px] text-slate-500 leading-normal"></p>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ `;
332
+ updateKeyMode(setupData.keyMode);
333
+ }
334
+ }
335
+
336
+ function renderDialectOptions() {
337
+ const container = document.getElementById('dialect-options-container');
338
+ if (setupData.dbDialect === 'sqlite') {
339
+ container.innerHTML = `
340
+ <div class="space-y-1.5">
341
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">SQLite Database Storage File Path</label>
342
+ <input type="text" id="db-storage" oninput="setupData.dbStorage = this.value" value="${setupData.dbStorage}" class="w-full rounded-lg px-3 py-2 text-xs font-mono">
343
+ </div>
344
+ `;
345
+ } else {
346
+ container.innerHTML = `
347
+ <div class="grid grid-cols-6 gap-4">
348
+ <div class="col-span-4 space-y-1.5">
349
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Hostname / IP</label>
350
+ <input type="text" id="db-host" oninput="setupData.dbHost = this.value" value="${setupData.dbHost}" class="w-full rounded-lg px-3 py-2 text-xs">
351
+ </div>
352
+
353
+ <div class="col-span-2 space-y-1.5">
354
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Port</label>
355
+ <input type="number" id="db-port" oninput="setupData.dbPort = parseInt(this.value) || 0" value="${setupData.dbPort}" class="w-full rounded-lg px-3 py-2 text-xs">
356
+ </div>
357
+
358
+ <div class="col-span-3 space-y-1.5">
359
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Username</label>
360
+ <input type="text" id="db-user" oninput="setupData.dbUsername = this.value" value="${setupData.dbUsername}" class="w-full rounded-lg px-3 py-2 text-xs">
361
+ </div>
362
+
363
+ <div class="col-span-3 space-y-1.5">
364
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Password</label>
365
+ <input type="password" id="db-pass" oninput="setupData.dbPassword = this.value" value="${setupData.dbPassword}" placeholder="••••••••" class="w-full rounded-lg px-3 py-2 text-xs">
366
+ </div>
367
+
368
+ <div class="col-span-6 space-y-1.5">
369
+ <label class="block text-[9px] uppercase font-bold text-slate-400 tracking-wider">Database Name</label>
370
+ <input type="text" id="db-name" oninput="setupData.dbName = this.value" value="${setupData.dbName}" class="w-full rounded-lg px-3 py-2 text-xs">
371
+ </div>
372
+ </div>
373
+ `;
374
+ }
375
+ }
376
+
377
+ function updateDbDialect(val) {
378
+ setupData.dbDialect = val;
379
+ if (val === 'mysql') setupData.dbPort = 3306;
380
+ else if (val === 'postgres') setupData.dbPort = 5432;
381
+ renderDialectOptions();
382
+ }
383
+
384
+ // Toggle custom key visibility
385
+ function updateKeyMode(val) {
386
+ setupData.keyMode = val;
387
+ const keyValEl = document.getElementById('master-key-value');
388
+ const keyLabel = document.getElementById('key-label');
389
+ const keyHint = document.getElementById('key-hint');
390
+
391
+ if (val === 'custom') {
392
+ keyLabel.textContent = "Custom Encryption Key (Base64 or Hex)";
393
+ keyValEl.readOnly = false;
394
+ keyValEl.placeholder = "Enter your custom Base64 or Hex key here...";
395
+ keyHint.innerHTML = "<strong>NOTICE:</strong> Make sure you have this key securely saved. If you lose it, you will lose access to all encrypted connection profiles.";
396
+ } else {
397
+ keyLabel.textContent = "Generated Master Encryption Key";
398
+ keyValEl.readOnly = true;
399
+ keyHint.innerHTML = "<strong>IMPORTANT:</strong> Copy this key and set it as the <code>VAULT_MASTER_KEY</code> environment variable on your server hosting environment before starting. The installer will also save a backup locally, but this is insecure for production.";
400
+ }
401
+ }
402
+
403
+ function copyMasterKey() {
404
+ const val = document.getElementById('master-key-value').value;
405
+ navigator.clipboard.writeText(val);
406
+ showAlert('Master key copied to clipboard!', 'success');
407
+ }
408
+
409
+ // Step Navigation logic
410
+ async function nextStep() {
411
+ // Input Validations
412
+ if (currentStep === 1) {
413
+ if (setupData.dbDialect !== 'sqlite' && (!setupData.dbHost || !setupData.dbName || !setupData.dbUsername)) {
414
+ await showAlert('Please fill out database connection details.', 'warning');
415
+ return;
416
+ }
417
+ } else if (currentStep === 2) {
418
+ if (!setupData.adminEmail || !setupData.adminPassword) {
419
+ await showAlert('Email and password are required.', 'warning');
420
+ return;
421
+ }
422
+ if (setupData.adminPassword.length < 8) {
423
+ await showAlert('Password must be at least 8 characters long.', 'warning');
424
+ return;
425
+ }
426
+ }
427
+
428
+ if (currentStep < 3) {
429
+ currentStep++;
430
+ renderStep();
431
+ } else {
432
+ finalizeInstallation();
433
+ }
434
+ }
435
+
436
+ function prevStep() {
437
+ if (currentStep > 1) {
438
+ currentStep--;
439
+ renderStep();
440
+ }
441
+ }
442
+
443
+ // Call Test DB API
444
+ async function testDatabaseConnection() {
445
+ const btn = document.getElementById('btn-test-db');
446
+ btn.textContent = 'Testing...';
447
+ btn.disabled = true;
448
+
449
+ try {
450
+ const res = await fetch('/admin/api/install/test-db', {
451
+ method: 'POST',
452
+ headers: { 'Content-Type': 'application/json' },
453
+ body: JSON.stringify({
454
+ dialect: setupData.dbDialect,
455
+ host: setupData.dbHost,
456
+ port: setupData.dbPort,
457
+ username: setupData.dbUsername,
458
+ password: setupData.dbPassword,
459
+ database: setupData.dbName,
460
+ storage: setupData.dbStorage
461
+ })
462
+ });
463
+
464
+ const data = await res.json();
465
+ if (res.ok && data.success) {
466
+ await showAlert('Database Connection Successful! The engine is ready to communicate.', 'success');
467
+ } else {
468
+ await showAlert('Connection Failed:\n' + (data.error || 'Check credentials.'), 'warning');
469
+ }
470
+ } catch (err) {
471
+ await showAlert('Server API error: ' + err.message, 'warning');
472
+ } finally {
473
+ btn.textContent = 'Test Connection';
474
+ btn.disabled = false;
475
+ }
476
+ }
477
+
478
+ // Finalize Wizard
479
+ async function finalizeInstallation() {
480
+ const nextBtn = document.getElementById('btn-next');
481
+ nextBtn.textContent = 'Installing...';
482
+ nextBtn.disabled = true;
483
+
484
+ try {
485
+ const res = await fetch('/admin/api/install/submit', {
486
+ method: 'POST',
487
+ headers: { 'Content-Type': 'application/json' },
488
+ body: JSON.stringify(setupData)
489
+ });
490
+
491
+ const data = await res.json();
492
+ if (res.ok && data.success) {
493
+ await showAlert('Installation Successful! Seeding complete. Redirecting to admin console...', 'success');
494
+ window.location.href = '/admin';
495
+ } else {
496
+ await showAlert('Installation failed:\n' + (data.error || 'Server error.'), 'warning');
497
+ nextBtn.textContent = 'Finalize Installation';
498
+ nextBtn.disabled = false;
499
+ }
500
+ } catch (err) {
501
+ await showAlert('Server API error: ' + err.message, 'warning');
502
+ nextBtn.textContent = 'Finalize Installation';
503
+ nextBtn.disabled = false;
504
+ }
505
+ }
506
+
507
+ // Initialize View
508
+ renderStep();
509
+ </script>
510
+ </body>
511
+ </html>