PageSourceSearch

https://haiphen.io/components/managed-trading/managed-trading.js

js haiphen.io collected 2026-10-01 12:02:30 UTC 27,658 bytes, 754 lines download raw bytes

1/* docs/components/managed-trading/managed-trading.js
2 * Managed Trading section — pro-tier gated.
3 * Pre-eligibility checklist, tax guidance, broker/custody, SOW, risk disclosure.
4 */
5(function () {
6  'use strict';
7
8  var NS = (window.HAIPHEN = window.HAIPHEN || {});
9  var LOG = '[managed-trading]';
10
11  var AUTH_ORIGIN = 'https://auth.haiphen.io';
12  var API_ORIGIN = 'https://api.haiphen.io';
13  var AGREEMENT_VERSION = 'maa_v1.0_2026-05-01';
14  var AGREEMENT_URL = '/assets/legal/managed-trading/agreement.html';
15  var MOUNT_ID = 'content-widget';
16
17  var _sidebarObserver = null;
18
19  function qs(sel, root) {
20    return (root || document).querySelector(sel);
21  }
22
23  function qa(sel, root) {
24    return [].slice.call((root || document).querySelectorAll(sel));
25  }
26
27  async function fetchText(url) {
28    var r = await fetch(url, { cache: 'no-store' });
29    if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
30    return await r.text();
31  }
32
33  async function injectCssOnce(href) {
34    var already = document.querySelector('link[rel="stylesheet"][href="' + href + '"]');
35    if (already) return;
36    var link = document.createElement('link');
37    link.rel = 'stylesheet';
38    link.href = href;
39    document.head.appendChild(link);
40  }
41
42  /* ================================================================
43     SECTIONS — sidebar pill data
44     ================================================================ */
45
46  var SECTIONS = [
47    { id: 'mt-overview',    label: 'Overview' },
48    { id: 'mt-terms',       label: 'Key Terms' },
49    { id: 'mt-broker',      label: 'Broker' },
50    { id: 'mt-checklist',   label: 'Checklist' },
51    { id: 'mt-tax',         label: 'Tax Scenarios' },
52    { id: 'mt-filing',      label: 'Tax Filing' },
53    { id: 'mt-reports',     label: 'Reports' },
54    { id: 'mt-transition',  label: 'Transition' },
55    { id: 'mt-risk',        label: 'Risk' },
56    { id: 'mt-agreement',   label: 'Agreement' },
57    { id: 'mt-tracker',     label: 'Tracker' },
58  ];
59
60  /* ================================================================
61     SIDEBAR
62     ================================================================ */
63
64  function renderSidebar(root) {
65    var nav = qs('.hp-mt__sidebar', root);
66    if (!nav) return;
67
68    var pills = SECTIONS.map(function (sec) {
69      return '<button class="hp-mt__pill" data-section="' + sec.id + '">' + sec.label + '</button>';
70    }).join('');
71
72    nav.innerHTML = pills;
73  }
74
75  function wireSidebarClicks(root) {
76    var nav = qs('.hp-mt__sidebar', root);
77    if (!nav) return;
78
79    nav.addEventListener('click', function (e) {
80      var pill = e.target.closest('.hp-mt__pill');
81      if (!pill) return;
82
83      var sectionId = pill.getAttribute('data-section');
84      var target = document.getElementById(sectionId);
85      if (!target) return;
86
87      target.scrollIntoView({ behavior: 'smooth', block: 'start' });
88
89      qa('.hp-mt__pill', nav).forEach(function (p) {
90        p.classList.toggle('is-active', p === pill);
91      });
92    });
93  }
94
95  function wireSidebarScrollTracking(root) {
96    if (_sidebarObserver) {
97      _sidebarObserver.disconnect();
98      _sidebarObserver = null;
99    }
100
101    var nav = qs('.hp-mt__sidebar', root);
102    if (!nav) return;
103
104    _sidebarObserver = new IntersectionObserver(function (entries) {
105      entries.forEach(function (entry) {
106        if (!entry.isIntersecting) return;
107        var id = entry.target.id;
108        if (!id) return;
109
110        qa('.hp-mt__pill', nav).forEach(function (pill) {
111          pill.classList.toggle('is-active', pill.getAttribute('data-section') === id);
112        });
113      });
114    }, {
115      rootMargin: '-80px 0px -60% 0px',
116      threshold: 0,
117    });
118
119    SECTIONS.forEach(function (sec) {
120      var el = document.getElementById(sec.id);
121      if (el) _sidebarObserver.observe(el);
122    });
123  }
124
125  /* ================================================================
126     ENTITLEMENT CHECK — pro tier required
127     Returns: { entitled: bool, loggedIn: bool }
128     ================================================================ */
129
130  async function checkEntitlement() {
131    // First check if user is logged in via /v1/me
132    var loggedIn = false;
133    try {
134      var meRes = await fetch(API_ORIGIN + '/v1/me', {
135        credentials: 'include',
136        cache: 'no-store',
137      });
138      loggedIn = meRes.ok;
139    } catch (_) {}
140
141    // Then check entitlement (pro tier)
142    var entitled = false;
143    try {
144      var r = await fetch(AUTH_ORIGIN + '/entitlement', {
145        credentials: 'include',
146        cache: 'no-store',
147      });
148      if (r.ok) {
149        var data = await r.json();
150        entitled = !!data?.entitled;
151      }
152    } catch (_) {}
153
154    return { entitled: entitled, loggedIn: loggedIn };
155  }
156
157  /* ================================================================
158     AGREEMENT INLINE SIGNING
159     ================================================================ */
160
161  async function sha256Hex(text) {
162    var buf = new TextEncoder().encode(text);
163    var hash = await crypto.subtle.digest('SHA-256', buf);
164    return Array.from(new Uint8Array(hash)).map(function (b) {
165      return b.toString(16).padStart(2, '0');
166    }).join('');
167  }
168
169  async function initAgreementFlow(root) {
170    var elLoading = qs('[data-mt-agree-loading]', root);
171    var elSign = qs('[data-mt-agree-sign]', root);
172    var elDone = qs('[data-mt-agree-done]', root);
173    if (!elLoading || !elSign || !elDone) return;
174
175    // 1. Fetch user profile
176    var profile = null;
177    try {
178      var r = await fetch(API_ORIGIN + '/v1/me', { credentials: 'include', cache: 'no-store' });
179      if (r.ok) profile = await r.json();
180    } catch (e) { console.warn(LOG, '/me fetch failed', e); }
181
182    // 2. Check signing status
183    var status = null;
184    try {
185      var r2 = await fetch(API_ORIGIN + '/v1/managed-account/agreement/status?version=' + encodeURIComponent(AGREEMENT_VERSION), {
186        credentials: 'include', cache: 'no-store',
187      });
188      if (r2.ok) status = await r2.json();
189    } catch (e) { console.warn(LOG, 'status fetch failed', e); }
190
191    // Always hide loading — no matter what happens below
192    elLoading.hidden = true;
193
194    // 3. If already signed — show confirmation
195    if (status && status.signed) {
196      elSign.hidden = true;
197      elDone.hidden = false;
198      var detail = qs('[data-mt-agree-done-detail]', elDone);
199      if (detail) {
200        detail.textContent = 'Signed by ' + status.signer_name +
201          ' on ' + (status.accepted_at || '').slice(0, 10) +
202          (status.agreement_id ? '  (ID: ' + status.agreement_id.slice(0, 8) + '...)' : '');
203      }
204      return;
205    }
206
207    // 4. Show signing flow
208    elSign.hidden = false;
209
210    // 5. Fetch agreement HTML, compute SHA-256, inject
211    var agreementHtml = '';
212    try {
213      agreementHtml = await fetchText(AGREEMENT_URL);
214    } catch (e) {
215      console.warn(LOG, 'Failed to fetch agreement', e);
216      return;
217    }
218
219    // Extract just the <body> content (strip <html>, <head>, <style>)
220    var bodyMatch = agreementHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
221    var bodyContent = bodyMatch ? bodyMatch[1] : agreementHtml;
222
223    var contentHash = await sha256Hex(agreementHtml);
224    var scrollEl = qs('[data-mt-agree-scroll]', elSign);
225    if (scrollEl) scrollEl.innerHTML = bodyContent;
226
227    // 6. Pre-fill fields
228    var nameField = qs('[data-mt-field="name"]', elSign);
229    var emailField = qs('[data-mt-field="email"]', elSign);
230    var dateField = qs('[data-mt-field="date"]', elSign);
231    var sigField = qs('[data-mt-field="signature"]', elSign);
232    var submitBtn = qs('[data-mt-agree-submit]', elSign);
233
234    var userName = (profile && (profile.name || profile.user_login)) || '';
235    var userEmail = (profile && profile.email) || '';
236
237    if (nameField) nameField.value = userName;
238    if (emailField) emailField.value = userEmail;
239
240    // Effective date: max of today vs May 1 2026
241    var today = new Date().toISOString().slice(0, 10);
242    var earliest = '2026-05-01';
243    var effectiveDate = today >= earliest ? today : earliest;
244    if (dateField) dateField.value = effectiveDate;
245
246    // 7. Scroll tracking — unlock sig block when user scrolls to bottom
247    var sigBlock = qs('[data-mt-agree-sig]', elSign);
248    var hintEl = qs('[data-mt-agree-hint]', elSign);
249    var scrollUnlocked = false;
250
251    function checkScroll() {
252      if (scrollUnlocked || !scrollEl) return;
253      var atBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 20;
254      if (atBottom) {
255        scrollUnlocked = true;
256        if (sigBlock) sigBlock.classList.remove('is-locked');
257        if (hintEl) hintEl.classList.add('is-hidden');
258      }
259    }
260
261    if (scrollEl) scrollEl.addEventListener('scroll', checkScroll);
262    // Also check immediately in case content is short enough
263    setTimeout(checkScroll, 200);
264
265    // 8. Enable submit button when signature matches name
266    function checkSigMatch() {
267      if (!sigField || !submitBtn || !nameField) return;
268      var sig = sigField.value.trim();
269      var name = nameField.value.trim();
270      submitBtn.disabled = !sig || sig.toLowerCase() !== name.toLowerCase();
271    }
272
273    if (sigField) sigField.addEventListener('input', checkSigMatch);
274
275    // 9. Submit
276    if (submitBtn) {
277      submitBtn.addEventListener('click', async function () {
278        if (submitBtn.disabled) return;
279        submitBtn.disabled = true;
280        submitBtn.textContent = 'Submitting...';
281
282        try {
283          var payload = {
284            agreement_version: AGREEMENT_VERSION,
285            content_sha256: contentHash,
286            signer_name: nameField.value.trim(),
287            signer_email: emailField ? emailField.value.trim() : '',
288            effective_date: dateField ? dateField.value : effectiveDate,
289            typed_signature: sigField.value.trim(),
290            broker: 'alpaca',
291          };
292
293          var resp = await fetch(API_ORIGIN + '/v1/managed-account/agreement/accept', {
294            method: 'POST',
295            credentials: 'include',
296            headers: { 'Content-Type': 'application/json' },
297            body: JSON.stringify(payload),
298          });
299
300          var result = await resp.json();
301          if (!resp.ok) throw new Error((result.error && result.error.message) || 'Submit failed');
302
303          // Swap to confirmed state
304          elSign.hidden = true;
305          elDone.hidden = false;
306          var detail = qs('[data-mt-agree-done-detail]', elDone);
307          if (detail) {
308            detail.textContent = 'Signed by ' + payload.signer_name +
309              ' on ' + (result.accepted_at || '').slice(0, 10) +
310              (result.agreement_id ? '  (ID: ' + result.agreement_id.slice(0, 8) + '...)' : '');
311          }
312        } catch (e) {
313          console.error(LOG, 'Agreement submit failed', e);
314          submitBtn.disabled = false;
315          submitBtn.textContent = 'Sign & Accept Agreement';
316          alert('Failed to submit agreement: ' + (e.message || 'Unknown error'));
317        }
318      });
319    }
320  }
321
322  /* ================================================================
323     MONTHLY REPORTS
324     ================================================================ */
325
326  async function initReportsSection(root) {
327    var section = qs('#mt-reports', root);
328    if (!section) return;
329
330    var listEl = qs('[data-mt-reports-list]', section);
331    var chartEl = qs('[data-mt-reports-chart]', section);
332    var detailEl = qs('[data-mt-reports-detail]', section);
333    var selectEl = qs('[data-mt-reports-select]', section);
334    var downloadBtn = qs('[data-mt-reports-download]', section);
335    var cardsEl = qs('[data-mt-reports-cards]', section);
336    if (!listEl) return;
337
338    // Fetch available reports
339    var reports = [];
340    try {
341      var r = await fetch(API_ORIGIN + '/v1/managed-account/monthly-reports', {
342        credentials: 'include', cache: 'no-store',
343      });
344      if (r.ok) {
345        var data = await r.json();
346        reports = data.reports || [];
347      }
348    } catch (e) { console.warn(LOG, 'reports fetch failed', e); }
349
350    if (reports.length === 0) {
351      listEl.innerHTML = '<p style="font-size:0.85rem;color:#889;">No reports available yet. Reports are generated on the 1st of each month.</p>';
352      return;
353    }
354
355    // Populate month selector
356    if (selectEl) {
357      selectEl.innerHTML = reports.map(function (rpt) {
358        return '<option value="' + rpt.month + '">' + rpt.month_label + '</option>';
359      }).join('');
360      selectEl.addEventListener('change', function () {
361        loadReportDetail(root, selectEl.value);
362      });
363    }
364
365    // Load first report
366    loadReportDetail(root, reports[0].month);
367  }
368
369  async function loadReportDetail(root, month) {
370    var cardsEl = qs('[data-mt-reports-cards]', root);
371    var chartEl = qs('[data-mt-reports-chart]', root);
372    var detailEl = qs('[data-mt-reports-detail]', root);
373    var downloadBtn = qs('[data-mt-reports-download]', root);
374
375    if (cardsEl) cardsEl.innerHTML = '<p style="font-size:0.82rem;color:#889;">Loading...</p>';
376
377    try {
378      var r = await fetch(API_ORIGIN + '/v1/managed-account/monthly-reports/' + month, {
379        credentials: 'include', cache: 'no-store',
380      });
381      if (!r.ok) throw new Error('HTTP ' + r.status);
382      var rpt = await r.json();
383
384      // Summary cards
385      if (cardsEl) {
386        cardsEl.innerHTML = buildSummaryCards(rpt);
387      }
388
389      // Daily PnL chart
390      if (chartEl && rpt.daily && rpt.daily.length > 0) {
391        chartEl.innerHTML = buildPnlChart(rpt.daily);
392      }
393
394      // Detail tables
395      if (detailEl) {
396        detailEl.innerHTML = buildDetailTables(rpt);
397      }
398
399      // Download button
400      if (downloadBtn) {
401        downloadBtn.style.display = rpt.month ? '' : 'none';
402        downloadBtn.onclick = function () {
403          window.open(API_ORIGIN + '/v1/managed-account/monthly-reports/' + month + '/pdf', '_blank');
404        };
405      }
406    } catch (e) {
407      console.warn(LOG, 'report detail failed', e);
408      if (cardsEl) cardsEl.innerHTML = '<p style="color:#e2725b;font-size:0.85rem;">Failed to load report.</p>';
409    }
410  }
411
412  function fmtMoney(n) {
413    if (n == null || !isFinite(n)) return '$0.00';
414    var sign = n < 0 ? '-' : '';
415    return sign + '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
416  }
417
418  function pnlColor(n) {
419    if (n == null || n === 0) return '#556';
420    return n > 0 ? '#22c55e' : '#ef4444';
421  }
422
423  function buildSummaryCards(rpt) {
424    var winRate = (rpt.winners + rpt.losers) > 0 ? ((rpt.winners / (rpt.winners + rpt.losers)) * 100).toFixed(1) : '0.0';
425    var cards = [
426      { label: 'Equity Change', value: fmtMoney(rpt.equity_change), sub: (rpt.equity_roi_pct >= 0 ? '+' : '') + rpt.equity_roi_pct.toFixed(2) + '%', color: pnlColor(rpt.equity_change) },
427      { label: 'Win Rate', value: winRate + '%', sub: rpt.winners + 'W / ' + rpt.losers + 'L', color: '#556' },
428    ];
429    if (rpt.sharpe_ratio != null) {
430      cards.push({ label: 'Sharpe Ratio', value: rpt.sharpe_ratio.toFixed(2), sub: 'annualized', color: pnlColor(rpt.sharpe_ratio) });
431    }
432    if (rpt.beta != null) {
433      cards.push({ label: 'Beta', value: rpt.beta.toFixed(2), sub: 'vs benchmark', color: '#556' });
434    }
435    return '<div class="hp-mt__report-cards">' + cards.map(function (c) {
436      return '<div class="hp-mt__report-card">' +
437        '<div class="hp-mt__report-card-label">' + c.label + '</div>' +
438        '<div class="hp-mt__report-card-value" style="color:' + c.color + '">' + c.value + '</div>' +
439        '<div class="hp-mt__report-card-sub">' + c.sub + '</div>' +
440        '</div>';
441    }).join('') + '</div>';
442  }
443
444  function buildPnlChart(daily) {
445    var w = 500, h = 160, pad = 40;
446    var barW = Math.max(4, Math.floor((w - pad * 2) / daily.length) - 2);
447    var pnls = daily.map(function (d) { return d.pnl; });
448    var maxAbs = Math.max(Math.abs(Math.min.apply(null, pnls)), Math.abs(Math.max.apply(null, pnls)), 1);
449    var scale = (h - pad * 2) / 2 / maxAbs;
450    var midY = h / 2;
451
452    var bars = daily.map(function (d, i) {
453      var x = pad + i * (barW + 2);
454      var barH = Math.abs(d.pnl) * scale;
455      var y = d.pnl >= 0 ? midY - barH : midY;
456      var fill = d.pnl >= 0 ? '#22c55e' : '#ef4444';
457      return '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + Math.max(barH, 1) + '" fill="' + fill + '" rx="1"/>';
458    }).join('');
459
460    // Cumulative line
461    var cums = daily.map(function (d) { return d.cumulative; });
462    var cumMax = Math.max(Math.abs(Math.min.apply(null, cums)), Math.abs(Math.max.apply(null, cums)), 1);
463    var cumScale = (h - pad * 2) / 2 / cumMax;
464    var points = daily.map(function (d, i) {
465      var x = pad + i * (barW + 2) + barW / 2;
466      var y = midY - d.cumulative * cumScale;
467      return x + ',' + y;
468    }).join(' ');
469    var cumLine = '<polyline points="' + points + '" fill="none" stroke="#5A9BD4" stroke-width="2" stroke-linejoin="round"/>';
470
471    // Zero line
472    var zeroLine = '<line x1="' + pad + '" y1="' + midY + '" x2="' + (w - 10) + '" y2="' + midY + '" stroke="#e2e8f0" stroke-width="1"/>';
473
474    // X-axis labels (show every ~5th date)
475    var labels = daily.map(function (d, i) {
476      if (i % Math.max(1, Math.floor(daily.length / 6)) !== 0 && i !== daily.length - 1) return '';
477      var x = pad + i * (barW + 2) + barW / 2;
478      return '<text x="' + x + '" y="' + (h - 4) + '" text-anchor="middle" font-size="7" fill="#94a3b8">' + d.date.slice(5) + '</text>';
479    }).join('');
480
481    return '<svg viewBox="0 0 ' + w + ' ' + h + '" style="width:100%;max-width:' + w + 'px;height:auto;">' +
482      zeroLine + bars + cumLine + labels +
483      '<text x="' + pad + '" y="12" font-size="8" fill="#94a3b8">Daily P&amp;L (bars) / Cumulative (line)</text>' +
484      '</svg>';
485  }
486
487  function buildDetailTables(rpt) {
488    var html = '';
489
490    // By Ticker table
491    if (rpt.by_ticker && rpt.by_ticker.length > 0) {
492      html += '<h4 class="hp-mt__report-table-title">P&amp;L by Underlying</h4>';
493      html += '<table class="hp-mt__report-table"><thead><tr><th>
493Ticker</th><th>Trades</th><th>W/L</th><th>Net P&amp;L</th></tr></thead><tbody>';
494      rpt.by_ticker.slice(0, 10).forEach(function (t) {
495        html += '<tr><td style="font-weight:700">' + t.ticker + '</td><td>' + t.trades + '</td><td><span style="color:#22c55e">' + t.wins + '</span>/<span style="color:#ef4444">' + t.losses + '</span></td><td style="color:' + pnlColor(t.net_pnl) + '">' + fmtMoney(t.net_pnl) + '</td></tr>';
496      });
497      html += '</tbody></table>';
498    }
499
500    // Exit analysis
501    if (rpt.by_exit_reason && rpt.by_exit_reason.length > 0) {
502      html += '<h4 class="hp-mt__report-table-title">Exit Analysis</h4>';
503      html += '<table class="hp-mt__report-table"><thead><tr><th>Reason</th><th>Count</th><th>Net P&amp;L</th></tr></thead><tbody>';
504      rpt.by_exit_reason.forEach(function (e) {
505        html += '<tr><td>' + e.reason + '</td><td>' + e.count + '</td><td style="color:' + pnlColor(e.pnl) + '">' + fmtMoney(e.pnl) + '</td></tr>';
506      });
507      html += '</tbody></table>';
508    }
509
510    return html;
511  }
512
513  /* ================================================================
514     GO-LIVE PROGRESS TRACKER
515     ================================================================ */
516
517  var TRACKER_KEY = 'haiphen.mt.tracker';
518  var TRACKER_TOTAL = 16;
519
520  function initTracker(root, isLoggedIn) {
521    var section = qs('#mt-tracker', root);
522    if (!section) return;
523
524    var checkboxes = qa('input[data-tracker]', section);
525    var fill = qs('[data-mt-tracker-fill]', section);
526    var label = qs('[data-mt-tracker-label]', section);
527
528    // Load localStorage state
529    var saved = {};
530    try {
531      saved = JSON.parse(localStorage.getItem(TRACKER_KEY)) || {};
532    } catch (_) { saved = {}; }
533
534    // If logged in, fetch server state and merge (server wins)
535    if (isLoggedIn) {
536      fetch(API_ORIGIN + '/v1/managed-account/tracker', {
537        credentials: 'include', cache: 'no-store',
538      }).then(function (r) { return r.ok ? r.json() : null; })
539        .then(function (data) {
540          if (!data || !data.items) return;
541          var serverItems = data.items;
542
543          // Server wins: apply server state to checkboxes + localStorage
544          checkboxes.forEach(function (cb) {
545            var key = cb.getAttribute('data-tracker');
546            if (serverItems[key]) {
547              saved[key] = true;
548              cb.checked = true;
549            }
550          });
551
552          // Sync local-only items up to server (fire-and-forget)
553          checkboxes.forEach(function (cb) {
554            var key = cb.getAttribute('data-tracker');
555            if (saved[key] && !serverItems[key]) {
556              fetch(API_ORIGIN + '/v1/managed-account/tracker', {
557                method: 'POST', credentials: 'include',
558                headers: { 'Content-Type': 'application/json' },
559                body: JSON.stringify({ item_id: key, checked: true }),
560              }).catch(function () {});
561            }
562          });
563
564          try { localStorage.setItem(TRACKER_KEY, JSON.stringify(saved)); } catch (_) {}
565          checkboxes.forEach(function (cb) { updateRowStyle(cb); });
566          updateProgress();
567        }).catch(function (e) { console.warn(LOG, 'tracker fetch failed', e); });
568    }
569
570    // Restore checkbox state from localStorage (immediate, before server responds)
571    checkboxes.forEach(function (cb) {
572      var key = cb.getAttribute('data-tracker');
573      if (saved[key]) cb.checked = true;
574      updateRowStyle(cb);
575    });
576
577    updateProgress();
578
579    // Listen for changes
580    checkboxes.forEach(function (cb) {
581      cb.addEventListener('change', function () {
582        var key = cb.getAttribute('data-tracker');
583        if (cb.checked) {
584          saved[key] = true;
585        } else {
586          delete saved[key];
587        }
588        try { localStorage.setItem(TRACKER_KEY, JSON.stringify(saved)); } catch (_) {}
589        updateRowStyle(cb);
590        updateProgress();
591
592        // Sync to server (fire-and-forget)
593        if (isLoggedIn) {
594          fetch(API_ORIGIN + '/v1/managed-account/tracker', {
595            method: 'POST', credentials: 'include',
596            headers: { 'Content-Type': 'application/json' },
597            body: JSON.stringify({ item_id: key, checked: cb.checked }),
598          }).catch(function () {});
599        }
600      });
601    });
602
603    function updateRowStyle(cb) {
604      var row = cb.closest('.hp-mt__tracker-item');
605      if (row) row.classList.toggle('is-checked', cb.checked);
606    }
607
608    function updateProgress() {
609      var count = 0;
610      checkboxes.forEach(function (cb) { if (cb.checked) count++; });
611      var pct = Math.round(count / TRACKER_TOTAL * 100);
612      if (fill) fill.style.width = pct + '%';
613      if (label) label.textContent = count + ' of ' + TRACKER_TOTAL + ' complete';
614      updateSubmitButton();
615    }
616
617    // Submit for Review button
618    var submitWrap = qs('#mt-submit-review', section);
619    var submitBtn = qs('#mt-submit-review-btn', section);
620    var submitHint = qs('#mt-submit-review-hint', section);
621    var submitSuccess = qs('#mt-submit-review-success', section);
622    var submitError = qs('#mt-submit-review-error', section);
623
624    if (submitWrap && isLoggedIn) submitWrap.style.display = '';
625
626    function updateSubmitButton() {
627      if (!submitBtn) return;
628      var maaChecked = false;
629      var brokerChecked = false;
630      checkboxes.forEach(function (cb) {
631        var k = cb.getAttribute('data-tracker');
632        if (k === '4.3' && cb.checked) maaChecked = true;
633        if ((k === '1.2' || k === '1.3') && cb.checked) brokerChecked = true;
634      });
635      var ready = maaChecked && brokerChecked;
636      submitBtn.disabled = !ready;
637      submitBtn.style.opacity = ready ? '1' : '0.45';
638      submitBtn.style.cursor = ready ? 'pointer' : 'not-allowed';
639      if (submitHint) submitHint.style.display = ready ? 'none' : '';
640    }
641
642    if (submitBtn) {
643      submitBtn.addEventListener('click', function () {
644        if (submitBtn.disabled) return;
645        submitBtn.disabled = true;
646        submitBtn.textContent = 'Submitting…';
647        if (submitError) submitError.style.display = 'none';
648
649        fetch(API_ORIGIN + '/v1/managed-account/go-live/submit', {
650          method: 'POST', credentials: 'include',
651        }).then(function (r) { return r.json(); })
652          .then(function (data) {
653            if (data.ok || data.submitted_at) {
654              submitBtn.style.display = 'none';
655              if (submitHint) submitHint.style.display = 'none';
656              if (submitSuccess) submitSuccess.style.display = '';
657            } else {
658              throw new Error(data.error && data.error.message ? data.error.message : 'Submission failed');
659            }
660          }).catch(function (e) {
661            submitBtn.disabled = false;
662            submitBtn.textContent = 'Submit for Admin Review';
663            if (submitError) {
664              submitError.textContent = e.message || 'Something went wrong. Please try again.';
665              submitError.style.display = '';
666            }
667          });
668      });
669    }
670  }
671
672  /* ================================================================
673     MOUNT
674     ================================================================ */
675
676  async function ensureMounted() {
677    var mount = document.getElementById(MOUNT_ID);
678    if (!mount) throw new Error(LOG + ' missing #' + MOUNT_ID);
679
680    await injectCssOnce('assets/base.css');
681    await injectCssOnce('components/managed-trading/managed-trading.css');
682
683    if (!mount.__haiphenMTMounted) {
684      var html = await fetchText('components/managed-trading/managed-trading.html');
685      mount.innerHTML = html;
686      mount.__haiphenMTMounted = true;
687    }
688
689    var root = qs('.hp-mt', mount);
690    if (!root) return;
691
692    renderSidebar(root);
693    wireSidebarClicks(root);
694    wireSidebarScrollTracking(root);
695
696    // Entitlement check — pro tier required, OR active broker connection (invited managed-account client)
697    var check = await checkEntitlement();
698    var gate = qs('[data-mt-gate]', root);
699
700    if (!check.loggedIn) {
701      window.location.assign(AUTH_ORIGIN + '/login?provider=google&to=' + encodeURIComponent(window.location.href));
702      return;
703    }
704
705    // Bypass entitlement gate for users with an active broker connection
706    var hasAccess = check.entitled;
707    if (!hasAccess) {
708      try {
709        var bcRes = await fetch(API_ORIGIN + '/v1/broker/status', {
710          credentials: 'include', cache: 'no-store',
711        });
712        if (bcRes.ok) {
713          var bcData = await bcRes.json();
714          hasAccess = !!bcData.connected;
715        }
716      } catch (_) {}
717    }
718
719    if (!hasAccess) {
720      root.classList.add('hp-mt--gated');
721      if (gate) {
722        gate.hidden = false;
723        var gateBtn = qs('.hp-mt__gate-btn', gate);
724        if (gateBtn) gateBtn.href = 'https://checkout.haiphen.io/v1/checkout/service?service=haiphen_platform';
725      }
726    } else {
727      root.classList.remove('hp-mt--gated');
728      if (gate) gate.hidden = true;
729
730      initAgreementFlow(root).catch(function (e) {
731        console.warn(LOG, 'agreement flow error', e);
732      });
733
734      initReportsSection(root).catch(function (e) {
735        console.warn(LOG, 'reports init error', e);
736      });
737
738      initTracker(root, true);
739    }
740  }
741
742  /* ================================================================
743     PUBLIC API
744     ================================================================ */
745
746  NS.loadManagedTrading = async function () {
747    try {
748      await ensureMounted();
749    } catch (e) {
750      console.warn(LOG, 'load failed', e);
751    }
752  };
753
754})();

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.