diff --git a/CLAUDE.md b/CLAUDE.md index 7d088f2..78cbf7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # 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 @@ -36,9 +36,9 @@ btcprice's docker-compose includes btcdata via `include: ../btcdata/docker-compo - **Framework:** Kit (Integrant-based) - **Server:** Undertow - **Routing:** Reitit -- **Templates:** Hiccup + vanilla JS EventSource +- **Templates:** Hiccup + vanilla JS WebSocket - **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 ## Source Layout @@ -58,6 +58,6 @@ src/clj/pmagnus/btcprice/ │ └── formats.clj # Content negotiation └── routes/ ├── api.clj # /api routes (JSON) - ├── ui.clj # UI routes (HTML + EventSource) + ├── ui.clj # UI routes (HTML + WebSocket) └── utils.clj # Route utilities ``` diff --git a/Dockerfile b/Dockerfile index 48b8a56..36268d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,6 @@ COPY --from=build /build/target/btcprice-standalone.jar /btcprice/btcprice-stand EXPOSE 4040 ENV PORT=4040 -ENV BTCDATA_URL=http://btcdata:4100 +ENV BTCDATA_URL=http://btcdata:4101 CMD ["java", "-jar", "/btcprice/btcprice-standalone.jar"] diff --git a/src/clj/pmagnus/btcprice/web/routes/ui.clj b/src/clj/pmagnus/btcprice/web/routes/ui.clj index f02e187..07ceaaa 100644 --- a/src/clj/pmagnus/btcprice/web/routes/ui.clj +++ b/src/clj/pmagnus/btcprice/web/routes/ui.clj @@ -23,33 +23,43 @@ (str "document.addEventListener('DOMContentLoaded', function() {\n" " var btcdataUrl = '" btcdata-url "';\n" + " var wsUrl = btcdataUrl.replace(/^http/, 'ws');\n" " var display = document.getElementById('price-display');\n" - " var source = new EventSource(btcdataUrl + '/api/price/stream');\n" "\n" - " source.addEventListener('price-update', function(e) {\n" - " var d = JSON.parse(e.data);\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 = '
' +\n" - " '
' +\n" - " '

' + fmt + '

' +\n" - " '

Updated ' + timeStr + '

' +\n" - " '
';\n" - " });\n" + " function connect() {\n" + " var ws = new WebSocket(wsUrl + '/api/price/ws');\n" "\n" - " source.onerror = function() {\n" - " display.innerHTML = '
' +\n" - " '

Connection lost. Reconnecting...

';\n" - " };\n" + " ws.onmessage = function(e) {\n" + " var d = JSON.parse(e.data);\n" + " if (d.ping) return;\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 = '
' +\n" + " '
' +\n" + " '

' + fmt + '

' +\n" + " '

Updated ' + timeStr + '

' +\n" + " '
';\n" + " };\n" + "\n" + " ws.onclose = function() {\n" + " display.innerHTML = '
' +\n" + " '

Connection lost. Reconnecting...

';\n" + " setTimeout(connect, 3000);\n" + " };\n" + "\n" + " ws.onerror = function() {};\n" + " }\n" + "\n" + " connect();\n" "});\n"))])) (defn- ui-routes [opts]