PageSourceSearch

https://souqmadar.com/js/comprehensive-analysis-ux.js?v=muidaww5.90n

js souqmadar.com collected 2026-10-02 01:44:10 UTC 29,570 bytes, 746 lines download raw bytes

1/**
2 * CA-05 — Comprehensive Analysis website UX.
3 * Consumes existing Comprehensive start/status APIs. No engine changes.
4 */
5(function(global) {
6  'use strict';
7
8  var _inFlightByOriginal = Object.create(null);
9  var _pollById = Object.create(null);
10  var _cardByOriginal = Object.create(null);
11  var _LS_KEY = 'sm_ca_run_v1';
12
13  function _isAr() {
14    if (typeof global._ui === 'function') {
15      try { return document.documentElement.lang === 'ar'; } catch (_e) {}
16    }
17    return (global.PresentationContext && global.PresentationContext.language === 'ar')
18      || (document.documentElement && document.documentElement.lang === 'ar');
19  }
20
21  function _t(en, ar) {
22    return _isAr() ? ar : en;
23  }
24
25  var _runtime = null;
26
27  function _phaseMessage(phase, status, freshness) {
28    if (status === 'FAILED') {
29      return _t('Could not complete comprehensive analysis. Please try again.',
30        'تعذر إكمال التحليل الشامل. حاول مرة أخرى.');
31    }
32    if (freshness === 'STALE' && status === 'READY' && phase === 'PROCESSED_READY') {
33      return _t(
34        'This result needs an update before the latest analysis can be shown.',
35        'هذه النتيجة تحتاج إلى تحديث قبل عرض أحدث تحليل.',
36      );
37    }
38    if (status === 'READY' && phase === 'PROCESSED_READY') {
39      return _t('Comprehensive analysis complete', 'اكتمل التحليل الشامل');
40    }
41    if (status === 'READY' && phase !== 'PROCESSED_READY') {
42      return _t('Almost done…', 'اقتربنا من الانتهاء...');
43    }
44    var map = {
45      INITIALIZING: _t('Searching for the best results…', 'جاري البحث عن أفضل النتائج...'),
46      EXPANDED_RUNNING: _t('Expanding the search…', 'جاري توسيع نطاق البحث...'),
47      EXPANDED_READY: _t('Collecting more products…', 'جاري جمع المزيد من المنتجات...'),
48      SECOND_LEVEL_RUNNING: _t('Analyzing the results…', 'جاري تحليل النتائج...'),
49      SECOND_LEVEL_READY: _t('Ranking the best options…', 'جاري ترتيب أفضل الخيارات...'),
50      AGGREGATING: _t('Ranking the best options…', 'جاري ترتيب أفضل الخيارات...'),
51      PROCESSING: _t('Preparing the best results for you…', 'جاري تجهيز أفضل النتائج لك...'),
52    };
53    return map[phase] || _t('Searching for the best results…', 'جاري البحث عن أفضل النتائج...');
54  }
55
56  function _progressIcon(phase, status, freshness) {
57    if (status === 'FAILED') return '⚠️';
58    if (freshness === 'STALE' && phase === 'PROCESSED_READY') return 'ℹ️';
59    if (status === 'READY' && phase === 'PROCESSED_READY') return '✓';
60    if (phase === 'PROCESSING') return '✨';
61    if (phase === 'AGGREGATING' || phase === 'SECOND_LEVEL_READY') return '✨';
62    if (phase === 'SECOND_LEVEL_RUNNING') return '📊';
63    return '🔎';
64  }
65
66  function _progressPercent(data, previousPct) {
67    data = data || {};
68    var status = data.status || 'RUNNING';
69    var phase = data.phase || 'INITIALIZING';
70    var freshness = data.freshnessStatus || '';
71    var prev = Math.max(0, Math.min(100, Math.round(previousPct || 0)));
72    if (status === 'FAILED') {
73      return Math.min(prev || 0, 99);
74    }
75    if (status === 'READY' && phase === 'PROCESSED_READY' && freshness === 'STALE') {
76      return Math.min(Math.max(prev, 90), 90);
77    }
78    if (status === 'READY' && phase === 'PROCESSED_READY') {
79      return 100;
80    }
81    var next = 5;
82    if (status === 'READY' && phase !== 'PROCESSED_READY') next = 95;
83    else if (phase === 'INITIALIZING' || phase === 'PENDING') next = 5;
84    else if (phase === 'EXPANDED_RUNNING') next = 20;
85    else if (phase === 'EXPANDED_READY') next = 30;
86    else if (phase === 'SECOND_LEVEL_RUNNING') {
87      var sl = data.secondLevel || {};
88      var total = Number(sl.seedsDiscovered) || 0;
89      var done = (Number(sl.completed) || 0) + (Number(sl.failed) || 0);
90      if (total > 0) next = Math.round(35 + (40 * (done / total)));
91      else next = 35;
92    } else if (phase === 'SECOND_LEVEL_READY' || phase === 'AGGREGATING') next = 80;
93    else if (phase === 'PROCESSING') next = 90;
94    if (next > 100) next = 100;
95    if (next < 0) next = 0;
96    return Math.max(prev, next);
97  }
98
99  function _stageIndex(phase) {
100    if (phase === 'PROCESSING' || phase === 'PROCESSED_READY') return 4;
101    if (phase === 'AGGREGATING') return 3;
102    if (phase === 'SECOND_LEVEL_RUNNING' || phase === 'SECOND_LEVEL_READY') return 2;
103    if (phase === 'EXPANDED_RUNNING' || phase === 'EXPANDED_READY') return 1;
104    return 0;
105  }
106
107  function _loadStore() {
108    try {
109      return JSON.parse(localStorage.getItem(_LS_KEY) || '{}') || {};
110    } catch (_e) {
111      return {};
112    }
113  }
114
115  function _saveRun(originalAnalysisId, comprehensiveAnalysisId, productId) {
116    if (!originalAnalysisId || !comprehensiveAnalysisId) return;
117    var store = _loadStore();
118    store[String(originalAnalysisId)] = {
119      ca: String(comprehensiveAnalysisId),
120      pid: productId ? String(productId) : '',
121    };
122    try { localStorage.setItem(_LS_KEY, JSON.stringify(store)); } catch (_e) {}
123  }
124
125  function _savedEntry(originalAnalysisId) {
126    var raw = _loadStore()[String(originalAnalysisId)];
127    if (!raw) return null;
128    if (typeof raw === 'string') return { ca: raw, pid: '' };
129    return { ca: String(raw.ca || ''), pid: String(raw.pid || '') };
130  }
131
132  function _savedRun(originalAnalysisId) {
133    var entry = _savedEntry(originalAnalysisId);
134    return entry && entry.ca ? entry.ca : '';
135  }
136
137  function _clearRun(originalAnalysisId) {
138    var store = _loadStore();
139    delete store[String(originalAnalysisId)];
140    try { localStorage.setItem(_LS_KEY, JSON.stringify(store)); } catch (_e) {}
141  }
142
143  function _findCardByPid(pid) {
144    if (!pid || !global.document) return null;
145    if (typeof global._hpCardByPid === 'function') {
146      var found = global._hpCardByPid(pid);
147      if (found) return found;
148    }
149    return global.document.querySelector(
150      '.pc-card[data-pid="' + pid + '"], .hp2-card[data-pid="' + pid + '"], [data-pid="' + pid + '"].upc-anl-card'
151    );
152  }
153
154  function _attachCard(cardEl, pid) {
155    if (!cardEl || !pid) return;
156    cardEl.setAttribute('data-ca-running', '1');
157    cardEl.setAttribute('data-ea-running', '1');
158    cardEl.setAttribute('data-ca-pid', String(pid));
159    cardEl.classList.add('upc-anl-card', 'upc-anl-card--expanded');
160    var toggle = cardEl.querySelector('.upc-analyze-toggle');
161    if (toggle) {
162      toggle.disabled = true;
163      toggle.setAttribute('aria-busy', 'true');
164      toggle.classList.add('upc-analyze-toggle-busy');
165    }
166    var prev = cardEl.querySelector('.upc-anl-section--ca');
167    if (prev) prev.remove();
168    var sec = global.document.createElement('div');
169    sec.className = 'upc-anl-section upc-anl-section--expanded upc-anl-section--ca';
170    sec.setAttribute('data-ca-card-progress', '1');
171    sec.innerHTML =
172      '<div class="upc-anl-type-badge">' + _t('Comprehensive analysis', 'التحليل الشامل') + '</div>' +
173      '<div class="upc-anl-header">' +
174        '<div class="upc-anl-spinner upc-anl-spinner--expanded" aria-hidden="true"></div>' +
175        '<span class="upc-anl-msg-main" data-ca-card-msg="1">' +
176          _phaseMessage('INITIALIZING', 'RUNNING') +
177        '</span>' +
178        '<span class="upc-anl-pct" data-ca-card-pct="1"></span>' +
179      '</div>' +
180      '<p class="upc-anl-exp-hint">⏱ ' +
181        _t('Takes several minutes on average', 'يستغرق في المتوسط عدة دقائق') +
182      '</p>';
183    cardEl.appendChild(sec);
184  }
185
186  function _updateCard(cardEl, phase, status, freshness, pct) {
187    if (!cardEl) return;
188    var msgEl = cardEl.querySelector('[data-ca-card-msg]');
189    if (msgEl) msgEl.textContent = _phaseMessage(phase, status, freshness);
190    var pctEl = cardEl.querySelector('[data-ca-card-pct]');
191    if (pctEl && typeof pct === 'number') pctEl.textContent = Math.round(pct) + '%';
192    try {
193      global.document.dispatchEvent(new CustomEvent('ca:progress-update', {
194        detail: {
195          pid: cardEl.getAttribute('data-ca-pid') || '',
196          phase: phase || '',
197          status: status || '',
198          msg: _phaseMessage(phase, status),
199        },
200      }));
201    } catch (_e) {}
202  }
203
204  function _releaseCard(cardEl, pid, outcome) {
205    outcome = outcome || {};
206    if (cardEl) {
207      var sec = cardEl.querySelector('.upc-anl-section--ca');
208      if (sec) sec.remove();
209      cardEl.removeAttribute('data-ca-running');
210      cardEl.removeAttribute('data-ea-running');
211      cardEl.removeAttribute('data-analyzing');
212      cardEl.removeAttribute('data-ca-pid');
213      cardEl.classList.remove('upc-anl-card', 'upc-anl-card--expanded');
214      var toggle = cardEl.querySelector('.upc-analyze-toggle');
215      if (toggle) {
216        toggle.disabled = false;
217        toggle.removeAttribute('aria-busy');
218        toggle.classList.remove('upc-analyze-toggle-busy', 'upc-act-find-loading');
219      }
220    }
221    if (pid && typeof global.unlockCardForAnalysis === 'function') {
222      global.unlockCardForAnalysis(String(pid));
223    }
224    try {
225      global.document.dispatchEvent(new CustomEvent(outcome.failed ? 'ca:job-failed' : 'ca:job-complete', {
226        detail: { pid: pid || '', url: outcome.url || '', stale: !!outcome.stale },
227      }));
228    } catch (_e) {}
229  }
230
231  function _setBusy(btn, busy) {
232    if (!btn) return;
233    btn.disabled = !!busy;
234    if (busy) btn.setAttribute('aria-busy', 'true');
235    else btn.removeAttribute('aria-busy');
236  }
237
238  function _ensureBanner() {
239    var el = document.getElementById('caUxBanner');
240    if (el) return el;
241    el = document.createElement('div');
242    el.id = 'caUxBanner';
243    el.className = 'ea-ux-banner';
244    el.setAttribute('role', 'status');
245    el.setAttribute('aria-live', 'polite');
246    el.innerHTML =
247      '<div class="ea-ux-banner__inner">' +
248        '<div class="ea-ux-banner__icon" id="caUxBannerIcon" aria-hidden="true">🔎</div>' +
249        '<div class="ea-ux-banner__body">' +
250          '<div class="ea-ux-banner__type" id="caUxBannerType"></div>' +
251          '<div class="ea-ux-banner__msg" id="caUxBannerMsg"></div>' +
252          '<div class="ea-ux-banner__pct" id="caUxBannerPct"></div>' +
253          '<div class="ea-ux-banner__bar-wrap" id="caUxBannerBarWrap" aria-hidden="true">' +
254            '<div class="ea-ux-banner__bar" id="caUxBannerBar"></div>' +
255          '</div>' +
256          '<div class="ea-ux-banner__stages" id="caUxBannerStages" hidden></div>' +
257        '</div>' +
258        '<button type="button" class="ea-ux-banner__close" id="caUxBannerClose" aria-label="' +
259          _t('Close', 'إغلاق') + '">×</button>' +
260      '</div>';
261    document.body.appendChild(el);
262    var closeBtn = document.getElementById('caUxBannerClose');
263    if (closeBtn) {
264      closeBtn.addEventListener('click', function(ev) {
265        ev.stopPropagation();
266        el.hidden = true;
267      });
268    }
269    return el;
270  }
271
272  function _paintBannerBar(pct, failed, stale) {
273    var bar = document.getElementById('caUxBannerBar');
274    var wrap = document.getElementById('caUxBannerBarWrap');
275    var pctEl = document.getElementById('caUxBannerPct');
276    if (pctEl) {
277      pctEl.hidden = !!failed;
278      if (!failed && typeof pct === 'number') pctEl.textContent = Math.round(pct) + '%';
279    }
280    if (!bar || !wrap) return;
281    wrap.hidden = !!failed;
282    if (failed) return;
283    var safe = Math.max(0, Math.min(100, Math.round(pct || 0)));
284    bar.classList.add('ea-ux-banner__bar--determinate');
285    bar.style.width = safe + '%';
286    bar.style.animation = (safe >= 100 && !stale) ? 'none' : '';
287  }
288
289  function _syncSharedState(originalAnalysisId, data) {
290    data = data || {};
291    var phase = data.phase || 'INITIALIZING';
292    var status = data.status || 'RUNNING';
293    var freshness = data.freshnessStatus || '';
294    var caId = data.comprehensiveAnalysisId || '';
295    var msg = _phaseMessage(phase, status, freshness);
296    var icon = _progressIcon(phase, status, freshness);
297    var ready = status === 'READY' && phase === 'PROCESSED_READY';
298    var failed = status === 'FAILED';
299    var stale = freshness === 'STALE' && ready;
300    var url = (caId && originalAnalysisId) ? _resultUrl(originalAnalysisId, caId) : '';
301    var pct = _progressPercent(data, _runtime && _runtime.pct);
302    _runtime = {
303      originalAnalysisId: originalAnalysisId || '',
304      comprehensiveAnalysisId: caId,
305      phase: phase,
306      status: status,
307      freshnessStatus: freshness,
308      msg: msg,
309      icon: icon,
310      url: url,
311      pct: pct,
312    };
313    var el = _ensureBanner();
314    el.hidden = false;
315    var typeEl = document.getElementById('caUxBannerType');
316    var msgEl = document.getElementById('caUxBannerMsg');
317    var iconEl = document.getElementById('caUxBannerIcon');
318    if (typeEl) typeEl.textContent = _t('Comprehensive analysis', 'التحليل الشامل');
319    if (msgEl) msgEl.textContent = msg;
320    if (iconEl) iconEl.textContent = icon;
321    _paintBannerBar(pct, failed, stale);
322    if (ready && url && !stale) {
323      el.style = el.style || {};
324      el.style.cursor = 'pointer';
325      el.onclick = function(ev) {
326        if (ev.target && ev.target.id === 'caUxBannerClose') return;
327        global.location.href = url;
328      };
329    } else {
330      el.onclick = null;
331      el.style = el.style || {};
332      el.style.cursor = '';
333    }
334    _syncCardFromStatus(originalAnalysisId, phase, status, freshness, pct);
335    if (originalAnalysisId && global.SMQ && typeof global.SMQ.setJobMeta === 'function') {
336      if (typeof global.SMQ.handoffComprehensiveProgress === 'function') {
337        global.SMQ.handoffComprehensiveProgress(originalAnalysisId);
338      }
339      global.SMQ.setJobMeta(originalAnalysisId, {
340        analysisType: 'comprehensive',
341        caProgressOwned: true,
342        eaProgressOwned: false,
343        clearExpandedPct: false,
344        comprehensiveAnalysisId: caId,
345        freshnessStatus: freshness,
346        expandedPhase: failed ? 'failed' : (ready ? 'done' : 'running'),
347        expandedProgressMsg: msg,
348        expandedDisplayPct: pct,
349        displayPct: pct,
350        targetPct: pct,
351        progress: pct,
352        status: failed ? 'failed' : (ready ? 'done' : 'running'),
353        url: url || undefined,
354      });
355    }
356    try {
357      global.document.dispatchEvent(new CustomEvent('ca:state', { detail: _runtime }));
358      global.document.dispatchEvent(new CustomEvent('ca:progress-update', {
359        detail: {
360          pid: (_cardByOriginal[originalAnalysisId] || {}).pid || '',
361          originalAnalysisId: originalAnalysisId || '',
362          comprehensiveAnalysisId: caId,
363          phase: phase,
364          status: status,
365          freshnessStatus: freshness,
366          msg: msg,
367        },
368      }));
369    } catch (_e) {}
370    return _runtime;
371  }
372
373  function _showProgress(phase, status, extras) {
374    extras = extras || {};
375    _syncSharedState(extras.originalAnalysisId || extras.jobId || '', {
376      phase: phase,
377      status: status,
378      freshnessStatus: extras.freshnessStatus || '',
379      comprehensiveAnalysisId: extras.comprehensiveAnalysisId || extras.caId || '',
380    });
381  }
382
383  function _showFailed(onRetry, originalAnalysisId) {
384    _syncSharedState(originalAnalysisId || '', { phase: 'FAILED', status: 'FAILED' });
385    var el = _ensureBanner();
386    var actions = document.getElementById('caUxBannerStages');
387    if (!actions) return;
388    actions.hidden = false;
389    actions.innerHTML = '<button type="button" class="ea-ux-btn ea-ux-btn--secondary" id="caUxRetry">' +
390      _t('Try again', 'إعادة المحاولة') + '</button>';
391    var retry = document.getElementById('caUxRetry');
392    if (retry && typeof onRetry === 'function') {
393      retry.addEventListener('click', function() {
394        el.hidden = true;
395        onRetry();
396      });
397    }
398  }
399
400  function _resultUrl(originalAnalysisId, comprehensiveAnalysisId) {
401    return '/analysis-summary/' + encodeURIComponent(originalAnalysisId)
402      + '?comprehensive_analysis_id=' + encodeURIComponent(comprehensiveAnalysisId);
403  }
404
405  function _parseBody(json) {
406    if (!json || json.ok === false) return null;
407    return json.data || json;
408  }
409
410  function _syncCardFromStatus(originalAnalysisId, phase, status, freshness, pct) {
411    var ctx = _cardByOriginal[originalAnalysisId];
412    if (ctx && ctx.el) _updateCard(ctx.el, phase, status, freshness, pct);
413  }
414
415  function _bindCardWhenReady(originalAnalysisId, pid) {
416    if (!pid) return;
417    function tryBind() {
418      var card = _findCardByPid(pid);
419      if (!card) return false;
420      _cardByOriginal[originalAnalysisId] = { el: card, pid: String(pid) };
421      if (card.getAttribute('data-ca-running') !== '1') _attachCard(card, pid);
422      return true;
423    }
424    if (tryBind() || !global.document) return;
425    var root = global.document.getElementById('hpContent') || global.document.body;
426    if (!root || !global.MutationObserver) return;
427    var obs = new MutationObserver(function() {
428      if (tryBind()) obs.disconnect();
429    });
430    obs.observe(root, { childList: true, subtree: true });
431    setTimeout(function() { try { obs.disconnect(); } catch (_e) {} }, 60000);
432  }
433
434  function _isStillRunning(data) {
435    if (!data) return false;
436    if (data.status === 'FAILED') return false;
437    if (data.status === 'READY' && data.phase === 'PROCESSED_READY') return false;
438    return data.status === 'PENDING' || data.status === 'RUNNING'
439      || (data.status === 'READY' && data.phase !== 'PROCESSED_READY');
440  }
441
442  function _pollStatus(comprehensiveAnalysisId, originalAnalysisId, btn) {
443    if (_pollById[comprehensiveAnalysisId]) return _pollById[comprehensiveAnalysisId];
444    var promise = new Promise(function(resolve, reject) {
445      var attempts = 0;
446      function tick() {
447        attempts += 1;
448        fetch('/api/comprehensive-analysis/customer/' + encodeURIComponent(comprehensiveAnalysisId) + '/status', {
449          cache: 'no-store',
450        })
451          .then(function(r) { return r.json(); })
452          .then(function(body) {
453            var data = _parseBody(body);
454            if (!data) {
455              if (attempts > 900) return reject(new Error('status_failed'));
456              return setTimeout(tick, 2000);
457            }
458            _syncSharedState(originalAnalysisId, data);
459            if (data.status === 'FAILED') return resolve(data);
460            if (data.status === 'READY' && data.phase === 'PROCESSED_READY') return resolve(data);
461            if (attempts > 900) return reject(new Error('timeout'));
462            setTimeout(tick, 2500);
463          })
464          .catch(function() {
465            if (attempts > 900) return reject(new Error('timeout'));
466            setTimeout(tick, 3000);
467          });
468      }
469      tick();
470    }).finally(function() {
471      delete _pollById[comprehensiveAnalysisId];
472      _setBusy(btn, false);
473    });
474    _pollById[comprehensiveAnalysisId] = promise;
475    return promise;
476  }
477
478  function _handleTerminal(data, originalAnalysisId, btn) {
479    if (!data) return;
480    var ctx = _cardByOriginal[originalAnalysisId] || {};
481    var cardEl = ctx.el || null;
482    var pid = ctx.pid || '';
483    if (data.status === 'FAILED') {
484      _releaseCard(cardEl, pid, { failed: true });
485      delete _cardByOriginal[originalAnalysisId];
486      _clearRun(originalAnalysisId);
487      _showFailed(function() { startForJob(originalAnalysisId, { button: btn, force: true, cardEl: cardEl, pid: pid }); }, originalAnalysisId);
488      return;
489    }
490    if (data.status === 'READY' && data.phase === 'PROCESSED_READY') {
491      _releaseCard(cardEl, pid, { stale: data.freshnessStatus === 'STALE', url: _resultUrl(originalAnalysisId, data.comprehensiveAnalysisId) });
492      delete _cardByOriginal[originalAnalysisId];
493    }
494    if (data.freshnessStatus === 'STALE') {
495      _syncSharedState(originalAnalysisId, data);
496      return;
497    }
498    if (data.freshnessStatus === 'UNKNOWN' && data.processedReady) {
499      _syncSharedState(originalAnalysisId, {
500        phase: 'PROCESSING',
501        status: 'RUNNING',
502        comprehensiveAnalysisId: data.comprehensiveAnalysisId,
503        freshnessStatus: data.freshnessStatus,
504      });
505      return;
506    }
507    if (data.processedReady && data.freshnessStatus !== 'STALE') {
508      var url = _resultUrl(originalAnalysisId, data.comprehensiveAnalysisId);
509      _clearRun(originalAnalysisId);
510      _syncSharedState(originalAnalysisId, data);
511      setTimeout(function() { global.location.href = url; }, 600);
512    }
513  }
514
515  function startForJob(originalAnalysisId, opts) {
516    opts = opts || {};
517    var jobId = String(originalAnalysisId || '').trim();
518    var btn = opts.button || null;
519    var pid = opts.pid ? String(opts.pid) : '';
520    var cardEl = opts.cardEl || null;
521    if (cardEl && pid) {
522      _cardByOriginal[jobId] = { el: cardEl, pid: pid };
523      _attachCard(cardEl, pid);
524    }
525    if (!jobId) return Promise.resolve(null);
526    if (_inFlightByOriginal[jobId] && !opts.force) {
527      _setBusy(btn, true);
528      return _inFlightByOriginal[jobId];
529    }
530
531    _setBusy(btn, true);
532    _syncSharedState(jobId, { phase: 'INITIALIZING', status: 'RUNNING' });
533
534    function persistAndContinue(data, caId) {
535      var keepPid = pid || ((_savedEntry(jobId) || {}).pid || '');
536      _saveRun(jobId, caId, keepPid);
537      if (data.status === 'FAILED') return data;
538      if (data.status === 'READY' && data.phase === 'PROCESSED_READY') return data;
539      return _pollStatus(caId, jobId, btn);
540    }
541
542    var saved = !opts.force ? _savedRun(jobId) : '';
543    var work = (saved
544      ? fetch('/api/comprehensive-analysis/customer/' + encodeURIComponent(saved) + '/status', { cache: 'no-store' })
545          .then(function(r) { return r.json(); })
546          .then(function(body) {
547            var data = _parseBody(body);
548            if (!data) throw new Error('resume_failed');
549            _syncSharedState(jobId, data);
550            return persistAndContinue(data, saved);
551          })
552      : fetch('/api/comprehensive-analysis/customer/start', {
553          method: 'POST',
554          headers: { 'Content-Type': 'application/json' },
555          body: JSON.stringify({ originalAnalysisId: jobId }),
556        })
557          .then(function(r) { return r.json(); })
558          .then(function(body) {
559            if (!body || body.ok === false) throw new Error('start_failed');
560            var data = body.data || {};
561            var caId = data.comprehensiveAnalysisId;
562            if (!caId) throw new Error('start_failed');
563            _syncSharedState(jobId, data);
564            return persistAndContinue(data, caId);
565          })
566    )
567      .then(function(data) {
568        _handleTerminal(data, jobId, btn);
569        return data;
570      })
571      .catch(function() {
572        _releaseCard(cardEl, pid, { failed: true });
573        delete _cardByOriginal[jobId];
574        _showFailed(function() { startForJob(jobId, { button: btn, force: true, cardEl: cardEl, pid: pid }); }, jobId);
575        return null;
576      })
577      .finally(function() {
578        delete _inFlightByOriginal[jobId];
579        _setBusy(btn, false);
580      });
581
582    _inFlightByOriginal[jobId] = work;
583    return work;
584  }
585
586  async function startFromHomepage(candidate, sourceType, cardEl) {
587    if (!candidate) return;
588    var pid = String(candidate.product_id || '');
589    var btn = cardEl && (cardEl.querySelector('.upc-analyze-toggle') || cardEl.querySelector('.upc-act-expanded'));
590    if (typeof global.lockCardForAnalysis === 'function') {
591      global.lockCardForAnalysis(pid);
592    }
593    if (cardEl && pid) {
594      _cardByOriginal['pid:' + pid] = { el: cardEl, pid: pid };
595      _attachCard(cardEl, pid);
596    }
597    _syncSharedState('', { phase: 'INITIALIZING', status: 'RUNNING' });
598    var jobId = null;
599    var result = null;
600    try {
601      if (typeof global.startAnalysisJob === 'function') {
602        result = await global.startAnalysisJob(candidate, sourceType === 'homepage-expanded' ? 'homepage-comprehensive' : (sourceType || 'homepage-comprehensive'));
603        jobId = result && result.jobId;
604      }
605      if (!jobId) throw new Error('no_job');
606      if (cardEl && pid) {
607        delete _cardByOriginal['pid:' + pid];
608        _cardByOriginal[jobId] = { el: cardEl, pid: pid };
609      }
610      _syncSharedState(jobId, { phase: 'INITIALIZING', status: 'RUNNING' });
611      if (!result || !result.cacheHit) {
612        await _waitJobComplete(jobId);
613      }
614      return startForJob(jobId, { button: btn, cardEl: cardEl, pid: pid });
615    } catch (_e) {
616      _releaseCard(cardEl, pid, { failed: true });
617      delete _cardByOriginal['pid:' + pid];
618      if (jobId) delete _cardByOriginal[jobId];
619      _showFailed(function() { startFromHomepage(candidate, sourceType, cardEl); }, jobId || '');
620    }
621  }
622
623  function restoreHomepageRuns() {
624    var init = global.__ASP_INIT__ || {};
625    if (init.JOB_ID) return;
626    var store = _loadStore();
627    Object.keys(store).forEach(function(jobId) {
628      var entry = _savedEntry(jobId);
629      if (!entry || !entry.ca) return;
630      fetch('/api/comprehensive-analysis/customer/' + encodeURIComponent(entry.ca) + '/status', { cache: 'no-store' })
631        .then(function(r) { return r.json(); })
632        .then(function(body) {
633          var data = _parseBody(body);
634          if (!data) return;
635          if (!_isStillRunning(data)) return;
636          _syncSharedState(jobId, data);
637          if (entry.pid) _bindCardWhenReady(jobId, entry.pid);
638          if (_inFlightByOriginal[jobId]) return;
639          var work = _pollStatus(entry.ca, jobId, null).then(function(done) {
640            _handleTerminal(done, jobId, null);
641            return done;
642          });
643          _inFlightByOriginal[jobId] = work;
644        })
645        .catch(function() {});
646    });
647  }
648
649  function _waitJobComplete(jobId) {
650    return new Promise(function(resolve, reject) {
651      var attempts = 0;
652      function tick() {
653        attempts += 1;
654        fetch('/api/analysis/' + encodeURIComponent(jobId) + '/data', { cache: 'no-store' })
655          .then(function(r) { return r.json(); })
656          .then(function(body) {
657            if (body && body.ok && body.job && body.job.status === 'complete') return resolve(body.job);
658            if (body && body.job && (body.job.status === 'error' || body.job.status === 'cancelled')) {
659              return reject(new Error(body.job.status));
660            }
661            if (attempts > 900) return reject(new Error('timeout'));
662            setTimeout(tick, 2000);
663          })
664          .catch(function(err) {
665            if (attempts > 900) return reject(err);
666            setTimeout(tick, 2500);
667          });
668      }
669      tick();
670    });
671  }
672
673  function renderCtaHtml() {
674    var title = _t('Comprehensive analysis', 'التحليل الشامل');
675    var hint = _t('Takes several minutes on average', 'يستغرق في المتوسط عدة دقائق');
676    return (
677      '<section class="ea-ux-cta ea-ux-cta--top" aria-label="' + title + '">' +
678        '<div class="ea-ux-cta__hd">' + title + '</div>' +
679        '<p class="ea-ux-cta__hint">⏱ ' + hint + '</p>' +
680        '<button type="button" class="ea-ux-btn ea-ux-btn--primary ca-ux-start-btn">' + title + '</button>' +
681      '</section>'
682    );
683  }
684
685  function bindAnalysisPageCtas() {
686    var init = global.__ASP_INIT__ || {};
687    if (init.COMPREHENSIVE_ANALYSIS_ID || init.EXPANDED_SEARCH_ID) return;
688    var jobId = init.JOB_ID || '';
689    if (!jobId) return;
690
691    var topHost = document.getElementById('eaUxCtaTop');
692    var bottomHost = document.getElementById('eaUxCtaBottom');
693    if (topHost && !topHost.innerHTML) topHost.innerHTML = renderCtaHtml();
694    if (bottomHost && !bottomHost.innerHTML) bottomHost.innerHTML = renderCtaHtml();
695
696    document.querySelectorAll('.ca-ux-start-btn').forEach(function(btn) {
697      if (btn.getAttribute('data-ca-bound') === '1') return;
698      btn.setAttribute('data-ca-bound', '1');
699      btn.addEventListener('click', function() {
700        if (btn.disabled) return;
701        startForJob(jobId, { button: btn });
702      });
703    });
704
705    var saved = _savedRun(jobId);
706    if (saved) {
707      fetch('/api/comprehensive-analysis/customer/' + encodeURIComponent(saved) + '/status', { cache: 'no-store' })
708        .then(function(r) { return r.json(); })
709        .then(function(body) {
710          var data = _parseBody(body);
711          if (!data) return;
712          if (_isStillRunning(data)) {
713            document.querySelectorAll('.ca-ux-start-btn').forEach(function(b) { _setBusy(b, true); });
714            _syncSharedState(jobId, data);
715            if (!_inFlightByOriginal[jobId]) {
716              _inFlightByOriginal[jobId] = _pollStatus(saved, jobId, null).then(function(done) {
717                _handleTerminal(done, jobId, null);
718                return done;
719              });
720            }
721          }
722        })
723        .catch(function() {});
724    }
725  }
726
727  global.ComprehensiveAnalysisUX = {
728    startForJob: startForJob,
729    startFromHomepage: startFromHomepage,
730    bindAnalysisPageCtas: bindAnalysisPageCtas,
731    restoreHomepageRuns: restoreHomepageRuns,
732    renderCtaHtml: renderCtaHtml,
733    phaseMessage: _phaseMessage,
734    stageIndex: _stageIndex,
735    progressPercent: _progressPercent,
736    getRuntime: function() { return _runtime; },
737  };
738
739  if (global.document) {
740    if (global.document.readyState === 'loading') {
741      global.document.addEventListener('DOMContentLoaded', restoreHomepageRuns);
742    } else {
743      setTimeout(restoreHomepageRuns, 0);
744    }
745  }
746})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.