;(function () {
  /* Voice tool dispatcher registry — `window.VOICE_TOOL_DISPATCHERS`.
   *
   * Each entry is a (args) => result function. Results are returned to
   * the OpenAI Realtime model as JSON via the realtime data channel
   * (see realtime-client.js — function_call_arguments.done handler).
   *
   * Tool schemas live server-side in mcp-server/src/realtime.ts under
   * CLIENT_TOOL_SCHEMAS — they get baked into the ephemeral token mint
   * so the model knows what's available. Names must match between the
   * two files; this registry is dispatch-only.
   *
   * Adding a new tool: register a schema in realtime.ts AND a
   * dispatcher here. Keep dispatchers small — they should read state
   * via window.equiti.* / window.use* and mutate via the same. */

  function get_total_balance() {
    return { usd: window.getTotal ? window.getTotal() : 0 };
  }

  function get_account_balances() {
    const get = window.getBalance || function () { return 0; };
    return {
      trading: get('trading'),
      gold: get('gold'),
      wealth: get('wealth'),
      shares: get('shares'),
      savings: get('savings'),
      total: window.getTotal ? window.getTotal() : 0,
      currency: 'USD',
    };
  }

  function get_current_screen() {
    const eq = window.equiti || {};
    const route = eq.getRoute ? eq.getRoute() : 'home';
    if (route === 'leveraged-trade') {
      const sub = eq.getActiveTradeTab ? eq.getActiveTradeTab() : 'discover';
      return { route: 'leveraged-trade', section: sub };
    }
    return { route: 'home', tab: eq.getActiveTab ? eq.getActiveTab() : 'home' };
  }

  function navigate(args) {
    const valid = ['home', 'wallet', 'news', 'performance'];
    const target = args && args.screen;
    if (valid.indexOf(target) < 0) {
      return { ok: false, error: 'unknown screen: ' + target + '. Try one of: ' + valid.join(', ') };
    }
    /* If currently inside the leveraged-trade subapp, bounce out first
       so the navigate target lands on the home shell. */
    if (window.equiti && window.equiti.getRoute && window.equiti.getRoute() === 'leveraged-trade') {
      if (window.equiti.closeLeveragedTrade) window.equiti.closeLeveragedTrade();
    }
    if (window.equiti && window.equiti.navigateTo) window.equiti.navigateTo(target);
    return { ok: true, screen: target };
  }

  function escalate_to_human(args) {
    const reason = (args && args.reason) || 'user requested human support';
    const conv = window.equiti && window.equiti.conversation;
    if (!conv || !conv.escalate) {
      return { ok: false, error: 'Conversation not initialized.' };
    }
    conv.escalate(reason);
    return { ok: true, message: 'I have flagged this conversation for a human agent. Someone will join the chat shortly.' };
  }

  /* ── NEW: trading ─────────────────────────────────────────────────── */

  function get_open_positions() {
    const eq = window.equiti || {};
    const positions = eq.listPositions ? eq.listPositions() : [];
    return {
      count: positions.length,
      positions: positions.map((p) => ({
        id: p.id,
        symbol: p.sym,
        side: p.side,
        size: p.size,
        openPrice: p.open,
        currentPrice: p.currentPrice,
        unrealizedPL: Number(p.livePL ? p.livePL.toFixed(2) : 0),
        stopLoss: p.sl || null,
        takeProfit: p.tp || null,
        openedAt: p.openedAt,
      })),
    };
  }

  /* place_trade — the UI-driven orchestration flow:
       1. enter leveraged-trade route (if not already)
       2. switch to the trade section and pick the symbol
       3. pop the order ticket on the requested side
       4. fill in the volume in the ticket (user watches the lot tick)
       5. fill SL / TP if provided
       6. wait long enough for the user to read the values
       7. click Place (handlePlace → addPosition → ticket closes)
       8. hand the user off to the Portfolio section to see the position.
     Each step calls a window.equiti.* bridge that the corresponding
     React component publishes. If any bridge is missing (e.g. trade
     subapp not loaded), falls back to a silent dispatch via
     window.equiti.openTrade so the trade still happens. */
  async function place_trade(args) {
    const eq = window.equiti || {};
    const symbol = args && args.symbol;
    const side = args && args.side;
    const volume = args && args.volume;
    if (!symbol || (side !== 'buy' && side !== 'sell') || !volume) {
      return { ok: false, error: 'symbol, side, and volume required' };
    }
    const sym = eq.getSymbol ? eq.getSymbol(symbol) : null;
    if (!sym) return { ok: false, error: 'unknown symbol: ' + symbol };
    if (sym.realFeed !== 'dummy') return { ok: false, error: 'market closed for ' + symbol };

    /* 1+2 — route + symbol selection. */
    if (eq.getRoute && eq.getRoute() !== 'leveraged-trade' && eq.openLeveragedTrade) {
      eq.openLeveragedTrade();
      await sleep(700);
    }
    if (eq.selectSymbol) {
      eq.selectSymbol(symbol);
      await sleep(700);
    }

    /* 3 — open order ticket. */
    if (!eq.openOrderTicket) {
      /* Trade page not mounted — fall back to silent dispatch. */
      const fallback = eq.openTrade ? eq.openTrade({ symbol, side, volume, stop_loss: args.stop_loss, take_profit: args.take_profit }) : { ok: false, error: 'trade not available' };
      if (fallback.ok && eq.openTradeSection) eq.openTradeSection('portfolio');
      return Object.assign({ executed: 'silent' }, fallback);
    }
    eq.openOrderTicket({ side, symbol });
    /* Wait for the ticket's slide-up (420ms) + a beat. */
    await sleep(700);

    /* 4+5 — fill in volume + SL/TP. */
    if (!eq.orderTicket) return { ok: false, error: 'order ticket not ready' };
    eq.orderTicket.setVolume(volume);
    if (args.stop_loss != null) eq.orderTicket.setStopLoss(args.stop_loss);
    if (args.take_profit != null) eq.orderTicket.setTakeProfit(args.take_profit);

    /* 6 — let the user read the lot/margin breakdown for a beat. */
    await sleep(1400);

    /* 7 — gate on canPlace (catches insufficient_free_margin). */
    if (!eq.orderTicket.canPlace || !eq.orderTicket.canPlace()) {
      eq.orderTicket.close();
      return { ok: false, error: 'cannot place (insufficient margin or invalid volume)' };
    }
    eq.orderTicket.place();
    /* Ticket close animation is 420ms; wait it out then hand off. */
    await sleep(700);

    /* 8 — show the new position in Portfolio. */
    if (eq.openTradeSection) eq.openTradeSection('portfolio');

    return {
      ok: true,
      executed: true,
      symbol: symbol,
      side: side,
      volume: volume,
      message: 'Order placed — you are on the Portfolio page now.',
    };
  }

  function close_position(args) {
    const eq = window.equiti || {};
    if (!eq.closePosition) return { ok: false, error: 'trade bridge not available' };
    return eq.closePosition(args || {});
  }

  /* ── Read-only: market data ───────────────────────────────────────── */

  function get_instrument_quote(args) {
    const symbol = args && args.symbol;
    if (!symbol) return { ok: false, error: 'symbol required' };
    const eq = window.equiti || {};
    const s = eq.getSymbol ? eq.getSymbol(symbol) : null;
    if (!s) return { ok: false, error: 'unknown symbol: ' + symbol };
    return {
      symbol: s.sym,
      name: s.name,
      category: s.cat,
      bid: s.bid,
      ask: s.ask,
      spread: Number((s.ask - s.bid).toFixed(s.cat === 'forex' ? 5 : 2)),
      tradable: s.realFeed === 'dummy',
    };
  }

  function list_symbols(args) {
    const eq = window.equiti || {};
    const all = eq.listAllSymbols ? eq.listAllSymbols() : [];
    const cat = (args && args.category) || null;
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 30;
    const filtered = cat ? all.filter((s) => s.cat === cat) : all;
    return {
      count: filtered.length,
      category: cat || 'all',
      symbols: filtered.slice(0, limit).map((s) => ({
        symbol: s.sym, name: s.name, category: s.cat,
        bid: s.bid, ask: s.ask, change: s.change,
      })),
    };
  }

  function get_top_movers(args) {
    const eq = window.equiti || {};
    const all = eq.listAllSymbols ? eq.listAllSymbols() : [];
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 5;
    const direction = args && args.direction;
    let sorted = all.slice().sort((a, b) => Math.abs(b.change || 0) - Math.abs(a.change || 0));
    if (direction === 'up') sorted = all.slice().sort((a, b) => (b.change || 0) - (a.change || 0));
    else if (direction === 'down') sorted = all.slice().sort((a, b) => (a.change || 0) - (b.change || 0));
    return {
      direction: direction || 'absolute',
      movers: sorted.slice(0, limit).map((s) => ({
        symbol: s.sym, name: s.name, category: s.cat,
        price: s.bid, change: s.change,
      })),
    };
  }

  function get_market_overview() {
    const eq = window.equiti || {};
    const all = eq.listAllSymbols ? eq.listAllSymbols() : [];
    const byCategory = {};
    for (const s of all) {
      byCategory[s.cat] = (byCategory[s.cat] || 0) + 1;
    }
    return { totalInstruments: all.length, byCategory: byCategory };
  }

  /* ── Read-only: news ──────────────────────────────────────────────── */

  function get_latest_news(args) {
    const all = (window.NEWS_STORIES || []).slice();
    const cat = (args && args.category) ? args.category.toUpperCase() : null;
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 5;
    const filtered = cat ? all.filter((s) => (s.tag || '').toUpperCase() === cat) : all;
    return {
      featured: window.NEWS_FEATURED || null,
      category: cat || 'all',
      count: filtered.length,
      stories: filtered.slice(0, limit).map((s) => ({
        category: s.tag, headline: s.t, time: s.time,
      })),
    };
  }

  /* ── Read-only: portfolio + performance ───────────────────────────── */

  function get_portfolio_summary() {
    const eq = window.equiti || {};
    const t = eq.getTradingNumbers ? eq.getTradingNumbers() : null;
    if (!t) return { ok: false, error: 'trade store not ready' };
    return {
      totalValue: Number(t.total.toFixed(2)),
      tradingEquity: Number(t.equity.toFixed(2)),
      tradingBalance: Number(t.tradingBalance.toFixed(2)),
      openPL: Number(t.openPL.toFixed(2)),
      closedPL: Number(t.closedPL.toFixed(2)),
      openPositionsCount: t.positionsCount,
    };
  }

  function get_allocation() {
    const get = window.getBalance || function () { return 0; };
    const eq = window.equiti || {};
    const t = eq.getTradingNumbers ? eq.getTradingNumbers() : null;
    /* Trading slice uses equity (balance + open P&L); other pockets
       are static balances. Same logic as the Performance screen. */
    const slices = [
      { account: 'trading', value: t ? t.equity : get('trading') },
      { account: 'gold',    value: get('gold') },
      { account: 'wealth',  value: get('wealth') },
      { account: 'shares',  value: get('shares') },
      { account: 'savings', value: get('savings') },
    ];
    const total = slices.reduce((s, x) => s + x.value, 0) || 1;
    return {
      total: Number(total.toFixed(2)),
      slices: slices.map((s) => ({
        account: s.account,
        value: Number(s.value.toFixed(2)),
        percent: Number(((s.value / total) * 100).toFixed(1)),
      })),
    };
  }

  function get_free_margin() {
    const eq = window.equiti || {};
    const fm = eq.getFreeMargin ? eq.getFreeMargin() : null;
    if (!fm) return { ok: false, error: 'trade store not ready' };
    return {
      equity: Number(fm.equity.toFixed(2)),
      usedMargin: Number(fm.usedMargin.toFixed(2)),
      freeMargin: Number(fm.freeMargin.toFixed(2)),
      marginLevel: fm.usedMargin > 0 ? Math.round((fm.equity / fm.usedMargin) * 100) : null,
    };
  }

  /* ── Read-only: trade history + transactions ──────────────────────── */

  function get_trade_history(args) {
    const eq = window.equiti || {};
    const closed = eq.listClosedPositions ? eq.listClosedPositions() : [];
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 10;
    return {
      count: closed.length,
      trades: closed.slice(-limit).reverse().map((p) => ({
        id: p.id, symbol: p.sym, side: p.side, size: p.size,
        openPrice: p.open, closePrice: p.close,
        realizedPL: Number((p.realizedPL || 0).toFixed(2)),
        openedAt: p.openedAt, closedAt: p.closedAt,
      })),
    };
  }

  function get_recent_transactions(args) {
    const all = (window.WALLET_TRANSACTIONS || []).slice();
    const eq = window.equiti || {};
    const pending = eq.listPendingWithdrawals ? eq.listPendingWithdrawals() : [];
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 5;
    /* Merge pending withdrawals into the feed so the agent can mention
       them without a separate tool call. */
    const merged = pending.map((p) => ({
      id: p.id, kind: 'out', status: 'pending',
      title: 'Withdrawal pending', amount: p.amount, account: p.account, meta: 'Just requested',
    })).concat(all);
    return {
      count: merged.length,
      transactions: merged.slice(0, limit).map((t) => ({
        id: t.id, kind: t.kind, status: t.status,
        title: t.title, amount: t.amount,
        direction: t.direction, account: t.account, meta: t.meta,
      })),
    };
  }

  function get_watchlist() {
    const eq = window.equiti || {};
    const ids = window.WATCHLIST_IDS || [];
    const all = eq.listAllSymbols ? eq.listAllSymbols() : [];
    const byId = {};
    for (const s of all) byId[s.sym] = s;
    return {
      count: ids.length,
      symbols: ids.map((id) => byId[id]).filter(Boolean).map((s) => ({
        symbol: s.sym, name: s.name, category: s.cat,
        bid: s.bid, ask: s.ask, change: s.change,
      })),
    };
  }

  function get_economic_calendar(args) {
    const all = (window.ECONOMIC_CALENDAR || []).slice();
    const minImpact = (args && Number.isInteger(args.min_impact)) ? args.min_impact : 1;
    const filtered = all.filter((e) => (e.impact || 0) >= minImpact);
    return {
      count: filtered.length,
      events: filtered.map((e) => ({
        time: e.time, currency: e.currency, impact: e.impact,
        event: e.event, forecast: e.forecast, previous: e.previous,
      })),
    };
  }

  function get_account_info() {
    const a = window.TRADE_ACCOUNT || {};
    return {
      id: a.id || null, mode: a.mode || null, type: a.type || null,
      leverage: a.leverage || 500, currency: a.currency || 'USD',
    };
  }

  function get_equity_curve(args) {
    const series = window.PERF_SERIES || [];
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : series.length;
    const points = series.slice(-limit);
    if (points.length === 0) {
      return { count: 0, values: [], change: 0, changePct: 0 };
    }
    const first = points[0];
    const last = points[points.length - 1];
    return {
      count: points.length,
      values: points,
      first: first, last: last, high: Math.max.apply(null, points), low: Math.min.apply(null, points),
      change: Number((last - first).toFixed(2)),
      changePct: Number((((last - first) / first) * 100).toFixed(2)),
    };
  }

  function get_today_trading_summary() {
    const eq = window.equiti || {};
    const positions = eq.listPositions ? eq.listPositions() : [];
    const closed = eq.listClosedPositions ? eq.listClosedPositions() : [];
    const symbols = eq.listAllSymbols ? eq.listAllSymbols() : [];
    /* Today-filter is loose: in the demo all session activity counts.
       Returns open positions, closed-today, notional volume, net P&L. */
    const openPL = positions.reduce((s, p) => s + (p.livePL || 0), 0);
    const closedPL = closed.reduce((s, p) => s + (p.realizedPL || 0), 0);
    const contractFor = (sym) => {
      const symObj = symbols.find((x) => x.sym === sym);
      const cat = symObj ? symObj.cat : null;
      if (cat === 'forex') return 100000;
      if (sym === 'XAGUSD') return 5000;
      if (cat === 'commodities') return 100;
      return 1;
    };
    const volumeOpen = positions.reduce((s, p) => s + Math.abs(p.size * contractFor(p.sym) * p.open), 0);
    const volumeClosed = closed.reduce((s, p) => s + Math.abs(p.size * contractFor(p.sym) * p.open), 0);
    return {
      openPositions: positions.length,
      closedPositions: closed.length,
      totalTrades: positions.length + closed.length,
      notionalVolume: Number((volumeOpen + volumeClosed).toFixed(2)),
      openPL: Number(openPL.toFixed(2)),
      closedPL: Number(closedPL.toFixed(2)),
      netPL: Number((openPL + closedPL).toFixed(2)),
    };
  }

  function get_activity_feed(args) {
    const eq = window.equiti || {};
    const closed = eq.listClosedPositions ? eq.listClosedPositions() : [];
    const baseActivity = window.TRADE_ACTIVITY || [];
    const limit = (args && Number.isInteger(args.limit)) ? args.limit : 10;
    /* Convert closed positions into activity rows + merge with the
       hardcoded TRADE_ACTIVITY feed (deposits, settlements, etc.). */
    const closedAsActivity = closed.slice().reverse().map((p) => ({
      kind: 'fill', title: p.side === 'buy' ? 'Closed buy' : 'Closed sell',
      detail: p.sym + ' · ' + p.size + ' lots',
      amt: Number((p.realizedPL || 0).toFixed(2)),
      time: p.closedAt, dateLabel: 'Today',
    }));
    const feed = closedAsActivity.concat(baseActivity);
    return {
      count: feed.length,
      activity: feed.slice(0, limit).map((a) => ({
        kind: a.kind, title: a.title, detail: a.detail,
        amount: a.amt, time: a.time, date: a.dateLabel,
      })),
    };
  }

  function get_pending_withdrawals() {
    const eq = window.equiti || {};
    const pending = eq.listPendingWithdrawals ? eq.listPendingWithdrawals() : [];
    return {
      count: pending.length,
      pending: pending.map((p) => ({
        id: p.id, account: p.account, amount: p.amount,
        requestedAt: p.requestedAt, status: 'pending',
      })),
    };
  }

  /* ── NEW: cash flow ───────────────────────────────────────────────── */

  function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }

  async function open_cash_flow(args) {
    const kind = args && args.kind;
    const valid = ['deposit', 'withdraw', 'transfer'];
    if (valid.indexOf(kind) < 0) {
      return { ok: false, error: 'kind must be one of: ' + valid.join(', ') };
    }
    if (!window.openCashFlow) return { ok: false, error: 'cash flow bridge not available' };
    const amount = (args && args.amount != null && isFinite(args.amount)) ? Number(args.amount) : undefined;
    /* execute defaults to TRUE so cash-flow actions complete without
       requiring a verbal confirmation. Pass execute:false explicitly to
       just open the sheet with no auto-submit (e.g., when the user said
       'open the deposit sheet' with no amount). */
    const execute = !(args && args.execute === false);
    const transferTo = args && args.transfer_to;

    /* Step 1 — slide the sheet up with the amount pre-filled. */
    window.openCashFlow(kind, amount);
    if (!execute) {
      return {
        ok: true,
        kind: kind,
        message: 'Opened the ' + kind + ' sheet' + (amount != null ? ' pre-filled with $' + amount : '') +
          '. The user needs to tap Confirm to complete it.',
      };
    }

    /* Step 2 — wait for the sheet's slide-up animation (420ms) + a
       beat so the user can read the values that just dropped in.
       Always re-fetch window.equiti.cashFlow before each call — the
       bridge re-binds every render with the latest closures, and an
       old reference would commit with stale state (e.g. default toId). */
    await sleep(900);
    if (!window.equiti || !window.equiti.cashFlow) {
      return { ok: false, error: 'sheet bridge not mounted' };
    }
    if (kind === 'transfer' && transferTo) {
      window.equiti.cashFlow.setTransferTo(transferTo);
      await sleep(400);
    }
    const canConfirm = window.equiti.cashFlow.canConfirm && window.equiti.cashFlow.canConfirm();
    if (!canConfirm) {
      return { ok: false, error: 'sheet not in confirmable state (missing amount or invalid?)' };
    }
    /* Step 3 — programmatic confirm. The sheet handles closing + toast. */
    window.equiti.cashFlow.confirm();
    return {
      ok: true,
      kind: kind,
      executed: true,
      amount: amount,
      message: 'Filled in and confirmed the ' + kind + (amount != null ? ' of $' + amount : '') + '.',
    };
  }

  /* ── NEW: trade subapp navigation ─────────────────────────────────── */

  function open_trade_section(args) {
    const section = args && args.section;
    const eq = window.equiti || {};
    /* Ensure we're inside the leveraged-trade route first. */
    if (eq.getRoute && eq.getRoute() !== 'leveraged-trade') {
      if (eq.openLeveragedTrade) eq.openLeveragedTrade();
      /* Defer the tab change until the route swap commits — the
         openTradeSection bridge mounts a frame later. */
      setTimeout(function () {
        if (window.equiti && window.equiti.openTradeSection) {
          window.equiti.openTradeSection(section);
        }
      }, 500);
      return { ok: true, route: 'leveraged-trade', section: section, deferred: true };
    }
    if (!eq.openTradeSection) return { ok: false, error: 'trade subapp not mounted' };
    const ok = eq.openTradeSection(section);
    return ok ? { ok: true, section: section } : { ok: false, error: 'invalid section: ' + section };
  }

  function select_symbol(args) {
    const symbol = args && args.symbol;
    if (!symbol) return { ok: false, error: 'symbol required' };
    const eq = window.equiti || {};
    if (eq.getRoute && eq.getRoute() !== 'leveraged-trade') {
      if (eq.openLeveragedTrade) eq.openLeveragedTrade();
      setTimeout(function () {
        if (window.equiti && window.equiti.selectSymbol) window.equiti.selectSymbol(symbol);
      }, 500);
      return { ok: true, symbol: symbol, deferred: true };
    }
    if (!eq.selectSymbol) return { ok: false, error: 'trade subapp not mounted' };
    const ok = eq.selectSymbol(symbol);
    return ok ? { ok: true, symbol: symbol } : { ok: false, error: 'unable to select ' + symbol };
  }

  /* ── KB search: 209 Equiti UAE help-center articles baked into the app
       at data/equiti-kb-v2.json, loaded into window.EQUITI_KB by a script
       tag in Equiti SuperApp.html. No MCP, no server roundtrip — purely
       in-memory keyword search.

       Scoring (same weights as the previous server-side version):
         question hit  +4   (strongest signal)
         alias hit     +3   (curated synonyms)
         topic hit     +2
         body / emb    +1
       Optional topic_filter restricts to a single topic_slug. */
  function search_knowledge_base(args) {
    const kbFile = window.EQUITI_KB;
    if (!kbFile || !Array.isArray(kbFile.knowledge_base)) {
      return { ok: false, error: 'KB not loaded' };
    }
    const articles = kbFile.knowledge_base;
    const query = String((args && args.query) || '').toLowerCase();
    const topic = args && args.topic_filter;
    const topK = Math.min(Math.max(Number((args && args.top_k) || 3), 1), 10);

    const words = query.split(/\s+/).map(w => w.replace(/[^a-z0-9+]/g, '')).filter(w => w.length > 2);
    if (words.length === 0) return { query: args.query, count: 0, results: [] };

    let scored = articles.map(a => {
      let score = 0;
      const q = (a.question || '').toLowerCase();
      const t = (a.topic || '').toLowerCase();
      const al = ((a.aliases || []).join(' ')).toLowerCase();
      const det = (a.details || '').toLowerCase();
      const emb = (a.embedding_text || '').toLowerCase();
      for (const w of words) {
        if (q.includes(w)) score += 4;
        if (al.includes(w)) score += 3;
        if (t.includes(w)) score += 2;
        if (det.includes(w)) score += 1;
        if (emb.includes(w) && !q.includes(w) && !al.includes(w)) score += 1;
      }
      return { a: a, score: score };
    });
    if (topic) scored = scored.filter(s => s.a.topic_slug === topic);
    const results = scored
      .filter(s => s.score > 0)
      .sort((x, y) => y.score - x.score)
      .slice(0, topK)
      .map(s => ({
        id: s.a.id,
        topic: s.a.topic,
        topic_slug: s.a.topic_slug,
        question: s.a.question,
        answer: s.a.answer,
        details: s.a.details,
      }));
    return { query: args.query, topic_filter: topic || null, count: results.length, results: results };
  }

  /* ── get_platform_health — baked-in platform status.
       Mirrors the dashboard's payments-degraded demo state so the voice
       agent gives consistent answers about service health. The MCP
       connection that previously sourced live status was removed; this
       returns a static snapshot of the same shape the old endpoint
       served, hard-coding "payments: degraded" as the default
       (matching what the operator sees on the dashboard's HealthPanel).

       The model speaks 1-2 sentences from this — it should NOT read the
       JSON aloud verbatim. See the get_platform_health entry in
       PLAN_C_INSTRUCTIONS for the voice contract. */
  function get_platform_health() {
    return {
      ok: true,
      overall_status: 'degraded',
      summary: 'Payments are currently degraded; bank transfers may take 2-4 hours longer than usual. All other services are operational.',
      services: {
        trading: { status: 'operational' },
        payments: {
          status: 'degraded',
          message: 'Bank transfers experiencing 2-4 hour delays due to provider maintenance.',
        },
        portal: { status: 'operational' },
        market_data: { status: 'operational' },
        authentication: { status: 'operational' },
        mobile_app: { status: 'operational' },
      },
      active_incidents: [
        {
          incident_id: 'INC-2001',
          service: 'payments',
          severity: 'minor',
          title: 'Bank Transfer Delays',
          description: 'Our banking partner is performing scheduled maintenance. Bank transfers may experience 2-4 hour delays during this period.',
          started_at: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
          affected_count: 38,
        },
      ],
    };
  }

  window.VOICE_TOOL_DISPATCHERS = {
    /* Reads — account */
    get_total_balance: get_total_balance,
    get_account_balances: get_account_balances,
    get_portfolio_summary: get_portfolio_summary,
    get_allocation: get_allocation,
    get_free_margin: get_free_margin,
    /* Reads — market */
    get_instrument_quote: get_instrument_quote,
    list_symbols: list_symbols,
    get_top_movers: get_top_movers,
    get_market_overview: get_market_overview,
    get_watchlist: get_watchlist,
    get_economic_calendar: get_economic_calendar,
    /* Reads — news */
    get_latest_news: get_latest_news,
    /* Reads — trading */
    get_open_positions: get_open_positions,
    get_trade_history: get_trade_history,
    get_today_trading_summary: get_today_trading_summary,
    get_activity_feed: get_activity_feed,
    /* Reads — transactions */
    get_recent_transactions: get_recent_transactions,
    get_pending_withdrawals: get_pending_withdrawals,
    /* Reads — account / metrics */
    get_account_info: get_account_info,
    get_equity_curve: get_equity_curve,
    /* Reads — UI state */
    get_current_screen: get_current_screen,
    /* Navigation */
    navigate: navigate,
    open_trade_section: open_trade_section,
    select_symbol: select_symbol,
    /* Actions */
    place_trade: place_trade,
    close_position: close_position,
    open_cash_flow: open_cash_flow,
    escalate_to_human: escalate_to_human,
    /* KB — in-memory keyword search over baked-in JSON (no MCP) */
    search_knowledge_base: search_knowledge_base,
    /* Platform status — baked-in snapshot, no MCP roundtrip */
    get_platform_health: get_platform_health,
  };
})();
