seo-intel 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.
Files changed (46) hide show
  1. package/.env.example +41 -0
  2. package/LICENSE +75 -0
  3. package/README.md +243 -0
  4. package/Start SEO Intel.bat +9 -0
  5. package/Start SEO Intel.command +8 -0
  6. package/cli.js +3727 -0
  7. package/config/example.json +29 -0
  8. package/config/setup-wizard.js +522 -0
  9. package/crawler/index.js +566 -0
  10. package/crawler/robots.js +103 -0
  11. package/crawler/sanitize.js +124 -0
  12. package/crawler/schema-parser.js +168 -0
  13. package/crawler/sitemap.js +103 -0
  14. package/crawler/stealth.js +393 -0
  15. package/crawler/subdomain-discovery.js +341 -0
  16. package/db/db.js +213 -0
  17. package/db/schema.sql +120 -0
  18. package/exports/competitive.js +186 -0
  19. package/exports/heuristics.js +67 -0
  20. package/exports/queries.js +197 -0
  21. package/exports/suggestive.js +230 -0
  22. package/exports/technical.js +180 -0
  23. package/exports/templates.js +77 -0
  24. package/lib/gate.js +204 -0
  25. package/lib/license.js +369 -0
  26. package/lib/oauth.js +432 -0
  27. package/lib/updater.js +324 -0
  28. package/package.json +68 -0
  29. package/reports/generate-html.js +6194 -0
  30. package/reports/generate-site-graph.js +949 -0
  31. package/reports/gsc-loader.js +190 -0
  32. package/scheduler.js +142 -0
  33. package/seo-audit.js +619 -0
  34. package/seo-intel.png +0 -0
  35. package/server.js +602 -0
  36. package/setup/ROADMAP.md +109 -0
  37. package/setup/checks.js +483 -0
  38. package/setup/config-builder.js +227 -0
  39. package/setup/engine.js +65 -0
  40. package/setup/installers.js +197 -0
  41. package/setup/models.js +328 -0
  42. package/setup/openclaw-bridge.js +329 -0
  43. package/setup/validator.js +395 -0
  44. package/setup/web-routes.js +688 -0
  45. package/setup/wizard.html +2920 -0
  46. package/start-seo-intel.sh +8 -0
@@ -0,0 +1,2920 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SEO Intel &mdash; Setup Wizard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer">
11
+ <style>
12
+ /* ═══════════════════════════════════════════════════════════════════════════
13
+ DESIGN SYSTEM — matches SEO Intel dashboard exactly
14
+ ═══════════════════════════════════════════════════════════════════════════ */
15
+ :root {
16
+ --bg-primary: #0a0a0a;
17
+ --bg-card: #111111;
18
+ --bg-elevated: #161616;
19
+ --border-card: #222222;
20
+ --border-subtle: #262626;
21
+ --accent-gold: #e8d5a3;
22
+ --accent-purple: #7c6deb;
23
+ --color-success: #8ecba8;
24
+ --color-danger: #d98e8e;
25
+ --color-info: #8bbdd9;
26
+ --color-warning: #d9c78b;
27
+ --text-primary: #f0f0f0;
28
+ --text-secondary: #b8b8b8;
29
+ --text-muted: #555555;
30
+ --text-subtle: #888888;
31
+ --text-dark: #0a0a0a;
32
+ --font-display: 'Syne', sans-serif;
33
+ --font-body: 'Inter', system-ui, -apple-system, sans-serif;
34
+ --radius: 6px;
35
+ --max-width: 820px;
36
+ }
37
+
38
+ * { box-sizing: border-box; margin: 0; padding: 0; }
39
+
40
+ body {
41
+ font-family: var(--font-body);
42
+ background: var(--bg-primary);
43
+ color: var(--text-secondary);
44
+ padding: 32px 24px;
45
+ min-height: 100vh;
46
+ line-height: 1.7;
47
+ -webkit-font-smoothing: antialiased;
48
+ -moz-osx-font-smoothing: grayscale;
49
+ font-weight: 400;
50
+ }
51
+
52
+ /* ─── Header / Branding ──────────────────────────────────────────────── */
53
+ .wizard-header {
54
+ max-width: var(--max-width);
55
+ margin: 0 auto 24px;
56
+ text-align: center;
57
+ }
58
+ .wizard-header h1 {
59
+ font-family: var(--font-display);
60
+ color: var(--text-primary);
61
+ font-size: 1.6rem;
62
+ font-weight: 800;
63
+ letter-spacing: -0.03em;
64
+ margin-bottom: 4px;
65
+ }
66
+ .wizard-header .subtitle {
67
+ color: var(--text-muted);
68
+ font-size: 0.8rem;
69
+ font-weight: 300;
70
+ }
71
+
72
+ /* ─── Step Indicator ─────────────────────────────────────────────────── */
73
+ .step-indicator {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ gap: 0;
78
+ max-width: var(--max-width);
79
+ margin: 0 auto 32px;
80
+ padding: 0 16px;
81
+ }
82
+ .step-node {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 6px;
86
+ cursor: default;
87
+ padding: 6px 10px;
88
+ border-radius: var(--radius);
89
+ border: 1px solid transparent;
90
+ font-size: 0.75rem;
91
+ font-weight: 500;
92
+ color: var(--text-muted);
93
+ transition: all 0.25s ease;
94
+ white-space: nowrap;
95
+ user-select: none;
96
+ }
97
+ .step-node .step-num {
98
+ width: 22px;
99
+ height: 22px;
100
+ min-width: 22px;
101
+ border-radius: 50%;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ font-size: 0.65rem;
106
+ font-weight: 600;
107
+ border: 1px solid var(--text-muted);
108
+ color: var(--text-muted);
109
+ flex-shrink: 0;
110
+ transition: all 0.25s ease;
111
+ }
112
+ .step-node.completed {
113
+ cursor: pointer;
114
+ color: var(--accent-gold);
115
+ }
116
+ .step-node.completed .step-num {
117
+ border-color: var(--accent-gold);
118
+ background: var(--accent-gold);
119
+ color: var(--text-dark);
120
+ }
121
+ .step-node.completed:hover {
122
+ background: rgba(232, 213, 163, 0.06);
123
+ }
124
+ .step-node.active {
125
+ color: var(--text-primary);
126
+ border-color: var(--border-subtle);
127
+ background: var(--bg-elevated);
128
+ }
129
+ .step-node.active .step-num {
130
+ border-color: var(--text-primary);
131
+ color: var(--text-primary);
132
+ }
133
+ .step-connector {
134
+ width: 24px;
135
+ height: 1px;
136
+ background: var(--border-card);
137
+ flex-shrink: 0;
138
+ }
139
+ .step-connector.completed {
140
+ background: var(--accent-gold);
141
+ }
142
+
143
+ /* ─── Cards ──────────────────────────────────────────────────────────── */
144
+ .card {
145
+ background: var(--bg-card);
146
+ border-radius: var(--radius);
147
+ padding: 22px;
148
+ border: 1px solid var(--border-card);
149
+ min-width: 0;
150
+ overflow: hidden;
151
+ }
152
+ .card h2 {
153
+ font-family: var(--font-display);
154
+ color: var(--text-primary);
155
+ font-size: 0.95rem;
156
+ font-weight: 600;
157
+ margin-bottom: 16px;
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 10px;
161
+ }
162
+ .card h2 i {
163
+ color: var(--accent-gold);
164
+ font-size: 0.85rem;
165
+ }
166
+
167
+ /* ─── Step Panels ────────────────────────────────────────────────────── */
168
+ .wizard-body {
169
+ max-width: var(--max-width);
170
+ margin: 0 auto;
171
+ }
172
+ .step-panel {
173
+ display: none;
174
+ animation: fadeIn 0.3s ease;
175
+ }
176
+ .step-panel.active {
177
+ display: block;
178
+ }
179
+ @keyframes fadeIn {
180
+ from { opacity: 0; transform: translateY(8px); }
181
+ to { opacity: 1; transform: translateY(0); }
182
+ }
183
+
184
+ /* ─── Dependency Checks (Step 1) ─────────────────────────────────────── */
185
+ .dep-grid {
186
+ display: grid;
187
+ grid-template-columns: 1fr 1fr;
188
+ gap: 12px;
189
+ margin-bottom: 20px;
190
+ }
191
+ .dep-card {
192
+ background: var(--bg-elevated);
193
+ border: 1px solid var(--border-subtle);
194
+ border-radius: var(--radius);
195
+ padding: 16px;
196
+ display: flex;
197
+ align-items: flex-start;
198
+ gap: 12px;
199
+ transition: border-color 0.25s ease;
200
+ }
201
+ .dep-card.ok { border-color: rgba(142, 203, 168, 0.35); }
202
+ .dep-card.warn { border-color: rgba(217, 199, 139, 0.35); }
203
+ .dep-card.fail { border-color: rgba(217, 142, 142, 0.35); }
204
+ .dep-icon {
205
+ width: 32px;
206
+ height: 32px;
207
+ border-radius: 50%;
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ font-size: 0.8rem;
212
+ flex-shrink: 0;
213
+ background: var(--bg-card);
214
+ border: 1px solid var(--border-card);
215
+ color: var(--text-muted);
216
+ }
217
+ .dep-card.ok .dep-icon { color: var(--color-success); border-color: rgba(142,203,168,0.3); }
218
+ .dep-card.warn .dep-icon { color: var(--color-warning); border-color: rgba(217,199,139,0.3); }
219
+ .dep-card.fail .dep-icon { color: var(--color-danger); border-color: rgba(217,142,142,0.3); }
220
+ .dep-info { flex: 1; min-width: 0; }
221
+ .dep-name {
222
+ font-weight: 500;
223
+ font-size: 0.82rem;
224
+ color: var(--text-primary);
225
+ margin-bottom: 2px;
226
+ }
227
+ .dep-detail {
228
+ font-size: 0.72rem;
229
+ color: var(--text-subtle);
230
+ word-break: break-word;
231
+ }
232
+ .dep-actions { margin-top: 8px; }
233
+
234
+ /* ─── Buttons ────────────────────────────────────────────────────────── */
235
+ .btn {
236
+ font-family: var(--font-body);
237
+ font-size: 0.75rem;
238
+ font-weight: 500;
239
+ padding: 7px 16px;
240
+ border-radius: var(--radius);
241
+ border: 1px solid var(--border-card);
242
+ background: var(--bg-elevated);
243
+ color: var(--text-secondary);
244
+ cursor: pointer;
245
+ transition: all 0.2s ease;
246
+ display: inline-flex;
247
+ align-items: center;
248
+ gap: 6px;
249
+ white-space: nowrap;
250
+ }
251
+ .btn:hover {
252
+ border-color: var(--text-muted);
253
+ color: var(--text-primary);
254
+ background: var(--bg-card);
255
+ }
256
+ .btn:disabled {
257
+ opacity: 0.35;
258
+ cursor: not-allowed;
259
+ }
260
+ .btn:disabled:hover {
261
+ border-color: var(--border-card);
262
+ color: var(--text-secondary);
263
+ background: var(--bg-elevated);
264
+ }
265
+ .btn-gold {
266
+ background: var(--accent-gold);
267
+ color: var(--text-dark);
268
+ border-color: var(--accent-gold);
269
+ font-weight: 600;
270
+ }
271
+ .btn-gold:hover {
272
+ background: #d4c290;
273
+ border-color: #d4c290;
274
+ color: var(--text-dark);
275
+ }
276
+ .btn-gold:disabled {
277
+ background: var(--bg-elevated);
278
+ border-color: var(--border-card);
279
+ color: var(--text-muted);
280
+ }
281
+ .btn-gold:disabled:hover {
282
+ background: var(--bg-elevated);
283
+ border-color: var(--border-card);
284
+ color: var(--text-muted);
285
+ }
286
+ .btn-sm {
287
+ font-size: 0.68rem;
288
+ padding: 4px 10px;
289
+ }
290
+ .btn-danger {
291
+ color: var(--color-danger);
292
+ border-color: rgba(217, 142, 142, 0.3);
293
+ }
294
+ .btn-danger:hover {
295
+ border-color: var(--color-danger);
296
+ background: rgba(217, 142, 142, 0.08);
297
+ color: var(--color-danger);
298
+ }
299
+
300
+ /* ─── Step Navigation ────────────────────────────────────────────────── */
301
+ .step-nav {
302
+ display: flex;
303
+ justify-content: space-between;
304
+ align-items: center;
305
+ margin-top: 24px;
306
+ padding-top: 20px;
307
+ border-top: 1px solid var(--border-card);
308
+ }
309
+
310
+ /* ─── Log Output ─────────────────────────────────────────────────────── */
311
+ .log-output {
312
+ background: var(--bg-primary);
313
+ border: 1px solid var(--border-subtle);
314
+ border-radius: var(--radius);
315
+ padding: 12px 14px;
316
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
317
+ font-size: 0.68rem;
318
+ line-height: 1.6;
319
+ color: var(--text-subtle);
320
+ max-height: 200px;
321
+ overflow-y: auto;
322
+ margin-top: 12px;
323
+ display: none;
324
+ white-space: pre-wrap;
325
+ word-break: break-all;
326
+ }
327
+ .log-output.visible { display: block; }
328
+ .log-output .log-ok { color: var(--color-success); }
329
+ .log-output .log-err { color: var(--color-danger); }
330
+ .log-output .log-info { color: var(--color-info); }
331
+
332
+ /* ─── Model Cards (Step 2) ───────────────────────────────────────────── */
333
+ .model-columns {
334
+ display: grid;
335
+ grid-template-columns: 1fr 1fr;
336
+ gap: 20px;
337
+ }
338
+ .model-section {
339
+ position: relative;
340
+ }
341
+ .model-section h3 {
342
+ font-family: var(--font-display);
343
+ font-size: 0.82rem;
344
+ font-weight: 600;
345
+ color: var(--text-primary);
346
+ margin-bottom: 12px;
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 8px;
350
+ }
351
+ .model-section h3 i { color: var(--accent-purple); font-size: 0.75rem; }
352
+ .model-section .section-note {
353
+ font-size: 0.7rem;
354
+ color: var(--text-muted);
355
+ margin-bottom: 12px;
356
+ }
357
+ .model-radio-card {
358
+ background: var(--bg-elevated);
359
+ border: 1px solid var(--border-subtle);
360
+ border-radius: var(--radius);
361
+ padding: 14px;
362
+ margin-bottom: 8px;
363
+ cursor: pointer;
364
+ transition: all 0.2s ease;
365
+ position: relative;
366
+ }
367
+ .model-radio-card:hover {
368
+ border-color: var(--text-muted);
369
+ }
370
+ .model-radio-card.selected {
371
+ border-color: var(--accent-gold);
372
+ border-left: 3px solid var(--accent-gold);
373
+ background: rgba(232, 213, 163, 0.04);
374
+ }
375
+ .model-radio-card.legacy {
376
+ opacity: 0.55;
377
+ }
378
+ .model-radio-card.legacy:hover {
379
+ opacity: 0.75;
380
+ }
381
+ .model-radio-card.selected.legacy {
382
+ opacity: 0.85;
383
+ }
384
+ .model-radio-card.cloud-model {
385
+ border-left: 3px solid var(--accent-purple);
386
+ }
387
+ .model-radio-card.cloud-model.selected {
388
+ border-color: var(--accent-purple);
389
+ border-left: 3px solid var(--accent-purple);
390
+ background: rgba(168, 130, 232, 0.04);
391
+ }
392
+ .cloud-privacy-note {
393
+ font-size: 0.6rem;
394
+ color: var(--color-warning, #e8a838);
395
+ margin-top: 4px;
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 4px;
399
+ }
400
+ .model-card-header {
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: space-between;
404
+ margin-bottom: 6px;
405
+ }
406
+ .model-card-name {
407
+ font-weight: 500;
408
+ font-size: 0.8rem;
409
+ color: var(--text-primary);
410
+ }
411
+ .model-badge {
412
+ font-size: 0.6rem;
413
+ font-weight: 600;
414
+ padding: 2px 7px;
415
+ border-radius: 3px;
416
+ text-transform: uppercase;
417
+ letter-spacing: 0.04em;
418
+ }
419
+ .model-badge.recommended {
420
+ background: rgba(232, 213, 163, 0.15);
421
+ color: var(--accent-gold);
422
+ }
423
+ .model-badge.installed {
424
+ background: rgba(142, 203, 168, 0.15);
425
+ color: var(--color-success);
426
+ }
427
+ .model-badge.configured {
428
+ background: rgba(142, 203, 168, 0.15);
429
+ color: var(--color-success);
430
+ }
431
+ .model-badge.no-fit {
432
+ background: rgba(217, 142, 142, 0.12);
433
+ color: var(--color-danger);
434
+ }
435
+ .model-card-meta {
436
+ display: flex;
437
+ gap: 12px;
438
+ flex-wrap: wrap;
439
+ margin-bottom: 6px;
440
+ }
441
+ .model-meta-item {
442
+ font-size: 0.65rem;
443
+ color: var(--text-subtle);
444
+ }
445
+ .model-meta-item i {
446
+ margin-right: 3px;
447
+ font-size: 0.6rem;
448
+ }
449
+ .model-card-desc {
450
+ font-size: 0.7rem;
451
+ color: var(--text-muted);
452
+ line-height: 1.5;
453
+ }
454
+ .model-pull-row {
455
+ margin-top: 12px;
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 8px;
459
+ }
460
+ /* Free tier: hide model selection entirely, show unified upgrade card */
461
+ .is-free .model-columns {
462
+ display: none;
463
+ }
464
+
465
+ /* Page-level particle field — ambient on all tiers */
466
+ body::before,
467
+ body::after {
468
+ content: '';
469
+ position: fixed;
470
+ inset: -50%;
471
+ width: 200%;
472
+ height: 200%;
473
+ pointer-events: none;
474
+ z-index: 0;
475
+ background-image:
476
+ radial-gradient(1.5px 1.5px at 13% 27%, rgba(232, 213, 163, 0.16) 50%, transparent 50%),
477
+ radial-gradient(1px 1px at 67% 83%, rgba(232, 213, 163, 0.09) 50%, transparent 50%),
478
+ radial-gradient(2px 2px at 41% 11%, rgba(232, 213, 163, 0.13) 50%, transparent 50%),
479
+ radial-gradient(1px 1px at 89% 53%, rgba(232, 213, 163, 0.07) 50%, transparent 50%),
480
+ radial-gradient(1.5px 1.5px at 7% 71%, rgba(232, 213, 163, 0.11) 50%, transparent 50%),
481
+ radial-gradient(1px 1px at 53% 37%, rgba(232, 213, 163, 0.08) 50%, transparent 50%);
482
+ background-size: 220px 180px, 180px 260px, 310px 190px, 170px 290px, 260px 210px, 290px 170px;
483
+ animation: bgDrift 25s linear infinite;
484
+ }
485
+ body::after {
486
+ background-image:
487
+ radial-gradient(1px 1px at 31% 59%, rgba(183, 160, 110, 0.1) 50%, transparent 50%),
488
+ radial-gradient(1.5px 1.5px at 73% 17%, rgba(232, 213, 163, 0.11) 50%, transparent 50%),
489
+ radial-gradient(1px 1px at 19% 91%, rgba(232, 213, 163, 0.07) 50%, transparent 50%),
490
+ radial-gradient(1.5px 1.5px at 83% 43%, rgba(183, 160, 110, 0.09) 50%, transparent 50%),
491
+ radial-gradient(1px 1px at 47% 67%, rgba(232, 213, 163, 0.08) 50%, transparent 50%);
492
+ background-size: 250px 310px, 190px 230px, 330px 170px, 210px 280px, 270px 200px;
493
+ animation: bgDrift2 35s linear infinite;
494
+ opacity: 0.7;
495
+ }
496
+
497
+ /* Animated dot-grid upgrade overlay */
498
+ .upgrade-overlay {
499
+ display: none;
500
+ position: absolute;
501
+ inset: 0;
502
+ border-radius: var(--radius);
503
+ overflow: hidden;
504
+ z-index: 20;
505
+ pointer-events: all;
506
+ align-items: center;
507
+ justify-content: center;
508
+ flex-direction: column;
509
+ gap: 14px;
510
+ }
511
+ /* Individual overlays hidden — unified card replaces them */
512
+ .is-free .upgrade-overlay {
513
+ display: none;
514
+ }
515
+ .upgrade-overlay-bg {
516
+ position: absolute;
517
+ inset: 0;
518
+ background: rgba(8, 8, 10, 0.75);
519
+ backdrop-filter: blur(2px);
520
+ }
521
+ /* Animated particle field */
522
+ .upgrade-overlay-dots {
523
+ position: absolute;
524
+ inset: 0;
525
+ overflow: hidden;
526
+ }
527
+ .upgrade-overlay-dots::before,
528
+ .upgrade-overlay-dots::after {
529
+ content: '';
530
+ position: absolute;
531
+ inset: -50%;
532
+ width: 200%;
533
+ height: 200%;
534
+ background-image:
535
+ radial-gradient(1.5px 1.5px at 13% 27%, rgba(232, 213, 163, 0.45) 50%, transparent 50%),
536
+ radial-gradient(1px 1px at 67% 83%, rgba(232, 213, 163, 0.25) 50%, transparent 50%),
537
+ radial-gradient(2px 2px at 41% 11%, rgba(232, 213, 163, 0.4) 50%, transparent 50%),
538
+ radial-gradient(1px 1px at 89% 53%, rgba(232, 213, 163, 0.2) 50%, transparent 50%),
539
+ radial-gradient(1.5px 1.5px at 7% 71%, rgba(232, 213, 163, 0.35) 50%, transparent 50%),
540
+ radial-gradient(1px 1px at 53% 37%, rgba(232, 213, 163, 0.28) 50%, transparent 50%);
541
+ background-size: 180px 150px, 150px 210px, 250px 160px, 140px 230px, 210px 170px, 230px 140px;
542
+ animation: bgDrift 25s linear infinite;
543
+ }
544
+ .upgrade-overlay-dots::after {
545
+ background-image:
546
+ radial-gradient(1px 1px at 31% 59%, rgba(183, 160, 110, 0.3) 50%, transparent 50%),
547
+ radial-gradient(1.5px 1.5px at 73% 17%, rgba(232, 213, 163, 0.35) 50%, transparent 50%),
548
+ radial-gradient(1px 1px at 19% 91%, rgba(232, 213, 163, 0.2) 50%, transparent 50%),
549
+ radial-gradient(1.5px 1.5px at 83% 43%, rgba(183, 160, 110, 0.25) 50%, transparent 50%),
550
+ radial-gradient(1px 1px at 47% 67%, rgba(232, 213, 163, 0.3) 50%, transparent 50%);
551
+ background-size: 200px 250px, 160px 190px, 270px 140px, 170px 230px, 220px 160px;
552
+ animation: bgDrift2 35s linear infinite;
553
+ opacity: 0.7;
554
+ }
555
+ /* Slow pulsing glow in center */
556
+ .upgrade-overlay-dots .glow-orb {
557
+ position: absolute;
558
+ top: 50%;
559
+ left: 50%;
560
+ width: 200px;
561
+ height: 200px;
562
+ transform: translate(-50%, -50%);
563
+ background: radial-gradient(circle, rgba(232, 213, 163, 0.08) 0%, transparent 70%);
564
+ animation: glowPulse 3s ease-in-out infinite;
565
+ pointer-events: none;
566
+ }
567
+ @keyframes particleDrift {
568
+ 0% { transform: translate(0, 0); }
569
+ 100% { transform: translate(-40px, -30px); }
570
+ }
571
+ @keyframes particleDrift2 {
572
+ 0% { transform: translate(0, 0); }
573
+ 100% { transform: translate(30px, -20px); }
574
+ }
575
+ /* Seamless looping drift — wanders and returns to origin */
576
+ @keyframes bgDrift {
577
+ 0% { transform: translate(0, 0); }
578
+ 25% { transform: translate(-30px, -20px); }
579
+ 50% { transform: translate(-15px, -40px); }
580
+ 75% { transform: translate(20px, -15px); }
581
+ 100% { transform: translate(0, 0); }
582
+ }
583
+ @keyframes bgDrift2 {
584
+ 0% { transform: translate(0, 0); }
585
+ 20% { transform: translate(25px, -35px); }
586
+ 40% { transform: translate(-10px, -20px); }
587
+ 60% { transform: translate(-30px, 10px); }
588
+ 80% { transform: translate(15px, -10px); }
589
+ 100% { transform: translate(0, 0); }
590
+ }
591
+ @keyframes glowPulse {
592
+ 0% { opacity: 0.4; transform: translate(-50%, -50%) scale(0.8); }
593
+ 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
594
+ 100% { opacity: 0.4; transform: translate(-50%, -50%) scale(0.8); }
595
+ }
596
+ .upgrade-overlay-content {
597
+ position: relative;
598
+ z-index: 2;
599
+ display: flex;
600
+ flex-direction: column;
601
+ align-items: center;
602
+ gap: 10px;
603
+ text-align: center;
604
+ padding: 0 20px;
605
+ }
606
+ .upgrade-overlay-icon {
607
+ font-size: 1.6rem;
608
+ opacity: 0.9;
609
+ }
610
+ .upgrade-overlay-title {
611
+ font-size: 0.82rem;
612
+ font-weight: 600;
613
+ color: var(--text-primary);
614
+ letter-spacing: 0.03em;
615
+ }
616
+ .upgrade-overlay-sub {
617
+ font-size: 0.7rem;
618
+ color: var(--text-muted);
619
+ max-width: 200px;
620
+ line-height: 1.5;
621
+ }
622
+ .btn-upgrade {
623
+ margin-top: 4px;
624
+ padding: 9px 20px;
625
+ background: var(--accent-gold);
626
+ color: #0a0a0a;
627
+ border: none;
628
+ border-radius: var(--radius);
629
+ font-family: var(--font-body);
630
+ font-size: 0.78rem;
631
+ font-weight: 700;
632
+ letter-spacing: 0.04em;
633
+ cursor: pointer;
634
+ pointer-events: all;
635
+ transition: opacity 0.2s;
636
+ text-decoration: none;
637
+ display: inline-block;
638
+ }
639
+ .btn-upgrade:hover { opacity: 0.88; }
640
+
641
+ /* ─── API Key Input ──────────────────────────────────────────────────── */
642
+ .api-key-section {
643
+ margin-top: 16px;
644
+ padding: 16px;
645
+ background: var(--bg-elevated);
646
+ border: 1px solid var(--border-subtle);
647
+ border-radius: var(--radius);
648
+ display: none;
649
+ }
650
+ .api-key-section.visible { display: block; }
651
+ .api-key-section label {
652
+ display: block;
653
+ font-size: 0.75rem;
654
+ font-weight: 500;
655
+ color: var(--text-primary);
656
+ margin-bottom: 6px;
657
+ }
658
+ .api-key-section .key-note {
659
+ font-size: 0.68rem;
660
+ color: var(--text-muted);
661
+ margin-bottom: 10px;
662
+ }
663
+ .api-key-section .key-note a {
664
+ color: var(--accent-gold);
665
+ text-decoration: none;
666
+ }
667
+ .api-key-section .key-note a:hover { text-decoration: underline; }
668
+ .input-row {
669
+ display: flex;
670
+ gap: 8px;
671
+ align-items: center;
672
+ }
673
+ .key-status {
674
+ font-size: 0.7rem;
675
+ margin-top: 6px;
676
+ }
677
+ .key-status.ok { color: var(--color-success); }
678
+ .key-status.fail { color: var(--color-danger); }
679
+
680
+ /* ─── Form Inputs (Step 3) ───────────────────────────────────────────── */
681
+ .form-group {
682
+ margin-bottom: 16px;
683
+ }
684
+ .form-group label {
685
+ display: block;
686
+ font-size: 0.75rem;
687
+ font-weight: 500;
688
+ color: var(--text-primary);
689
+ margin-bottom: 5px;
690
+ }
691
+ .form-group .hint {
692
+ font-size: 0.65rem;
693
+ color: var(--text-muted);
694
+ margin-top: 3px;
695
+ }
696
+ input[type="text"],
697
+ input[type="url"],
698
+ input[type="number"],
699
+ input[type="password"] {
700
+ width: 100%;
701
+ font-family: var(--font-body);
702
+ font-size: 0.8rem;
703
+ padding: 9px 12px;
704
+ background: var(--bg-primary);
705
+ border: 1px solid var(--border-subtle);
706
+ border-radius: var(--radius);
707
+ color: var(--text-primary);
708
+ outline: none;
709
+ transition: border-color 0.2s ease;
710
+ }
711
+ input:focus {
712
+ border-color: var(--accent-gold);
713
+ }
714
+ input::placeholder {
715
+ color: var(--text-muted);
716
+ }
717
+ .form-row {
718
+ display: grid;
719
+ grid-template-columns: 1fr 1fr;
720
+ gap: 16px;
721
+ }
722
+ .slug-preview {
723
+ font-size: 0.65rem;
724
+ color: var(--accent-gold);
725
+ margin-top: 3px;
726
+ font-family: 'SF Mono', 'Cascadia Code', monospace;
727
+ }
728
+
729
+ /* ─── Dynamic Lists ──────────────────────────────────────────────────── */
730
+ .dyn-list-item {
731
+ display: flex;
732
+ gap: 8px;
733
+ align-items: center;
734
+ margin-bottom: 6px;
735
+ }
736
+ .dyn-list-item input { flex: 1; }
737
+ .dyn-list-item .btn-remove {
738
+ width: 28px;
739
+ height: 28px;
740
+ padding: 0;
741
+ display: flex;
742
+ align-items: center;
743
+ justify-content: center;
744
+ border-radius: 50%;
745
+ font-size: 0.65rem;
746
+ flex-shrink: 0;
747
+ }
748
+
749
+ /* ─── Pill Selector (crawl mode) ─────────────────────────────────────── */
750
+ .pill-group {
751
+ display: inline-flex;
752
+ border: 1px solid var(--border-subtle);
753
+ border-radius: var(--radius);
754
+ overflow: hidden;
755
+ background: var(--bg-primary);
756
+ }
757
+ .pill-option {
758
+ padding: 7px 16px;
759
+ font-size: 0.75rem;
760
+ font-weight: 500;
761
+ color: var(--text-muted);
762
+ cursor: pointer;
763
+ transition: all 0.2s ease;
764
+ border-right: 1px solid var(--border-subtle);
765
+ user-select: none;
766
+ }
767
+ .pill-option:last-child { border-right: none; }
768
+ .pill-option:hover { color: var(--text-secondary); background: var(--bg-elevated); }
769
+ .pill-option.active {
770
+ background: var(--accent-gold);
771
+ color: var(--text-dark);
772
+ font-weight: 600;
773
+ }
774
+ .pill-option input[type="radio"] { display: none; }
775
+
776
+ /* ─── Pipeline Test Cards (Step 4) ───────────────────────────────────── */
777
+ .test-card {
778
+ background: var(--bg-elevated);
779
+ border: 1px solid var(--border-subtle);
780
+ border-radius: var(--radius);
781
+ padding: 16px;
782
+ margin-bottom: 10px;
783
+ display: flex;
784
+ align-items: center;
785
+ gap: 14px;
786
+ transition: border-color 0.3s ease;
787
+ }
788
+ .test-card.running { border-color: rgba(139, 189, 217, 0.4); }
789
+ .test-card.pass { border-color: rgba(142, 203, 168, 0.4); }
790
+ .test-card.fail { border-color: rgba(217, 142, 142, 0.4); }
791
+ .test-card.skip { border-color: var(--border-subtle); opacity: 0.5; }
792
+ .test-icon {
793
+ width: 34px;
794
+ height: 34px;
795
+ border-radius: 50%;
796
+ display: flex;
797
+ align-items: center;
798
+ justify-content: center;
799
+ font-size: 0.85rem;
800
+ flex-shrink: 0;
801
+ background: var(--bg-card);
802
+ border: 1px solid var(--border-card);
803
+ color: var(--text-muted);
804
+ transition: all 0.3s ease;
805
+ }
806
+ .test-card.running .test-icon { color: var(--color-info); border-color: rgba(139,189,217,0.3); }
807
+ .test-card.pass .test-icon { color: var(--color-success); border-color: rgba(142,203,168,0.3); }
808
+ .test-card.fail .test-icon { color: var(--color-danger); border-color: rgba(217,142,142,0.3); }
809
+ .test-info { flex: 1; min-width: 0; }
810
+ .test-name {
811
+ font-weight: 500;
812
+ font-size: 0.8rem;
813
+ color: var(--text-primary);
814
+ margin-bottom: 2px;
815
+ }
816
+ .test-detail {
817
+ font-size: 0.7rem;
818
+ color: var(--text-subtle);
819
+ word-break: break-word;
820
+ }
821
+ .test-latency {
822
+ font-size: 0.65rem;
823
+ color: var(--text-muted);
824
+ white-space: nowrap;
825
+ }
826
+ .test-retry {
827
+ flex-shrink: 0;
828
+ }
829
+
830
+ /* ─── Spinner ────────────────────────────────────────────────────────── */
831
+ @keyframes spin {
832
+ to { transform: rotate(360deg); }
833
+ }
834
+ .fa-spin-pulse { animation: spin 1s linear infinite; }
835
+
836
+ /* ─── Done Screen (Step 5) ───────────────────────────────────────────── */
837
+ .done-icon {
838
+ text-align: center;
839
+ margin-bottom: 20px;
840
+ }
841
+ .done-icon i {
842
+ font-size: 2.8rem;
843
+ color: var(--color-success);
844
+ }
845
+ .summary-table {
846
+ width: 100%;
847
+ border-collapse: collapse;
848
+ margin-bottom: 16px;
849
+ }
850
+ .summary-table tr {
851
+ border-bottom: 1px solid var(--border-subtle);
852
+ }
853
+ .summary-table td {
854
+ padding: 8px 0;
855
+ font-size: 0.78rem;
856
+ }
857
+ .summary-table td:first-child {
858
+ color: var(--text-muted);
859
+ font-weight: 500;
860
+ width: 140px;
861
+ text-transform: uppercase;
862
+ font-size: 0.65rem;
863
+ letter-spacing: 0.06em;
864
+ }
865
+ .summary-table td:last-child {
866
+ color: var(--text-primary);
867
+ }
868
+ .cli-block {
869
+ background: var(--bg-primary);
870
+ border: 1px solid var(--border-subtle);
871
+ border-radius: var(--radius);
872
+ padding: 14px 16px;
873
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
874
+ font-size: 0.72rem;
875
+ color: var(--color-info);
876
+ line-height: 1.8;
877
+ position: relative;
878
+ overflow-x: auto;
879
+ white-space: pre-wrap;
880
+ }
881
+ .cli-block .copy-btn {
882
+ position: absolute;
883
+ top: 8px;
884
+ right: 8px;
885
+ }
886
+ .cli-block .prompt { color: var(--color-success); }
887
+ .cli-block .comment { color: var(--text-muted); }
888
+
889
+ /* ─── OpenClaw Banner ────────────────────────────────────────────────── */
890
+ .openclaw-banner {
891
+ margin-top: 16px;
892
+ }
893
+ .openclaw-banner-recommended {
894
+ display: flex;
895
+ gap: 14px;
896
+ padding: 18px 16px;
897
+ background: linear-gradient(135deg, rgba(124,109,235,0.08), rgba(124,109,235,0.02));
898
+ border: 1px solid rgba(124,109,235,0.3);
899
+ border-radius: var(--radius);
900
+ }
901
+ .openclaw-banner-minimal {
902
+ display: flex;
903
+ gap: 10px;
904
+ align-items: center;
905
+ padding: 10px 14px;
906
+ background: var(--bg-card);
907
+ border: 1px solid var(--border-subtle);
908
+ border-radius: var(--radius);
909
+ }
910
+ .openclaw-banner-icon {
911
+ font-size: 1.3rem;
912
+ color: var(--accent-purple);
913
+ margin-top: 2px;
914
+ flex-shrink: 0;
915
+ }
916
+ .openclaw-banner-title {
917
+ font-family: var(--font-display);
918
+ font-size: 0.88rem;
919
+ font-weight: 700;
920
+ color: var(--text-primary);
921
+ margin-bottom: 4px;
922
+ }
923
+ .openclaw-banner-desc {
924
+ font-size: 0.76rem;
925
+ color: var(--text-secondary);
926
+ line-height: 1.5;
927
+ }
928
+ .openclaw-banner-actions {
929
+ display: flex;
930
+ gap: 8px;
931
+ margin-top: 12px;
932
+ }
933
+
934
+ /* ─── Agent Chat Panel ───────────────────────────────────────────────── */
935
+ .agent-panel {
936
+ display: none;
937
+ max-width: var(--max-width);
938
+ margin: 0 auto;
939
+ }
940
+ .agent-panel.active { display: block; }
941
+
942
+ .agent-chat {
943
+ background: var(--bg-card);
944
+ border: 1px solid var(--border-card);
945
+ border-radius: var(--radius);
946
+ overflow: hidden;
947
+ }
948
+ .agent-chat-header {
949
+ display: flex;
950
+ align-items: center;
951
+ justify-content: space-between;
952
+ padding: 12px 16px;
953
+ background: rgba(124,109,235,0.06);
954
+ border-bottom: 1px solid var(--border-subtle);
955
+ }
956
+ .agent-chat-header h3 {
957
+ font-family: var(--font-display);
958
+ font-size: 0.85rem;
959
+ font-weight: 700;
960
+ color: var(--text-primary);
961
+ }
962
+ .agent-chat-header h3 i { color: var(--accent-purple); margin-right: 6px; }
963
+ .agent-chat-header .btn { font-size: 0.68rem; padding: 4px 10px; }
964
+
965
+ .agent-chat-messages {
966
+ padding: 16px;
967
+ max-height: 50vh;
968
+ min-height: 200px;
969
+ overflow-y: auto;
970
+ display: flex;
971
+ flex-direction: column;
972
+ gap: 12px;
973
+ }
974
+ .agent-msg {
975
+ max-width: 85%;
976
+ padding: 10px 14px;
977
+ border-radius: var(--radius);
978
+ font-size: 0.78rem;
979
+ line-height: 1.6;
980
+ white-space: pre-wrap;
981
+ word-break: break-word;
982
+ }
983
+ .agent-msg.assistant {
984
+ background: rgba(124,109,235,0.08);
985
+ border: 1px solid rgba(124,109,235,0.15);
986
+ color: var(--text-secondary);
987
+ align-self: flex-start;
988
+ }
989
+ .agent-msg.user {
990
+ background: rgba(232,213,163,0.08);
991
+ border: 1px solid rgba(232,213,163,0.15);
992
+ color: var(--text-primary);
993
+ align-self: flex-end;
994
+ }
995
+ .agent-msg.system {
996
+ background: var(--bg-elevated);
997
+ border: 1px solid var(--border-subtle);
998
+ color: var(--text-muted);
999
+ align-self: center;
1000
+ font-size: 0.72rem;
1001
+ text-align: center;
1002
+ max-width: 100%;
1003
+ }
1004
+ .agent-msg code {
1005
+ background: rgba(0,0,0,0.3);
1006
+ padding: 1px 5px;
1007
+ border-radius: 3px;
1008
+ font-size: 0.74rem;
1009
+ }
1010
+ .agent-msg pre {
1011
+ background: rgba(0,0,0,0.3);
1012
+ padding: 8px 10px;
1013
+ border-radius: 4px;
1014
+ margin: 6px 0;
1015
+ overflow-x: auto;
1016
+ font-size: 0.72rem;
1017
+ }
1018
+ .agent-typing {
1019
+ color: var(--text-muted);
1020
+ font-size: 0.72rem;
1021
+ padding: 4px 0;
1022
+ display: none;
1023
+ }
1024
+ .agent-typing.active { display: block; }
1025
+ .agent-typing i { animation: blink 1s infinite; }
1026
+ @keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
1027
+
1028
+ .agent-chat-input {
1029
+ display: flex;
1030
+ gap: 8px;
1031
+ padding: 12px 16px;
1032
+ border-top: 1px solid var(--border-subtle);
1033
+ background: var(--bg-primary);
1034
+ }
1035
+ .agent-chat-input input {
1036
+ flex: 1;
1037
+ background: var(--bg-elevated);
1038
+ border: 1px solid var(--border-subtle);
1039
+ border-radius: var(--radius);
1040
+ color: var(--text-primary);
1041
+ padding: 8px 12px;
1042
+ font-size: 0.78rem;
1043
+ font-family: var(--font-body);
1044
+ outline: none;
1045
+ transition: border-color 0.2s;
1046
+ }
1047
+ .agent-chat-input input:focus { border-color: var(--accent-purple); }
1048
+ .agent-chat-input input::placeholder { color: var(--text-muted); }
1049
+ .agent-chat-input .btn {
1050
+ background: var(--accent-purple);
1051
+ color: #fff;
1052
+ padding: 8px 14px;
1053
+ }
1054
+ .agent-chat-input .btn:hover { background: #6b5ce0; }
1055
+
1056
+ /* ─── GSC Step ───────────────────────────────────────────────────────── */
1057
+ .gsc-status-box {
1058
+ display: flex;
1059
+ align-items: center;
1060
+ gap: 12px;
1061
+ padding: 14px 16px;
1062
+ background: var(--bg-card);
1063
+ border: 1px solid var(--border-subtle);
1064
+ border-radius: var(--radius);
1065
+ }
1066
+ .gsc-status-box.ok { border-color: var(--color-success); }
1067
+ .gsc-status-box.warn { border-color: var(--color-warning); }
1068
+ .gsc-status-box .gsc-status-icon { font-size: 1.2rem; }
1069
+ .gsc-status-box .gsc-status-text { font-size: 0.78rem; }
1070
+ .gsc-status-box .gsc-status-detail { font-size: 0.68rem; color: var(--text-muted); margin-top: 2px; }
1071
+
1072
+ .gsc-methods {
1073
+ display: grid;
1074
+ grid-template-columns: 1fr 1fr;
1075
+ gap: 12px;
1076
+ }
1077
+ .gsc-method-card {
1078
+ padding: 16px;
1079
+ background: var(--bg-card);
1080
+ border: 1px solid var(--border-subtle);
1081
+ border-radius: var(--radius);
1082
+ cursor: pointer;
1083
+ transition: border-color 0.2s, background 0.2s;
1084
+ }
1085
+ .gsc-method-card:hover:not(.disabled) { border-color: var(--accent-gold); }
1086
+ .gsc-method-card.active { border-color: var(--accent-gold); background: rgba(232, 213, 163, 0.05); }
1087
+ .gsc-method-icon { font-size: 1.4rem; margin-bottom: 8px; color: var(--accent-gold); }
1088
+ .gsc-method-title { font-weight: 600; font-size: 0.82rem; margin-bottom: 4px; }
1089
+ .gsc-method-desc { font-size: 0.7rem; color: var(--text-muted); line-height: 1.5; }
1090
+
1091
+ .gsc-guide {
1092
+ background: var(--bg-card);
1093
+ border: 1px solid var(--border-subtle);
1094
+ border-radius: var(--radius);
1095
+ padding: 14px 16px;
1096
+ margin-bottom: 16px;
1097
+ }
1098
+ .gsc-guide-title {
1099
+ font-size: 0.78rem;
1100
+ font-weight: 600;
1101
+ margin-bottom: 10px;
1102
+ color: var(--accent-gold);
1103
+ }
1104
+ .gsc-guide-steps {
1105
+ font-size: 0.72rem;
1106
+ color: var(--text-secondary);
1107
+ line-height: 1.8;
1108
+ padding-left: 18px;
1109
+ margin: 0;
1110
+ }
1111
+ .gsc-guide-steps strong { color: var(--text-primary); }
1112
+ .gsc-guide-steps a { text-decoration: underline; }
1113
+
1114
+ .gsc-dropzone {
1115
+ border: 2px dashed var(--border-subtle);
1116
+ border-radius: var(--radius);
1117
+ padding: 32px 20px;
1118
+ text-align: center;
1119
+ transition: border-color 0.2s, background 0.2s;
1120
+ font-size: 0.78rem;
1121
+ color: var(--text-secondary);
1122
+ }
1123
+ .gsc-dropzone.dragover {
1124
+ border-color: var(--accent-gold);
1125
+ background: rgba(232, 213, 163, 0.05);
1126
+ }
1127
+ .gsc-browse {
1128
+ color: var(--accent-gold);
1129
+ cursor: pointer;
1130
+ text-decoration: underline;
1131
+ }
1132
+
1133
+ .gsc-file-list {
1134
+ display: flex;
1135
+ flex-wrap: wrap;
1136
+ gap: 8px;
1137
+ margin-top: 12px;
1138
+ }
1139
+ .gsc-file-chip {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ gap: 6px;
1143
+ padding: 4px 10px;
1144
+ background: var(--bg-card);
1145
+ border: 1px solid var(--border-subtle);
1146
+ border-radius: 20px;
1147
+ font-size: 0.7rem;
1148
+ }
1149
+ .gsc-file-chip.ok { border-color: var(--color-success); color: var(--color-success); }
1150
+ .gsc-file-chip.bad { border-color: var(--color-danger); color: var(--color-danger); }
1151
+
1152
+ /* ─── Responsive ─────────────────────────────────────────────────────── */
1153
+ @media (max-width: 700px) {
1154
+ body { padding: 16px 12px; }
1155
+ .dep-grid { grid-template-columns: 1fr; }
1156
+ .model-columns { grid-template-columns: 1fr; }
1157
+ .form-row { grid-template-columns: 1fr; }
1158
+ .step-indicator { flex-wrap: nowrap; gap: 0; overflow-x: auto; }
1159
+ .step-connector { width: 12px; min-width: 12px; }
1160
+ .step-node { padding: 4px 6px; font-size: 0.65rem; }
1161
+ .step-node .step-num { width: 22px; height: 22px; font-size: 0.6rem; }
1162
+ .step-node .step-label { display: none; }
1163
+ .gsc-methods { grid-template-columns: 1fr; }
1164
+ }
1165
+ </style>
1166
+ </head>
1167
+ <body>
1168
+
1169
+ <!-- ═══════════════════════════════════════════════════════════════════════
1170
+ HEADER
1171
+ ═══════════════════════════════════════════════════════════════════════ -->
1172
+ <div class="wizard-header">
1173
+ <h1>SEO Intel</h1>
1174
+ <div class="subtitle">Setup Wizard</div>
1175
+ </div>
1176
+
1177
+ <!-- ═══════════════════════════════════════════════════════════════════════
1178
+ STEP INDICATOR
1179
+ ═══════════════════════════════════════════════════════════════════════ -->
1180
+ <div class="step-indicator" id="stepIndicator">
1181
+ <div class="step-node active" data-step="1" onclick="goToStep(1)">
1182
+ <span class="step-num">1</span>
1183
+ <span class="step-label">System</span>
1184
+ </div>
1185
+ <div class="step-connector" data-conn="1"></div>
1186
+ <div class="step-node" data-step="2" onclick="goToStep(2)">
1187
+ <span class="step-num">2</span>
1188
+ <span class="step-label">Models</span>
1189
+ </div>
1190
+ <div class="step-connector" data-conn="2"></div>
1191
+ <div class="step-node" data-step="3" onclick="goToStep(3)">
1192
+ <span class="step-num">3</span>
1193
+ <span class="step-label">Project</span>
1194
+ </div>
1195
+ <div class="step-connector" data-conn="3"></div>
1196
+ <div class="step-node" data-step="4" onclick="goToStep(4)">
1197
+ <span class="step-num">4</span>
1198
+ <span class="step-label">GSC</span>
1199
+ </div>
1200
+ <div class="step-connector" data-conn="4"></div>
1201
+ <div class="step-node" data-step="5" onclick="goToStep(5)">
1202
+ <span class="step-num">5</span>
1203
+ <span class="step-label">Test</span>
1204
+ </div>
1205
+ <div class="step-connector" data-conn="5"></div>
1206
+ <div class="step-node" data-step="6" onclick="goToStep(6)">
1207
+ <span class="step-num">6</span>
1208
+ <span class="step-label">Done</span>
1209
+ </div>
1210
+ </div>
1211
+
1212
+ <!-- ═══════════════════════════════════════════════════════════════════════
1213
+ WIZARD BODY
1214
+ ═══════════════════════════════════════════════════════════════════════ -->
1215
+ <div class="wizard-body">
1216
+
1217
+ <!-- ─── Step 1: System Check ──────────────────────────────────────────── -->
1218
+ <div class="step-panel active" id="step1">
1219
+ <div class="card">
1220
+ <!-- License & Tier Section -->
1221
+ <div id="licenseSection" style="margin-bottom:20px; padding:16px; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius);">
1222
+ <div id="licenseFree" style="display:none;">
1223
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
1224
+ <i class="fa-solid fa-unlock" style="font-size:0.9rem; color:var(--text-muted);"></i>
1225
+ <span style="font-weight:500; color:var(--text-primary); font-size:0.85rem;">Free Tier — Crawling & Raw Data</span>
1226
+ </div>
1227
+ <p style="font-size:0.72rem; color:var(--text-muted); margin-bottom:12px;">
1228
+ You're set up for free crawling. Data stored in SQLite. Upgrade to Solo for AI analysis and dashboards.
1229
+ </p>
1230
+ <div style="display:flex; gap:8px; align-items:center;">
1231
+ <input type="text" id="licenseKeyInput" placeholder="SI-xxxx-xxxx-xxxx-xxxx" style="flex:1; background:var(--bg-card); border:1px solid var(--border-card); border-radius:var(--radius); padding:8px 10px; color:var(--text-primary); font-family:var(--font-body); font-size:0.78rem;">
1232
+ <button class="btn btn-gold" onclick="saveLicenseKey()" id="activateBtn" style="white-space:nowrap;">Activate →</button>
1233
+ </div>
1234
+ <p style="font-size:0.65rem; color:var(--text-muted); margin-top:6px;">
1235
+ Get Solo at <a href="https://ukkometa.fi/seo-intel/" target="_blank" style="color:var(--accent-gold);">ukkometa.fi/seo-intel</a> or <a href="https://froggo.pro/seo-intel" target="_blank" style="color:var(--accent-purple);">froggo.pro</a>
1236
+ </p>
1237
+ </div>
1238
+ <div id="licenseSolo" style="display:none;">
1239
+ <div style="display:flex; align-items:center; gap:8px;">
1240
+ <i class="fa-solid fa-star" style="font-size:0.9rem; color:var(--accent-gold);"></i>
1241
+ <span style="font-weight:500; color:var(--accent-gold); font-size:0.85rem;">Solo — All Features Unlocked</span>
1242
+ </div>
1243
+ <p id="licenseKeyDisplay" style="font-size:0.7rem; color:var(--text-muted); margin-top:4px;"></p>
1244
+ </div>
1245
+ </div>
1246
+
1247
+ <!-- Pipeline Explanation -->
1248
+ <div style="margin-bottom:20px; padding:16px; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius);">
1249
+ <div style="font-weight:500; color:var(--text-primary); font-size:0.85rem; margin-bottom:12px;">
1250
+ <i class="fa-solid fa-diagram-project" style="color:var(--accent-gold); margin-right:6px;"></i> How SEO Intel Works
1251
+ </div>
1252
+ <div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:10px;">
1253
+ <div style="text-align:center; padding:12px 8px; background:var(--bg-card); border-radius:var(--radius); border:1px solid var(--border-card);">
1254
+ <i class="fa-solid fa-spider" style="font-size:1.1rem; color:var(--color-success); margin-bottom:6px; display:block;"></i>
1255
+ <div style="font-size:0.72rem; font-weight:500; color:var(--text-primary);">Crawl</div>
1256
+ <div style="font-size:0.62rem; color:var(--text-muted); margin-top:2px;">Playwright fetches pages &rarr; SQLite</div>
1257
+ <span style="font-size:0.58rem; padding:1px 6px; border-radius:3px; background:rgba(142,203,168,.12); color:var(--color-success); margin-top:4px; display:inline-block;">FREE</span>
1258
+ </div>
1259
+ <div style="text-align:center; padding:12px 8px; background:var(--bg-card); border-radius:var(--radius); border:1px solid rgba(232,213,163,.15);">
1260
+ <i class="fa-solid fa-brain" style="font-size:1.1rem; color:var(--accent-gold); margin-bottom:6px; display:block;"></i>
1261
+ <div style="font-size:0.72rem; font-weight:500; color:var(--text-primary);">Extract</div>
1262
+ <div style="font-size:0.62rem; color:var(--text-muted); margin-top:2px;">Ollama reads each page locally</div>
1263
+ <span style="font-size:0.58rem; padding:1px 6px; border-radius:3px; background:rgba(232,213,163,.12); color:var(--accent-gold); margin-top:4px; display:inline-block;">SOLO</span>
1264
+ </div>
1265
+ <div style="text-align:center; padding:12px 8px; background:var(--bg-card); border-radius:var(--radius); border:1px solid rgba(232,213,163,.15);">
1266
+ <i class="fa-solid fa-chart-column" style="font-size:1.1rem; color:var(--accent-gold); margin-bottom:6px; display:block;"></i>
1267
+ <div style="font-size:0.72rem; font-weight:500; color:var(--text-primary);">Analyze</div>
1268
+ <div style="font-size:0.62rem; color:var(--text-muted); margin-top:2px;">Cloud AI analyzes full dataset</div>
1269
+ <span style="font-size:0.58rem; padding:1px 6px; border-radius:3px; background:rgba(232,213,163,.12); color:var(--accent-gold); margin-top:4px; display:inline-block;">SOLO</span>
1270
+ </div>
1271
+ <div style="text-align:center; padding:12px 8px; background:var(--bg-card); border-radius:var(--radius); border:1px solid rgba(232,213,163,.15);">
1272
+ <i class="fa-solid fa-map" style="font-size:1.1rem; color:var(--accent-gold); margin-bottom:6px; display:block;"></i>
1273
+ <div style="font-size:0.72rem; font-weight:500; color:var(--text-primary);">Dashboard</div>
1274
+ <div style="font-size:0.62rem; color:var(--text-muted); margin-top:2px;">Interactive HTML reports</div>
1275
+ <span style="font-size:0.58rem; padding:1px 6px; border-radius:3px; background:rgba(232,213,163,.12); color:var(--accent-gold); margin-top:4px; display:inline-block;">SOLO</span>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+
1280
+ <h2><i class="fa-solid fa-stethoscope"></i> System Check</h2>
1281
+ <div class="dep-grid" id="depGrid">
1282
+ <!-- Populated by JS -->
1283
+ </div>
1284
+
1285
+ <!-- OpenClaw Setup Banner -->
1286
+ <div id="openclawBanner" class="openclaw-banner" style="display:none;">
1287
+ <div class="openclaw-banner-recommended" id="ocBannerRecommended" style="display:none;">
1288
+ <div class="openclaw-banner-icon">
1289
+ <!-- OpenClaw logo placeholder — replace with actual SVG when available -->
1290
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
1291
+ <circle cx="14" cy="14" r="13" stroke="currentColor" stroke-width="1.5" opacity="0.6"/>
1292
+ <path d="M9 12c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.8-1 3.4-2.4 4.3L18 20H10l1.4-3.7C10 15.4 9 13.8 9 12z" fill="currentColor" opacity="0.8"/>
1293
+ </svg>
1294
+ </div>
1295
+ <div class="openclaw-banner-content">
1296
+ <div class="openclaw-banner-title">OpenClaw Setup Available</div>
1297
+ <div class="openclaw-banner-desc">OpenClaw guides you through the entire setup conversationally — LLM configuration, cloud model routing, OAuth, and troubleshooting. <strong>Recommended for the best experience.</strong></div>
1298
+ <div style="margin-top:10px; padding:8px 10px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.72rem; color:var(--text-secondary); display:flex; align-items:center; gap:8px;">
1299
+ <span style="color:var(--text-muted);">$</span>
1300
+ <span id="clawhubCmd">clawhub install seo-intel</span>
1301
+ <button class="btn btn-sm" style="margin-left:auto; padding:3px 8px; font-size:0.65rem;" onclick="navigator.clipboard.writeText('clawhub install seo-intel');this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',1500);">Copy</button>
1302
+ </div>
1303
+ <div class="openclaw-banner-actions">
1304
+ <button class="btn btn-gold" onclick="startAgentSetup()"><i class="fa-solid fa-play"></i> Start Setup</button>
1305
+ <button class="btn" onclick="continueManualSetup()"><i class="fa-solid fa-list-check"></i> Continue Manually</button>
1306
+ </div>
1307
+ </div>
1308
+ </div>
1309
+ <div class="openclaw-banner-minimal" id="ocBannerMinimal" style="display:none;">
1310
+ <div class="openclaw-banner-icon" style="color:var(--text-muted); font-size:1rem;">
1311
+ <svg width="20" height="20" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
1312
+ <circle cx="14" cy="14" r="13" stroke="currentColor" stroke-width="1.5" opacity="0.6"/>
1313
+ <path d="M9 12c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.8-1 3.4-2.4 4.3L18 20H10l1.4-3.7C10 15.4 9 13.8 9 12z" fill="currentColor" opacity="0.8"/>
1314
+ </svg>
1315
+ </div>
1316
+ <div class="openclaw-banner-content">
1317
+ <div class="openclaw-banner-desc" style="color:var(--text-muted); font-size:0.72rem;">
1318
+ <strong style="color:var(--text-secondary);">Tip:</strong> Run <code style="background:rgba(124,109,235,0.1); padding:1px 5px; border-radius:3px; color:var(--accent-purple);">clawhub install seo-intel</code> for guided agent setup with cloud model routing.
1319
+ </div>
1320
+ </div>
1321
+ </div>
1322
+ </div>
1323
+
1324
+ <div id="installLog" class="log-output"></div>
1325
+ <div style="display:flex; gap:10px; justify-content: flex-end; margin-top: 8px;">
1326
+ <button class="btn" onclick="runSystemCheck()"><i class="fa-solid fa-arrows-rotate"></i> Re-check</button>
1327
+ </div>
1328
+
1329
+ <div class="step-nav">
1330
+ <div></div>
1331
+ <button class="btn btn-gold" id="step1Next" disabled onclick="goToStep(2)">
1332
+ Next <i class="fa-solid fa-arrow-right"></i>
1333
+ </button>
1334
+ </div>
1335
+ </div>
1336
+ </div>
1337
+
1338
+ <!-- ─── Step 2: Model Selection ───────────────────────────────────────── -->
1339
+ <div class="step-panel" id="step2">
1340
+ <div class="card">
1341
+ <h2><i class="fa-solid fa-microchip"></i> Model Selection</h2>
1342
+ <div class="model-columns">
1343
+ <div class="model-section" id="extractionSection">
1344
+ <h3><i class="fa-solid fa-robot"></i> Extraction Tier</h3>
1345
+ <p class="section-note">Local Ollama model for structured data extraction during crawl</p>
1346
+ <div id="extractionModels">
1347
+ <!-- Populated by JS -->
1348
+ </div>
1349
+ <!-- Custom model input -->
1350
+ <div style="margin-top:12px; padding:10px; background:var(--bg-card); border:1px solid var(--border-card); border-radius:var(--radius);">
1351
+ <p style="font-size:0.7rem; color:var(--text-muted); margin-bottom:6px;">
1352
+ Or use your own model from <a href="https://ollama.com/search" target="_blank" style="color:var(--accent-gold);">ollama.com/search</a>
1353
+ </p>
1354
+ <div style="display:flex; gap:6px; align-items:center;">
1355
+ <input type="text" id="customModelInput" placeholder="e.g. llama3.3:8b" style="flex:1; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius); padding:6px 8px; color:var(--text-primary); font-family:var(--font-mono); font-size:0.72rem;">
1356
+ <button class="btn btn-sm" onclick="useCustomModel()">Use</button>
1357
+ </div>
1358
+ </div>
1359
+ <div class="model-pull-row" id="pullRow" style="display:none;">
1360
+ <button class="btn btn-sm" id="pullBtn" onclick="pullModel()"><i class="fa-solid fa-download"></i> Pull Model</button>
1361
+ <span id="pullStatus" style="font-size:0.7rem; color:var(--text-muted);"></span>
1362
+ </div>
1363
+ <div id="pullLog" class="log-output"></div>
1364
+ <!-- Upgrade overlay for extraction (shown on free tier) -->
1365
+ <div class="upgrade-overlay">
1366
+ <div class="upgrade-overlay-bg"></div>
1367
+ <div class="upgrade-overlay-dots"><div class="glow-orb"></div></div>
1368
+ <div class="upgrade-overlay-content">
1369
+ <div class="upgrade-overlay-icon"><i class="fa-solid fa-star" style="color:var(--accent-gold);"></i></div>
1370
+ <div class="upgrade-overlay-title">Solo Feature</div>
1371
+ <div class="upgrade-overlay-sub">AI extraction requires a Solo or Agency license</div>
1372
+ <a class="btn-upgrade" href="https://ukkometa.fi/seo-intel/" target="_blank">Upgrade to Solo →</a>
1373
+ </div>
1374
+ </div>
1375
+ </div>
1376
+ <div class="model-section" id="analysisSection">
1377
+ <h3><i class="fa-solid fa-brain"></i> Analysis Tier</h3>
1378
+ <p class="section-note">Local Ollama model for competitive gap analysis (needs more VRAM than extraction)</p>
1379
+ <div id="analysisModels">
1380
+ <!-- Populated by JS -->
1381
+ </div>
1382
+ <!-- Custom analysis model input -->
1383
+ <div style="margin-top:12px; padding:10px; background:var(--bg-card); border:1px solid var(--border-card); border-radius:var(--radius);">
1384
+ <p style="font-size:0.7rem; color:var(--text-muted); margin-bottom:6px;">
1385
+ Or use your own model from <a href="https://ollama.com/search" target="_blank" style="color:var(--accent-gold);">ollama.com/search</a>
1386
+ </p>
1387
+ <div style="display:flex; gap:6px; align-items:center;">
1388
+ <input type="text" id="customAnalysisModelInput" placeholder="e.g. llama3.3:70b" style="flex:1; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius); padding:6px 8px; color:var(--text-primary); font-family:var(--font-mono); font-size:0.72rem;">
1389
+ <button class="btn btn-sm" onclick="useCustomAnalysisModel()">Use</button>
1390
+ </div>
1391
+ </div>
1392
+ <div class="model-pull-row" id="analysisPullRow" style="display:none;">
1393
+ <button class="btn btn-sm" id="analysisPullBtn" onclick="pullAnalysisModel()"><i class="fa-solid fa-download"></i> Pull Model</button>
1394
+ <span id="analysisPullStatus" style="font-size:0.7rem; color:var(--text-muted);"></span>
1395
+ </div>
1396
+ <div id="analysisPullLog" class="log-output"></div>
1397
+ <!-- OpenClaw cloud upgrade banner -->
1398
+ <div style="margin-top:12px; padding:12px 14px; background:rgba(124,109,235,0.06); border:1px solid rgba(124,109,235,0.15); border-radius:var(--radius);">
1399
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
1400
+ <svg width="16" height="16" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0;">
1401
+ <circle cx="14" cy="14" r="13" stroke="var(--accent-purple)" stroke-width="1.5" opacity="0.6"/>
1402
+ <path d="M9 12c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.8-1 3.4-2.4 4.3L18 20H10l1.4-3.7C10 15.4 9 13.8 9 12z" fill="var(--accent-purple)" opacity="0.8"/>
1403
+ </svg>
1404
+ <span style="font-size:0.75rem; font-weight:500; color:var(--text-primary);">Want cloud-quality analysis?</span>
1405
+ </div>
1406
+ <p style="font-size:0.68rem; color:var(--text-muted); line-height:1.5; margin-bottom:8px;">
1407
+ Route analysis through Claude, GPT-4o, or DeepSeek for deeper strategic insights on large datasets.
1408
+ </p>
1409
+ <div style="padding:6px 8px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.68rem; color:var(--text-secondary); display:flex; align-items:center; gap:6px; margin-bottom:8px;">
1410
+ <span style="color:var(--text-muted);">$</span>
1411
+ <span>clawhub install seo-intel</span>
1412
+ <button class="btn btn-sm" style="margin-left:auto; padding:2px 6px; font-size:0.6rem;" onclick="navigator.clipboard.writeText('clawhub install seo-intel');this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',1500);">Copy</button>
1413
+ </div>
1414
+ </div>
1415
+ <!-- Upgrade overlay (shown on free tier) -->
1416
+ <div class="upgrade-overlay">
1417
+ <div class="upgrade-overlay-bg"></div>
1418
+ <div class="upgrade-overlay-dots"><div class="glow-orb"></div></div>
1419
+ <div class="upgrade-overlay-content">
1420
+ <div class="upgrade-overlay-icon"><i class="fa-solid fa-star" style="color:var(--accent-gold);"></i></div>
1421
+ <div class="upgrade-overlay-title">Solo Feature</div>
1422
+ <div class="upgrade-overlay-sub">AI analysis requires a Solo or Agency license</div>
1423
+ <a class="btn-upgrade" href="https://ukkometa.fi/seo-intel/" target="_blank">Upgrade to Solo →</a>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+ </div>
1428
+ <!-- Unified upgrade card (free tier only) -->
1429
+ <div id="unifiedUpgrade" style="display:none; position:relative; margin-top:0; padding:48px 32px; border:1px solid rgba(232,213,163,0.15); border-radius:var(--radius); overflow:hidden; text-align:center;">
1430
+ <div class="upgrade-overlay-bg" style="position:absolute;inset:0;"></div>
1431
+ <div class="upgrade-overlay-dots" style="position:absolute;inset:0;"><div class="glow-orb"></div></div>
1432
+ <div style="position:relative; z-index:2; display:flex; flex-direction:column; align-items:center; gap:14px;">
1433
+ <div style="font-size:2rem;"><i class="fa-solid fa-star" style="color:var(--accent-gold);"></i></div>
1434
+ <div style="font-size:1rem; font-weight:600; color:var(--text-primary); letter-spacing:0.03em;">AI-Powered Analysis</div>
1435
+ <div style="font-size:0.78rem; color:var(--text-muted); max-width:380px; line-height:1.6;">
1436
+ Solo unlocks local AI extraction (Ollama) and cloud-powered competitive analysis.
1437
+ Your data stays on your machine — AI just makes it actionable.
1438
+ </div>
1439
+ <div style="display:flex; gap:24px; margin:8px 0; font-size:0.72rem; color:var(--text-secondary);">
1440
+ <div style="display:flex; align-items:center; gap:6px;"><i class="fa-solid fa-brain" style="color:var(--accent-gold); font-size:0.8rem;"></i> Ollama Extraction</div>
1441
+ <div style="display:flex; align-items:center; gap:6px;"><i class="fa-solid fa-chart-column" style="color:var(--accent-gold); font-size:0.8rem;"></i> Gap Analysis</div>
1442
+ <div style="display:flex; align-items:center; gap:6px;"><i class="fa-solid fa-map" style="color:var(--accent-gold); font-size:0.8rem;"></i> Dashboards</div>
1443
+ </div>
1444
+ <a class="btn-upgrade" href="https://ukkometa.fi/seo-intel/" target="_blank" style="margin-top:4px;">Upgrade to Solo →</a>
1445
+ <div style="font-size:0.65rem; color:var(--text-muted); margin-top:2px;">
1446
+ Skip this step — crawling works without AI. Upgrade anytime.
1447
+ </div>
1448
+ </div>
1449
+ </div>
1450
+
1451
+ <div class="step-nav">
1452
+ <button class="btn" onclick="goToStep(1)"><i class="fa-solid fa-arrow-left"></i> Back</button>
1453
+ <button class="btn btn-gold" id="step2Next" onclick="goToStep(3)">
1454
+ Next <i class="fa-solid fa-arrow-right"></i>
1455
+ </button>
1456
+ </div>
1457
+ </div>
1458
+ </div>
1459
+
1460
+ <!-- ─── Step 3: Project Configuration ─────────────────────────────────── -->
1461
+ <div class="step-panel" id="step3">
1462
+ <div class="card">
1463
+ <h2><i class="fa-solid fa-folder-open"></i> Project Configuration</h2>
1464
+
1465
+ <div class="form-row">
1466
+ <div class="form-group">
1467
+ <label>Project Name</label>
1468
+ <input type="text" id="cfgProjectName" placeholder="My SaaS Site" oninput="updateSlug()">
1469
+ <div class="slug-preview" id="slugPreview"></div>
1470
+ </div>
1471
+ <div class="form-group">
1472
+ <label>Target Domain</label>
1473
+ <input type="url" id="cfgTargetUrl" placeholder="https://example.com">
1474
+ </div>
1475
+ </div>
1476
+ <div class="form-row">
1477
+ <div class="form-group">
1478
+ <label>Site Name</label>
1479
+ <input type="text" id="cfgSiteName" placeholder="Example Inc">
1480
+ </div>
1481
+ <div class="form-group">
1482
+ <label>Industry</label>
1483
+ <input type="text" id="cfgIndustry" placeholder="e.g. Developer Tools, E-commerce">
1484
+ </div>
1485
+ </div>
1486
+ <div class="form-row">
1487
+ <div class="form-group">
1488
+ <label>Target Audience</label>
1489
+ <input type="text" id="cfgAudience" placeholder="e.g. Web developers, small businesses">
1490
+ </div>
1491
+ <div class="form-group">
1492
+ <label>SEO Goal</label>
1493
+ <input type="text" id="cfgGoal" placeholder="e.g. Rank for developer tooling keywords">
1494
+ </div>
1495
+ </div>
1496
+
1497
+ <!-- Owned Subdomains -->
1498
+ <div class="form-group">
1499
+ <label>Owned Subdomains <span style="color:var(--text-muted); font-weight:300;">(optional)</span></label>
1500
+ <div id="ownedList"></div>
1501
+ <button class="btn btn-sm" onclick="addOwnedRow()" style="margin-top: 4px;"><i class="fa-solid fa-plus"></i> Add subdomain</button>
1502
+ </div>
1503
+
1504
+ <!-- Competitors -->
1505
+ <div class="form-group">
1506
+ <label>Competitor Domains</label>
1507
+ <div id="competitorList"></div>
1508
+ <button class="btn btn-sm" onclick="addCompetitorRow()" style="margin-top: 4px;"><i class="fa-solid fa-plus"></i> Add competitor</button>
1509
+ </div>
1510
+
1511
+ <!-- Crawl Settings -->
1512
+ <div class="form-row">
1513
+ <div class="form-group">
1514
+ <label>Crawl Mode</label>
1515
+ <div class="pill-group" id="crawlModePills">
1516
+ <label class="pill-option active" onclick="selectCrawlMode('standard')">
1517
+ <input type="radio" name="crawlMode" value="standard" checked> Standard
1518
+ </label>
1519
+ <label class="pill-option" onclick="selectCrawlMode('stealth')">
1520
+ <input type="radio" name="crawlMode" value="stealth"> Advanced
1521
+ </label>
1522
+ </div>
1523
+ <div id="crawlModeDesc" style="margin-top:8px; padding:8px 10px; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius); font-size:0.68rem; color:var(--text-muted); line-height:1.5;">
1524
+ <strong style="color:var(--text-secondary);">Standard</strong> — Headless Chromium via Playwright. Fast (~1.5s/page). Respects robots.txt. Works on most sites. Use this unless you get blocked.
1525
+ </div>
1526
+
1527
+ <!-- Estimated throughput -->
1528
+ <div id="crawlEstimate" style="margin-top:10px; padding:10px; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius); display:none;">
1529
+ <p style="font-size:0.7rem; font-weight:500; color:var(--text-secondary); margin-bottom:6px;">
1530
+ <i class="fa-solid fa-gauge-high" style="margin-right:4px;"></i> Estimated Throughput
1531
+ </p>
1532
+ <table style="width:100%; font-size:0.65rem; color:var(--text-muted); border-collapse:collapse;">
1533
+ <thead>
1534
+ <tr style="border-bottom:1px solid var(--border-subtle);">
1535
+ <th style="text-align:left; padding:3px 6px; color:var(--text-secondary);">Mode</th>
1536
+ <th style="text-align:right; padding:3px 6px; color:var(--text-secondary);">Speed/page</th>
1537
+ <th style="text-align:right; padding:3px 6px; color:var(--text-secondary);">Pages/day</th>
1538
+ </tr>
1539
+ </thead>
1540
+ <tbody id="crawlEstimateBody">
1541
+ </tbody>
1542
+ </table>
1543
+ <p style="font-size:0.6rem; color:var(--text-muted); margin-top:4px; font-style:italic;" id="crawlEstimateNote"></p>
1544
+ </div>
1545
+ </div>
1546
+ <div class="form-group">
1547
+ <label>Pages per Domain</label>
1548
+ <input type="number" id="cfgPagesPerDomain" value="50" min="1" max="500">
1549
+ <div class="hint">Max pages to crawl from each domain.</div>
1550
+ </div>
1551
+ </div>
1552
+
1553
+ <!-- Time Estimate -->
1554
+ <div id="timeEstimate" style="margin-top:16px; padding:12px 14px; background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--radius); display:none;">
1555
+ <div style="font-size:0.75rem; font-weight:500; color:var(--text-secondary); margin-bottom:8px;">
1556
+ <i class="fa-solid fa-clock" style="color:var(--accent-gold); margin-right:4px;"></i> Estimated Pipeline Time
1557
+ </div>
1558
+ <div id="timeEstimateBody" style="font-size:0.68rem; color:var(--text-muted); line-height:1.6;"></div>
1559
+ </div>
1560
+
1561
+ <div id="configErrors" style="color: var(--color-danger); font-size: 0.72rem; margin-top: 8px;"></div>
1562
+
1563
+ <div class="step-nav">
1564
+ <button class="btn" onclick="goToStep(2)"><i class="fa-solid fa-arrow-left"></i> Back</button>
1565
+ <button class="btn btn-gold" onclick="saveConfig()">
1566
+ Save &amp; Next <i class="fa-solid fa-arrow-right"></i>
1567
+ </button>
1568
+ </div>
1569
+ </div>
1570
+ </div>
1571
+
1572
+ <!-- ─── Step 4: Google Search Console ─────────────────────────────────── -->
1573
+ <div class="step-panel" id="step4">
1574
+ <div class="card">
1575
+ <h2><i class="fa-brands fa-google" style="color:#4285F4;"></i> Google Search Console</h2>
1576
+ <div class="hint" style="margin-bottom: 16px;">
1577
+ GSC data powers your ranking insights, CTR analysis, and wasted impressions detection.
1578
+ You can skip this step and add it later.
1579
+ </div>
1580
+
1581
+ <!-- GSC Status -->
1582
+ <div id="gscStatus" class="gsc-status-box">
1583
+ <div class="gsc-status-icon"><i class="fa-solid fa-spinner fa-spin"></i></div>
1584
+ <div class="gsc-status-text">Checking for existing GSC data...</div>
1585
+ </div>
1586
+
1587
+ <!-- Method Selection -->
1588
+ <div id="gscMethodSelect" style="display:none; margin-top: 20px;">
1589
+ <h3 style="font-size: 0.82rem; margin-bottom: 12px;">How would you like to connect GSC?</h3>
1590
+
1591
+ <div class="gsc-methods">
1592
+ <!-- Method 1: CSV Upload -->
1593
+ <div class="gsc-method-card active" onclick="selectGscMethod('csv')">
1594
+ <div class="gsc-method-icon"><i class="fa-solid fa-file-csv"></i></div>
1595
+ <div class="gsc-method-title">Upload CSV Export</div>
1596
+ <div class="gsc-method-desc">Export data from Google Search Console and upload the CSV files here. Quick and works right away.</div>
1597
+ <div class="badge" style="margin-top:8px;">Recommended</div>
1598
+ </div>
1599
+
1600
+ <!-- Method 2: API (future) -->
1601
+ <div class="gsc-method-card disabled" style="opacity:0.5; cursor:not-allowed;">
1602
+ <div class="gsc-method-icon"><i class="fa-solid fa-plug"></i></div>
1603
+ <div class="gsc-method-title">API Connection</div>
1604
+ <div class="gsc-method-desc">Connect directly via Google service account for automatic data sync.</div>
1605
+ <div class="badge" style="margin-top:8px; background: var(--bg-card);">Coming Soon</div>
1606
+ </div>
1607
+ </div>
1608
+ </div>
1609
+
1610
+ <!-- CSV Upload Section -->
1611
+ <div id="gscCsvSection" style="display:none; margin-top: 20px;">
1612
+ <h3 style="font-size: 0.82rem; margin-bottom: 12px;"><i class="fa-solid fa-cloud-arrow-up"></i> Upload GSC CSV Files</h3>
1613
+
1614
+ <!-- How to export guide -->
1615
+ <div class="gsc-guide">
1616
+ <div class="gsc-guide-title"><i class="fa-solid fa-circle-info"></i> How to export from Google Search Console</div>
1617
+ <ol class="gsc-guide-steps">
1618
+ <li>Go to <a href="https://search.google.com/search-console" target="_blank" style="color:var(--accent-gold);">search.google.com/search-console</a></li>
1619
+ <li>Select your property (domain)</li>
1620
+ <li>Click <strong>Performance</strong> in the left sidebar</li>
1621
+ <li>Set your date range (last 3 months recommended)</li>
1622
+ <li>Click the <strong>Export</strong> button (top right) → <strong>Download CSV</strong></li>
1623
+ <li>Unzip the downloaded file — you'll get Chart.csv, Queries.csv, Pages.csv, etc.</li>
1624
+ </ol>
1625
+ </div>
1626
+
1627
+ <!-- Drop zone -->
1628
+ <div class="gsc-dropzone" id="gscDropzone"
1629
+ ondragover="event.preventDefault(); this.classList.add('dragover');"
1630
+ ondragleave="this.classList.remove('dragover');"
1631
+ ondrop="handleGscDrop(event);">
1632
+ <i class="fa-solid fa-cloud-arrow-up" style="font-size: 2rem; color: var(--accent-gold); margin-bottom: 8px;"></i>
1633
+ <div>Drop CSV files here or <label class="gsc-browse" for="gscFileInput">browse</label></div>
1634
+ <div class="hint">Accepts: Chart.csv, Queries.csv, Pages.csv, Countries.csv, Devices.csv</div>
1635
+ <input type="file" id="gscFileInput" multiple accept=".csv" style="display:none;" onchange="handleGscFiles(this.files)">
1636
+ </div>
1637
+
1638
+ <!-- Upload status -->
1639
+ <div id="gscFileList" class="gsc-file-list"></div>
1640
+ <div id="gscUploadStatus" style="margin-top: 10px;"></div>
1641
+ </div>
1642
+
1643
+ <div class="step-nav">
1644
+ <button class="btn" onclick="goToStep(3)"><i class="fa-solid fa-arrow-left"></i> Back</button>
1645
+ <button class="btn" onclick="goToStep(5)" style="margin-right: auto; margin-left: 8px;">
1646
+ Skip <i class="fa-solid fa-forward"></i>
1647
+ </button>
1648
+ <button class="btn btn-gold" id="gscNextBtn" onclick="goToStep(5)">
1649
+ Next <i class="fa-solid fa-arrow-right"></i>
1650
+ </button>
1651
+ </div>
1652
+ </div>
1653
+ </div>
1654
+
1655
+ <!-- ─── Step 5: Pipeline Test ─────────────────────────────────────────── -->
1656
+ <div class="step-panel" id="step5">
1657
+ <div class="card">
1658
+ <h2><i class="fa-solid fa-vial"></i> Pipeline Test</h2>
1659
+
1660
+ <div id="testCards">
1661
+ <div class="test-card" id="testOllama">
1662
+ <div class="test-icon"><i class="fa-solid fa-server"></i></div>
1663
+ <div class="test-info">
1664
+ <div class="test-name">Ollama Connectivity</div>
1665
+ <div class="test-detail" id="testOllamaDetail">Pending</div>
1666
+ </div>
1667
+ <div class="test-latency" id="testOllamaLatency"></div>
1668
+ </div>
1669
+ <div class="test-card" id="testApiKey">
1670
+ <div class="test-icon"><i class="fa-solid fa-key"></i></div>
1671
+ <div class="test-info">
1672
+ <div class="test-name">API Key Validation</div>
1673
+ <div class="test-detail" id="testApiKeyDetail">Pending</div>
1674
+ </div>
1675
+ <div class="test-latency" id="testApiKeyLatency"></div>
1676
+ </div>
1677
+ <div class="test-card" id="testCrawl">
1678
+ <div class="test-icon"><i class="fa-solid fa-spider"></i></div>
1679
+ <div class="test-info">
1680
+ <div class="test-name">Test Crawl (1 page)</div>
1681
+ <div class="test-detail" id="testCrawlDetail">Pending</div>
1682
+ </div>
1683
+ <div class="test-latency" id="testCrawlLatency"></div>
1684
+ </div>
1685
+ <div class="test-card" id="testExtraction">
1686
+ <div class="test-icon"><i class="fa-solid fa-wand-magic-sparkles"></i></div>
1687
+ <div class="test-info">
1688
+ <div class="test-name">Test Extraction</div>
1689
+ <div class="test-detail" id="testExtractionDetail">Pending</div>
1690
+ </div>
1691
+ <div class="test-latency" id="testExtractionLatency"></div>
1692
+ </div>
1693
+ </div>
1694
+
1695
+ <div style="display:flex; gap:10px; justify-content:flex-end; margin-top: 12px;">
1696
+ <button class="btn btn-gold" id="runTestsBtn" onclick="runTests()"><i class="fa-solid fa-play"></i> Run All Tests</button>
1697
+ </div>
1698
+
1699
+ <div class="step-nav">
1700
+ <button class="btn" onclick="goToStep(4)"><i class="fa-solid fa-arrow-left"></i> Back</button>
1701
+ <button class="btn btn-gold" id="step5Next" disabled onclick="buildSummaryAndFinish()">
1702
+ Finish <i class="fa-solid fa-arrow-right"></i>
1703
+ </button>
1704
+ </div>
1705
+ </div>
1706
+ </div>
1707
+
1708
+ <!-- ─── Step 6: Done ──────────────────────────────────────────────────── -->
1709
+ <div class="step-panel" id="step6">
1710
+ <div class="card">
1711
+ <div class="done-icon"><i class="fa-solid fa-circle-check"></i></div>
1712
+ <h2 style="justify-content:center;"><i class="fa-solid fa-flag-checkered"></i> Setup Complete</h2>
1713
+
1714
+ <table class="summary-table" id="summaryTable">
1715
+ <!-- Populated by JS -->
1716
+ </table>
1717
+
1718
+ <h3 style="font-family:var(--font-display); font-size:0.85rem; color:var(--text-primary); margin: 20px 0 10px; font-weight:600;">
1719
+ <i class="fa-solid fa-terminal" style="color:var(--accent-purple); margin-right:6px;"></i> Next Steps
1720
+ </h3>
1721
+ <div class="cli-block" id="cliBlock">
1722
+ <button class="btn btn-sm copy-btn" onclick="copyCli()"><i class="fa-regular fa-copy"></i></button>
1723
+ </div>
1724
+
1725
+ <div style="text-align:center; margin-top: 24px;">
1726
+ <a href="/" class="btn btn-gold" style="text-decoration:none;"><i class="fa-solid fa-chart-line"></i> Open Dashboard</a>
1727
+ </div>
1728
+ </div>
1729
+ </div>
1730
+
1731
+ </div><!-- /wizard-body -->
1732
+
1733
+ <!-- ─── Agent Chat Panel (shown when OpenClaw agent setup is chosen) ──── -->
1734
+ <div class="agent-panel" id="agentPanel">
1735
+ <div class="agent-chat">
1736
+ <div class="agent-chat-header">
1737
+ <h3><i class="fa-solid fa-wand-magic-sparkles"></i> Agent Setup</h3>
1738
+ <button class="btn" onclick="exitAgentSetup()"><i class="fa-solid fa-list-check"></i> Switch to Manual</button>
1739
+ </div>
1740
+ <div class="agent-chat-messages" id="agentMessages">
1741
+ <div class="agent-msg system">Connecting to OpenClaw agent...</div>
1742
+ </div>
1743
+ <div class="agent-typing" id="agentTyping"><i class="fa-solid fa-circle"></i> Agent is thinking...</div>
1744
+ <div class="agent-chat-input">
1745
+ <input type="text" id="agentInput" placeholder="Type your answer..." onkeydown="if(event.key==='Enter')sendAgentMessage()">
1746
+ <button class="btn" onclick="sendAgentMessage()"><i class="fa-solid fa-paper-plane"></i></button>
1747
+ </div>
1748
+ </div>
1749
+ </div>
1750
+
1751
+ <!-- ═══════════════════════════════════════════════════════════════════════
1752
+ JAVASCRIPT
1753
+ ═══════════════════════════════════════════════════════════════════════ -->
1754
+ <script>
1755
+ (function() {
1756
+ 'use strict';
1757
+
1758
+ // ── Global State ──────────────────────────────────────────────────────
1759
+ const state = {
1760
+ currentStep: 1,
1761
+ maxVisited: 1,
1762
+ systemStatus: null,
1763
+ modelData: null,
1764
+ selectedExtraction: null,
1765
+ selectedAnalysis: null,
1766
+ savedApiKey: null,
1767
+ savedApiProvider: null,
1768
+ configSaved: false,
1769
+ projectSlug: '',
1770
+ crawlMode: 'standard',
1771
+ testsPassed: false,
1772
+ gscFiles: [], // Files staged for upload
1773
+ gscMethod: 'csv', // 'csv' or 'api'
1774
+ gscUploaded: false,
1775
+ tier: 'free', // 'free' | 'solo' | 'agency'
1776
+ };
1777
+
1778
+ // ── API Helpers ───────────────────────────────────────────────────────
1779
+ const API = {
1780
+ async get(path) {
1781
+ const res = await fetch(path);
1782
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1783
+ return res.json();
1784
+ },
1785
+ async post(path, body) {
1786
+ const res = await fetch(path, {
1787
+ method: 'POST',
1788
+ headers: { 'Content-Type': 'application/json' },
1789
+ body: JSON.stringify(body),
1790
+ });
1791
+ if (!res.ok) {
1792
+ const data = await res.json().catch(() => ({}));
1793
+ throw new Error(data.error || data.errors?.join(', ') || `HTTP ${res.status}`);
1794
+ }
1795
+ return res.json();
1796
+ },
1797
+ sse(path, body, onEvent) {
1798
+ return new Promise((resolve, reject) => {
1799
+ fetch(path, {
1800
+ method: 'POST',
1801
+ headers: { 'Content-Type': 'application/json' },
1802
+ body: JSON.stringify(body),
1803
+ }).then(res => {
1804
+ if (!res.ok) {
1805
+ reject(new Error(`HTTP ${res.status}`));
1806
+ return;
1807
+ }
1808
+ const reader = res.body.getReader();
1809
+ const decoder = new TextDecoder();
1810
+ let buffer = '';
1811
+
1812
+ function pump() {
1813
+ reader.read().then(({ value, done }) => {
1814
+ if (done) { resolve(); return; }
1815
+ buffer += decoder.decode(value, { stream: true });
1816
+ const lines = buffer.split('\n');
1817
+ buffer = lines.pop() || '';
1818
+ for (const line of lines) {
1819
+ if (line.startsWith('data: ')) {
1820
+ try {
1821
+ const data = JSON.parse(line.slice(6));
1822
+ onEvent(data);
1823
+ } catch {}
1824
+ }
1825
+ }
1826
+ pump();
1827
+ }).catch(reject);
1828
+ }
1829
+ pump();
1830
+ }).catch(reject);
1831
+ });
1832
+ },
1833
+ };
1834
+
1835
+ // ── Step Navigation ───────────────────────────────────────────────────
1836
+ window.goToStep = function(n) {
1837
+ // Only allow navigating to completed steps or the next step
1838
+ if (n > state.maxVisited + 1) return;
1839
+ if (n > state.currentStep + 1) return;
1840
+
1841
+ state.currentStep = n;
1842
+ if (n > state.maxVisited) state.maxVisited = n;
1843
+
1844
+ // Update panels
1845
+ document.querySelectorAll('.step-panel').forEach(p => p.classList.remove('active'));
1846
+ const panel = document.getElementById('step' + n);
1847
+ if (panel) panel.classList.add('active');
1848
+
1849
+ // Update indicator
1850
+ document.querySelectorAll('.step-node').forEach(node => {
1851
+ const s = parseInt(node.dataset.step);
1852
+ node.classList.remove('active', 'completed');
1853
+ if (s < n) node.classList.add('completed');
1854
+ else if (s === n) node.classList.add('active');
1855
+ });
1856
+ document.querySelectorAll('.step-connector').forEach(conn => {
1857
+ const c = parseInt(conn.dataset.conn);
1858
+ conn.classList.toggle('completed', c < n);
1859
+ });
1860
+
1861
+ // Step-specific initialization
1862
+ if (n === 2 && !state.modelData) loadModels();
1863
+ if (n === 3) updateCrawlEstimate();
1864
+ if (n === 4) checkGscStatus();
1865
+ };
1866
+
1867
+ // ── Step 1: System Check ──────────────────────────────────────────────
1868
+ const DEP_DEFS = [
1869
+ { key: 'node', icon: 'fa-brands fa-node-js', name: 'Node.js' },
1870
+ { key: 'npm', icon: 'fa-solid fa-cube', name: 'npm' },
1871
+ { key: 'npmDeps', icon: 'fa-solid fa-cubes', name: 'npm Dependencies' },
1872
+ { key: 'playwright', icon: 'fa-solid fa-globe', name: 'Playwright' },
1873
+ { key: 'ollama', icon: 'fa-solid fa-robot', name: 'Ollama' },
1874
+ { key: 'vram', icon: 'fa-solid fa-memory', name: 'GPU / VRAM' },
1875
+ { key: 'openclaw', icon: 'fa-solid fa-wand-magic-sparkles', name: 'OpenClaw' },
1876
+ ];
1877
+
1878
+ function depStatus(status, key) {
1879
+ switch (key) {
1880
+ case 'node':
1881
+ if (status.node.meetsMinimum) return { cls: 'ok', icon: 'fa-check', detail: status.node.version };
1882
+ if (status.node.installed) return { cls: 'warn', icon: 'fa-exclamation', detail: `${status.node.version} (need v18+)` };
1883
+ return { cls: 'fail', icon: 'fa-xmark', detail: 'Not found', action: null };
1884
+ case 'npm':
1885
+ if (status.npm.installed) return { cls: 'ok', icon: 'fa-check', detail: `v${status.npm.version}` };
1886
+ return { cls: 'fail', icon: 'fa-xmark', detail: 'Not found' };
1887
+ case 'npmDeps':
1888
+ if (status.npmDeps.installed) return { cls: 'ok', icon: 'fa-check', detail: 'All installed' };
1889
+ return { cls: 'fail', icon: 'fa-xmark', detail: `Missing: ${status.npmDeps.missing.slice(0,3).join(', ')}`, action: 'npm' };
1890
+ case 'playwright':
1891
+ if (status.playwright.installed && status.playwright.chromiumReady) return { cls: 'ok', icon: 'fa-check', detail: 'Chromium ready' };
1892
+ if (status.playwright.installed) return { cls: 'warn', icon: 'fa-exclamation', detail: 'Installed, Chromium needs setup', action: 'playwright' };
1893
+ return { cls: 'fail', icon: 'fa-xmark', detail: 'Not installed', action: 'playwright' };
1894
+ case 'ollama':
1895
+ if (status.ollama.available) return { cls: 'ok', icon: 'fa-check', detail: `${status.ollama.models.length} model(s) at ${status.ollama.host}` };
1896
+ if (status.ollama.installed) return { cls: 'warn', icon: 'fa-exclamation', detail: 'Installed but not running or no models' };
1897
+ return { cls: 'warn', icon: 'fa-exclamation', detail: 'Not installed (optional for extraction)' };
1898
+ case 'vram':
1899
+ if (status.vram.available) return { cls: 'ok', icon: 'fa-check', detail: `${status.vram.gpuName} \u2014 ${Math.round(status.vram.vramMB / 1024)}GB` };
1900
+ return { cls: 'warn', icon: 'fa-exclamation', detail: 'No GPU detected' };
1901
+ case 'openclaw':
1902
+ if (status.openclaw?.canAgentSetup) return { cls: 'ok', icon: 'fa-check', detail: `v${status.openclaw.version} — gateway active` };
1903
+ if (status.openclaw?.installed) return { cls: 'warn', icon: 'fa-exclamation', detail: `v${status.openclaw.version} — gateway offline` };
1904
+ return { cls: 'warn', icon: 'fa-exclamation', detail: 'Not installed (optional)' };
1905
+ default:
1906
+ return { cls: '', icon: 'fa-question', detail: '' };
1907
+ }
1908
+ }
1909
+
1910
+ function renderDepGrid(status) {
1911
+ const grid = document.getElementById('depGrid');
1912
+ grid.innerHTML = '';
1913
+ for (const def of DEP_DEFS) {
1914
+ const s = depStatus(status, def.key);
1915
+ const card = document.createElement('div');
1916
+ card.className = 'dep-card ' + s.cls;
1917
+ let actionsHtml = '';
1918
+ if (s.action) {
1919
+ actionsHtml = `<div class="dep-actions"><button class="btn btn-sm" onclick="installDep('${s.action}')"><i class="fa-solid fa-download"></i> Install</button></div>`;
1920
+ }
1921
+ card.innerHTML = `
1922
+ <div class="dep-icon"><i class="${def.icon}"></i></div>
1923
+ <div class="dep-info">
1924
+ <div class="dep-name">${def.name}</div>
1925
+ <div class="dep-detail"><i class="fa-solid ${s.icon}" style="margin-right:4px;"></i>${s.detail}</div>
1926
+ ${actionsHtml}
1927
+ </div>
1928
+ `;
1929
+ grid.appendChild(card);
1930
+ }
1931
+
1932
+ // Enable Next if Node 18+ is detected
1933
+ const nextBtn = document.getElementById('step1Next');
1934
+ nextBtn.disabled = !status.node.meetsMinimum;
1935
+
1936
+ // Show OpenClaw banner
1937
+ const banner = document.getElementById('openclawBanner');
1938
+ const bannerRec = document.getElementById('ocBannerRecommended');
1939
+ const bannerMin = document.getElementById('ocBannerMinimal');
1940
+
1941
+ if (status.openclaw?.canAgentSetup) {
1942
+ banner.style.display = '';
1943
+ bannerRec.style.display = 'flex';
1944
+ bannerMin.style.display = 'none';
1945
+ } else if (status.openclaw?.installed) {
1946
+ banner.style.display = '';
1947
+ bannerRec.style.display = 'none';
1948
+ bannerMin.style.display = 'flex';
1949
+ } else {
1950
+ banner.style.display = '';
1951
+ bannerRec.style.display = 'none';
1952
+ bannerMin.style.display = 'flex';
1953
+ }
1954
+ }
1955
+
1956
+ window.runSystemCheck = async function() {
1957
+ const grid = document.getElementById('depGrid');
1958
+ grid.innerHTML = '<div style="grid-column:1/-1; text-align:center; padding:30px; color:var(--text-muted); font-size:0.8rem;"><i class="fa-solid fa-spinner fa-spin-pulse" style="margin-right:8px;"></i>Checking system...</div>';
1959
+ document.getElementById('step1Next').disabled = true;
1960
+ try {
1961
+ state.systemStatus = await API.get('/api/setup/status');
1962
+ renderDepGrid(state.systemStatus);
1963
+ } catch (err) {
1964
+ grid.innerHTML = `<div style="grid-column:1/-1; text-align:center; padding:30px; color:var(--color-danger); font-size:0.8rem;"><i class="fa-solid fa-triangle-exclamation" style="margin-right:6px;"></i>Failed to reach server: ${err.message}</div>`;
1965
+ }
1966
+ };
1967
+
1968
+ window.installDep = async function(action) {
1969
+ const logEl = document.getElementById('installLog');
1970
+ logEl.classList.add('visible');
1971
+ logEl.innerHTML = '';
1972
+ appendLog(logEl, `Starting ${action} install...\n`, 'info');
1973
+
1974
+ try {
1975
+ await API.sse('/api/setup/install', { action }, (ev) => {
1976
+ const cls = ev.status === 'error' ? 'err' : ev.status === 'done' ? 'ok' : 'info';
1977
+ appendLog(logEl, ev.message + '\n', cls);
1978
+ });
1979
+ appendLog(logEl, 'Done! Re-checking system...\n', 'ok');
1980
+ await runSystemCheck();
1981
+ } catch (err) {
1982
+ appendLog(logEl, 'Error: ' + err.message + '\n', 'err');
1983
+ }
1984
+ };
1985
+
1986
+ function appendLog(el, text, cls) {
1987
+ const span = document.createElement('span');
1988
+ span.className = 'log-' + cls;
1989
+ span.textContent = text;
1990
+ el.appendChild(span);
1991
+ el.scrollTop = el.scrollHeight;
1992
+ }
1993
+
1994
+ // ── Step 2: Model Selection ───────────────────────────────────────────
1995
+ async function loadModels() {
1996
+ const extDiv = document.getElementById('extractionModels');
1997
+ const anaDiv = document.getElementById('analysisModels');
1998
+ extDiv.innerHTML = '<div style="text-align:center; padding:16px; color:var(--text-muted); font-size:0.75rem;"><i class="fa-solid fa-spinner fa-spin-pulse"></i> Loading models...</div>';
1999
+ anaDiv.innerHTML = extDiv.innerHTML;
2000
+
2001
+ try {
2002
+ state.modelData = await API.get('/api/setup/models');
2003
+ renderExtractionModels();
2004
+ renderAnalysisModels();
2005
+ } catch (err) {
2006
+ extDiv.innerHTML = `<div style="color:var(--color-danger); font-size:0.75rem;">Failed: ${err.message}</div>`;
2007
+ anaDiv.innerHTML = extDiv.innerHTML;
2008
+ }
2009
+ }
2010
+
2011
+ function renderExtractionModels() {
2012
+ const div = document.getElementById('extractionModels');
2013
+ div.innerHTML = '';
2014
+ const models = [...(state.modelData.allExtraction || [])];
2015
+ // Sort: installed first, then recommended, then by minVramMB
2016
+ models.sort((a, b) => (b.installed ? 1 : 0) - (a.installed ? 1 : 0) || (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0) || a.minVramMB - b.minVramMB);
2017
+ const rec = state.modelData.extraction;
2018
+
2019
+ for (const m of models) {
2020
+ const isRec = rec && rec.model && rec.model.id === m.id;
2021
+ const isSelected = state.selectedExtraction === m.id || (!state.selectedExtraction && isRec);
2022
+ if (isSelected) state.selectedExtraction = m.id;
2023
+
2024
+ const badges = [];
2025
+ if (isRec && rec.autoRecommended) badges.push('<span class="model-badge recommended">recommended</span>');
2026
+ if (m.installed) badges.push('<span class="model-badge installed">installed</span>');
2027
+ if (!m.fitsVram && !m.installed) badges.push('<span class="model-badge no-fit">needs more VRAM</span>');
2028
+
2029
+ const card = document.createElement('div');
2030
+ card.className = 'model-radio-card' + (isSelected ? ' selected' : '') + (m.legacy ? ' legacy' : '');
2031
+ card.dataset.modelId = m.id;
2032
+ card.innerHTML = `
2033
+ <div class="model-card-header">
2034
+ <span class="model-card-name">${m.name}</span>
2035
+ <span>${badges.join(' ')}</span>
2036
+ </div>
2037
+ <div class="model-card-meta">
2038
+ <span class="model-meta-item"><i class="fa-solid fa-memory"></i> ${m.vram}</span>
2039
+ <span class="model-meta-item"><i class="fa-solid fa-gauge-high"></i> ${m.speed}</span>
2040
+ <span class="model-meta-item"><i class="fa-solid fa-star"></i> ${m.quality}</span>
2041
+ </div>
2042
+ <div class="model-card-desc">${m.description}</div>
2043
+ `;
2044
+ card.addEventListener('click', () => selectExtractionModel(m.id));
2045
+ div.appendChild(card);
2046
+ }
2047
+
2048
+ updatePullRow();
2049
+ }
2050
+
2051
+ function selectExtractionModel(id) {
2052
+ state.selectedExtraction = id;
2053
+ document.querySelectorAll('#extractionModels .model-radio-card').forEach(c => {
2054
+ c.classList.toggle('selected', c.dataset.modelId === id);
2055
+ });
2056
+ updatePullRow();
2057
+ }
2058
+
2059
+ function updatePullRow() {
2060
+ const pullRow = document.getElementById('pullRow');
2061
+ if (!state.selectedExtraction || !state.modelData) { pullRow.style.display = 'none'; return; }
2062
+ const m = state.modelData.allExtraction.find(x => x.id === state.selectedExtraction);
2063
+ if (m && !m.installed) {
2064
+ pullRow.style.display = 'flex';
2065
+ document.getElementById('pullStatus').textContent = `${m.id} is not installed yet.`;
2066
+ } else {
2067
+ pullRow.style.display = 'none';
2068
+ }
2069
+ }
2070
+
2071
+ window.pullModel = async function() {
2072
+ if (!state.selectedExtraction) return;
2073
+ const logEl = document.getElementById('pullLog');
2074
+ logEl.classList.add('visible');
2075
+ logEl.innerHTML = '';
2076
+ const btn = document.getElementById('pullBtn');
2077
+ btn.disabled = true;
2078
+ appendLog(logEl, `Pulling ${state.selectedExtraction}...\n`, 'info');
2079
+
2080
+ try {
2081
+ const host = (state.systemStatus && state.systemStatus.ollama.host) || 'http://localhost:11434';
2082
+ let hadError = false;
2083
+ await API.sse('/api/setup/install', { action: 'ollama-pull', model: state.selectedExtraction, host }, (ev) => {
2084
+ const cls = ev.status === 'error' ? 'err' : ev.status === 'done' ? 'ok' : 'info';
2085
+ if (ev.status === 'error') hadError = true;
2086
+ appendLog(logEl, ev.message + '\n', cls);
2087
+ });
2088
+ if (hadError) {
2089
+ appendLog(logEl, 'Pull encountered errors. Check model name and try again.\n', 'err');
2090
+ } else {
2091
+ appendLog(logEl, 'Model pulled successfully!\n', 'ok');
2092
+ // Refresh model data
2093
+ state.modelData = await API.get('/api/setup/models');
2094
+ renderExtractionModels();
2095
+ }
2096
+ } catch (err) {
2097
+ appendLog(logEl, 'Error: ' + err.message + '\n', 'err');
2098
+ } finally {
2099
+ btn.disabled = false;
2100
+ }
2101
+ };
2102
+
2103
+ window.useCustomModel = function() {
2104
+ const input = document.getElementById('customModelInput');
2105
+ const modelId = input.value.trim();
2106
+ if (!modelId) return;
2107
+ state.selectedExtraction = modelId;
2108
+ // Deselect all radio cards
2109
+ document.querySelectorAll('#extractionModels .model-radio-card').forEach(c => {
2110
+ c.classList.remove('selected');
2111
+ const radio = c.querySelector('input[type="radio"]');
2112
+ if (radio) radio.checked = false;
2113
+ });
2114
+ updatePullRow();
2115
+ // Show the custom model in pull status
2116
+ const pullRow = document.getElementById('pullRow');
2117
+ pullRow.style.display = 'flex';
2118
+ document.getElementById('pullStatus').textContent = `Custom: ${modelId} — click Pull to download`;
2119
+ };
2120
+
2121
+ function renderAnalysisModels() {
2122
+ const div = document.getElementById('analysisModels');
2123
+ div.innerHTML = '';
2124
+ const models = [...(state.modelData.allAnalysis || [])];
2125
+ // Sort: installed first, then recommended, then by minVramMB
2126
+ models.sort((a, b) => (b.installed ? 1 : 0) - (a.installed ? 1 : 0) || (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0) || a.minVramMB - b.minVramMB);
2127
+ const rec = state.modelData.analysis;
2128
+
2129
+ for (const m of models) {
2130
+ const isRec = rec && rec.model && rec.model.id === m.id;
2131
+ const isSelected = state.selectedAnalysis === m.id || (!state.selectedAnalysis && isRec);
2132
+ if (isSelected) state.selectedAnalysis = m.id;
2133
+
2134
+ const badges = [];
2135
+ if (m.recommended) badges.push('<span class="model-badge recommended">recommended</span>');
2136
+ if (m.installed) badges.push('<span class="model-badge installed">installed</span>');
2137
+ if (!m.fitsVram && m.vram) badges.push('<span class="model-badge vram-warn">needs more vram</span>');
2138
+
2139
+ const card = document.createElement('div');
2140
+ card.className = 'model-radio-card' + (isSelected ? ' selected' : '') + (!m.fitsVram ? ' dimmed' : '');
2141
+ card.dataset.modelId = m.id;
2142
+ card.innerHTML = `
2143
+ <div class="model-card-header">
2144
+ <span class="model-card-name">${m.name}</span>
2145
+ <span>${badges.join(' ')}</span>
2146
+ </div>
2147
+ <div class="model-card-meta">
2148
+ <span class="model-meta-item"><i class="fa-solid fa-microchip"></i> ${m.vram}</span>
2149
+ <span class="model-meta-item"><i class="fa-solid fa-arrows-left-right"></i> ${m.context}</span>
2150
+ <span class="model-meta-item"><i class="fa-solid fa-star"></i> ${m.quality}</span>
2151
+ </div>
2152
+ <div class="model-card-desc">${m.description}</div>
2153
+ `;
2154
+ card.addEventListener('click', () => selectAnalysisModel(m.id));
2155
+ div.appendChild(card);
2156
+ }
2157
+
2158
+ // Show pull row if selected model is not installed
2159
+ const selected = models.find(m => m.id === state.selectedAnalysis);
2160
+ if (selected && !selected.installed) {
2161
+ const pullRow = document.getElementById('analysisPullRow');
2162
+ if (pullRow) {
2163
+ pullRow.style.display = 'flex';
2164
+ document.getElementById('analysisPullStatus').textContent = `${selected.name} not installed`;
2165
+ }
2166
+ }
2167
+ }
2168
+
2169
+ function selectAnalysisModel(id) {
2170
+ state.selectedAnalysis = id;
2171
+ document.querySelectorAll('#analysisModels .model-radio-card').forEach(c => {
2172
+ c.classList.toggle('selected', c.dataset.modelId === id);
2173
+ });
2174
+ // Update pull row
2175
+ const models = state.modelData?.allAnalysis || [];
2176
+ const selected = models.find(m => m.id === id);
2177
+ const pullRow = document.getElementById('analysisPullRow');
2178
+ if (pullRow) {
2179
+ if (selected && !selected.installed) {
2180
+ pullRow.style.display = 'flex';
2181
+ document.getElementById('analysisPullStatus').textContent = `${selected.name} not installed`;
2182
+ } else {
2183
+ pullRow.style.display = 'none';
2184
+ }
2185
+ }
2186
+ }
2187
+
2188
+ window.useCustomAnalysisModel = function() {
2189
+ const input = document.getElementById('customAnalysisModelInput');
2190
+ const modelId = input.value.trim();
2191
+ if (!modelId) return;
2192
+ state.selectedAnalysis = modelId;
2193
+ document.querySelectorAll('#analysisModels .model-radio-card').forEach(c => c.classList.remove('selected'));
2194
+ const pullRow = document.getElementById('analysisPullRow');
2195
+ if (pullRow) {
2196
+ pullRow.style.display = 'flex';
2197
+ document.getElementById('analysisPullStatus').textContent = `Custom model: ${modelId}`;
2198
+ }
2199
+ };
2200
+
2201
+ window.pullAnalysisModel = async function() {
2202
+ const modelId = state.selectedAnalysis;
2203
+ if (!modelId) return;
2204
+ const btn = document.getElementById('analysisPullBtn');
2205
+ const statusEl = document.getElementById('analysisPullStatus');
2206
+ const logEl = document.getElementById('analysisPullLog');
2207
+ btn.disabled = true;
2208
+ statusEl.textContent = 'Pulling...';
2209
+ logEl.style.display = 'block';
2210
+ logEl.textContent = '';
2211
+
2212
+ try {
2213
+ const res = await fetch('/api/setup/install', {
2214
+ method: 'POST',
2215
+ headers: { 'Content-Type': 'application/json' },
2216
+ body: JSON.stringify({ action: 'pull-model', model: modelId }),
2217
+ });
2218
+ const reader = res.body.getReader();
2219
+ const decoder = new TextDecoder();
2220
+ while (true) {
2221
+ const { done, value } = await reader.read();
2222
+ if (done) break;
2223
+ logEl.textContent += decoder.decode(value);
2224
+ logEl.scrollTop = logEl.scrollHeight;
2225
+ }
2226
+ statusEl.textContent = 'Pull complete!';
2227
+ // Refresh model data
2228
+ state.modelData = await API.get('/api/setup/models');
2229
+ renderAnalysisModels();
2230
+ } catch (err) {
2231
+ statusEl.textContent = 'Error: ' + err.message;
2232
+ } finally {
2233
+ btn.disabled = false;
2234
+ }
2235
+ };
2236
+
2237
+ // ── Step 3: Project Configuration ─────────────────────────────────────
2238
+
2239
+ function slugify(s) {
2240
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
2241
+ }
2242
+
2243
+ window.updateSlug = function() {
2244
+ const name = document.getElementById('cfgProjectName').value;
2245
+ const slug = slugify(name);
2246
+ state.projectSlug = slug;
2247
+ document.getElementById('slugPreview').textContent = slug ? 'config/' + slug + '.json' : '';
2248
+ };
2249
+
2250
+ window.selectCrawlMode = function(mode) {
2251
+ state.crawlMode = mode;
2252
+ document.querySelectorAll('#crawlModePills .pill-option').forEach(p => {
2253
+ p.classList.toggle('active', p.querySelector('input').value === mode);
2254
+ });
2255
+ // Update description
2256
+ const descEl = document.getElementById('crawlModeDesc');
2257
+ if (descEl) {
2258
+ const descs = {
2259
+ standard: '<strong style="color:var(--text-secondary);">Standard</strong> — Headless Chromium via Playwright. Fast (~1.5s/page). Respects robots.txt. Works on most sites. Use this unless you get blocked.',
2260
+ stealth: '<strong style="color:var(--text-secondary);">Advanced</strong> — Full browser rendering with enhanced compatibility. Persistent sessions, realistic browsing patterns. Slower (~3-4s/page). Use for JavaScript-heavy or dynamically-loaded sites.',
2261
+ };
2262
+ descEl.innerHTML = descs[mode] || descs.standard;
2263
+ }
2264
+ updateCrawlEstimate();
2265
+ };
2266
+
2267
+ function updateCrawlEstimate() {
2268
+ const el = document.getElementById('crawlEstimate');
2269
+ const body = document.getElementById('crawlEstimateBody');
2270
+ const note = document.getElementById('crawlEstimateNote');
2271
+ if (!el) return;
2272
+
2273
+ // Get extraction model speed
2274
+ const extractionId = state.selectedExtraction;
2275
+ let extractionSpeed = 0;
2276
+ if (state.modelData && extractionId) {
2277
+ const m = (state.modelData.allExtraction || []).find(x => x.id === extractionId);
2278
+ if (m && m.speed) {
2279
+ const match = m.speed.match(/~?(\d+)s/);
2280
+ if (match) extractionSpeed = parseInt(match[1]);
2281
+ }
2282
+ }
2283
+
2284
+ const modes = {
2285
+ standard: { label: 'Standard', secPerPage: 2 + extractionSpeed },
2286
+ stealth: { label: 'Advanced', secPerPage: 4 + extractionSpeed },
2287
+ hybrid: { label: 'Hybrid', secPerPage: 3 + extractionSpeed },
2288
+ manual: { label: 'Manual (curl)', secPerPage: 1 + extractionSpeed },
2289
+ };
2290
+
2291
+ const pagesPerDomain = parseInt(document.getElementById('cfgPagesPerDomain')?.value) || 50;
2292
+ const competitorCount = (document.querySelectorAll('#competitorList .dyn-list-item input') || []).length + 1;
2293
+ const totalPages = pagesPerDomain * competitorCount;
2294
+
2295
+ const rows = Object.entries(modes).map(([key, m]) => {
2296
+ const pagesPerDay = Math.floor(86400 / m.secPerPage);
2297
+ const highlight = key === state.crawlMode ? 'color:var(--accent-gold); font-weight:500;' : '';
2298
+ return `<tr style="${highlight}">
2299
+ <td style="padding:3px 6px;">${m.label}${key === state.crawlMode ? ' ●' : ''}</td>
2300
+ <td style="text-align:right; padding:3px 6px;">~${m.secPerPage}s</td>
2301
+ <td style="text-align:right; padding:3px 6px;">~${pagesPerDay.toLocaleString()}</td>
2302
+ </tr>`;
2303
+ });
2304
+
2305
+ body.innerHTML = rows.join('');
2306
+ const selectedMode = modes[state.crawlMode];
2307
+ const estTime = Math.ceil((totalPages * selectedMode.secPerPage) / 60);
2308
+ note.textContent = `~${totalPages} pages × ~${selectedMode.secPerPage}s = ~${estTime} min for full crawl${extractionSpeed ? ` (incl. ${extractionSpeed}s extraction per page)` : ''}`;
2309
+ el.style.display = 'block';
2310
+ }
2311
+
2312
+ // Dynamic list helpers
2313
+ function addListRow(containerId, placeholder) {
2314
+ const list = document.getElementById(containerId);
2315
+ const item = document.createElement('div');
2316
+ item.className = 'dyn-list-item';
2317
+ item.innerHTML = `
2318
+ <input type="text" placeholder="${placeholder}">
2319
+ <button class="btn btn-sm btn-remove btn-danger" onclick="this.parentElement.remove()"><i class="fa-solid fa-xmark"></i></button>
2320
+ `;
2321
+ list.appendChild(item);
2322
+ item.querySelector('input').focus();
2323
+ }
2324
+
2325
+ window.addOwnedRow = function() { addListRow('ownedList', 'https://docs.example.com'); setTimeout(updateTimeEstimate, 100); };
2326
+ window.addCompetitorRow = function() { addListRow('competitorList', 'https://competitor.com'); setTimeout(updateTimeEstimate, 100); };
2327
+
2328
+ function updateTimeEstimate() {
2329
+ const el = document.getElementById('timeEstimate');
2330
+ const body = document.getElementById('timeEstimateBody');
2331
+ if (!el || !body) return;
2332
+
2333
+ const pagesPerDomain = parseInt(document.getElementById('cfgPagesPerDomain')?.value) || 50;
2334
+ const competitors = getListValues('competitorList').length;
2335
+ const owned = getListValues('ownedList').length;
2336
+ const target = document.getElementById('cfgTargetUrl')?.value.trim() ? 1 : 0;
2337
+ const totalDomains = target + owned + competitors;
2338
+
2339
+ if (totalDomains === 0) { el.style.display = 'none'; return; }
2340
+
2341
+ const totalPages = totalDomains * pagesPerDomain;
2342
+ const mode = state.crawlMode || 'standard';
2343
+ const crawlSec = mode === 'stealth' ? 3.5 : 1.5;
2344
+ const extractSec = 3; // ~3s/page with qwen3.5:9b
2345
+ const crawlMin = Math.ceil((totalPages * crawlSec) / 60);
2346
+ const extractMin = Math.ceil((totalPages * extractSec) / 60);
2347
+ const analyzeMin = totalPages > 200 ? 3 : 1; // analysis is one call
2348
+
2349
+ el.style.display = 'block';
2350
+ body.innerHTML = `
2351
+ <div style="display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; margin-bottom:8px;">
2352
+ <div style="text-align:center; padding:8px; background:var(--bg-card); border-radius:var(--radius);">
2353
+ <div style="font-size:0.85rem; font-weight:600; color:var(--color-success);">${totalDomains}</div>
2354
+ <div style="font-size:0.6rem;">domains</div>
2355
+ </div>
2356
+ <div style="text-align:center; padding:8px; background:var(--bg-card); border-radius:var(--radius);">
2357
+ <div style="font-size:0.85rem; font-weight:600; color:var(--color-info);">${totalPages.toLocaleString()}</div>
2358
+ <div style="font-size:0.6rem;">total pages</div>
2359
+ </div>
2360
+ <div style="text-align:center; padding:8px; background:var(--bg-card); border-radius:var(--radius);">
2361
+ <div style="font-size:0.85rem; font-weight:600; color:var(--accent-gold);">~${crawlMin + extractMin + analyzeMin} min</div>
2362
+ <div style="font-size:0.6rem;">full pipeline</div>
2363
+ </div>
2364
+ </div>
2365
+ <div style="font-size:0.62rem; color:var(--text-muted);">
2366
+ <i class="fa-solid fa-spider" style="color:var(--color-success); width:14px;"></i> Crawl: ~${crawlMin} min (${crawlSec}s/page, ${mode})
2367
+ &nbsp;&nbsp;
2368
+ <i class="fa-solid fa-brain" style="color:var(--accent-gold); width:14px;"></i> Extract: ~${extractMin} min (Solo)
2369
+ &nbsp;&nbsp;
2370
+ <i class="fa-solid fa-chart-column" style="color:var(--accent-gold); width:14px;"></i> Analyze: ~${analyzeMin} min (Solo)
2371
+ </div>
2372
+ ${totalPages > 500 ? '<div style="font-size:0.6rem; color:var(--color-warning); margin-top:4px;"><i class="fa-solid fa-triangle-exclamation"></i> Large crawl — consider lowering pages per domain or running overnight.</div>' : ''}
2373
+ `;
2374
+ }
2375
+
2376
+ // Hook pages-per-domain input
2377
+ document.getElementById('cfgPagesPerDomain')?.addEventListener('input', updateTimeEstimate);
2378
+ document.getElementById('cfgTargetUrl')?.addEventListener('input', updateTimeEstimate);
2379
+
2380
+ function getListValues(containerId) {
2381
+ const inputs = document.querySelectorAll('#' + containerId + ' .dyn-list-item input');
2382
+ return Array.from(inputs).map(i => i.value.trim()).filter(Boolean);
2383
+ }
2384
+
2385
+ window.saveConfig = async function() {
2386
+ const errEl = document.getElementById('configErrors');
2387
+ errEl.textContent = '';
2388
+
2389
+ const projectName = document.getElementById('cfgProjectName').value.trim();
2390
+ const targetUrl = document.getElementById('cfgTargetUrl').value.trim();
2391
+ const siteName = document.getElementById('cfgSiteName').value.trim();
2392
+ const industry = document.getElementById('cfgIndustry').value.trim();
2393
+ const audience = document.getElementById('cfgAudience').value.trim();
2394
+ const goal = document.getElementById('cfgGoal').value.trim();
2395
+ const pagesPerDomain = parseInt(document.getElementById('cfgPagesPerDomain').value) || 50;
2396
+
2397
+ // Validation
2398
+ const errors = [];
2399
+ if (!projectName) errors.push('Project name is required.');
2400
+ if (!targetUrl) errors.push('Target domain is required.');
2401
+ if (!siteName) errors.push('Site name is required.');
2402
+
2403
+ if (targetUrl && !targetUrl.match(/^https?:\/\/.+\..+|^[a-z0-9-]+\.[a-z]{2,}/i)) {
2404
+ errors.push('Target domain looks invalid.');
2405
+ }
2406
+
2407
+ if (errors.length > 0) {
2408
+ errEl.innerHTML = errors.map(e => '<div>' + e + '</div>').join('');
2409
+ return;
2410
+ }
2411
+
2412
+ const ownedUrls = getListValues('ownedList');
2413
+ const competitorUrls = getListValues('competitorList');
2414
+
2415
+ const body = {
2416
+ projectName,
2417
+ targetUrl,
2418
+ siteName,
2419
+ industry,
2420
+ audience,
2421
+ goal,
2422
+ competitors: competitorUrls.map(u => ({ url: u })),
2423
+ owned: ownedUrls.map(u => ({ url: u })),
2424
+ crawlMode: state.crawlMode,
2425
+ pagesPerDomain,
2426
+ ollamaHost: state.systemStatus?.ollama?.host || undefined,
2427
+ extractionModel: state.selectedExtraction || undefined,
2428
+ };
2429
+
2430
+ try {
2431
+ const res = await API.post('/api/setup/config', body);
2432
+ state.configSaved = true;
2433
+ state.projectSlug = slugify(projectName);
2434
+ goToStep(4);
2435
+ } catch (err) {
2436
+ errEl.innerHTML = '<div>Save failed: ' + err.message + '</div>';
2437
+ }
2438
+ };
2439
+
2440
+ // Init competitor rows
2441
+ function initStep3() {
2442
+ for (let i = 0; i < 3; i++) addCompetitorRow();
2443
+ }
2444
+
2445
+ // ── Step 4: Pipeline Test ─────────────────────────────────────────────
2446
+ const TEST_MAP = {
2447
+ ollama: { card: 'testOllama', detail: 'testOllamaDetail', latency: 'testOllamaLatency' },
2448
+ 'api-key': { card: 'testApiKey', detail: 'testApiKeyDetail', latency: 'testApiKeyLatency' },
2449
+ crawl: { card: 'testCrawl', detail: 'testCrawlDetail', latency: 'testCrawlLatency' },
2450
+ extraction: { card: 'testExtraction', detail: 'testExtractionDetail', latency: 'testExtractionLatency' },
2451
+ };
2452
+
2453
+ function resetTests() {
2454
+ for (const [, ids] of Object.entries(TEST_MAP)) {
2455
+ const card = document.getElementById(ids.card);
2456
+ card.className = 'test-card';
2457
+ document.getElementById(ids.detail).textContent = 'Pending';
2458
+ document.getElementById(ids.latency).textContent = '';
2459
+ }
2460
+ }
2461
+
2462
+ function setTestStatus(step, status, detail, latencyMs) {
2463
+ const ids = TEST_MAP[step];
2464
+ if (!ids) return;
2465
+ const card = document.getElementById(ids.card);
2466
+ card.className = 'test-card ' + status;
2467
+ document.getElementById(ids.detail).textContent = detail;
2468
+ if (latencyMs !== undefined && latencyMs !== null) {
2469
+ document.getElementById(ids.latency).textContent = latencyMs + 'ms';
2470
+ }
2471
+ // Update icon
2472
+ const iconEl = card.querySelector('.test-icon i');
2473
+ if (status === 'running') iconEl.className = 'fa-solid fa-spinner fa-spin-pulse';
2474
+ else if (status === 'pass') iconEl.className = 'fa-solid fa-circle-check';
2475
+ else if (status === 'fail') iconEl.className = 'fa-solid fa-circle-xmark';
2476
+ else if (status === 'skip') iconEl.className = 'fa-solid fa-circle-minus';
2477
+ // Restore default icon for pending
2478
+ else {
2479
+ const defaults = { ollama: 'fa-server', 'api-key': 'fa-key', crawl: 'fa-spider', extraction: 'fa-wand-magic-sparkles' };
2480
+ iconEl.className = 'fa-solid ' + (defaults[step] || 'fa-circle');
2481
+ }
2482
+ }
2483
+
2484
+ window.runTests = async function() {
2485
+ const btn = document.getElementById('runTestsBtn');
2486
+ btn.disabled = true;
2487
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin-pulse"></i> Running...';
2488
+ resetTests();
2489
+
2490
+ const targetUrl = document.getElementById('cfgTargetUrl').value.trim();
2491
+ const ollamaHost = state.systemStatus?.ollama?.host || '';
2492
+ const ollamaModel = state.selectedExtraction || '';
2493
+
2494
+ // Determine API provider and key
2495
+ let apiProvider = state.savedApiProvider || '';
2496
+ let apiKey = state.savedApiKey || '';
2497
+
2498
+ // If no saved key, check if we have a configured model
2499
+ if (!apiKey && state.modelData) {
2500
+ const m = state.modelData.allAnalysis.find(x => x.id === state.selectedAnalysis);
2501
+ if (m && m.configured) {
2502
+ apiProvider = m.provider;
2503
+ // The server will use the .env key
2504
+ apiKey = '__from_env__';
2505
+ }
2506
+ }
2507
+
2508
+ const body = { ollamaHost, ollamaModel, apiProvider, apiKey, targetUrl };
2509
+
2510
+ try {
2511
+ let summaryResult = null;
2512
+ await API.sse('/api/setup/test-pipeline', body, (ev) => {
2513
+ if (ev.step === 'summary') {
2514
+ summaryResult = ev;
2515
+ return;
2516
+ }
2517
+ setTestStatus(ev.step, ev.status, ev.detail, ev.latencyMs);
2518
+ });
2519
+
2520
+ if (summaryResult) {
2521
+ state.testsPassed = summaryResult.status === 'pass' || summaryResult.status === 'partial';
2522
+ document.getElementById('step5Next').disabled = false;
2523
+ }
2524
+ } catch (err) {
2525
+ setTestStatus('ollama', 'fail', 'Connection failed: ' + err.message);
2526
+ } finally {
2527
+ btn.disabled = false;
2528
+ btn.innerHTML = '<i class="fa-solid fa-play"></i> Run All Tests';
2529
+ }
2530
+ };
2531
+
2532
+ // ── Step 4: GSC / Data Sources ──────────────────────────────────────────
2533
+
2534
+ async function checkGscStatus() {
2535
+ const statusBox = document.getElementById('gscStatus');
2536
+ const methodSelect = document.getElementById('gscMethodSelect');
2537
+ const csvSection = document.getElementById('gscCsvSection');
2538
+
2539
+ statusBox.innerHTML = `
2540
+ <div class="gsc-status-icon"><i class="fa-solid fa-spinner fa-spin"></i></div>
2541
+ <div class="gsc-status-text">Checking for existing GSC data...</div>
2542
+ `;
2543
+
2544
+ try {
2545
+ const project = state.projectSlug || '';
2546
+ const gsc = await API.get('/api/setup/gsc?project=' + encodeURIComponent(project));
2547
+
2548
+ if (gsc.hasData) {
2549
+ statusBox.className = 'gsc-status-box ok';
2550
+ statusBox.innerHTML = `
2551
+ <div class="gsc-status-icon"><i class="fa-solid fa-circle-check" style="color:var(--color-success);"></i></div>
2552
+ <div>
2553
+ <div class="gsc-status-text">GSC data found</div>
2554
+ <div class="gsc-status-detail">
2555
+ Folder: <strong>${gsc.folder}</strong> &mdash;
2556
+ ${gsc.found.length} files: ${gsc.found.join(', ')}
2557
+ ${gsc.missing.length ? '<br>Missing: ' + gsc.missing.join(', ') : ''}
2558
+ </div>
2559
+ </div>
2560
+ `;
2561
+ state.gscUploaded = true;
2562
+ } else if (gsc.allFolders && gsc.allFolders.length > 0) {
2563
+ statusBox.className = 'gsc-status-box warn';
2564
+ statusBox.innerHTML = `
2565
+ <div class="gsc-status-icon"><i class="fa-solid fa-triangle-exclamation" style="color:var(--color-warning);"></i></div>
2566
+ <div>
2567
+ <div class="gsc-status-text">GSC folders found but no matching data for "${project || 'any project'}"</div>
2568
+ <div class="gsc-status-detail">Available folders: ${gsc.allFolders.join(', ')}</div>
2569
+ </div>
2570
+ `;
2571
+ } else {
2572
+ statusBox.className = 'gsc-status-box';
2573
+ statusBox.innerHTML = `
2574
+ <div class="gsc-status-icon"><i class="fa-solid fa-circle-minus" style="color:var(--text-muted);"></i></div>
2575
+ <div>
2576
+ <div class="gsc-status-text">No GSC data found</div>
2577
+ <div class="gsc-status-detail">Upload your Google Search Console export to enable ranking insights</div>
2578
+ </div>
2579
+ `;
2580
+ }
2581
+ } catch (err) {
2582
+ statusBox.className = 'gsc-status-box';
2583
+ statusBox.innerHTML = `
2584
+ <div class="gsc-status-icon"><i class="fa-solid fa-circle-xmark" style="color:var(--color-danger);"></i></div>
2585
+ <div class="gsc-status-text">Could not check GSC status: ${err.message}</div>
2586
+ `;
2587
+ }
2588
+
2589
+ // Always show method selection and CSV section
2590
+ methodSelect.style.display = 'block';
2591
+ csvSection.style.display = 'block';
2592
+ }
2593
+
2594
+ window.selectGscMethod = function(method) {
2595
+ state.gscMethod = method;
2596
+ document.querySelectorAll('.gsc-method-card').forEach(c => c.classList.remove('active'));
2597
+ event.currentTarget.classList.add('active');
2598
+ };
2599
+
2600
+ // File handling
2601
+ const VALID_GSC_FILES = ['Chart.csv', 'Queries.csv', 'Pages.csv', 'Countries.csv', 'Devices.csv', 'Search appearance.csv', 'Filters.csv'];
2602
+
2603
+ window.handleGscDrop = function(event) {
2604
+ event.preventDefault();
2605
+ event.currentTarget.classList.remove('dragover');
2606
+ const files = event.dataTransfer.files;
2607
+ handleGscFiles(files);
2608
+ };
2609
+
2610
+ window.handleGscFiles = async function(fileList) {
2611
+ const fileListEl = document.getElementById('gscFileList');
2612
+ const statusEl = document.getElementById('gscUploadStatus');
2613
+ state.gscFiles = [];
2614
+
2615
+ fileListEl.innerHTML = '';
2616
+
2617
+ for (const file of fileList) {
2618
+ if (!file.name.endsWith('.csv')) {
2619
+ fileListEl.innerHTML += `<div class="gsc-file-chip bad"><i class="fa-solid fa-xmark"></i> ${file.name} (not CSV)</div>`;
2620
+ continue;
2621
+ }
2622
+
2623
+ const isExpected = VALID_GSC_FILES.includes(file.name);
2624
+ const content = await readFileAsBase64(file);
2625
+ state.gscFiles.push({ name: file.name, content });
2626
+
2627
+ fileListEl.innerHTML += `<div class="gsc-file-chip ${isExpected ? 'ok' : ''}">
2628
+ <i class="fa-solid fa-${isExpected ? 'check' : 'file'}"></i> ${file.name}
2629
+ </div>`;
2630
+ }
2631
+
2632
+ if (state.gscFiles.length > 0) {
2633
+ statusEl.innerHTML = `<button class="btn btn-gold btn-sm" onclick="uploadGscFiles()">
2634
+ <i class="fa-solid fa-cloud-arrow-up"></i> Upload ${state.gscFiles.length} file${state.gscFiles.length > 1 ? 's' : ''}
2635
+ </button>`;
2636
+ }
2637
+ };
2638
+
2639
+ function readFileAsBase64(file) {
2640
+ return new Promise((resolve, reject) => {
2641
+ const reader = new FileReader();
2642
+ reader.onload = () => {
2643
+ // Strip the data:...;base64, prefix
2644
+ const base64 = reader.result.split(',')[1];
2645
+ resolve(base64);
2646
+ };
2647
+ reader.onerror = reject;
2648
+ reader.readAsDataURL(file);
2649
+ });
2650
+ }
2651
+
2652
+ window.uploadGscFiles = async function() {
2653
+ const statusEl = document.getElementById('gscUploadStatus');
2654
+ const project = state.projectSlug || 'default';
2655
+
2656
+ statusEl.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading...';
2657
+
2658
+ try {
2659
+ const result = await API.post('/api/setup/gsc/upload', {
2660
+ project,
2661
+ files: state.gscFiles,
2662
+ });
2663
+
2664
+ if (result.success) {
2665
+ statusEl.innerHTML = `<div style="color:var(--color-success); font-size: 0.78rem;">
2666
+ <i class="fa-solid fa-circle-check"></i> Uploaded ${result.saved.length} files to gsc/${result.folder}/
2667
+ </div>`;
2668
+ state.gscUploaded = true;
2669
+ // Refresh status box
2670
+ checkGscStatus();
2671
+ } else {
2672
+ statusEl.innerHTML = `<div style="color:var(--color-danger); font-size: 0.78rem;">
2673
+ Upload failed: ${result.error || 'Unknown error'}
2674
+ </div>`;
2675
+ }
2676
+ } catch (err) {
2677
+ statusEl.innerHTML = `<div style="color:var(--color-danger); font-size: 0.78rem;">
2678
+ Upload failed: ${err.message}
2679
+ </div>`;
2680
+ }
2681
+ };
2682
+
2683
+ // ── Step 6: Done ──────────────────────────────────────────────────────
2684
+ window.buildSummaryAndFinish = function() {
2685
+ const table = document.getElementById('summaryTable');
2686
+ const slug = state.projectSlug || 'my-project';
2687
+
2688
+ const rows = [
2689
+ ['Project', slug],
2690
+ ['Target Domain', document.getElementById('cfgTargetUrl').value || '-'],
2691
+ ['Extraction', state.selectedExtraction || 'none'],
2692
+ ['Analysis', state.selectedAnalysis || 'none'],
2693
+ ['Crawl Mode', state.crawlMode],
2694
+ ['Competitor Domains', getListValues('competitorList').length + ' configured'],
2695
+ ['Search Console', state.gscUploaded ? 'Connected' : 'Not configured'],
2696
+ ];
2697
+
2698
+ table.innerHTML = rows.map(r =>
2699
+ `<tr><td>${r[0]}</td><td>${r[1]}</td></tr>`
2700
+ ).join('');
2701
+
2702
+ // Populate CLI block — tier-aware
2703
+ const block = document.getElementById('cliBlock');
2704
+ const copyBtn = block.querySelector('.copy-btn').outerHTML;
2705
+
2706
+ if (state.tier === 'solo' || state.tier === 'agency') {
2707
+ block.innerHTML = copyBtn +
2708
+ `<span class="comment"># Run your first crawl</span>\n` +
2709
+ `<span class="prompt">$</span> seo-intel crawl ${slug}\n\n` +
2710
+ `<span class="comment"># AI gap analysis</span>\n` +
2711
+ `<span class="prompt">$</span> seo-intel analyze ${slug}\n\n` +
2712
+ `<span class="comment"># Open dashboard</span>\n` +
2713
+ `<span class="prompt">$</span> seo-intel serve`;
2714
+ } else {
2715
+ block.innerHTML = copyBtn +
2716
+ `<span class="comment"># Run your first crawl</span>\n` +
2717
+ `<span class="prompt">$</span> seo-intel crawl ${slug}\n\n` +
2718
+ `<span class="comment"># Browse your data</span>\n` +
2719
+ `<span class="prompt">$</span> seo-intel serve\n\n` +
2720
+ `<span class="comment"># Upgrade to Solo for AI analysis + dashboards</span>\n` +
2721
+ `<span class="comment"># ukkometa.fi/seo-intel</span>`;
2722
+ }
2723
+
2724
+ goToStep(6);
2725
+ };
2726
+
2727
+ window.copyCli = function() {
2728
+ const block = document.getElementById('cliBlock');
2729
+ const text = block.innerText
2730
+ .replace(/\n\s*\n/g, '\n')
2731
+ .split('\n')
2732
+ .filter(l => l.trim().startsWith('$') || l.trim().startsWith('#'))
2733
+ .map(l => l.trim().replace(/^\$\s*/, ''))
2734
+ .join('\n');
2735
+ navigator.clipboard.writeText(text).then(() => {
2736
+ const btn = block.querySelector('.copy-btn');
2737
+ btn.innerHTML = '<i class="fa-solid fa-check"></i>';
2738
+ setTimeout(() => { btn.innerHTML = '<i class="fa-regular fa-copy"></i>'; }, 1500);
2739
+ });
2740
+ };
2741
+
2742
+ // ── OpenClaw Agent Setup ──────────────────────────────────────────────
2743
+
2744
+ let agentHistory = [];
2745
+ let agentBusy = false;
2746
+
2747
+ window.startAgentSetup = function() {
2748
+ // Hide wizard steps, show agent panel
2749
+ document.querySelector('.wizard-body').style.display = 'none';
2750
+ document.querySelector('.step-indicator').style.display = 'none';
2751
+ document.getElementById('agentPanel').classList.add('active');
2752
+
2753
+ agentHistory = [];
2754
+ const msgs = document.getElementById('agentMessages');
2755
+ msgs.innerHTML = '<div class="agent-msg system">Starting agent-powered setup...</div>';
2756
+
2757
+ // Send initial context to agent
2758
+ sendToAgent('I just ran the system check. Guide me through setting up SEO Intel step by step.');
2759
+ };
2760
+
2761
+ window.exitAgentSetup = function() {
2762
+ document.querySelector('.wizard-body').style.display = '';
2763
+ document.querySelector('.step-indicator').style.display = '';
2764
+ document.getElementById('agentPanel').classList.remove('active');
2765
+ };
2766
+
2767
+ window.continueManualSetup = function() {
2768
+ // Just hide the banner and let user proceed with wizard
2769
+ document.getElementById('openclawBanner').style.display = 'none';
2770
+ };
2771
+
2772
+ window.sendAgentMessage = function() {
2773
+ const input = document.getElementById('agentInput');
2774
+ const text = input.value.trim();
2775
+ if (!text || agentBusy) return;
2776
+ input.value = '';
2777
+
2778
+ addAgentMsg('user', text);
2779
+ sendToAgent(text);
2780
+ };
2781
+
2782
+ function addAgentMsg(role, text) {
2783
+ const msgs = document.getElementById('agentMessages');
2784
+ const div = document.createElement('div');
2785
+ div.className = `agent-msg ${role}`;
2786
+
2787
+ // Simple markdown-ish rendering
2788
+ let html = text
2789
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
2790
+ .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
2791
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
2792
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
2793
+
2794
+ div.innerHTML = html;
2795
+ msgs.appendChild(div);
2796
+ msgs.scrollTop = msgs.scrollHeight;
2797
+ }
2798
+
2799
+ async function sendToAgent(message) {
2800
+ agentBusy = true;
2801
+ const typing = document.getElementById('agentTyping');
2802
+ typing.classList.add('active');
2803
+
2804
+ agentHistory.push({ role: 'user', content: message });
2805
+
2806
+ try {
2807
+ const res = await API.post('/api/setup/agent/chat', {
2808
+ message,
2809
+ history: agentHistory.slice(0, -1), // all except current
2810
+ systemCheck: state.systemStatus,
2811
+ });
2812
+
2813
+ if (res.error) {
2814
+ addAgentMsg('system', `⚠ ${res.error}${res.hint ? ` — ${res.hint}` : ''}`);
2815
+ } else if (res.response) {
2816
+ agentHistory.push({ role: 'assistant', content: res.response });
2817
+ addAgentMsg('assistant', res.response);
2818
+ }
2819
+ } catch (err) {
2820
+ addAgentMsg('system', `Connection error: ${err.message}`);
2821
+ } finally {
2822
+ agentBusy = false;
2823
+ typing.classList.remove('active');
2824
+ document.getElementById('agentInput').focus();
2825
+ }
2826
+ }
2827
+
2828
+ // ── License Management ───────────────────────────────────────────────
2829
+
2830
+ async function checkLicenseStatus() {
2831
+ // Debug: ?tier=free forces free tier view
2832
+ const urlParams = new URLSearchParams(window.location.search);
2833
+ const debugTier = urlParams.get('tier');
2834
+
2835
+ try {
2836
+ const data = await API.get('/api/license-status');
2837
+ state.tier = debugTier || data.tier || 'free';
2838
+ if (debugTier) {
2839
+ updateLicenseUI({ tier: debugTier, valid: debugTier !== 'free', key: null });
2840
+ } else {
2841
+ updateLicenseUI(data);
2842
+ }
2843
+ updateTierUI();
2844
+ } catch {
2845
+ state.tier = debugTier || 'free';
2846
+ updateLicenseUI({ tier: state.tier, valid: false, key: null });
2847
+ updateTierUI();
2848
+ }
2849
+ }
2850
+
2851
+ function updateLicenseUI(data) {
2852
+ const freeEl = document.getElementById('licenseFree');
2853
+ const soloEl = document.getElementById('licenseSolo');
2854
+ const keyDisplay = document.getElementById('licenseKeyDisplay');
2855
+
2856
+ if (data.tier !== 'free' && data.valid) {
2857
+ freeEl.style.display = 'none';
2858
+ soloEl.style.display = 'block';
2859
+ keyDisplay.textContent = 'License: ' + (data.key || '***') + ' ✓';
2860
+ } else {
2861
+ freeEl.style.display = 'block';
2862
+ soloEl.style.display = 'none';
2863
+ }
2864
+ }
2865
+
2866
+ function updateTierUI() {
2867
+ const step2 = document.getElementById('step2');
2868
+ const unifiedUpgrade = document.getElementById('unifiedUpgrade');
2869
+
2870
+ if (state.tier === 'free') {
2871
+ step2.classList.add('is-free');
2872
+ document.documentElement.classList.add('is-free');
2873
+ if (unifiedUpgrade) unifiedUpgrade.style.display = 'block';
2874
+ } else {
2875
+ step2.classList.remove('is-free');
2876
+ document.documentElement.classList.remove('is-free');
2877
+ if (unifiedUpgrade) unifiedUpgrade.style.display = 'none';
2878
+ }
2879
+ }
2880
+
2881
+ window.saveLicenseKey = async function() {
2882
+ const input = document.getElementById('licenseKeyInput');
2883
+ const btn = document.getElementById('activateBtn');
2884
+ const key = input.value.trim();
2885
+ if (!key) return;
2886
+
2887
+ btn.textContent = 'Checking...';
2888
+ btn.disabled = true;
2889
+
2890
+ try {
2891
+ const data = await API.post('/api/save-license', { key });
2892
+ if (data.ok && data.valid) {
2893
+ state.tier = data.tier;
2894
+ updateLicenseUI({ tier: data.tier, valid: true, key: key.slice(0, 7) + '...' + key.slice(-4) });
2895
+ updateTierUI();
2896
+ } else {
2897
+ input.style.borderColor = 'var(--color-danger)';
2898
+ btn.textContent = 'Invalid key';
2899
+ setTimeout(() => { btn.textContent = 'Activate →'; btn.disabled = false; input.style.borderColor = ''; }, 2500);
2900
+ return;
2901
+ }
2902
+ } catch {
2903
+ btn.textContent = 'Error';
2904
+ setTimeout(() => { btn.textContent = 'Activate →'; btn.disabled = false; }, 2000);
2905
+ return;
2906
+ }
2907
+
2908
+ btn.textContent = '✓ Activated';
2909
+ btn.disabled = false;
2910
+ };
2911
+
2912
+ // ── Init ──────────────────────────────────────────────────────────────
2913
+ initStep3();
2914
+ runSystemCheck();
2915
+ checkLicenseStatus();
2916
+
2917
+ })();
2918
+ </script>
2919
+ </body>
2920
+ </html>