Replace EventSource with WebSocket for price streaming

- Switch from SSE EventSource to WebSocket client connecting to
  btcdata's new /api/price/ws endpoint
- Add automatic reconnect on connection loss (3s delay)
- Update Dockerfile BTCDATA_URL to use btcdata's new Docker port 4101

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 21:32:21 +01:00
co-authored by Claude Opus 4.6
parent ce343dfcf6
commit 02cc607e73
3 changed files with 39 additions and 29 deletions
+4 -4
View File
@@ -1,6 +1,6 @@
# btcprice # btcprice
Bitcoin price tracker UI — Clojure Kit web application. Serves the HTML/Tailwind UI. Connects to btcdata for live price data via SSE. Bitcoin price tracker UI — Clojure Kit web application. Serves the HTML/Tailwind UI. Connects to btcdata for live price data via WebSocket.
## Build & Development Commands ## Build & Development Commands
@@ -36,9 +36,9 @@ btcprice's docker-compose includes btcdata via `include: ../btcdata/docker-compo
- **Framework:** Kit (Integrant-based) - **Framework:** Kit (Integrant-based)
- **Server:** Undertow - **Server:** Undertow
- **Routing:** Reitit - **Routing:** Reitit
- **Templates:** Hiccup + vanilla JS EventSource - **Templates:** Hiccup + vanilla JS WebSocket
- **Styling:** Tailwind CSS v4 - **Styling:** Tailwind CSS v4
- **Data backend:** btcdata (separate service at BTCDATA_URL, default http://localhost:4100) - **Data backend:** btcdata (separate service at BTCDATA_URL, default http://localhost:4100, WebSocket at ws://localhost:4100)
- **No database** — all data comes from btcdata - **No database** — all data comes from btcdata
## Source Layout ## Source Layout
@@ -58,6 +58,6 @@ src/clj/pmagnus/btcprice/
│ └── formats.clj # Content negotiation │ └── formats.clj # Content negotiation
└── routes/ └── routes/
├── api.clj # /api routes (JSON) ├── api.clj # /api routes (JSON)
├── ui.clj # UI routes (HTML + EventSource) ├── ui.clj # UI routes (HTML + WebSocket)
└── utils.clj # Route utilities └── utils.clj # Route utilities
``` ```
+1 -1
View File
@@ -23,6 +23,6 @@ COPY --from=build /build/target/btcprice-standalone.jar /btcprice/btcprice-stand
EXPOSE 4040 EXPOSE 4040
ENV PORT=4040 ENV PORT=4040
ENV BTCDATA_URL=http://btcdata:4100 ENV BTCDATA_URL=http://btcdata:4101
CMD ["java", "-jar", "/btcprice/btcprice-standalone.jar"] CMD ["java", "-jar", "/btcprice/btcprice-standalone.jar"]
+34 -24
View File
@@ -23,33 +23,43 @@
(str (str
"document.addEventListener('DOMContentLoaded', function() {\n" "document.addEventListener('DOMContentLoaded', function() {\n"
" var btcdataUrl = '" btcdata-url "';\n" " var btcdataUrl = '" btcdata-url "';\n"
" var wsUrl = btcdataUrl.replace(/^http/, 'ws');\n"
" var display = document.getElementById('price-display');\n" " var display = document.getElementById('price-display');\n"
" var source = new EventSource(btcdataUrl + '/api/price/stream');\n"
"\n" "\n"
" source.addEventListener('price-update', function(e) {\n" " function connect() {\n"
" var d = JSON.parse(e.data);\n" " var ws = new WebSocket(wsUrl + '/api/price/ws');\n"
" var price = parseFloat(d.price);\n"
" var prevPrice = d.prev_price ? parseFloat(d.prev_price) : null;\n"
" var color = 'text-gray-900';\n"
" if (prevPrice !== null) {\n"
" if (price > prevPrice) color = 'text-green-600';\n"
" else if (price < prevPrice) color = 'text-red-600';\n"
" }\n"
" var fmt = '$' + price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});\n"
" var ts = new Date(d.recorded_at);\n"
" var pad = function(n) { return n < 10 ? '0' + n : n; };\n"
" var timeStr = pad(ts.getDate()) + '-' + pad(ts.getMonth()+1) + ' ' + pad(ts.getHours()) + ':' + pad(ts.getMinutes()) + ':' + pad(ts.getSeconds());\n"
" display.innerHTML = '<div class=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6\">' +\n"
" '<div class=\"text-center\">' +\n"
" '<p class=\"text-4xl font-bold ' + color + '\">' + fmt + '</p>' +\n"
" '<p class=\"text-xs text-gray-400 mt-2\">Updated ' + timeStr + '</p>' +\n"
" '</div></div>';\n"
" });\n"
"\n" "\n"
" source.onerror = function() {\n" " ws.onmessage = function(e) {\n"
" display.innerHTML = '<div class=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6\">' +\n" " var d = JSON.parse(e.data);\n"
" '<p class=\"text-center text-red-400 text-sm\">Connection lost. Reconnecting...</p></div>';\n" " if (d.ping) return;\n"
" };\n" " var price = parseFloat(d.price);\n"
" var prevPrice = d.prev_price ? parseFloat(d.prev_price) : null;\n"
" var color = 'text-gray-900';\n"
" if (prevPrice !== null) {\n"
" if (price > prevPrice) color = 'text-green-600';\n"
" else if (price < prevPrice) color = 'text-red-600';\n"
" }\n"
" var fmt = '$' + price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});\n"
" var ts = new Date(d.recorded_at);\n"
" var pad = function(n) { return n < 10 ? '0' + n : n; };\n"
" var timeStr = pad(ts.getDate()) + '-' + pad(ts.getMonth()+1) + ' ' + pad(ts.getHours()) + ':' + pad(ts.getMinutes()) + ':' + pad(ts.getSeconds());\n"
" display.innerHTML = '<div class=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6\">' +\n"
" '<div class=\"text-center\">' +\n"
" '<p class=\"text-4xl font-bold ' + color + '\">' + fmt + '</p>' +\n"
" '<p class=\"text-xs text-gray-400 mt-2\">Updated ' + timeStr + '</p>' +\n"
" '</div></div>';\n"
" };\n"
"\n"
" ws.onclose = function() {\n"
" display.innerHTML = '<div class=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6\">' +\n"
" '<p class=\"text-center text-red-400 text-sm\">Connection lost. Reconnecting...</p></div>';\n"
" setTimeout(connect, 3000);\n"
" };\n"
"\n"
" ws.onerror = function() {};\n"
" }\n"
"\n"
" connect();\n"
"});\n"))])) "});\n"))]))
(defn- ui-routes [opts] (defn- ui-routes [opts]