1/* mt5.js â MetaTrader 5 DEMO bridge (metaapi.cloud) in the BROKERS panel. 2 * 3 * Mounts a card next to the Zerodha one. Users connect an MT5 demo 4 * account (login / password / server â server name must contain "demo", 5 * enforced again server-side), then see balance/equity, open positions 6 * with one-tap close, and a quick BUY/SELL ticket. All calls go through 7 * the metaapi edge function with the user's session token; the MT5 8 * password is sent once to MetaApi and never stored on our side. 9 */ 10(function () { 11 'use strict'; 12 13 const sb = () => window.__sb || null; 14 const FN = () => (window.SUPABASE_URL || '').replace(/\/$/, '') + '/functions/v1/metaapi'; 15 let POLL = null; 16 let LAST_STATE = null; 17 18 const esc = (v) => String(v == null ? '' : v).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); 19 const INP = 'width:100%;box-sizing:border-box;background:#0b111c;border:1px solid #263247;border-radius:8px;color:#e8edf4;font-size:12.5px;font-family:inherit;padding:9px 10px;outline:none;'; 20 const BTN = 'border:0;border-radius:8px;font-weight:800;cursor:pointer;font-family:inherit;'; 21 22 async function call(action, payload) { 23 const { data: s } = await sb().auth.getSession(); 24 const token = s?.session?.access_token; 25 if (!token) return { ok: false, error: 'Sign in first.' }; 26 const r = await fetch(FN(), { 27 method: 'POST', 28 headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'apikey': window.SUPABASE_ANON_KEY || '' }, 29 body: JSON.stringify({ action, ...(payload || {}) }), 30 }); 31 return r.json().catch(() => ({ ok: false, error: 'Bad response.' })); 32 } 33 34 function card() { return document.getElementById('mt5-card-body'); } 35 36 function ensureCard() { 37 if (document.getElementById('mt5-card')) return; 38 const main = document.querySelector('#brokers-modal .br-main'); 39 if (!main) return; 40 const el = document.createElement('div'); 41 el.className = 'al-channel br-card'; 42 el.id = 'mt5-card'; 43 el.innerHTML = ` 44 <div class="br-card-head"> 45 <div class="al-ch-icon" style="background:linear-gradient(135deg,#1f6feb,#0ea5b7);color:#fff;font-weight:800">M5</div> 46 <div class="al-ch-meta"> 47 <div class="al-ch-name">METATRADER 5 · DEMO</div> 48 <div class="al-ch-status" id="mt5-status-line">Checkingâ¦</div> 49 </div> 50 <span class="br-card-tag" style="background:rgba(34,211,238,.1);color:#22d3ee">Demo only</span> 51 </div> 52 <div class="al-ch-body br-card-body" id="mt5-card-body"> 53 <div style="color:#54617a;font-size:12px;padding:8px 0">Loadingâ¦</div> 54 </div>`; 55 // Prepend, not append: MetaTrader is the headline broker feature, so 56 // it renders ABOVE the other broker cards when the modal opens. 57 main.prepend(el); 58 } 59 60 // ââ header chip (#dh-mt5-chip): the always-visible entry point ââââââ 61 // The chip label flips to "connected" straight from localStorage at 62 // boot (instant, no network), then live status corrects it whenever 63 // the card refreshes. Clicking opens BROKERS focused on the MT5 card. 64 // Flag is keyed PER USER â a shared browser must never show user B 65 // "MT5 · CONNECTED" because user A connected. (The un-keyed original 66 // leaked exactly that.) DOM update is sync; persistence resolves the 67 // uid first and is fire-and-forget. 68 const CHIP_FLAG = 'tsai-mt5-connected'; 69 function paintHeaderChip(on) { 70 const lb = document.getElementById('dh-mt5-label'); 71 const chip = document.getElementById('dh-mt5-chip'); 72 if (lb) lb.textContent = on ? 'MT5 · CONNECTED' : 'CONNECT METATRADER'; 73 if (chip) { 74 chip.style.borderColor = on ? 'rgba(52,211,153,.5)' : 'rgba(34,211,238,.4)'; 75 chip.style.color = on ? '#34d399' : '#22d3ee'; 76 } 77 } 78 function setHeaderChip(on) { 79 paintHeaderChip(on); 80 (async () => { 81 try { 82 const uid = (await sb()?.auth.getSession())?.data?.session?.user?.id; 83 if (!uid) return; 84 const key = CHIP_FLAG + ':' + uid; 85 on ? localStorage.setItem(key, '1') : localStorage.removeItem(key); 86 } catch (_) {} 87 })(); 88 } 89 // Exposed for cross-links (TradingView wizard's "connect MetaTrader 90 // first" path and any future broker cross-sell). 91 window.tsFocusMt5Card = focusMt5Card; 92 function focusMt5Card() { 93 try { if (typeof openBrokersModal === 'function') openBrokersModal(); } catch (_) {} 94 setTimeout(() => { 95 ensureCard(); 96 const el = document.getElementById('mt5-card'); 97 if (!el) return;
98 try { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (_) {} 99 el.style.outline = '2px solid #22d3ee'; el.style.outlineOffset = '3px'; 100 el.style.transition = 'outline-color .4s'; 101 setTimeout(() => { el.style.outline = 'none'; }, 2600); 102 }, 350); 103 } 104 105 function setStatus(text, color) { 106 const el = document.getElementById('mt5-status-line'); 107 if (el) { el.textContent = text; el.style.color = color || ''; } 108 } 109 110 function renderDisconnected(note) { 111 setHeaderChip(false); 112 const b = card(); if (!b) return; 113 setStatus('Not connected', '#7b8699'); 114 b.innerHTML = ` 115 ${note ? `<div style="font-size:11.5px;color:#f87171;line-height:1.5;margin-bottom:10px">${esc(note)}</div>` : ''} 116 <div style="display:flex;gap:10px;margin-bottom:12px"> 117 <div style="flex:1;border:1px solid #223049;border-radius:10px;background:#0b111c;padding:9px 11px"> 118 <div style="font-size:9.5px;font-weight:800;letter-spacing:.1em;color:#22d3ee">STEP 1</div> 119 <div style="font-size:11px;color:#c7d0e0;margin-top:2px">Connect your MT5 <b style="color:#fbbf24">demo</b> account below</div> 120 </div> 121 <div style="flex:1;border:1px solid #1a2338;border-radius:10px;background:#0b111c;padding:9px 11px;opacity:.65"> 122 <div style="font-size:9.5px;font-weight:800;letter-spacing:.1em;color:#7b8699">STEP 2</div> 123 <div style="font-size:11px;color:#9aa6bd;margin-top:2px">Tick which bots place their trades there</div> 124 </div> 125 </div> 126 <div style="font-size:11.5px;color:#9aa6bd;line-height:1.6;margin-bottom:12px">Every ticked bot mirrors its paper trades as <b style="color:#dbe4f2">real orders on your demo account</b>. Live accounts are blocked â the server name must contain <b style="color:#fbbf24">demo</b>.</div> 127 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px"> 128 <input id="mt5-login" placeholder="Login (account number)" autocomplete="off" style="${INP}"/> 129 <input id="mt5-password" type="password" placeholder="Password" autocomplete="new-password" style="${INP}"/> 130 </div> 131 <input id="mt5-server" placeholder="Server · e.g. MetaQuotes-Demo" autocomplete="off" style="${INP}margin-bottom:10px"/> 132 <div id="mt5-msg" style="min-height:16px;font-size:11.5px;margin-bottom:8px"></div> 133 <button id="mt5-connect" style="${BTN}width:100%;background:linear-gradient(135deg,#1f6feb,#0ea5b7);color:#fff;padding:11px;font-size:13px">Connect MT5 Demo</button> 134 <div style="font-size:10px;color:#54617a;line-height:1.5;margin-top:8px">Your password goes directly to MetaApi (metaapi.cloud) over TLS to establish the terminal connection â tradingsmart.ai never stores it.</div>`; 135 b.querySelector('#mt5-connect').onclick = async () => { 136 const login = b.querySelector('#mt5-login').value.trim(); 137 const password = b.querySelector('#mt5-password').value; 138 const server = b.querySelector('#mt5-server').value.trim(); 139 const msg = b.querySelector('#mt5-msg'); 140 if (!login || !password || !server) { msg.textContent = 'All three fields are required.'; msg.style.color = '#f87171'; return; } 141 if (!/demo/i.test(server)) { msg.textContent = 'Demo servers only â the server name must contain "demo".'; msg.style.color = '#f87171'; return; } 142 const btn = b.querySelector('#mt5-connect'); 143 btn.disabled = true; btn.textContent = 'Connectingâ¦'; 144 const res = await call('connect', { login, password, server }); 145 if (!res.ok) { 146 btn.disabled = false; btn.textContent = 'Connect MT5 Demo'; 147 msg.textContent = res.error === 'metaapi_not_configured' 148 ? 'MetaApi is not configured yet â the METAAPI_TOKEN secret must be added by the administrator.' 149 : (res.error || 'Could not connect.'); 150 msg.style.color = '#f87171'; 151 return; 152 } 153 renderDeploying(); 154 startPoll(); 155 }; 156 } 157 158 function renderDeploying() { 159 const b = card(); if (!b) return; 160 setStatus('Deployingâ¦', '#fbbf24'); 161 b.innerHTML = ` 162 <div style="display:flex;align-items:center;gap:10px;padding:14px 0"> 163 <div style="width:14px;
163height:14px;border:2px solid #fbbf24;border-top-color:transparent;border-radius:50%;animation:mt5spin 1s linear infinite"></div> 164 <div style="font-size:12.5px;color:#cbb98a;line-height:1.5">Provisioning your MT5 terminal in the cloud â usually 1 to 3 minutes on first connect.</div> 165 </div> 166 <style>@keyframes mt5spin{to{transform:rotate(360deg)}}</style> 167 <button id="mt5-cancel" style="${BTN}background:transparent;border:1px solid #263247;color:#9aa6bd;padding:8px 14px;font-size:11.5px">Cancel & disconnect</button>`; 168 b.querySelector('#mt5-cancel').onclick = disconnect; 169 } 170 171 function fmt(n, ccy) { 172 if (n == null || !isFinite(n)) return 'â'; 173 return (ccy ? '' : '') + Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 }) + (ccy ? ' ' + ccy : ''); 174 } 175 176 async function renderConnected(st) { 177 setHeaderChip(true); 178 const b = card(); if (!b) return; 179 setStatus('â Connected · ' + (st.login || ''), '#34d399'); 180 const a = st.account || {}; 181 b.innerHTML = ` 182 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px;text-align:center"> 183 <div style="background:#0b111c;border:1px solid #1e293e;border-radius:9px;padding:9px"><b style="color:#dbe4f2;font-size:14px">${fmt(a.balance, a.currency)}</b><div style="color:#54617a;font-size:9.5px;margin-top:2px">BALANCE</div></div> 184 <div style="background:#0b111c;border:1px solid #1e293e;border-radius:9px;padding:9px"><b style="color:#dbe4f2;font-size:14px">${fmt(a.equity, a.currency)}</b><div style="color:#54617a;font-size:9.5px;margin-top:2px">EQUITY</div></div> 185 <div style="background:#0b111c;border:1px solid #1e293e;border-radius:9px;padding:9px"><b style="color:${(a.equity - a.balance) >= 0 ? '#34d399' : '#f87171'};font-size:14px">${fmt(a.equity != null && a.balance != null ? a.equity - a.balance : null)}</b><div style="color:#54617a;font-size:9.5px;margin-top:2px">FLOATING P&L</div></div> 186 </div> 187 <!-- The headline step after connecting: wire BOTS to this demo 188 account. This is the whole point of the integration, so it sits 189 directly under the account stats â the manual ticket is demoted 190 to the bottom of the card. --> 191 <div id="mt5-engines-box" style="border:1px solid rgba(34,211,238,.35);border-radius:12px;background:rgba(34,211,238,.05);padding:12px;margin-bottom:12px"> 192 <div style="font-size:10.5px;font-weight:800;letter-spacing:.1em;color:#22d3ee;margin-bottom:4px">â¡ CONNECT YOUR BOTS</div> 193 <div id="mt5-engines-hint" style="font-size:11.5px;color:#9aa6bd;line-height:1.55;margin-bottom:8px">Your demo account is live. Now tick a bot below â every position it opens or closes on paper is <b style="color:#dbe4f2">placed as a real order in this demo account</b> (your chosen lot size, within ~1 minute).</div> 194 <div id="mt5-mirror-list" style="font-size:12px;color:#54617a;margin-bottom:8px">Loading botsâ¦</div> 195 <div id="mt5-mirror-log" style="font-size:10.5px;color:#54617a"></div> 196 </div> 197 <div style="font-size:10px;font-weight:800;letter-spacing:.1em;color:#7b8699;margin:12px 0 6px">OPEN POSITIONS</div> 198 <div id="mt5-positions" style="font-size:12px;color:#54617a">Loadingâ¦</div> 199 <div style="font-size:10px;font-weight:800;letter-spacing:.1em;color:#7b8699;margin:14px 0 6px">QUICK ORDER · MANUAL</div> 200 <div style="display:grid;grid-template-columns:1.2fr .8fr;gap:8px;margin-bottom:8px"> 201 <input id="mt5-sym" placeholder="Symbol · EURUSD" value="EURUSD" autocomplete="off" style="${INP}text-transform:uppercase"/> 202 <input id="mt5-vol" type="number" min="0.01" max="10" step="0.01" value="0.01" placeholder="Lots" style="${INP}"/> 203 </div> 204 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px"> 205 <input id="mt5-sl" type="number" step="any" placeholder="Stop loss (optional)" style="${INP}"/> 206 <input id="mt5-tp" type="number" step="any" placeholder="Take profit (optional)" style="${INP}"/> 207 </div> 208 <div style="display:flex;gap:8px;margin-bottom:6px"> 209 <button id="mt5-buy" style="${BTN}flex:1;background:linear-gradient(180deg,#2ecc71,#1fa85a);color:#04170c;padding:10px;font-size:13px">â² BUY</button> 210 <button id="mt5-sell" style="${BTN}flex:1;background:linear-gradient(180deg,#ff6b6b,#e84343);color:#2b0505;padding:10px;font-size:13px">â¼ SELL</button> 211 </div> 212 <div id="mt5-msg" style="min-height:16px;font-size:11.5px;margin-bottom:8px"></div> 213 <div style="display:flex;gap:8px;margin-top:12px"> 214 <button id="mt5-refresh" style="${BTN}background:transparent;border:1px solid #263247;color:#9aa6bd;padding:7px 12px;font-size:11px">â» Refresh</button> 215 <button id="mt5-disconnect" style="${BTN}background:transparent;border:1px solid rgba(248,113,113,.4);color:#f87171;padding:7px 12px;font-size:11px">Disconnect</button> 216 </div> 217 <div style="font-size:10px;color:#54617a;line-height:1.5;margin-top:8px">Orders execute on your MT5 <b>demo</b> account via metaapi.cloud. Practice money only â not financial advice.</div>`; 218 b.querySelector('#mt5-refresh').onclick = refresh; 219 b.querySelector('#mt5-disconnect').onclick = disconnect; 220 const msg = b.querySelector('#mt5-msg'); 221 const order = async (side) => { 222 const symbol = b.querySelector('#mt5-sym').value.trim().toUpperCase(); 223 const volume = parseFloat(b.querySelector('#mt5-vol').value); 224 const sl = parseFloat(b.querySelector('#mt5-sl').value); 225 const tp = parseFloat(b.querySelector('#mt5-tp').value); 226 if (!symbol) { msg.textContent = 'Symbol required.'; msg.style.color = '#f87171'; return; } 227 if (!isFinite(volume) || volume <= 0) { msg.textContent = 'Volume must be at least 0.01 lots.'; msg.style.color = '#f87171'; return; } 228 const btns = [b.querySelector('#mt5-buy'), b.querySelector('#mt5-sell')]; 229 btns.forEach((x) => x.disabled = true); 230 msg.textContent = 'Placing ' + side.toUpperCase() + ' ' + volume + ' ' + symbol + 'â¦'; msg.style.color = '#9aa6bd'; 231 const res = await call('trade', { side, symbol, volume, sl: isFinite(sl) ? sl : undefined, tp: isFinite(tp) ? tp : undefined }); 232 btns.forEach((x) => x.disabled = false); 233 if (!res.ok) { msg.textContent = res.error || 'Order failed.'; msg.style.color = '#f87171'; return; } 234 msg.textContent = 'â Order placed on your demo account.'; msg.style.color = '#34d399'; 235 loadPositions(); 236 }; 237 b.querySelector('#mt5-buy').onclick = () => order('buy'); 238 b.querySelector('#mt5-sell').onclick = () => order('sell'); 239 loadPositions(); 240 loadMirrorList(); 241 loadMirrorLog(); 242 } 243 244 // Flip the "connect your bots" callout between attention (nothing 245 // wired yet) and confirmation (N bots mirroring) so the user always 246 // knows whether trades will actually reach their demo account. 247 function updateEnginesBoxState(mirroredCount) { 248 const box = document.getElementById('mt5-engines-box'); 249 const hint = document.getElementById('mt5-engines-hint'); 250 if (!box || !hint) return; 251 if (mirroredCount > 0) { 252 box.style.borderColor = 'rgba(52,211,153,.4)'; 253 box.style.background = 'rgba(52,211,153,.05)'; 254 hint.innerHTML = `â <b style="color:#34d399">${mirroredCount} bot${mirroredCount === 1 ? '' : 's'} connected</b> â every position ${mirroredCount === 1 ? 'it opens' : 'they open'} or close${mirroredCount === 1 ? 's' : ''} on paper is placed as a real order in this demo account within ~1 minute.`; 255 } else { 256 box.style.borderColor = 'rgba(34,211,238,.35)'; 257 box.style.background = 'rgba(34,211,238,.05)'; 258 hint.innerHTML = `Your demo account is live. Now tick a bot below â every position it opens or closes on paper is <b style="color:#dbe4f2">placed as a real order in this demo account</b> (your chosen lot size, within ~1 minute).`; 259 } 260 } 261 262 // ââ signal mirroring: per-engine opt-in + fixed lots ââââââââââââââââ 263 async function loadMirrorList() { 264 const box = document.getElementById('mt5-mirror-list'); 265 if (!box) return; 266 try { 267 const { data: s2 } = await sb().auth.getSession(); 268 const uid = s2?.session?.user?.id; 269 const { data: bots } = await sb().from('bots') 270 .select('id,name,status,mt5_mirror,mt5_lots') 271 .eq('owner', uid).neq('status', 'killed').order('created_at', { ascending: false }).limit(20); 272 if (!bots || !bots.length) { box.innerHTML = '<div style="color:#54617a">No bots yet â create one first.</div>'; return; } 273 updateEnginesBoxState(bots.filter((bt) => bt.mt5_mirror).length); 274 box.innerHTML = bots.map((bt) => ` 275 <div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-top:1px solid #141d2e"> 276 <input type="checkbox" data-mt5-mirror="${esc(bt.id)}" ${bt.mt5_mirror ? 'checked' : ''} style="width:15px;height:15px;accent-color:#22d3ee;flex:0 0 auto;cursor:pointer"/> 277 <div style="min-width:0;flex:1"> 278 <b style="display:block;color:#dbe4f2;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(bt.name)}
278</b> 279 <span style="color:#54617a;font-size:10px">${bt.status === 'running' ? 'â running' : esc(bt.status)}</span> 280 </div> 281 <input type="number" data-mt5-lots="${esc(bt.id)}" value="${Number(bt.mt5_lots) || 0.01}" min="0.01" max="10" step="0.01" title="Lot size per signal" style="${INP}width:74px;flex:0 0 auto;padding:6px 8px;font-size:11px" ${bt.mt5_mirror ? '' : 'disabled'}/> 282 </div>`).join(''); 283 box.querySelectorAll('[data-mt5-mirror]').forEach((cb) => cb.onchange = async () => { 284 const id = cb.dataset.mt5Mirror; 285 const lotsInp = box.querySelector(`[data-mt5-lots="${id}"]`); 286 const lots = Math.min(Math.max(parseFloat(lotsInp.value) || 0.01, 0.01), 10); 287 cb.disabled = true; 288 const { error } = await sb().from('bots').update({ mt5_mirror: cb.checked, mt5_lots: lots }).eq('id', id); 289 cb.disabled = false; 290 if (error) { cb.checked = !cb.checked; try { toast({ title: 'MT5 mirror', sub: error.message, kind: 'warn' }); } catch (_) {} return; } 291 lotsInp.disabled = !cb.checked; 292 updateEnginesBoxState(box.querySelectorAll('[data-mt5-mirror]:checked').length); 293 try { toast({ title: cb.checked ? 'Signals will mirror to MT5' : 'Mirroring off', sub: cb.checked ? lots + ' lots per signal · demo account' : '', kind: 'ok' }); } catch (_) {} 294 }); 295 box.querySelectorAll('[data-mt5-lots]').forEach((inp) => inp.onchange = async () => { 296 const id = inp.dataset.mt5Lots; 297 const lots = Math.min(Math.max(parseFloat(inp.value) || 0.01, 0.01), 10); 298 inp.value = lots; 299 await sb().from('bots').update({ mt5_lots: lots }).eq('id', id); 300 }); 301 } catch (_) { box.textContent = 'Could not load bots.'; } 302 } 303 304 async function loadMirrorLog() { 305 const box = document.getElementById('mt5-mirror-log'); 306 if (!box) return; 307 const res = await call('mirror-log'); 308 if (!res.ok || !res.log || !res.log.length) { box.innerHTML = ''; return; } 309 box.innerHTML = '<div style="color:#7b8699;font-weight:800;letter-spacing:.08em;font-size:9.5px;margin-bottom:4px">RECENT SIGNALS</div>' 310 + res.log.slice(0, 5).map((l) => { 311 const color = l.status === 'done' ? '#34d399' : l.status === 'error' ? '#f87171' : '#7b8699'; 312 return `<div style="display:flex;gap:6px;align-items:center;padding:2px 0"> 313 <span style="color:${color}">â</span> 314 <span style="color:#9aa6bd">${esc(l.event_type)} ${esc(l.side)} ${esc(l.sym)}</span> 315 <span style="color:${color};margin-left:auto">${esc(l.status)}${l.error ? ' · ' + esc(String(l.error).slice(0, 40)) : ''}</span> 316 </div>`; 317 }).join(''); 318 } 319 320 async function loadPositions() { 321 const box = document.getElementById('mt5-positions'); 322 if (!box) return; 323 const res = await call('positions'); 324 if (!res.ok) { box.textContent = res.error || 'Could not load positions.'; return; } 325 const ps = res.positions || []; 326 if (!ps.length) { box.innerHTML = '<div style="color:#54617a">No open positions.</div>'; return; } 327 box.innerHTML = ps.map((p) => ` 328 <div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-top:1px solid #141d2e"> 329 <b style="color:${/buy/i.test(p.type) ? '#34d399' : '#f87171'};font-size:11px;flex:0 0 34px">${/buy/i.test(p.type) ? 'BUY' : 'SELL'}</b> 330 <div style="min-width:0;flex:1"><b style="color:#dbe4f2;font-size:12px">${esc(p.symbol)}</b> 331 <span style="color:#54617a;font-size:10.5px"> · ${p.volume} lots @ ${p.openPrice}</span></div> 332 <b class="mono" style="font-size:12px;color:${(p.profit || 0) >= 0 ? '#34d399' : '#f87171'}">${(p.profit || 0) >= 0 ? '+' : ''}${Number(p.profit || 0).toFixed(2)}</b> 333 <button data-mt5-close="${esc(p.id)}" style="${BTN}background:transparent;border:1px solid #263247;color:#9aa6bd;padding:4px 9px;font-size:10px">Close</button> 334 </div>`).join(''); 335 box.querySelectorAll('[data-mt5-close]').forEach((btn) => btn.onclick = async () => { 336 btn.disabled = true; btn.textContent = 'â¦'; 337 const r = await call('close', { positionId: btn.dataset.mt5Close }); 338 if (!r.ok) { btn.disabled = false; btn.textContent = 'Close'; try { toast({ title: 'MT5', sub: r.error, kind: 'warn' }); } catch (_) {} return; } 339 loadPositions(); refresh(); 340 }); 341 } 342 343 async function disconnect() { 344 stopPoll(); 345 const b = card(); 346 if (b) b.innerHTML = '<div style="color:#54617a;font-size:12px;padding:8px 0">Disconnectingâ¦</div>'; 347 await call('disconnect'); 348 renderDisconnected(); 349 } 350 351 async function refresh() { 352 const res = await call('status'); 353 if (!res.ok) { 354 renderDisconnected(res.error === 'metaapi_not_configured' 355 ? 'MetaApi is not configured yet â the METAAPI_TOKEN secret must be added by the administrator.' 356 : res.error); 357 return; 358 } 359 LAST_STATE = res.state; 360 if (res.state === 'none') { stopPoll(); renderDisconnected(); return; }
361 if (res.state === 'DEPLOYED' && res.connection === 'CONNECTED') { stopPoll(); renderConnected(res); return; } 362 renderDeploying(); startPoll(); 363 } 364 365 function startPoll() { 366 if (POLL) return; 367 POLL = setInterval(() => { 368 const modal = document.getElementById('brokers-modal'); 369 if (!modal || !modal.classList.contains('show')) { stopPoll(); return; } 370 refresh(); 371 }, 6000); 372 } 373 function stopPoll() { if (POLL) { clearInterval(POLL); POLL = null; } } 374 375 // mount + refresh whenever the BROKERS modal opens 376 function init() { 377 // Header chip: bind + restore last-known state instantly from 378 // localStorage (live status corrects it when the card refreshes). 379 const chip = document.getElementById('dh-mt5-chip'); 380 if (chip && !chip.dataset.wired) { 381 chip.dataset.wired = '1'; 382 chip.addEventListener('click', focusMt5Card); 383 // Restore last-known state once we know WHO is signed in (per-user 384 // key). Also drop the legacy un-keyed flag so old browsers don't 385 // keep showing a stale "connected" from another account. 386 (async () => { 387 try { localStorage.removeItem(CHIP_FLAG); } catch (_) {} 388 for (let i = 0; i < 40; i++) { 389 try { 390 const s = (await sb()?.auth.getSession())?.data?.session; 391 if (s?.user?.id) { 392 try { if (localStorage.getItem(CHIP_FLAG + ':' + s.user.id)) paintHeaderChip(true); } catch (_) {} 393 return; 394 } 395 } catch (_) {} 396 await new Promise((r) => setTimeout(r, 500)); 397 } 398 })(); 399 } 400 const modal = document.getElementById('brokers-modal'); 401 if (!modal) return; 402 ensureCard(); 403 new MutationObserver(() => { 404 if (modal.classList.contains('show')) { ensureCard(); refresh(); } 405 else stopPoll(); 406 }).observe(modal, { attributes: true, attributeFilter: ['class'] }); 407 } 408 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 409 else init(); 410})();
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.