@runcontext/ui 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/setup.js CHANGED
@@ -1,31 +1,134 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ var STORAGE_KEY = 'runcontext_wizard_state';
5
+ var STEP_LABELS = ['Connect', 'Define', 'Scaffold', 'Checkpoint', 'Enrich', 'Serve'];
6
+
7
+ // ---- State persistence ----
8
+
9
+ function loadSavedState() {
10
+ try {
11
+ var saved = sessionStorage.getItem(STORAGE_KEY);
12
+ if (saved) return JSON.parse(saved);
13
+ } catch (e) { /* ignore */ }
14
+ return null;
15
+ }
16
+
17
+ function saveState() {
18
+ try {
19
+ var toSave = {
20
+ step: state.step,
21
+ brief: state.brief,
22
+ sources: state.sources,
23
+ pipelineId: state.pipelineId,
24
+ };
25
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
26
+ } catch (e) { /* ignore */ }
27
+ }
28
+
29
+ var saved = loadSavedState();
4
30
  var state = {
5
- step: 1,
6
- brief: {
31
+ step: saved ? saved.step : 1,
32
+ brief: saved ? saved.brief : {
7
33
  product_name: '',
8
34
  description: '',
9
35
  owner: { name: '', team: '', email: '' },
10
36
  sensitivity: 'internal',
11
37
  docs: [],
12
38
  },
13
- sources: [],
14
- pipelineId: null,
39
+ sources: saved ? saved.sources : [],
40
+ pipelineId: saved ? saved.pipelineId : null,
15
41
  pollTimer: null,
42
+ mcpPollTimer: null,
16
43
  };
17
44
 
45
+ // ---- WebSocket Client ----
46
+ var ws = null;
47
+ var wsSessionId = null;
48
+
49
+ function connectWebSocket(sessionId) {
50
+ wsSessionId = sessionId;
51
+ var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
52
+ ws = new WebSocket(protocol + '//' + location.host + '/ws?session=' + encodeURIComponent(sessionId) + '&role=wizard');
53
+
54
+ ws.onmessage = function (evt) {
55
+ try {
56
+ var event = JSON.parse(evt.data);
57
+ handleWsEvent(event);
58
+ } catch (e) { /* ignore */ }
59
+ };
60
+
61
+ ws.onclose = function () {
62
+ setTimeout(function () {
63
+ if (wsSessionId) connectWebSocket(wsSessionId);
64
+ }, 2000);
65
+ };
66
+
67
+ ws.onerror = function () { /* ignore, onclose handles reconnect */ };
68
+ }
69
+
70
+ function sendWsEvent(type, payload) {
71
+ if (ws && ws.readyState === WebSocket.OPEN) {
72
+ ws.send(JSON.stringify({ type: type, sessionId: wsSessionId, payload: payload || {} }));
73
+ }
74
+ }
75
+
76
+ function handleWsEvent(event) {
77
+ switch (event.type) {
78
+ case 'setup:step':
79
+ if (event.payload && event.payload.step) {
80
+ goToStep(event.payload.step);
81
+ }
82
+ break;
83
+ case 'setup:field':
84
+ if (event.payload && event.payload.fieldId) {
85
+ var input = document.getElementById(event.payload.fieldId);
86
+ if (input) {
87
+ input.value = event.payload.value || '';
88
+ input.dispatchEvent(new Event('input'));
89
+ }
90
+ }
91
+ break;
92
+ case 'pipeline:stage':
93
+ updateStageFromWs(event.payload || {});
94
+ break;
95
+ case 'enrich:progress':
96
+ updateEnrichProgress(event.payload || {});
97
+ break;
98
+ case 'enrich:log':
99
+ appendEnrichLog(event.payload || {});
100
+ break;
101
+ case 'tier:update':
102
+ if (event.payload && event.payload.tier) {
103
+ var badge = document.getElementById('tier-badge');
104
+ if (badge) badge.textContent = event.payload.tier;
105
+ }
106
+ break;
107
+ }
108
+ }
109
+
110
+ function updateStageFromWs(payload) {
111
+ // Update stage dots in the scaffold step
112
+ var stageEl = document.querySelector('[data-stage="' + payload.stage + '"]');
113
+ if (!stageEl) return;
114
+ var dot = stageEl.querySelector('.stage-dot');
115
+ if (dot) {
116
+ dot.className = 'stage-dot';
117
+ if (payload.status === 'running') dot.className += ' running';
118
+ else if (payload.status === 'done') dot.className += ' done';
119
+ else if (payload.status === 'error') dot.className += ' error';
120
+ }
121
+ var summary = stageEl.querySelector('.stage-summary');
122
+ if (summary && payload.summary) {
123
+ summary.textContent = payload.summary;
124
+ }
125
+ }
126
+
18
127
  // ---- Helpers ----
19
128
 
20
129
  function $(sel) { return document.querySelector(sel); }
21
130
  function $$(sel) { return document.querySelectorAll(sel); }
22
131
 
23
- function esc(s) {
24
- var d = document.createElement('div');
25
- d.textContent = s;
26
- return d.innerHTML;
27
- }
28
-
29
132
  function showError(fieldId, msg) {
30
133
  var field = document.getElementById(fieldId);
31
134
  if (!field) return;
@@ -62,7 +165,7 @@
62
165
  return ct.includes('json') ? res.json() : res.text();
63
166
  }
64
167
 
65
- // ---- DOM builder helpers (avoid innerHTML for security) ----
168
+ // ---- DOM builder helper (avoid innerHTML for security) ----
66
169
 
67
170
  function createElement(tag, attrs, children) {
68
171
  var el = document.createElement(tag);
@@ -70,6 +173,8 @@
70
173
  Object.keys(attrs).forEach(function (key) {
71
174
  if (key === 'className') el.className = attrs[key];
72
175
  else if (key === 'textContent') el.textContent = attrs[key];
176
+ else if (key === 'htmlFor') el.htmlFor = attrs[key];
177
+ else if (key === 'type') el.type = attrs[key];
73
178
  else el.setAttribute(key, attrs[key]);
74
179
  });
75
180
  }
@@ -85,328 +190,1217 @@
85
190
  return el;
86
191
  }
87
192
 
193
+ // ---- Breadcrumb Stepper ----
194
+
195
+ function renderStepper() {
196
+ var container = document.getElementById('stepper');
197
+ if (!container) return;
198
+ container.textContent = '';
199
+
200
+ STEP_LABELS.forEach(function (label, i) {
201
+ var stepNum = i + 1;
202
+ var span = document.createElement('span');
203
+ span.textContent = label;
204
+
205
+ if (stepNum < state.step) {
206
+ span.className = 'step-completed';
207
+ span.addEventListener('click', function () {
208
+ goToStep(stepNum);
209
+ });
210
+ } else if (stepNum === state.step) {
211
+ span.className = 'step-active';
212
+ } else {
213
+ span.className = 'step-future';
214
+ }
215
+
216
+ container.appendChild(span);
217
+
218
+ if (stepNum < STEP_LABELS.length) {
219
+ var sep = document.createElement('span');
220
+ sep.className = 'step-separator';
221
+ sep.textContent = '>';
222
+ container.appendChild(sep);
223
+ }
224
+ });
225
+ }
226
+
88
227
  // ---- Navigation ----
89
228
 
90
229
  function goToStep(n) {
91
- if (n < 1 || n > 5) return;
92
- for (var i = 1; i < n; i++) {
93
- var ps = $('.progress-step[data-step="' + i + '"]');
94
- if (ps) { ps.classList.remove('active'); ps.classList.add('completed'); }
95
- }
96
- var active = $('.progress-step[data-step="' + n + '"]');
97
- if (active) { active.classList.remove('completed'); active.classList.add('active'); }
98
- for (var j = n + 1; j <= 5; j++) {
99
- var fut = $('.progress-step[data-step="' + j + '"]');
100
- if (fut) { fut.classList.remove('active', 'completed'); }
101
- }
102
- $$('.step').forEach(function (el) { el.classList.remove('active'); });
103
- var panel = $('#step-' + n);
104
- if (panel) panel.classList.add('active');
230
+ if (n < 1 || n > 6) return;
231
+
232
+ // Clear any running pipeline poll timer when navigating away
233
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
234
+
235
+ var content = document.getElementById('wizard-content');
236
+ if (content) content.textContent = '';
105
237
 
106
238
  state.step = n;
239
+ saveState();
107
240
 
108
- if (n === 3) loadSources();
109
- if (n === 4) renderReview();
110
- if (n === 5) startBuild();
241
+ renderStepper();
242
+
243
+ switch (n) {
244
+ case 1: renderConnectStep(); break;
245
+ case 2: renderDefineStep(); break;
246
+ case 3: renderScaffoldStep(); break;
247
+ case 4: renderCheckpointStep(); break;
248
+ case 5: renderEnrichStep(); break;
249
+ case 6: renderServeStep(); break;
250
+ }
111
251
  }
112
252
 
113
253
  function validateStep(n) {
114
254
  clearErrors();
115
- if (n === 1) {
116
- var name = $('#product-name').value.trim();
117
- if (!name) { showError('product-name', 'Product name is required'); return false; }
118
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) { showError('product-name', 'Only letters, numbers, dashes, underscores'); return false; }
119
- state.brief.product_name = name;
120
- state.brief.description = $('#description').value.trim();
121
- return true;
255
+ // Step-specific validation will be added as each step is implemented
256
+ return true;
257
+ }
258
+
259
+ // ---- Step action buttons helper ----
260
+
261
+ function createStepActions(showBack, showNext, nextLabel) {
262
+ var actions = createElement('div', { className: 'step-actions' });
263
+ if (showBack) {
264
+ var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
265
+ backBtn.addEventListener('click', function () {
266
+ goToStep(state.step - 1);
267
+ });
268
+ actions.appendChild(backBtn);
269
+ } else {
270
+ actions.appendChild(createElement('span'));
122
271
  }
123
- if (n === 2) {
124
- state.brief.owner.name = $('#owner-name').value.trim();
125
- state.brief.owner.team = $('#owner-team').value.trim();
126
- state.brief.owner.email = $('#owner-email').value.trim();
127
- return true;
272
+ if (showNext) {
273
+ var nextBtn = createElement('button', { className: 'btn btn-primary', textContent: nextLabel || 'Next' });
274
+ nextBtn.addEventListener('click', function () {
275
+ if (validateStep(state.step)) goToStep(state.step + 1);
276
+ });
277
+ actions.appendChild(nextBtn);
128
278
  }
129
- return true;
279
+ return actions;
130
280
  }
131
281
 
132
- // ---- Step 3: Sources & Upload ----
282
+ // ---- Step 1: Connect ----
133
283
 
134
- async function loadSources() {
135
- var container = $('#sources-list');
136
- container.textContent = '';
137
- container.appendChild(createElement('p', { className: 'muted', textContent: 'Detecting data sources...' }));
138
- try {
139
- var data = await api('GET', '/api/sources');
140
- state.sources = data.sources || data || [];
284
+ function renderConnectStep() {
285
+ var content = document.getElementById('wizard-content');
286
+ if (!content) return;
287
+
288
+ var card = createElement('div', { className: 'card' });
289
+
290
+ card.appendChild(createElement('h2', { className: 'connect-heading', textContent: 'Connect Your Database' }));
291
+ card.appendChild(createElement('p', { className: 'connect-subheading', textContent: 'RunContext connects directly to your database via OAuth \u2014 your credentials never pass through the AI agent.' }));
292
+
293
+ // Detected databases hint (from MCP/IDE configs)
294
+ var detectedHint = createElement('div', { id: 'connect-detected-hint' });
295
+ card.appendChild(detectedHint);
296
+
297
+ // OAuth result area — positioned right after detected hint so db cards appear here
298
+ var oauthResult = createElement('div', { id: 'connect-oauth-result' });
299
+ card.appendChild(oauthResult);
300
+
301
+ // Platform picker grid (populated after fetching providers)
302
+ var platformGrid = createElement('div', { className: 'platform-grid', id: 'connect-platforms' });
303
+ platformGrid.appendChild(createElement('p', { className: 'muted', textContent: 'Loading providers\u2026' }));
304
+ card.appendChild(platformGrid);
305
+
306
+ // Manual connection string
307
+ var manual = createElement('div', { className: 'manual-connect' });
308
+ manual.appendChild(createElement('label', { className: 'label-uppercase', textContent: 'Manual Connection' }));
309
+ var manualRow = createElement('div', { className: 'manual-connect-row' });
310
+ var connInput = createElement('input', {
311
+ className: 'input',
312
+ id: 'connect-url',
313
+ type: 'text',
314
+ placeholder: 'postgres://user:pass@host:5432/dbname',
315
+ });
316
+ manualRow.appendChild(connInput);
317
+ var connBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Connect' });
318
+ manualRow.appendChild(connBtn);
319
+ manual.appendChild(manualRow);
320
+ card.appendChild(manual);
321
+
322
+ content.appendChild(card);
323
+
324
+ // --- Detect sources first, then render providers (detected ones get highlighted) ---
325
+ fetchDetectedSources(detectedHint, platformGrid, oauthResult).then(function () {
326
+ fetchAuthProviders(platformGrid, oauthResult);
327
+ });
328
+
329
+ // --- Manual connect handler ---
330
+ connBtn.addEventListener('click', async function () {
331
+ var url = connInput.value.trim();
332
+ if (!url) return;
333
+ connBtn.textContent = 'Connecting\u2026';
334
+ connBtn.disabled = true;
335
+ try {
336
+ var result = await api('POST', '/api/sources', { connection: url });
337
+ var src = result.source || result;
338
+ state.sources = state.sources || [];
339
+ state.sources.push(src);
340
+ saveState();
341
+ updateDbStatus(src);
342
+ goToStep(2);
343
+ } catch (e) {
344
+ connBtn.textContent = 'Connect';
345
+ connBtn.disabled = false;
346
+ var errP = manual.querySelector('.field-error');
347
+ if (errP) errP.remove();
348
+ manual.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Connection failed' }));
349
+ }
350
+ });
351
+ }
352
+
353
+ /** Map adapter type to the provider ID for OAuth */
354
+ var ADAPTER_TO_PROVIDER = {
355
+ postgres: 'neon', // default; detected sources refine below
356
+ mysql: 'planetscale',
357
+ duckdb: null, // local file, no OAuth needed
358
+ sqlite: null,
359
+ snowflake: 'snowflake',
360
+ bigquery: 'gcp',
361
+ clickhouse: 'clickhouse',
362
+ databricks: 'databricks',
363
+ mssql: 'azure-sql',
364
+ mongodb: 'mongodb',
365
+ };
366
+
367
+ /** Refine provider from origin hint (e.g. "mcp:claude-code/neon" → neon) */
368
+ function providerFromOrigin(origin, adapter) {
369
+ if (!origin) return ADAPTER_TO_PROVIDER[adapter] || null;
370
+ var o = origin.toLowerCase();
371
+ if (o.includes('neon')) return 'neon';
372
+ if (o.includes('supabase')) return 'supabase';
373
+ if (o.includes('aws') || o.includes('rds')) return 'aws-rds';
374
+ if (o.includes('azure')) return 'azure-sql';
375
+ if (o.includes('gcp') || o.includes('bigquery')) return 'gcp';
376
+ if (o.includes('planetscale')) return 'planetscale';
377
+ if (o.includes('cockroach')) return 'cockroachdb';
378
+ if (o.includes('snowflake')) return 'snowflake';
379
+ if (o.includes('clickhouse')) return 'clickhouse';
380
+ if (o.includes('databricks')) return 'databricks';
381
+ if (o.includes('mongodb') || o.includes('atlas')) return 'mongodb';
382
+ if (o.includes('motherduck') || o.includes('duckdb')) return null;
383
+ return ADAPTER_TO_PROVIDER[adapter] || null;
384
+ }
385
+
386
+ function fetchDetectedSources(container, platformGrid, oauthResult) {
387
+ return api('GET', '/api/sources').then(function (data) {
388
+ var sources = data.sources || data || [];
141
389
  container.textContent = '';
142
- if (state.sources.length === 0) {
143
- container.appendChild(createElement('p', { className: 'muted', textContent: 'No data sources detected in this directory.' }));
390
+ if (sources.length === 0) {
391
+ updateDbStatus(null);
144
392
  return;
145
393
  }
146
- state.sources.forEach(function (src) {
147
- var card = createElement('div', { className: 'source-card' }, [
148
- createElement('span', { className: 'source-name', textContent: src.name || src.type }),
149
- createElement('span', { className: 'source-type', textContent: src.type || '' }),
150
- createElement('span', { className: 'source-status detected', textContent: 'Detected' }),
394
+
395
+ // Group detected sources by provider
396
+ var byProvider = {};
397
+ var localFiles = [];
398
+ sources.forEach(function (src) {
399
+ var prov = providerFromOrigin(src.origin, src.adapter);
400
+ if (!prov) {
401
+ localFiles.push(src);
402
+ } else {
403
+ byProvider[prov] = byProvider[prov] || [];
404
+ byProvider[prov].push(src);
405
+ }
406
+ });
407
+
408
+ // Show detected hint banner
409
+ var hint = createElement('div', { className: 'detected-hint' });
410
+ var hintIcon = createElement('span', { className: 'detected-hint-icon', textContent: '\u{1F50D}' });
411
+ var provNames = Object.keys(byProvider);
412
+ var hintText;
413
+ if (provNames.length > 0) {
414
+ var names = provNames.map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1); });
415
+ hintText = 'We detected ' + names.join(', ') + ' in your IDE configs. Click a provider below to connect securely via OAuth.';
416
+ } else {
417
+ hintText = 'We detected local database files. Use manual connection or select a provider below.';
418
+ }
419
+ hint.appendChild(hintIcon);
420
+ hint.appendChild(createElement('span', { textContent: hintText }));
421
+ container.appendChild(hint);
422
+
423
+ // For local files (duckdb, sqlite), show direct-use cards
424
+ localFiles.forEach(function (src) {
425
+ var card = createElement('div', { className: 'source-card source-card-local' }, [
426
+ createElement('span', { className: 'source-card-name', textContent: src.name || src.adapter }),
427
+ createElement('span', { className: 'source-card-meta', textContent: 'Local file' }),
428
+ createElement('button', { className: 'btn btn-primary', textContent: 'Use This' }),
151
429
  ]);
152
- card.addEventListener('click', function () {
153
- $$('#sources-list .source-card').forEach(function (c) { c.classList.remove('selected'); });
154
- card.classList.add('selected');
155
- state.brief.data_source = src.type + ':' + (src.name || src.type);
430
+ card.querySelector('.btn').addEventListener('click', function () {
431
+ state.sources = state.sources || [];
432
+ state.sources.push(src);
433
+ saveState();
434
+ updateDbStatus(src);
435
+ goToStep(2);
156
436
  });
157
437
  container.appendChild(card);
158
438
  });
159
- // Auto-select if only one source detected
160
- if (state.sources.length === 1) {
161
- var only = container.querySelector('.source-card');
162
- if (only) only.click();
439
+
440
+ // Highlight matching providers in the platform grid
441
+ state._detectedProviders = provNames;
442
+
443
+ }).catch(function () {
444
+ container.textContent = '';
445
+ updateDbStatus(null);
446
+ });
447
+ }
448
+
449
+ function fetchAuthProviders(container, oauthResult) {
450
+ api('GET', '/api/auth/providers').then(function (data) {
451
+ var providers = data.providers || data || [];
452
+ // Replace the platform-grid with a plain wrapper for mixed content
453
+ var wrapper = createElement('div', { className: 'connect-providers' });
454
+ container.replaceWith(wrapper);
455
+
456
+ if (providers.length === 0) {
457
+ wrapper.appendChild(createElement('p', { className: 'muted', textContent: 'No OAuth providers available.' }));
458
+ return;
163
459
  }
164
- } catch (e) {
460
+
461
+ var detected = state._detectedProviders || [];
462
+
463
+ // Show detected providers first (highlighted)
464
+ var detectedProvs = [];
465
+ var otherProvs = [];
466
+ providers.forEach(function (prov) {
467
+ if (detected.indexOf(prov.id) !== -1) {
468
+ detectedProvs.push(prov);
469
+ } else {
470
+ otherProvs.push(prov);
471
+ }
472
+ });
473
+
474
+ // Detected providers get prominent cards
475
+ if (detectedProvs.length > 0) {
476
+ var detectedGrid = createElement('div', { className: 'source-cards' });
477
+ detectedProvs.forEach(function (prov) {
478
+ var card = createElement('div', { className: 'source-card source-card-detected' }, [
479
+ createElement('span', { className: 'source-card-badge', textContent: 'Detected' }),
480
+ createElement('span', { className: 'source-card-name', textContent: prov.displayName || prov.display_name || prov.id }),
481
+ createElement('span', { className: 'source-card-meta', textContent: (prov.cliAuthenticated ? 'CLI authenticated' : prov.cliInstalled ? 'CLI installed' : 'OAuth available') }),
482
+ createElement('button', { className: 'btn btn-primary', textContent: 'Connect via OAuth' }),
483
+ ]);
484
+ card.querySelector('.btn').addEventListener('click', function () {
485
+ startOAuthFlow(prov, wrapper, oauthResult);
486
+ });
487
+ detectedGrid.appendChild(card);
488
+ });
489
+ wrapper.appendChild(detectedGrid);
490
+
491
+ if (otherProvs.length > 0) {
492
+ wrapper.appendChild(createElement('div', { className: 'section-divider' }, ['Other providers']));
493
+ }
494
+ }
495
+
496
+ // Other providers as button grid
497
+ if (otherProvs.length > 0) {
498
+ var grid = createElement('div', { className: 'platform-grid' });
499
+ otherProvs.forEach(function (prov) {
500
+ var btn = createElement('button', { className: 'platform-btn', textContent: prov.displayName || prov.display_name || prov.name || prov.id });
501
+ btn.addEventListener('click', function () {
502
+ startOAuthFlow(prov, wrapper, oauthResult);
503
+ });
504
+ grid.appendChild(btn);
505
+ });
506
+ wrapper.appendChild(grid);
507
+ }
508
+ }).catch(function () {
165
509
  container.textContent = '';
166
- container.appendChild(createElement('p', { className: 'muted', textContent: 'Could not detect sources.' }));
510
+ container.appendChild(createElement('p', { className: 'muted', textContent: 'Could not load providers.' }));
511
+ });
512
+ }
513
+
514
+ async function startOAuthFlow(provider, providerWrapper, oauthResult) {
515
+ // Disable all buttons
516
+ providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = true; });
517
+ providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = true; });
518
+
519
+ // Show loading in the oauth result area (right after detected cards)
520
+ oauthResult.textContent = '';
521
+ oauthResult.appendChild(createElement('p', { className: 'muted', textContent: 'Connecting to ' + (provider.displayName || provider.display_name || provider.id) + '\u2026 A browser window may open for authentication.' }));
522
+
523
+ try {
524
+ var data = await api('POST', '/api/auth/start', { provider: provider.id });
525
+ var databases = data.databases || data || [];
526
+ oauthResult.textContent = '';
527
+
528
+ if (databases.length === 0) {
529
+ oauthResult.appendChild(createElement('p', { className: 'muted', textContent: 'No databases found for this provider.' }));
530
+ providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
531
+ providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
532
+ return;
533
+ }
534
+
535
+ // Hide the other providers / platform grid — show only database results
536
+ providerWrapper.style.display = 'none';
537
+
538
+ var oauthHeader = createElement('div', { className: 'oauth-result-header' });
539
+ oauthHeader.appendChild(createElement('label', { className: 'label-uppercase', textContent: 'Select a database from ' + (provider.displayName || provider.display_name || provider.id) }));
540
+ var backLink = createElement('button', { className: 'btn btn-secondary btn-sm', textContent: '\u2190 Back to providers' });
541
+ backLink.addEventListener('click', function () {
542
+ oauthResult.textContent = '';
543
+ providerWrapper.style.display = '';
544
+ providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
545
+ providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
546
+ });
547
+ oauthHeader.appendChild(backLink);
548
+ oauthResult.appendChild(oauthHeader);
549
+
550
+ var dbGrid = createElement('div', { className: 'source-cards' });
551
+ databases.forEach(function (db) {
552
+ var m = db.metadata || {};
553
+ // Title: "project / branch" if available, else just db name
554
+ var title = m.project ? m.project + ' / ' + (m.branch || 'main') : (db.name || db.database);
555
+ // Subtitle line 1: db name + adapter + region + org
556
+ var parts = [db.name || db.database];
557
+ if (db.adapter) parts.push(db.adapter);
558
+ if (m.region) parts.push(m.region);
559
+ if (m.org && m.org !== 'Personal') parts.push(m.org);
560
+ else if (m.org === 'Personal') parts.push('personal');
561
+ var line1 = parts.join(' \u2022 ');
562
+ // Subtitle line 2: host (truncated)
563
+ var line2 = db.host || '';
564
+
565
+ var dbCard = createElement('div', { className: 'source-card' }, [
566
+ createElement('span', { className: 'source-card-name', textContent: title }),
567
+ createElement('span', { className: 'source-card-meta', textContent: line1 }),
568
+ line2 ? createElement('span', { className: 'source-card-host', textContent: line2 }) : null,
569
+ createElement('button', { className: 'btn btn-primary', textContent: 'Use This' }),
570
+ ].filter(Boolean));
571
+ dbCard.querySelector('.btn').addEventListener('click', async function () {
572
+ try {
573
+ await api('POST', '/api/auth/select-db', { provider: provider.id, database: db });
574
+ state.sources = state.sources || [];
575
+ state.sources.push(db);
576
+ saveState();
577
+ updateDbStatus(db);
578
+ goToStep(2);
579
+ } catch (e) {
580
+ oauthResult.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to select database' }));
581
+ }
582
+ });
583
+ dbGrid.appendChild(dbCard);
584
+ });
585
+ oauthResult.appendChild(dbGrid);
586
+ } catch (e) {
587
+ oauthResult.textContent = '';
588
+ oauthResult.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'OAuth flow failed' }));
589
+ providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
590
+ providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
167
591
  }
168
592
  }
169
593
 
170
- function setupUpload() {
171
- var area = $('#upload-area');
172
- var input = $('#file-input');
173
- if (!area || !input) return;
594
+ // ---- Step 2: Define ----
174
595
 
175
- area.addEventListener('click', function () { input.click(); });
596
+ function renderDefineStep() {
597
+ var content = document.getElementById('wizard-content');
598
+ if (!content) return;
176
599
 
177
- area.addEventListener('dragover', function (e) {
178
- e.preventDefault();
179
- area.classList.add('dragover');
180
- });
181
- area.addEventListener('dragleave', function () {
182
- area.classList.remove('dragover');
183
- });
184
- area.addEventListener('drop', function (e) {
185
- e.preventDefault();
186
- area.classList.remove('dragover');
187
- if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files);
600
+ var card = createElement('div', { className: 'card' });
601
+
602
+ card.appendChild(createElement('h2', { textContent: 'Define Your Data Product' }));
603
+ card.appendChild(createElement('p', { className: 'muted', textContent: 'Tell us about your data product. This metadata helps AI agents understand what they\u2019re working with.' }));
604
+
605
+ var form = createElement('div', { className: 'define-form' });
606
+
607
+ // Product Name (full width, required)
608
+ var nameGroup = createElement('div', { className: 'field full-width' });
609
+ nameGroup.appendChild(createElement('label', { htmlFor: 'product_name', textContent: 'Product Name *' }));
610
+ nameGroup.appendChild(createElement('input', {
611
+ className: 'input',
612
+ id: 'product_name',
613
+ type: 'text',
614
+ placeholder: 'my-data-product',
615
+ }));
616
+ nameGroup.appendChild(createElement('p', { className: 'hint', textContent: 'Alphanumeric, hyphens, and underscores only.' }));
617
+ form.appendChild(nameGroup);
618
+
619
+ // Description (full width, required)
620
+ var descGroup = createElement('div', { className: 'field full-width' });
621
+ descGroup.appendChild(createElement('label', { htmlFor: 'description', textContent: 'Description *' }));
622
+ var descInput = createElement('textarea', {
623
+ className: 'textarea',
624
+ id: 'description',
625
+ placeholder: 'What does this data product provide?',
188
626
  });
627
+ descGroup.appendChild(descInput);
628
+ form.appendChild(descGroup);
629
+
630
+ // Owner Name (left column)
631
+ var ownerNameGroup = createElement('div', { className: 'field' });
632
+ ownerNameGroup.appendChild(createElement('label', { htmlFor: 'owner_name', textContent: 'Owner Name' }));
633
+ ownerNameGroup.appendChild(createElement('input', {
634
+ className: 'input',
635
+ id: 'owner_name',
636
+ type: 'text',
637
+ placeholder: 'Jane Doe',
638
+ }));
639
+ form.appendChild(ownerNameGroup);
640
+
641
+ // Team (right column)
642
+ var teamGroup = createElement('div', { className: 'field' });
643
+ teamGroup.appendChild(createElement('label', { htmlFor: 'owner_team', textContent: 'Team' }));
644
+ teamGroup.appendChild(createElement('input', {
645
+ className: 'input',
646
+ id: 'owner_team',
647
+ type: 'text',
648
+ placeholder: 'Data Engineering',
649
+ }));
650
+ form.appendChild(teamGroup);
189
651
 
190
- input.addEventListener('change', function () {
191
- if (input.files.length) uploadFiles(input.files);
192
- input.value = '';
652
+ // Email (left column)
653
+ var emailGroup = createElement('div', { className: 'field' });
654
+ emailGroup.appendChild(createElement('label', { htmlFor: 'owner_email', textContent: 'Email' }));
655
+ emailGroup.appendChild(createElement('input', {
656
+ className: 'input',
657
+ id: 'owner_email',
658
+ type: 'text',
659
+ placeholder: 'jane@example.com',
660
+ }));
661
+ form.appendChild(emailGroup);
662
+
663
+ // Sensitivity (right column)
664
+ var sensGroup = createElement('div', { className: 'field' });
665
+ sensGroup.appendChild(createElement('label', { htmlFor: 'sensitivity', textContent: 'Sensitivity' }));
666
+ var sensSelect = createElement('select', { className: 'select', id: 'sensitivity' });
667
+ [
668
+ { value: 'public', text: 'Public' },
669
+ { value: 'internal', text: 'Internal' },
670
+ { value: 'confidential', text: 'Confidential' },
671
+ { value: 'restricted', text: 'Restricted' },
672
+ ].forEach(function (opt) {
673
+ var option = createElement('option', { value: opt.value, textContent: opt.text });
674
+ sensSelect.appendChild(option);
193
675
  });
194
- }
676
+ sensSelect.value = 'internal';
677
+ sensGroup.appendChild(sensSelect);
678
+ form.appendChild(sensGroup);
679
+
680
+ // Action buttons (full width)
681
+ var actions = createElement('div', { className: 'define-actions' });
682
+
683
+ var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
684
+ backBtn.addEventListener('click', function () { goToStep(1); });
685
+ actions.appendChild(backBtn);
686
+
687
+ var continueBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Continue' });
688
+ continueBtn.addEventListener('click', async function () {
689
+ clearErrors();
690
+ var valid = true;
691
+
692
+ var productName = document.getElementById('product_name').value.trim();
693
+ var description = document.getElementById('description').value.trim();
694
+
695
+ if (!productName) {
696
+ showError('product_name', 'Product name is required.');
697
+ valid = false;
698
+ } else if (!/^[a-zA-Z0-9_-]+$/.test(productName)) {
699
+ showError('product_name', 'Only letters, numbers, hyphens, and underscores allowed.');
700
+ valid = false;
701
+ }
702
+
703
+ if (!description) {
704
+ showError('description', 'Description is required.');
705
+ valid = false;
706
+ }
707
+
708
+ if (!valid) return;
709
+
710
+ state.brief.product_name = productName;
711
+ state.brief.description = description;
712
+ state.brief.owner.name = document.getElementById('owner_name').value.trim();
713
+ state.brief.owner.team = document.getElementById('owner_team').value.trim();
714
+ state.brief.owner.email = document.getElementById('owner_email').value.trim();
715
+ state.brief.sensitivity = document.getElementById('sensitivity').value;
716
+ saveState();
195
717
 
196
- async function uploadFiles(files) {
197
- var productName = state.brief.product_name || 'unnamed';
198
- for (var i = 0; i < files.length; i++) {
199
- var file = files[i];
200
- var fd = new FormData();
201
- fd.append('file', file);
202
- addFileRow(file.name, 'uploading...');
718
+ continueBtn.textContent = 'Saving\u2026';
719
+ continueBtn.disabled = true;
203
720
  try {
204
- await api('POST', '/api/upload/' + encodeURIComponent(productName), fd);
205
- updateFileRow(file.name, 'uploaded');
206
- state.brief.docs.push(file.name);
721
+ await api('POST', '/api/brief', state.brief);
722
+ goToStep(3);
207
723
  } catch (e) {
208
- updateFileRow(file.name, 'error');
724
+ continueBtn.textContent = 'Continue';
725
+ continueBtn.disabled = false;
726
+ var errP = createElement('p', { className: 'field-error', textContent: e.message || 'Failed to save. Please try again.' });
727
+ actions.appendChild(errP);
209
728
  }
210
- }
211
- }
729
+ });
730
+ actions.appendChild(continueBtn);
731
+ form.appendChild(actions);
212
732
 
213
- function addFileRow(name, status) {
214
- var container = $('#uploaded-files');
215
- var row = createElement('div', { className: 'uploaded-file', 'data-file': name }, [
216
- createElement('span', { className: 'file-name', textContent: name }),
217
- createElement('span', { className: 'file-status', textContent: status }),
218
- ]);
219
- container.appendChild(row);
220
- }
733
+ card.appendChild(form);
734
+ content.appendChild(card);
735
+
736
+ // Pre-fill from saved state first
737
+ if (state.brief.product_name) document.getElementById('product_name').value = state.brief.product_name;
738
+ if (state.brief.description) document.getElementById('description').value = state.brief.description;
739
+ if (state.brief.owner.name) document.getElementById('owner_name').value = state.brief.owner.name;
740
+ if (state.brief.owner.team) document.getElementById('owner_team').value = state.brief.owner.team;
741
+ if (state.brief.owner.email) document.getElementById('owner_email').value = state.brief.owner.email;
742
+ if (state.brief.sensitivity) document.getElementById('sensitivity').value = state.brief.sensitivity;
221
743
 
222
- function updateFileRow(name, status) {
223
- var row = $('[data-file="' + CSS.escape(name) + '"]');
224
- if (row) {
225
- var s = row.querySelector('.file-status');
226
- if (s) s.textContent = status;
744
+ // Auto-suggest from selected source (if fields are still empty)
745
+ var hasAnyField = state.brief.product_name || state.brief.description || state.brief.owner.name;
746
+ if (!hasAnyField && state.sources && state.sources.length > 0) {
747
+ var suggestNote = createElement('p', { className: 'muted suggest-loading', textContent: 'Auto-filling from your database\u2026' });
748
+ card.insertBefore(suggestNote, form);
749
+
750
+ api('POST', '/api/suggest-brief', { source: state.sources[0] }).then(function (data) {
751
+ suggestNote.remove();
752
+ // Only fill empty fields
753
+ var fields = [
754
+ { id: 'product_name', val: data.product_name, stateKey: 'product_name' },
755
+ { id: 'description', val: data.description, stateKey: 'description' },
756
+ { id: 'owner_name', val: data.owner && data.owner.name, stateKey: null },
757
+ { id: 'owner_team', val: data.owner && data.owner.team, stateKey: null },
758
+ { id: 'owner_email', val: data.owner && data.owner.email, stateKey: null },
759
+ { id: 'sensitivity', val: data.sensitivity, stateKey: 'sensitivity' },
760
+ ];
761
+ fields.forEach(function (f) {
762
+ var el = document.getElementById(f.id);
763
+ if (el && !el.value && f.val) {
764
+ el.value = f.val;
765
+ }
766
+ });
767
+ }).catch(function () {
768
+ suggestNote.textContent = '';
769
+ });
227
770
  }
228
771
  }
229
772
 
230
- // ---- Sensitivity Cards ----
773
+ // ---- Step 3: Scaffold ----
231
774
 
232
- function setupSensitivity() {
233
- $$('.sensitivity-cards .card').forEach(function (card) {
234
- card.addEventListener('click', function () {
235
- $$('.sensitivity-cards .card').forEach(function (c) { c.classList.remove('selected'); });
236
- card.classList.add('selected');
237
- state.brief.sensitivity = card.getAttribute('data-sensitivity');
238
- });
775
+ var SCAFFOLD_STAGES = [
776
+ { key: 'introspect', label: 'Extracting schema from database...' },
777
+ { key: 'scaffold', label: 'Building semantic plane files...' },
778
+ { key: 'verify', label: 'Validating semantic plane...' },
779
+ { key: 'autofix', label: 'Fixing any issues...' },
780
+ { key: 'agent-instructions', label: 'Generating agent instructions...' },
781
+ ];
782
+
783
+ function renderScaffoldStep() {
784
+ var content = document.getElementById('wizard-content');
785
+ if (!content) return;
786
+
787
+ var card = createElement('div', { className: 'card' });
788
+
789
+ card.appendChild(createElement('h2', { textContent: 'Building Your Semantic Plane' }));
790
+ card.appendChild(createElement('p', { className: 'muted', textContent: 'Connecting to your database and extracting schema metadata. This creates a Bronze-tier semantic plane.' }));
791
+
792
+ // Stage rows container
793
+ var stagesContainer = createElement('div', { className: 'scaffold-stages', id: 'scaffold-stages' });
794
+ SCAFFOLD_STAGES.forEach(function (stage) {
795
+ var row = createElement('div', { className: 'stage-row', id: 'stage-' + stage.key, 'data-stage': stage.key }, [
796
+ createElement('span', { className: 'stage-dot' }),
797
+ createElement('span', { className: 'stage-name', textContent: stage.label }),
798
+ ]);
799
+ stagesContainer.appendChild(row);
239
800
  });
801
+ card.appendChild(stagesContainer);
802
+
803
+ // Error area
804
+ var errorArea = createElement('div', { id: 'scaffold-error' });
805
+ card.appendChild(errorArea);
806
+
807
+ // Action area (Start / Retry button)
808
+ var actionArea = createElement('div', { className: 'step-actions', id: 'scaffold-actions' });
809
+
810
+ var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
811
+ backBtn.addEventListener('click', function () { goToStep(2); });
812
+ actionArea.appendChild(backBtn);
813
+
814
+ // If we already have a pipelineId, resume polling; otherwise show Start button
815
+ if (state.pipelineId) {
816
+ actionArea.appendChild(createElement('span', { className: 'muted', textContent: 'Build in progress...' }));
817
+ card.appendChild(actionArea);
818
+ content.appendChild(card);
819
+ startScaffoldPolling();
820
+ } else {
821
+ var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start Build', id: 'scaffold-start-btn' });
822
+ startBtn.addEventListener('click', function () { startScaffoldBuild(startBtn); });
823
+ actionArea.appendChild(startBtn);
824
+ card.appendChild(actionArea);
825
+ content.appendChild(card);
826
+ }
240
827
  }
241
828
 
242
- // ---- Voice Input ----
829
+ async function startScaffoldBuild(btn) {
830
+ btn.textContent = 'Starting...';
831
+ btn.disabled = true;
832
+
833
+ var errorArea = document.getElementById('scaffold-error');
834
+ if (errorArea) errorArea.textContent = '';
243
835
 
244
- function setupVoice() {
245
- var btn = $('#voice-btn');
246
- if (!btn) return;
247
- var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
248
- if (!SpeechRecognition) {
249
- btn.style.display = 'none';
250
- return;
836
+ var body = {
837
+ productName: state.brief.product_name,
838
+ targetTier: 'bronze',
839
+ };
840
+ if (state.sources && state.sources[0]) {
841
+ body.dataSource = state.sources[0];
251
842
  }
252
- var recognition = new SpeechRecognition();
253
- recognition.continuous = false;
254
- recognition.interimResults = false;
255
- recognition.lang = 'en-US';
256
- var recording = false;
257
-
258
- btn.addEventListener('click', function () {
259
- if (recording) {
260
- recognition.stop();
261
- return;
843
+
844
+ try {
845
+ var result = await api('POST', '/api/pipeline/start', body);
846
+ state.pipelineId = result.id;
847
+ saveState();
848
+ btn.textContent = 'Building...';
849
+ startScaffoldPolling();
850
+ } catch (e) {
851
+ btn.textContent = 'Start Build';
852
+ btn.disabled = false;
853
+ var errorArea = document.getElementById('scaffold-error');
854
+ if (errorArea) {
855
+ errorArea.textContent = '';
856
+ errorArea.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to start build.' }));
262
857
  }
263
- recording = true;
264
- btn.classList.add('recording');
265
- recognition.start();
266
- });
858
+ }
859
+ }
267
860
 
268
- recognition.addEventListener('result', function (e) {
269
- var transcript = e.results[0][0].transcript;
270
- var desc = $('#description');
271
- if (desc) desc.value = (desc.value ? desc.value + ' ' : '') + transcript;
272
- });
861
+ function startScaffoldPolling() {
862
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
273
863
 
274
- recognition.addEventListener('end', function () {
275
- recording = false;
276
- btn.classList.remove('recording');
277
- });
864
+ async function poll() {
865
+ if (!state.pipelineId) return;
866
+ try {
867
+ var status = await api('GET', '/api/pipeline/status/' + state.pipelineId);
868
+ var stages = status.stages || [];
869
+ var hasError = false;
870
+ var allDone = true;
278
871
 
279
- recognition.addEventListener('error', function () {
280
- recording = false;
281
- btn.classList.remove('recording');
282
- });
872
+ SCAFFOLD_STAGES.forEach(function (def) {
873
+ var row = document.getElementById('stage-' + def.key);
874
+ if (!row) return;
875
+
876
+ var match = null;
877
+ for (var i = 0; i < stages.length; i++) {
878
+ if (stages[i].name === def.key) { match = stages[i]; break; }
879
+ }
880
+
881
+ var dot = row.querySelector('.stage-dot');
882
+ // Reset classes
883
+ dot.className = 'stage-dot';
884
+
885
+ // Remove any previous summary/error elements
886
+ var oldSummary = row.querySelector('.stage-summary');
887
+ if (oldSummary) oldSummary.remove();
888
+ var oldError = row.querySelector('.stage-error');
889
+ if (oldError) oldError.remove();
890
+
891
+ if (!match || match.status === 'pending') {
892
+ allDone = false;
893
+ } else if (match.status === 'running') {
894
+ dot.classList.add('running');
895
+ allDone = false;
896
+ } else if (match.status === 'done') {
897
+ dot.classList.add('done');
898
+ if (match.summary) {
899
+ row.appendChild(createElement('span', { className: 'stage-summary', textContent: match.summary }));
900
+ }
901
+ } else if (match.status === 'error') {
902
+ dot.classList.add('error');
903
+ hasError = true;
904
+ allDone = false;
905
+ if (match.error) {
906
+ row.appendChild(createElement('span', { className: 'stage-error', textContent: match.error }));
907
+ }
908
+ }
909
+ });
910
+
911
+ if (hasError) {
912
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
913
+ showScaffoldError(status.error || 'A pipeline stage failed.');
914
+ } else if (allDone && stages.length > 0) {
915
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
916
+ goToStep(4);
917
+ }
918
+ } catch (e) {
919
+ // Network error — keep polling, it may recover
920
+ }
921
+ }
922
+
923
+ poll();
924
+ state.pollTimer = setInterval(poll, 2000);
283
925
  }
284
926
 
285
- // ---- Step 4: Review ----
286
-
287
- function renderReview() {
288
- var c = $('#review-content');
289
- if (!c) return;
290
- c.textContent = '';
291
- var rows = [
292
- ['Product Name', state.brief.product_name],
293
- ['Description', state.brief.description || '(none)'],
294
- ['Owner', state.brief.owner.name || '(not set)'],
295
- ['Team', state.brief.owner.team || '(not set)'],
296
- ['Email', state.brief.owner.email || '(not set)'],
297
- ['Sensitivity', state.brief.sensitivity],
298
- ['Data Source', state.brief.data_source || '(none selected) — ' + state.sources.length + ' detected'],
299
- ['Uploaded Docs', state.brief.docs.length > 0 ? state.brief.docs.join(', ') : '(none)'],
300
- ];
301
- rows.forEach(function (r) {
302
- var row = createElement('div', { className: 'review-row' }, [
303
- createElement('span', { className: 'review-label', textContent: r[0] }),
304
- createElement('span', { className: 'review-value', textContent: r[1] }),
305
- ]);
306
- c.appendChild(row);
927
+ function showScaffoldError(msg) {
928
+ var errorArea = document.getElementById('scaffold-error');
929
+ if (!errorArea) return;
930
+ errorArea.textContent = '';
931
+ errorArea.appendChild(createElement('p', { className: 'field-error', textContent: msg }));
932
+
933
+ var actions = document.getElementById('scaffold-actions');
934
+ if (!actions) return;
935
+ // Remove old start/building button if any
936
+ var oldBtn = actions.querySelector('.btn-primary');
937
+ if (oldBtn) oldBtn.remove();
938
+ var oldMuted = actions.querySelector('.muted');
939
+ if (oldMuted) oldMuted.remove();
940
+
941
+ var retryBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Retry' });
942
+ retryBtn.addEventListener('click', function () {
943
+ state.pipelineId = null;
944
+ saveState();
945
+ // Reset stage dots
946
+ SCAFFOLD_STAGES.forEach(function (def) {
947
+ var row = document.getElementById('stage-' + def.key);
948
+ if (!row) return;
949
+ var dot = row.querySelector('.stage-dot');
950
+ if (dot) dot.className = 'stage-dot';
951
+ var s = row.querySelector('.stage-summary');
952
+ if (s) s.remove();
953
+ var e = row.querySelector('.stage-error');
954
+ if (e) e.remove();
955
+ });
956
+ errorArea.textContent = '';
957
+ startScaffoldBuild(retryBtn);
307
958
  });
959
+ actions.appendChild(retryBtn);
308
960
  }
309
961
 
310
- // ---- Step 5: Build ----
962
+ // ---- Step 4: Checkpoint ----
311
963
 
312
- var STAGES = [
313
- 'Saving context brief',
314
- 'Scanning data sources',
315
- 'Extracting schema metadata',
316
- 'Generating semantic descriptions',
317
- 'Writing OSI-ready context',
318
- ];
964
+ function renderCheckpointStep() {
965
+ var content = document.getElementById('wizard-content');
966
+ if (!content) return;
319
967
 
320
- function buildStageElement(item) {
321
- var cls = 'pipeline-stage';
322
- var dotText = '';
323
- if (item.status === 'done' || item.status === 'completed' || item.status === 'complete') {
324
- cls += ' done';
325
- dotText = '\u2713';
326
- } else if (item.status === 'running' || item.status === 'in_progress') {
327
- cls += ' running';
328
- dotText = '\u2026';
329
- } else if (item.status === 'error') {
330
- cls += ' error';
331
- dotText = '!';
332
- }
333
- var children = [
334
- createElement('div', { className: 'stage-dot', textContent: dotText }),
335
- createElement('div', { className: 'stage-info' }, [
336
- createElement('div', { className: 'stage-name', textContent: item.stage }),
337
- item.detail ? createElement('div', { className: 'stage-status', textContent: item.detail }) : null,
338
- ]),
339
- ];
340
- return createElement('div', { className: cls }, children);
968
+ var card = createElement('div', { className: 'card checkpoint-card' });
969
+
970
+ card.appendChild(createElement('h2', { textContent: 'Bronze Tier Achieved' }));
971
+
972
+ // Tier scorecard
973
+ var scorecard = createElement('div', { className: 'tier-scorecard' });
974
+
975
+ // Bronze (achieved)
976
+ var bronzeRow = createElement('div', { className: 'tier-row achieved' }, [
977
+ createElement('span', { className: 'tier-label', textContent: 'Bronze' }),
978
+ createElement('span', { className: 'tier-desc', textContent: 'Schema metadata, table/column names, types, row counts' }),
979
+ ]);
980
+ scorecard.appendChild(bronzeRow);
981
+
982
+ // Silver
983
+ var silverRow = createElement('div', { className: 'tier-row' }, [
984
+ createElement('span', { className: 'tier-label', textContent: 'Silver' }),
985
+ createElement('span', { className: 'tier-desc', textContent: 'Column descriptions, sample values, trust tags' }),
986
+ ]);
987
+ scorecard.appendChild(silverRow);
988
+
989
+ // Gold
990
+ var goldRow = createElement('div', { className: 'tier-row' }, [
991
+ createElement('span', { className: 'tier-label', textContent: 'Gold' }),
992
+ createElement('span', { className: 'tier-desc', textContent: 'Join rules, grain statements, semantic roles, golden queries, guardrail filters' }),
993
+ ]);
994
+ scorecard.appendChild(goldRow);
995
+
996
+ card.appendChild(scorecard);
997
+
998
+ // Explanatory text
999
+ card.appendChild(createElement('p', { className: 'checkpoint-explain', textContent: 'Your semantic plane has basic schema metadata. AI tools can use this now, but with Gold tier they\'ll understand join relationships, business descriptions, and query patterns.' }));
1000
+
1001
+ // CTA buttons
1002
+ var ctas = createElement('div', { className: 'checkpoint-ctas' });
1003
+
1004
+ var serveBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Start MCP Server' });
1005
+ serveBtn.addEventListener('click', function () { goToStep(6); });
1006
+ ctas.appendChild(serveBtn);
1007
+
1008
+ var goldBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Continue to Gold' });
1009
+ goldBtn.addEventListener('click', function () { goToStep(5); });
1010
+ ctas.appendChild(goldBtn);
1011
+
1012
+ card.appendChild(ctas);
1013
+ content.appendChild(card);
341
1014
  }
342
1015
 
343
- function renderTimeline(items) {
344
- var el = $('#pipeline-timeline');
345
- if (!el) return;
346
- if (items.length === 0) {
347
- items = STAGES.map(function (name) {
348
- return { stage: name, status: 'pending', detail: '' };
1016
+ // ---- Step 5: Enrich ----
1017
+
1018
+ var ENRICH_REQUIREMENTS = [
1019
+ { key: 'column-descriptions', label: 'Column descriptions', initial: '0/45 columns' },
1020
+ { key: 'sample-values', label: 'Sample values', initial: '0/45 columns' },
1021
+ { key: 'join-rules', label: 'Join rules', initial: '0/0' },
1022
+ { key: 'grain-statements', label: 'Grain statements', initial: '0/0' },
1023
+ { key: 'semantic-roles', label: 'Semantic roles', initial: '0/0' },
1024
+ { key: 'golden-queries', label: 'Golden queries', initial: '0/0' },
1025
+ { key: 'guardrail-filters', label: 'Guardrail filters', initial: '0/0' },
1026
+ ];
1027
+
1028
+ function renderEnrichStep() {
1029
+ var content = document.getElementById('wizard-content');
1030
+ if (!content) return;
1031
+
1032
+ var card = createElement('div', { className: 'card' });
1033
+
1034
+ card.appendChild(createElement('h2', { textContent: 'Enriching to Gold' }));
1035
+ card.appendChild(createElement('p', { className: 'muted', textContent: 'RunContext is analyzing your schema to add descriptions, join rules, and query patterns.' }));
1036
+
1037
+ // Start Enrichment button area
1038
+ var startArea = createElement('div', { className: 'step-actions', id: 'enrich-start-area' });
1039
+ var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
1040
+ backBtn.addEventListener('click', function () { goToStep(4); });
1041
+ startArea.appendChild(backBtn);
1042
+
1043
+ var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start Enrichment', id: 'enrich-start-btn' });
1044
+ startBtn.addEventListener('click', function () { startEnrichment(startBtn); });
1045
+ startArea.appendChild(startBtn);
1046
+ card.appendChild(startArea);
1047
+
1048
+ // Dashboard (always visible — shows checklist pre-enrichment)
1049
+ var dashboard = createElement('div', { className: 'enrich-dashboard', id: 'enrich-dashboard' });
1050
+
1051
+ // Top panel: Requirements checklist
1052
+ var checklist = createElement('div', { className: 'enrich-checklist' });
1053
+ ENRICH_REQUIREMENTS.forEach(function (req) {
1054
+ var row = createElement('div', { className: 'enrich-row', id: 'enrich-req-' + req.key });
1055
+
1056
+ var header = createElement('div', { className: 'enrich-row-header' });
1057
+ header.appendChild(createElement('span', { className: 'stage-dot' }));
1058
+ header.appendChild(createElement('span', { className: 'enrich-req-name', textContent: req.label }));
1059
+ header.appendChild(createElement('span', { className: 'enrich-progress', textContent: req.initial }));
1060
+ header.appendChild(createElement('span', { className: 'enrich-arrow', textContent: '\u25B6' }));
1061
+
1062
+ header.addEventListener('click', function () {
1063
+ row.classList.toggle('expanded');
349
1064
  });
350
- }
351
- el.textContent = '';
352
- items.forEach(function (item) {
353
- el.appendChild(buildStageElement(item));
1065
+
1066
+ var detail = createElement('div', { className: 'enrich-row-detail' }, [
1067
+ 'Details will appear as enrichment progresses.',
1068
+ ]);
1069
+
1070
+ row.appendChild(header);
1071
+ row.appendChild(detail);
1072
+ checklist.appendChild(row);
354
1073
  });
1074
+ dashboard.appendChild(checklist);
1075
+
1076
+ // Bottom panel: Activity log
1077
+ var logSection = createElement('div', { className: 'activity-log' });
1078
+ logSection.appendChild(createElement('div', { className: 'activity-log-title', textContent: 'Activity Log' }));
1079
+ var logContainer = createElement('div', { id: 'activity-log' });
1080
+ logContainer.appendChild(createElement('div', { className: 'log-entry' }, [
1081
+ createElement('span', { className: 'log-time', textContent: new Date().toLocaleTimeString() }),
1082
+ createElement('span', {}, [' Waiting for enrichment to start...']),
1083
+ ]));
1084
+ logSection.appendChild(logContainer);
1085
+ dashboard.appendChild(logSection);
1086
+
1087
+ // Error area
1088
+ var errorArea = createElement('div', { id: 'enrich-error' });
1089
+ dashboard.appendChild(errorArea);
1090
+
1091
+ card.appendChild(dashboard);
1092
+ content.appendChild(card);
355
1093
  }
356
1094
 
357
- function renderTimelineFromStatus(data) {
358
- var stages = data.stages || data.steps || [];
359
- var items = stages.map(function (s) {
360
- return {
361
- stage: s.name || s.stage || s.label || '',
362
- status: s.status || 'pending',
363
- detail: s.detail || s.message || '',
364
- };
365
- });
366
- if (items.length === 0 && typeof data.currentStep === 'number') {
367
- items = STAGES.map(function (name, i) {
368
- var status = 'pending';
369
- if (i < data.currentStep) status = 'done';
370
- else if (i === data.currentStep) status = 'running';
371
- return { stage: name, status: status, detail: '' };
372
- });
1095
+ async function startEnrichment(btn) {
1096
+ btn.textContent = 'Starting...';
1097
+ btn.disabled = true;
1098
+
1099
+ var errorArea = document.getElementById('enrich-error');
1100
+ if (errorArea) errorArea.textContent = '';
1101
+
1102
+ var body = {
1103
+ productName: state.brief.product_name,
1104
+ targetTier: 'gold',
1105
+ };
1106
+ if (state.sources && state.sources[0]) {
1107
+ body.dataSource = state.sources[0];
373
1108
  }
374
- renderTimeline(items);
375
- }
376
1109
 
377
- async function startBuild() {
378
- renderTimeline([]);
379
1110
  try {
380
- await api('POST', '/api/brief', state.brief);
381
- var result = await api('POST', '/api/pipeline/start', {
382
- productName: state.brief.product_name,
383
- targetTier: 'gold',
384
- });
385
- state.pipelineId = result.id || result.pipelineId;
386
- pollPipeline();
1111
+ var result = await api('POST', '/api/pipeline/start', body);
1112
+ state.pipelineId = result.id;
1113
+ saveState();
1114
+
1115
+ // Hide start button area
1116
+ var startArea = document.getElementById('enrich-start-area');
1117
+ if (startArea) startArea.style.display = 'none';
1118
+
1119
+ appendEnrichLog({ message: 'Enrichment pipeline started.' });
1120
+ startEnrichPolling();
387
1121
  } catch (e) {
388
- renderTimeline([{ stage: 'Error', status: 'error', detail: e.message }]);
1122
+ btn.textContent = 'Start Enrichment';
1123
+ btn.disabled = false;
1124
+ if (errorArea) {
1125
+ errorArea.textContent = '';
1126
+ errorArea.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to start enrichment.' }));
1127
+ }
389
1128
  }
390
1129
  }
391
1130
 
392
- function pollPipeline() {
393
- if (!state.pipelineId) return;
394
- state.pollTimer = setInterval(async function () {
1131
+ function startEnrichPolling() {
1132
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
1133
+
1134
+ async function poll() {
1135
+ if (!state.pipelineId) return;
395
1136
  try {
396
- var data = await api('GET', '/api/pipeline/status/' + encodeURIComponent(state.pipelineId));
397
- renderTimelineFromStatus(data);
398
- if (data.status === 'done' || data.status === 'complete' || data.status === 'completed' || data.status === 'error') {
399
- clearInterval(state.pollTimer);
400
- if (data.status !== 'error') {
401
- var doneEl = $('#pipeline-done');
402
- if (doneEl) doneEl.style.display = '';
403
- }
1137
+ var status = await api('GET', '/api/pipeline/status/' + state.pipelineId);
1138
+ var stages = status.stages || [];
1139
+ var hasError = false;
1140
+ var silverDone = false;
1141
+ var goldDone = false;
1142
+
1143
+ for (var i = 0; i < stages.length; i++) {
1144
+ var s = stages[i];
1145
+ if (s.name === 'enrich-silver' && s.status === 'done') silverDone = true;
1146
+ if (s.name === 'enrich-gold' && s.status === 'done') goldDone = true;
1147
+ if (s.status === 'error') hasError = true;
1148
+ }
1149
+
1150
+ if (hasError) {
1151
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
1152
+ showEnrichError(status.error || 'An enrichment stage failed.');
1153
+ return;
1154
+ }
1155
+
1156
+ if (silverDone) {
1157
+ appendEnrichLog({ message: 'Silver enrichment complete.' });
1158
+ }
1159
+ if (silverDone && goldDone) {
1160
+ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
1161
+ appendEnrichLog({ message: 'Gold enrichment complete! Advancing...' });
1162
+ goToStep(6);
404
1163
  }
405
1164
  } catch (e) {
406
- clearInterval(state.pollTimer);
407
- renderTimeline([{ stage: 'Error', status: 'error', detail: e.message }]);
1165
+ // Network error — keep polling
408
1166
  }
409
- }, 1000);
1167
+ }
1168
+
1169
+ poll();
1170
+ state.pollTimer = setInterval(poll, 3000);
1171
+ }
1172
+
1173
+ function showEnrichError(msg) {
1174
+ var errorArea = document.getElementById('enrich-error');
1175
+ if (!errorArea) return;
1176
+ errorArea.textContent = '';
1177
+ errorArea.appendChild(createElement('p', { className: 'field-error', textContent: msg }));
1178
+
1179
+ var retryBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Retry' });
1180
+ retryBtn.addEventListener('click', function () {
1181
+ errorArea.textContent = '';
1182
+ state.pipelineId = null;
1183
+ saveState();
1184
+ // Reset requirement rows to pending
1185
+ ENRICH_REQUIREMENTS.forEach(function (req) {
1186
+ var row = document.getElementById('enrich-req-' + req.key);
1187
+ if (!row) return;
1188
+ var dot = row.querySelector('.stage-dot');
1189
+ if (dot) dot.className = 'stage-dot';
1190
+ var prog = row.querySelector('.enrich-progress');
1191
+ if (prog) prog.textContent = req.initial;
1192
+ });
1193
+ // Show start area again, hide dashboard
1194
+ var startArea = document.getElementById('enrich-start-area');
1195
+ if (startArea) startArea.style.display = '';
1196
+ var dashboard = document.getElementById('enrich-dashboard');
1197
+ if (dashboard) dashboard.style.display = 'none';
1198
+ var startBtn = document.getElementById('enrich-start-btn');
1199
+ if (startBtn) {
1200
+ startBtn.textContent = 'Start Enrichment';
1201
+ startBtn.disabled = false;
1202
+ }
1203
+ });
1204
+ errorArea.appendChild(retryBtn);
1205
+ }
1206
+
1207
+ function updateEnrichProgress(payload) {
1208
+ // payload: { requirement: string, status: string, progress: string }
1209
+ var row = document.getElementById('enrich-req-' + payload.requirement);
1210
+ if (!row) return;
1211
+ var dot = row.querySelector('.stage-dot');
1212
+ var prog = row.querySelector('.enrich-progress');
1213
+ if (dot) {
1214
+ dot.className = 'stage-dot' + (payload.status === 'working' ? ' running' : payload.status === 'done' ? ' done' : '');
1215
+ }
1216
+ if (prog) prog.textContent = payload.progress || '';
1217
+ }
1218
+
1219
+ function appendEnrichLog(payload) {
1220
+ // payload: { message: string, timestamp?: string }
1221
+ var log = document.getElementById('activity-log');
1222
+ if (!log) return;
1223
+ var ts = payload.timestamp ? new Date(payload.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
1224
+ var entry = createElement('div', { className: 'log-entry' }, [
1225
+ createElement('span', { className: 'log-time' }, [ts]),
1226
+ createElement('span', {}, [' ' + payload.message]),
1227
+ ]);
1228
+ log.appendChild(entry);
1229
+ log.scrollTop = log.scrollHeight;
1230
+ }
1231
+
1232
+ // ---- Step 6: Serve ----
1233
+
1234
+ function renderServeStep() {
1235
+ var content = document.getElementById('wizard-content');
1236
+ if (!content) return;
1237
+
1238
+ var card = createElement('div', { className: 'card serve-card' });
1239
+
1240
+ card.appendChild(createElement('h2', { textContent: 'Your Semantic Plane is Ready' }));
1241
+
1242
+ // Tier detection placeholder — will be filled async
1243
+ var tierBadge = createElement('span', { className: 'serve-tier-badge bronze', id: 'tier-badge', textContent: 'Bronze' });
1244
+ card.appendChild(tierBadge);
1245
+
1246
+ var messageEl = createElement('p', { className: 'muted' });
1247
+ messageEl.textContent = 'Your Bronze tier semantic plane is ready for AI agents.';
1248
+ card.appendChild(messageEl);
1249
+
1250
+ var upgradeHint = createElement('p', { className: 'muted' });
1251
+ upgradeHint.style.marginTop = '8px';
1252
+ upgradeHint.style.display = 'none';
1253
+ card.appendChild(upgradeHint);
1254
+
1255
+ // Fetch tier from pipeline status
1256
+ if (state.pipelineId) {
1257
+ fetch('/api/pipeline/status/' + state.pipelineId)
1258
+ .then(function (res) { return res.json(); })
1259
+ .then(function (data) {
1260
+ var stages = data.stages || [];
1261
+ var tier = 'Bronze';
1262
+ var tierClass = 'bronze';
1263
+ var hasSilver = false;
1264
+ var hasGold = false;
1265
+ for (var i = 0; i < stages.length; i++) {
1266
+ if (stages[i].name === 'enrich-gold' && stages[i].status === 'done') {
1267
+ hasGold = true;
1268
+ }
1269
+ if (stages[i].name === 'enrich-silver' && stages[i].status === 'done') {
1270
+ hasSilver = true;
1271
+ }
1272
+ }
1273
+ if (hasGold) {
1274
+ tier = 'Gold';
1275
+ tierClass = 'gold';
1276
+ } else if (hasSilver) {
1277
+ tier = 'Silver';
1278
+ tierClass = 'silver';
1279
+ }
1280
+
1281
+ tierBadge.className = 'serve-tier-badge ' + tierClass;
1282
+ tierBadge.textContent = tier;
1283
+ messageEl.textContent = 'Your ' + tier + ' tier semantic plane is ready for AI agents.';
1284
+
1285
+ if (!hasGold) {
1286
+ var missing = [];
1287
+ if (!hasSilver) missing.push('Silver enrichment');
1288
+ missing.push('Gold enrichment');
1289
+ upgradeHint.textContent = 'To reach Gold, you still need: ' + missing.join(', ') + '. You can run enrichment later with `context enrich --target gold`.';
1290
+ upgradeHint.style.display = 'block';
1291
+ }
1292
+ })
1293
+ .catch(function () { /* keep Bronze default */ });
1294
+ }
1295
+
1296
+ // CTAs
1297
+ var ctas = createElement('div', { className: 'serve-ctas' });
1298
+
1299
+ var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start MCP Server' });
1300
+ startBtn.addEventListener('click', function () {
1301
+ startBtn.disabled = true;
1302
+ startBtn.textContent = 'Loading...';
1303
+ fetch('/api/mcp-config')
1304
+ .then(function (res) { return res.json(); })
1305
+ .then(function (config) {
1306
+ startBtn.textContent = 'Start MCP Server';
1307
+ startBtn.disabled = false;
1308
+ // Remove existing config block if any
1309
+ var existing = card.querySelector('.mcp-config-block');
1310
+ if (existing) existing.remove();
1311
+ var configBlock = createElement('div', { className: 'mcp-config-block' });
1312
+ configBlock.textContent = JSON.stringify(config, null, 2);
1313
+ // Insert after CTAs
1314
+ var ctaParent = ctas.parentNode;
1315
+ if (ctaParent) ctaParent.insertBefore(configBlock, ctas.nextSibling);
1316
+
1317
+ var copyHint = card.querySelector('.mcp-copy-hint');
1318
+ if (!copyHint) {
1319
+ copyHint = createElement('p', { className: 'muted mcp-copy-hint' });
1320
+ copyHint.textContent = 'Copy the JSON above into your IDE\'s MCP settings, or run: context serve';
1321
+ copyHint.style.marginTop = '8px';
1322
+ copyHint.style.fontSize = '0.85rem';
1323
+ var configEl = card.querySelector('.mcp-config-block');
1324
+ if (configEl && configEl.nextSibling) {
1325
+ ctaParent.insertBefore(copyHint, configEl.nextSibling);
1326
+ } else if (configEl) {
1327
+ ctaParent.appendChild(copyHint);
1328
+ }
1329
+ }
1330
+ })
1331
+ .catch(function () {
1332
+ startBtn.textContent = 'Start MCP Server';
1333
+ startBtn.disabled = false;
1334
+ });
1335
+ });
1336
+ ctas.appendChild(startBtn);
1337
+
1338
+ var publishBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Publish to Cloud' });
1339
+ publishBtn.disabled = true;
1340
+ publishBtn.title = 'Coming soon';
1341
+ ctas.appendChild(publishBtn);
1342
+
1343
+ card.appendChild(ctas);
1344
+
1345
+ // CLI Commands section
1346
+ var cmds = createElement('div', { className: 'serve-commands' });
1347
+ cmds.appendChild(createElement('div', { className: 'serve-commands-title', textContent: 'CLI Commands' }));
1348
+
1349
+ var cmdData = [
1350
+ ['context serve', 'Start the MCP server'],
1351
+ ['context tier', 'Check your current tier'],
1352
+ ['context enrich --target gold', 'Enrich to Gold tier'],
1353
+ ['context verify', 'Validate your semantic plane'],
1354
+ ];
1355
+ for (var i = 0; i < cmdData.length; i++) {
1356
+ var row = createElement('div', { className: 'serve-cmd-row' }, [
1357
+ createElement('span', { className: 'serve-cmd', textContent: cmdData[i][0] }),
1358
+ createElement('span', { className: 'serve-cmd-desc', textContent: cmdData[i][1] }),
1359
+ ]);
1360
+ cmds.appendChild(row);
1361
+ }
1362
+ card.appendChild(cmds);
1363
+
1364
+ // Continue Enrichment link (shown if not Gold — updated async)
1365
+ var continueLink = createElement('p', {});
1366
+ continueLink.style.marginTop = '16px';
1367
+ var linkEl = createElement('a', { textContent: 'Continue Enrichment' });
1368
+ linkEl.style.color = 'var(--rc-color-accent, #c9a55a)';
1369
+ linkEl.style.cursor = 'pointer';
1370
+ linkEl.style.fontSize = '0.875rem';
1371
+ linkEl.addEventListener('click', function () {
1372
+ goToStep(5);
1373
+ });
1374
+ continueLink.appendChild(linkEl);
1375
+ card.appendChild(continueLink);
1376
+
1377
+ // Hide continue link if Gold
1378
+ if (state.pipelineId) {
1379
+ fetch('/api/pipeline/status/' + state.pipelineId)
1380
+ .then(function (res) { return res.json(); })
1381
+ .then(function (data) {
1382
+ var stages = data.stages || [];
1383
+ for (var j = 0; j < stages.length; j++) {
1384
+ if (stages[j].name === 'enrich-gold' && stages[j].status === 'done') {
1385
+ continueLink.style.display = 'none';
1386
+ break;
1387
+ }
1388
+ }
1389
+ })
1390
+ .catch(function () { /* keep visible */ });
1391
+ }
1392
+
1393
+ card.appendChild(createStepActions(true, false));
1394
+ content.appendChild(card);
1395
+ }
1396
+
1397
+ // ---- Review / Build helpers (will be re-implemented in later tasks) ----
1398
+
1399
+ function createReviewRow(label, value) {
1400
+ return createElement('div', { className: 'review-row' }, [
1401
+ createElement('span', { className: 'review-label', textContent: label }),
1402
+ createElement('span', { className: 'review-value', textContent: value }),
1403
+ ]);
410
1404
  }
411
1405
 
412
1406
  // ---- Existing Products Banner ----
@@ -416,27 +1410,27 @@
416
1410
  var res = await fetch('/api/products');
417
1411
  var products = await res.json();
418
1412
  if (products.length > 0) {
419
- var banner = document.createElement('div');
420
- banner.className = 'existing-products-banner';
1413
+ var content = document.getElementById('wizard-content');
1414
+ if (!content) return;
1415
+
1416
+ var banner = createElement('div', { className: 'existing-products-banner' });
421
1417
 
422
- var title = document.createElement('p');
423
- title.className = 'banner-title';
1418
+ var title = createElement('p', { className: 'banner-title' });
424
1419
  title.textContent = 'Your semantic plane has ' + products.length + ' data product' + (products.length === 1 ? '' : 's') + '. Adding another.';
425
1420
  banner.appendChild(title);
426
1421
 
427
- var list = document.createElement('div');
428
- list.className = 'product-chips';
1422
+ var list = createElement('div', { className: 'product-chips' });
429
1423
  for (var i = 0; i < products.length; i++) {
430
- var chip = document.createElement('span');
431
- chip.className = 'product-chip';
432
- chip.textContent = products[i].name;
433
- list.appendChild(chip);
1424
+ list.appendChild(createElement('span', { className: 'product-chip', textContent: products[i].name }));
434
1425
  }
435
1426
  banner.appendChild(list);
436
1427
 
437
- var step1 = document.getElementById('step-1');
438
- if (step1) {
439
- step1.parentNode.insertBefore(banner, step1);
1428
+ // Insert banner before the first card
1429
+ var firstCard = content.querySelector('.card');
1430
+ if (firstCard) {
1431
+ content.insertBefore(banner, firstCard);
1432
+ } else {
1433
+ content.appendChild(banner);
440
1434
  }
441
1435
  }
442
1436
  } catch (e) {
@@ -444,25 +1438,117 @@
444
1438
  }
445
1439
  }
446
1440
 
447
- // ---- Init ----
1441
+ // ---- Sidebar Interactions ----
448
1442
 
449
- function init() {
450
- $$('[data-next]').forEach(function (btn) {
451
- btn.addEventListener('click', function () {
452
- if (validateStep(state.step)) goToStep(state.step + 1);
1443
+ function setupSidebarLocked() {
1444
+ var tooltip = document.getElementById('locked-tooltip');
1445
+ if (!tooltip) return;
1446
+
1447
+ $$('.nav-item.locked').forEach(function (item) {
1448
+ item.addEventListener('click', function (e) {
1449
+ e.preventDefault();
1450
+ e.stopPropagation();
1451
+ var rect = item.getBoundingClientRect();
1452
+ tooltip.style.display = 'block';
1453
+ tooltip.style.left = rect.left + 'px';
1454
+ tooltip.style.top = (rect.bottom + 6) + 'px';
453
1455
  });
454
1456
  });
455
- $$('[data-prev]').forEach(function (btn) {
456
- btn.addEventListener('click', function () {
457
- goToStep(state.step - 1);
458
- });
1457
+
1458
+ document.addEventListener('click', function (e) {
1459
+ if (tooltip.style.display !== 'none') {
1460
+ var isLocked = false;
1461
+ $$('.nav-item.locked').forEach(function (item) {
1462
+ if (item.contains(e.target)) isLocked = true;
1463
+ });
1464
+ if (!isLocked) {
1465
+ tooltip.style.display = 'none';
1466
+ }
1467
+ }
459
1468
  });
1469
+ }
1470
+
1471
+ // ---- Sidebar Status Polling ----
460
1472
 
461
- setupSensitivity();
462
- setupUpload();
463
- setupVoice();
464
- checkExistingProducts();
465
- goToStep(1);
1473
+ function pollMcpStatus() {
1474
+ async function check() {
1475
+ var dot = document.getElementById('mcp-status-dot');
1476
+ var text = document.getElementById('mcp-status-text');
1477
+ var serverDot = document.getElementById('mcp-server-dot');
1478
+ var serverText = document.getElementById('mcp-server-text');
1479
+ if (!dot || !text) return;
1480
+ try {
1481
+ var controller = new AbortController();
1482
+ var timer = setTimeout(function () { controller.abort(); }, 2000);
1483
+ var res = await fetch('http://localhost:3333/health', {
1484
+ method: 'GET',
1485
+ mode: 'no-cors',
1486
+ signal: controller.signal,
1487
+ });
1488
+ clearTimeout(timer);
1489
+ // mode: 'no-cors' returns opaque response (status 0) but means server is reachable
1490
+ dot.classList.remove('error');
1491
+ dot.classList.add('success');
1492
+ text.textContent = 'connected';
1493
+ if (serverDot) { serverDot.classList.remove('error'); serverDot.classList.add('success'); }
1494
+ if (serverText) serverText.textContent = 'MCP running';
1495
+ } catch (e) {
1496
+ dot.classList.remove('success');
1497
+ text.textContent = 'offline';
1498
+ if (serverDot) { serverDot.classList.remove('success'); }
1499
+ if (serverText) serverText.textContent = 'MCP stopped';
1500
+ }
1501
+ }
1502
+ check();
1503
+ state.mcpPollTimer = setInterval(check, 10000);
1504
+ }
1505
+
1506
+ function updateDbStatus(source) {
1507
+ var dot = document.getElementById('db-status-dot');
1508
+ var text = document.getElementById('db-status-text');
1509
+ if (!dot || !text) return;
1510
+ if (source) {
1511
+ dot.classList.remove('error');
1512
+ dot.classList.add('success');
1513
+ text.textContent = (source.name || source.adapter) + ' connected';
1514
+ } else {
1515
+ dot.classList.remove('success');
1516
+ dot.classList.add('error');
1517
+ text.textContent = 'No database';
1518
+ }
1519
+ }
1520
+
1521
+ // ---- Init ----
1522
+
1523
+ function init() {
1524
+ setupSidebarLocked();
1525
+ pollMcpStatus();
1526
+
1527
+ // Restore DB status from saved state
1528
+ if (state.sources && state.sources.length > 0) {
1529
+ updateDbStatus(state.sources[0]);
1530
+ }
1531
+
1532
+ goToStep(state.step);
1533
+
1534
+ // Create or resume a WebSocket session
1535
+ var urlParams = new URLSearchParams(window.location.search);
1536
+ var sessionParam = urlParams.get('session');
1537
+ if (sessionParam) {
1538
+ connectWebSocket(sessionParam);
1539
+ } else {
1540
+ fetch('/api/session', { method: 'POST' })
1541
+ .then(function (r) { return r.json(); })
1542
+ .then(function (data) {
1543
+ if (data.sessionId) {
1544
+ connectWebSocket(data.sessionId);
1545
+ // Update URL without reload so session persists on refresh
1546
+ var newUrl = window.location.pathname + '?session=' + encodeURIComponent(data.sessionId);
1547
+ window.history.replaceState({}, '', newUrl);
1548
+ }
1549
+ })
1550
+ .catch(function () { /* WebSocket is best-effort */ });
1551
+ }
466
1552
  }
467
1553
 
468
1554
  document.addEventListener('DOMContentLoaded', init);