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&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&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&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&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.