| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>FIX UI Client</title> |
| <style> |
| body { font-family: Arial, sans-serif; margin: 20px; background: #f7f7f7; } |
| h1 { display: flex; align-items: center; gap: 15px; margin-bottom: 20px; } |
| h2 { margin: 5px 0 10px; font-size: 16px; } |
| .container { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 20px; } |
| |
| .panel { |
| background: #fff; |
| border-radius: 8px; |
| padding: 15px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| display: flex; |
| flex-direction: column; |
| min-height: 400px; |
| } |
| |
| |
| .status { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| padding: 4px 12px; |
| border-radius: 20px; |
| font-size: 12px; |
| font-weight: bold; |
| } |
| .status .dot { width: 10px; height: 10px; border-radius: 50%; } |
| .status.connected { background: #d4edda; color: #155724; } |
| .status.connected .dot { background: #28a745; } |
| .status.disconnected { background: #f8d7da; color: #721c24; } |
| .status.disconnected .dot { background: #dc3545; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } |
| .status.connecting { background: #fff3cd; color: #856404; } |
| .status.connecting .dot { background: #ffc107; animation: pulse 1s infinite; } |
| |
| |
| .btn-group { display: flex; gap: 8px; margin-top: 10px; } |
| .btn { |
| padding: 7px 18px; |
| border: none; |
| border-radius: 4px; |
| cursor: pointer; |
| font-size: 13px; |
| font-weight: bold; |
| } |
| .btn-connect { background: #28a745; color: #fff; } |
| .btn-connect:hover { background: #218838; } |
| .btn-disconnect { background: #dc3545; color: #fff; } |
| .btn-disconnect:hover { background: #c82333; } |
| |
| |
| .divider { border: none; border-top: 1px solid #eee; margin: 15px 0; } |
| |
| |
| .form-group { margin-bottom: 12px; } |
| .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } |
| .form-group input, |
| .form-group select { |
| width: 100%; |
| padding: 8px; |
| border: 1px solid #ccc; |
| border-radius: 4px; |
| font-size: 13px; |
| box-sizing: border-box; |
| } |
| .form-group select option[value="buy"] { color: #2e7d32; } |
| .form-group select option[value="sell"] { color: #c62828; } |
| .btn-send { |
| width: 100%; |
| padding: 10px; |
| background: #2196F3; |
| color: #fff; |
| border: none; |
| border-radius: 4px; |
| cursor: pointer; |
| font-size: 14px; |
| font-weight: bold; |
| } |
| .btn-send:hover { background: #1976D2; } |
| |
| |
| #order-status { |
| padding: 8px 12px; |
| border-radius: 4px; |
| font-size: 13px; |
| margin-top: 10px; |
| display: none; |
| } |
| #order-status.success { background: #d4edda; color: #155724; display: block; } |
| #order-status.error { background: #f8d7da; color: #721c24; display: block; } |
| |
| |
| .messages-box { |
| flex-grow: 1; |
| overflow-y: auto; |
| background: #1a1a2e; |
| color: #e0e0e0; |
| padding: 12px; |
| border-radius: 6px; |
| font-family: monospace; |
| font-size: 12px; |
| white-space: pre-wrap; |
| line-height: 1.5; |
| min-height: 300px; |
| } |
| |
| |
| @media (max-width: 768px) { |
| body { margin: 10px; } |
| h1 { font-size: 18px; flex-wrap: wrap; gap: 8px; } |
| .container { grid-template-columns: 1fr; } |
| .panel { min-height: unset; } |
| } |
| @media (max-width: 480px) { |
| body { margin: 6px; } |
| h1 { font-size: 14px; } |
| .btn { font-size: 12px; padding: 6px 12px; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <h1> |
| FIX UI Client |
| <span id="status-badge" class="status connecting"> |
| <span class="dot"></span> |
| <span id="status-text">Connecting...</span> |
| </span> |
| <a onclick="window.location.href='/'" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none; cursor:pointer;">← Dashboard</a> |
| </h1> |
|
|
| <div class="container"> |
|
|
| |
| <div class="panel"> |
| <h2>Connection</h2> |
| <div class="btn-group"> |
| <button onclick="doConnect()" class="btn btn-connect">Connect</button> |
| <button onclick="doDisconnect()" class="btn btn-disconnect">Disconnect</button> |
| </div> |
|
|
| <hr class="divider"> |
|
|
| <h2>Send Order</h2> |
| <form id="orderForm" onsubmit="sendOrder(event)"> |
| <div class="form-group"> |
| <label>Side</label> |
| <select id="f-side"> |
| <option value="buy">BUY</option> |
| <option value="sell">SELL</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>Symbol</label> |
| <select id="f-symbol" onchange="updatePrice()"> |
| {% for s in securities %} |
| <option value="{{ s }}">{{ s }}</option> |
| {% endfor %} |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>Quantity</label> |
| <input id="f-qty" type="number" step="1" value="100"> |
| </div> |
| <div class="form-group"> |
| <label>Price</label> |
| <input id="f-price" type="number" step="0.01" value=""> |
| </div> |
| <button type="submit" class="btn-send">Send Order</button> |
| </form> |
| <div id="order-status"></div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <h2>Messages <span style="font-size:12px; color:#999; font-weight:normal;">FIX Session Log</span></h2> |
| <div id="messages-box" class="messages-box">Loading...</div> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| const SECURITIES_PRICES = {{ securities | tojson }}; |
| |
| function updatePrice() { |
| const sym = document.getElementById('f-symbol').value; |
| const price = SECURITIES_PRICES[sym]; |
| if (price !== undefined) { |
| document.getElementById('f-price').value = price.toFixed(2); |
| } |
| } |
| |
| |
| const BASE = window.location.pathname === '/' ? '' |
| : window.location.pathname.replace(/\/$/, ''); |
| |
| function setStatus(cls, text) { |
| document.getElementById('status-badge').className = 'status ' + cls; |
| document.getElementById('status-text').textContent = text; |
| } |
| |
| async function pollStatus() { |
| try { |
| const r = await fetch(BASE + '/status'); |
| const d = await r.json(); |
| setStatus(d.connected ? 'connected' : 'disconnected', |
| d.connected ? 'CONNECTED' : 'DISCONNECTED'); |
| } catch(e) { |
| setStatus('disconnected', 'DISCONNECTED'); |
| } |
| } |
| |
| async function doConnect() { |
| setStatus('connecting', 'Connecting...'); |
| try { await fetch(BASE + '/connect'); } catch(e) {} |
| setTimeout(pollStatus, 2000); |
| } |
| |
| async function doDisconnect() { |
| try { await fetch(BASE + '/disconnect'); } catch(e) {} |
| setStatus('disconnected', 'DISCONNECTED'); |
| } |
| |
| async function sendOrder(evt) { |
| evt.preventDefault(); |
| const statusEl = document.getElementById('order-status'); |
| const data = { |
| side: document.getElementById('f-side').value, |
| symbol: document.getElementById('f-symbol').value, |
| qty: parseFloat(document.getElementById('f-qty').value), |
| price: parseFloat(document.getElementById('f-price').value), |
| }; |
| try { |
| const r = await fetch(BASE + '/order', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify(data) |
| }); |
| const d = await r.json(); |
| if (d.status === 'ok') { |
| statusEl.className = 'success'; |
| statusEl.textContent = `Order sent: ${data.side.toUpperCase()} ${data.qty} ${data.symbol} @ ${data.price.toFixed(2)}`; |
| } else { |
| statusEl.className = 'error'; |
| statusEl.textContent = d.message || 'Error sending order'; |
| } |
| } catch(e) { |
| statusEl.className = 'error'; |
| statusEl.textContent = 'Error: ' + e.message; |
| } |
| setTimeout(() => { statusEl.style.display = 'none'; }, 4000); |
| } |
| |
| let lastMsgCount = 0; |
| async function pollMessages() { |
| try { |
| const r = await fetch(BASE + '/messages'); |
| const msgs = await r.json(); |
| if (msgs.length !== lastMsgCount) { |
| const box = document.getElementById('messages-box'); |
| box.textContent = msgs.join('\n'); |
| lastMsgCount = msgs.length; |
| } |
| } catch(e) {} |
| } |
| |
| pollStatus(); |
| pollMessages(); |
| setInterval(pollStatus, 2000); |
| setInterval(pollMessages, 2000); |
| updatePrice(); |
| </script> |
|
|
| </body> |
| </html> |
|
|