RayMelius Claude Sonnet 4.6 commited on
Commit
db2a094
·
1 Parent(s): 9599ff2

FIX UI: convert to AJAX - fix connect/disconnect/order broken under nginx

Browse files

Replace <a href> and <form action> with fetch() calls so nginx proxy
rewrites don't interfere. Add BASE path detection, status polling every
2s, messages polling every 2s, and order feedback without page reload.
Remove unused redirect/url_for imports from backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix-ui-client/fix-ui-client.py CHANGED
@@ -4,7 +4,7 @@ sys.path.insert(0, "/app")
4
 
5
  import quickfix as fix
6
  import quickfix44 as fix44
7
- from flask import Flask, render_template, request, redirect, url_for, jsonify
8
  import json
9
  import threading, time, os
10
  from collections import deque
@@ -172,25 +172,32 @@ def status():
172
  @app.route("/connect")
173
  def connect():
174
  threading.Thread(target=start_fix, daemon=True).start()
175
- return redirect(url_for("index"))
176
 
177
  @app.route("/disconnect")
178
  def disconnect():
179
  stop_fix()
180
- return redirect(url_for("index"))
181
 
182
  @app.route("/order", methods=["POST"])
183
  def order():
184
  if not fix_app or not fix_app.connected:
185
  log("⚠️ Tried to send while disconnected")
186
- return redirect(url_for("index"))
187
- side = request.form.get("side", "buy")
 
188
  side_tag = "1" if side.lower() == "buy" else "2"
189
- symbol = request.form.get("symbol", "FOO")
190
- qty = float(request.form.get("qty", "100"))
191
- price = float(request.form.get("price", "10"))
192
  fix_app.send_order(side_tag, symbol, qty, price)
193
- return redirect(url_for("index"))
 
 
 
 
 
 
194
 
195
  # --- Configurable ---
196
  CONFIG_FILE = os.getenv("FIX_CONFIG", "client.cfg")
 
4
 
5
  import quickfix as fix
6
  import quickfix44 as fix44
7
+ from flask import Flask, render_template, request, jsonify
8
  import json
9
  import threading, time, os
10
  from collections import deque
 
172
  @app.route("/connect")
173
  def connect():
174
  threading.Thread(target=start_fix, daemon=True).start()
175
+ return jsonify({"status": "ok", "message": "Connecting..."})
176
 
177
  @app.route("/disconnect")
178
  def disconnect():
179
  stop_fix()
180
+ return jsonify({"status": "ok", "message": "Disconnected"})
181
 
182
  @app.route("/order", methods=["POST"])
183
  def order():
184
  if not fix_app or not fix_app.connected:
185
  log("⚠️ Tried to send while disconnected")
186
+ return jsonify({"status": "error", "message": "Not connected"}), 400
187
+ data = request.get_json(force=True) or {}
188
+ side = data.get("side", "buy")
189
  side_tag = "1" if side.lower() == "buy" else "2"
190
+ symbol = data.get("symbol", "FOO")
191
+ qty = float(data.get("qty", 100))
192
+ price = float(data.get("price", 10))
193
  fix_app.send_order(side_tag, symbol, qty, price)
194
+ return jsonify({"status": "ok", "message": "Order sent"})
195
+
196
+ @app.route("/messages")
197
+ def messages_route():
198
+ with _msgs_lock:
199
+ msgs = list(reversed(_messages))
200
+ return jsonify(msgs)
201
 
202
  # --- Configurable ---
203
  CONFIG_FILE = os.getenv("FIX_CONFIG", "client.cfg")
fix-ui-client/templates/index.html CHANGED
@@ -30,15 +30,14 @@
30
  font-size: 12px;
31
  font-weight: bold;
32
  }
33
- .status .dot {
34
- width: 10px;
35
- height: 10px;
36
- border-radius: 50%;
37
- }
38
- .status.connected { background: #d4edda; color: #155724; }
39
  .status.connected .dot { background: #28a745; }
40
  .status.disconnected { background: #f8d7da; color: #721c24; }
41
  .status.disconnected .dot { background: #dc3545; }
 
 
 
42
 
43
  /* Buttons */
44
  .btn-group { display: flex; gap: 8px; margin-top: 10px; }
@@ -49,11 +48,9 @@
49
  cursor: pointer;
50
  font-size: 13px;
51
  font-weight: bold;
52
- text-decoration: none;
53
- display: inline-block;
54
  }
55
- .btn-connect { background: #28a745; color: #fff; }
56
- .btn-connect:hover { background: #218838; }
57
  .btn-disconnect { background: #dc3545; color: #fff; }
58
  .btn-disconnect:hover { background: #c82333; }
59
 
@@ -87,19 +84,16 @@
87
  }
88
  .btn-send:hover { background: #1976D2; }
89
 
90
- /* ── Mobile responsive ───────────────────────────────────────────────────── */
91
- @media (max-width: 768px) {
92
- body { margin: 10px; }
93
- h1 { font-size: 18px; flex-wrap: wrap; gap: 8px; }
94
- .container { grid-template-columns: 1fr; }
95
- .panel { min-height: unset; }
96
- }
97
-
98
- @media (max-width: 480px) {
99
- body { margin: 6px; }
100
- h1 { font-size: 14px; }
101
- .btn { font-size: 12px; padding: 6px 12px; }
102
  }
 
 
103
 
104
  /* Messages box */
105
  .messages-box {
@@ -115,15 +109,28 @@
115
  line-height: 1.5;
116
  min-height: 300px;
117
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </style>
119
  </head>
120
  <body>
121
 
122
  <h1>
123
  FIX UI Client
124
- <span class="status {{ 'connected' if connected else 'disconnected' }}">
125
  <span class="dot"></span>
126
- <span>{{ 'CONNECTED' if connected else 'DISCONNECTED' }}</span>
127
  </span>
128
  <a href="/" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none;">← Dashboard</a>
129
  </h1>
@@ -134,24 +141,24 @@
134
  <div class="panel">
135
  <h2>Connection</h2>
136
  <div class="btn-group">
137
- <a href="{{ url_for('connect') }}" class="btn btn-connect">Connect</a>
138
- <a href="{{ url_for('disconnect') }}" class="btn btn-disconnect">Disconnect</a>
139
  </div>
140
 
141
  <hr class="divider">
142
 
143
  <h2>Send Order</h2>
144
- <form action="{{ url_for('order') }}" method="post">
145
  <div class="form-group">
146
  <label>Side</label>
147
- <select name="side">
148
  <option value="buy">BUY</option>
149
  <option value="sell">SELL</option>
150
  </select>
151
  </div>
152
  <div class="form-group">
153
  <label>Symbol</label>
154
- <select name="symbol">
155
  {% for s in securities %}
156
  <option value="{{ s }}">{{ s }}</option>
157
  {% endfor %}
@@ -159,24 +166,105 @@
159
  </div>
160
  <div class="form-group">
161
  <label>Quantity</label>
162
- <input type="number" step="1" name="qty" value="100">
163
  </div>
164
  <div class="form-group">
165
  <label>Price</label>
166
- <input type="number" step="0.01" name="price" value="150.00">
167
  </div>
168
  <button type="submit" class="btn-send">Send Order</button>
169
  </form>
 
170
  </div>
171
 
172
  <!-- Right panel: Messages log -->
173
  <div class="panel">
174
  <h2>Messages <span style="font-size:12px; color:#999; font-weight:normal;">FIX Session Log</span></h2>
175
- <div class="messages-box">{% for msg in messages %}{{ msg }}
176
- {% endfor %}</div>
177
  </div>
178
 
179
  </div>
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  </body>
182
  </html>
 
30
  font-size: 12px;
31
  font-weight: bold;
32
  }
33
+ .status .dot { width: 10px; height: 10px; border-radius: 50%; }
34
+ .status.connected { background: #d4edda; color: #155724; }
 
 
 
 
35
  .status.connected .dot { background: #28a745; }
36
  .status.disconnected { background: #f8d7da; color: #721c24; }
37
  .status.disconnected .dot { background: #dc3545; }
38
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
39
+ .status.connecting { background: #fff3cd; color: #856404; }
40
+ .status.connecting .dot { background: #ffc107; animation: pulse 1s infinite; }
41
 
42
  /* Buttons */
43
  .btn-group { display: flex; gap: 8px; margin-top: 10px; }
 
48
  cursor: pointer;
49
  font-size: 13px;
50
  font-weight: bold;
 
 
51
  }
52
+ .btn-connect { background: #28a745; color: #fff; }
53
+ .btn-connect:hover { background: #218838; }
54
  .btn-disconnect { background: #dc3545; color: #fff; }
55
  .btn-disconnect:hover { background: #c82333; }
56
 
 
84
  }
85
  .btn-send:hover { background: #1976D2; }
86
 
87
+ /* Order feedback */
88
+ #order-status {
89
+ padding: 8px 12px;
90
+ border-radius: 4px;
91
+ font-size: 13px;
92
+ margin-top: 10px;
93
+ display: none;
 
 
 
 
 
94
  }
95
+ #order-status.success { background: #d4edda; color: #155724; display: block; }
96
+ #order-status.error { background: #f8d7da; color: #721c24; display: block; }
97
 
98
  /* Messages box */
99
  .messages-box {
 
109
  line-height: 1.5;
110
  min-height: 300px;
111
  }
112
+
113
+ /* Mobile responsive */
114
+ @media (max-width: 768px) {
115
+ body { margin: 10px; }
116
+ h1 { font-size: 18px; flex-wrap: wrap; gap: 8px; }
117
+ .container { grid-template-columns: 1fr; }
118
+ .panel { min-height: unset; }
119
+ }
120
+ @media (max-width: 480px) {
121
+ body { margin: 6px; }
122
+ h1 { font-size: 14px; }
123
+ .btn { font-size: 12px; padding: 6px 12px; }
124
+ }
125
  </style>
126
  </head>
127
  <body>
128
 
129
  <h1>
130
  FIX UI Client
131
+ <span id="status-badge" class="status connecting">
132
  <span class="dot"></span>
133
+ <span id="status-text">Connecting...</span>
134
  </span>
135
  <a href="/" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none;">← Dashboard</a>
136
  </h1>
 
141
  <div class="panel">
142
  <h2>Connection</h2>
143
  <div class="btn-group">
144
+ <button onclick="doConnect()" class="btn btn-connect">Connect</button>
145
+ <button onclick="doDisconnect()" class="btn btn-disconnect">Disconnect</button>
146
  </div>
147
 
148
  <hr class="divider">
149
 
150
  <h2>Send Order</h2>
151
+ <form id="orderForm" onsubmit="sendOrder(event)">
152
  <div class="form-group">
153
  <label>Side</label>
154
+ <select id="f-side">
155
  <option value="buy">BUY</option>
156
  <option value="sell">SELL</option>
157
  </select>
158
  </div>
159
  <div class="form-group">
160
  <label>Symbol</label>
161
+ <select id="f-symbol">
162
  {% for s in securities %}
163
  <option value="{{ s }}">{{ s }}</option>
164
  {% endfor %}
 
166
  </div>
167
  <div class="form-group">
168
  <label>Quantity</label>
169
+ <input id="f-qty" type="number" step="1" value="100">
170
  </div>
171
  <div class="form-group">
172
  <label>Price</label>
173
+ <input id="f-price" type="number" step="0.01" value="150.00">
174
  </div>
175
  <button type="submit" class="btn-send">Send Order</button>
176
  </form>
177
+ <div id="order-status"></div>
178
  </div>
179
 
180
  <!-- Right panel: Messages log -->
181
  <div class="panel">
182
  <h2>Messages <span style="font-size:12px; color:#999; font-weight:normal;">FIX Session Log</span></h2>
183
+ <div id="messages-box" class="messages-box">Loading...</div>
 
184
  </div>
185
 
186
  </div>
187
 
188
+ <script>
189
+ // Works whether served at / or under /fix/
190
+ const BASE = window.location.pathname === '/' ? ''
191
+ : window.location.pathname.replace(/\/$/, '');
192
+
193
+ function setStatus(cls, text) {
194
+ document.getElementById('status-badge').className = 'status ' + cls;
195
+ document.getElementById('status-text').textContent = text;
196
+ }
197
+
198
+ async function pollStatus() {
199
+ try {
200
+ const r = await fetch(BASE + '/status');
201
+ const d = await r.json();
202
+ setStatus(d.connected ? 'connected' : 'disconnected',
203
+ d.connected ? 'CONNECTED' : 'DISCONNECTED');
204
+ } catch(e) {
205
+ setStatus('disconnected', 'DISCONNECTED');
206
+ }
207
+ }
208
+
209
+ async function doConnect() {
210
+ setStatus('connecting', 'Connecting...');
211
+ try { await fetch(BASE + '/connect'); } catch(e) {}
212
+ setTimeout(pollStatus, 2000);
213
+ }
214
+
215
+ async function doDisconnect() {
216
+ try { await fetch(BASE + '/disconnect'); } catch(e) {}
217
+ setStatus('disconnected', 'DISCONNECTED');
218
+ }
219
+
220
+ async function sendOrder(evt) {
221
+ evt.preventDefault();
222
+ const statusEl = document.getElementById('order-status');
223
+ const data = {
224
+ side: document.getElementById('f-side').value,
225
+ symbol: document.getElementById('f-symbol').value,
226
+ qty: parseFloat(document.getElementById('f-qty').value),
227
+ price: parseFloat(document.getElementById('f-price').value),
228
+ };
229
+ try {
230
+ const r = await fetch(BASE + '/order', {
231
+ method: 'POST',
232
+ headers: {'Content-Type': 'application/json'},
233
+ body: JSON.stringify(data)
234
+ });
235
+ const d = await r.json();
236
+ if (d.status === 'ok') {
237
+ statusEl.className = 'success';
238
+ statusEl.textContent = `Order sent: ${data.side.toUpperCase()} ${data.qty} ${data.symbol} @ ${data.price.toFixed(2)}`;
239
+ } else {
240
+ statusEl.className = 'error';
241
+ statusEl.textContent = d.message || 'Error sending order';
242
+ }
243
+ } catch(e) {
244
+ statusEl.className = 'error';
245
+ statusEl.textContent = 'Error: ' + e.message;
246
+ }
247
+ setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
248
+ }
249
+
250
+ let lastMsgCount = 0;
251
+ async function pollMessages() {
252
+ try {
253
+ const r = await fetch(BASE + '/messages');
254
+ const msgs = await r.json();
255
+ if (msgs.length !== lastMsgCount) {
256
+ const box = document.getElementById('messages-box');
257
+ box.textContent = msgs.join('\n');
258
+ lastMsgCount = msgs.length;
259
+ }
260
+ } catch(e) {}
261
+ }
262
+
263
+ pollStatus();
264
+ pollMessages();
265
+ setInterval(pollStatus, 2000);
266
+ setInterval(pollMessages, 2000);
267
+ </script>
268
+
269
  </body>
270
  </html>