PageSourceSearch

https://www.tradingsmart.ai/app/mt5.js?v=20260726a

js tradingsmart.ai collected 2026-10-01 14:25:26 UTC 25,016 bytes, 410 lines download raw bytes

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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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.