Switch to HTMX WebSocket, use LAN IP for btcdata URL
- Replace vanilla JS WebSocket with HTMX ws extension (hx-ext="ws") - Add HTMX + ws extension CDN scripts to page head - Zero client-side JavaScript — HTMX handles connection, reconnect, and DOM swap of HTML fragments from btcdata - Hidden div safelists Tailwind classes used in btcdata HTML fragments - Update BTCDATA_URL to LAN IP (192.168.50.42) for cross-machine access - Update CLAUDE.md and README.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 WebSocket
|
||||
- **Templates:** Hiccup + HTMX (WebSocket extension)
|
||||
- **Styling:** Tailwind CSS v4
|
||||
- **Data backend:** btcdata (separate service at BTCDATA_URL, default http://localhost:4100, WebSocket at ws://localhost:4100)
|
||||
- **Data backend:** btcdata (separate service at BTCDATA_URL, default http://192.168.50.42: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 + WebSocket)
|
||||
├── ui.clj # UI routes (HTML + HTMX WebSocket)
|
||||
└── utils.clj # Route utilities
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# btcprice
|
||||
|
||||
Bitcoin price tracker UI. Serves an HTML page with live price updates streamed from [btcdata](../btcdata) via Server-Sent Events.
|
||||
Bitcoin price tracker UI. Serves an HTML page with live price updates streamed from [btcdata](../btcdata) via WebSocket using HTMX.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -21,12 +21,12 @@ docker compose up -d # btcprice on 4041, btcdata on 4101
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PORT` | `4000` | HTTP server port |
|
||||
| `BTCDATA_URL` | `http://localhost:4100` | btcdata service URL (injected into browser JS) |
|
||||
| `BTCDATA_URL` | `http://192.168.50.42:4100` | btcdata service URL (browser connects via WebSocket) |
|
||||
|
||||
## How It Works
|
||||
|
||||
The server renders an HTML page that includes a vanilla JavaScript `EventSource` connecting directly to btcdata's SSE endpoint (`BTCDATA_URL/api/price/stream`). Price updates are rendered client-side with color-coded changes (green up, red down).
|
||||
The server renders an HTML page with HTMX and the WebSocket extension (`hx-ext="ws"`). HTMX connects directly to btcdata's WebSocket endpoint (`BTCDATA_URL/api/price/ws`), which sends HTML fragments. HTMX swaps the received `<div id="price-display">` into the DOM automatically. Price changes are color-coded (green up, red down). Reconnection is handled by HTMX with exponential backoff.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Clojure · Kit · Undertow · Reitit · Hiccup · Tailwind CSS v4
|
||||
Clojure · Kit · Undertow · Reitit · Hiccup · HTMX · Tailwind CSS v4
|
||||
|
||||
+1
-1
@@ -8,4 +8,4 @@ services:
|
||||
ports:
|
||||
- "4041:4040"
|
||||
environment:
|
||||
BTCDATA_URL: "http://localhost:4101"
|
||||
BTCDATA_URL: "http://192.168.50.42:4101"
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
[:meta {:name "apple-mobile-web-app-capable" :content "yes"}]
|
||||
[:meta {:name "apple-mobile-web-app-status-bar-style" :content "black-translucent"}]
|
||||
[:title (or (:title opts#) "BTC Price")]
|
||||
[:link {:rel "stylesheet" :href "/css/output.css"}]]
|
||||
[:link {:rel "stylesheet" :href "/css/output.css"}]
|
||||
[:script {:src "https://unpkg.com/htmx.org@2/dist/htmx.min.js"}]
|
||||
[:script {:src "https://unpkg.com/htmx-ext-ws@2/ws.js"}]]
|
||||
[:body
|
||||
[:div.mx-auto.max-w-lg.px-4.py-6
|
||||
~@content]]]))}))
|
||||
|
||||
@@ -8,59 +8,21 @@
|
||||
[reitit.ring.middleware.parameters :as parameters]))
|
||||
|
||||
(defn- home-page [{:keys [btcdata-url]} _req]
|
||||
(let [ws-url (str (.replaceFirst ^String btcdata-url "^http" "ws")
|
||||
"/api/price/ws")]
|
||||
(page {:title "BTC Price"}
|
||||
[:header.text-center.mb-8
|
||||
[:h1.text-3xl.font-bold.text-gray-900 "BTC Price"]
|
||||
[:p.text-sm.text-gray-500.mt-1 "Bitcoin price tracker"]]
|
||||
|
||||
[:div#price-panel
|
||||
[:div#price-panel {:hx-ext "ws" :ws-connect ws-url}
|
||||
[:div#price-display
|
||||
[:div.bg-white.rounded-2xl.shadow-sm.border.border-gray-200.p-6
|
||||
[:p.text-center.text-gray-400.text-sm "Connecting..."]]]]
|
||||
|
||||
[:script
|
||||
(hiccup2.core/raw
|
||||
(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"
|
||||
"\n"
|
||||
" function connect() {\n"
|
||||
" var ws = new WebSocket(wsUrl + '/api/price/ws');\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 = '<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"))]))
|
||||
;; Safelist classes used in btcdata HTML fragments
|
||||
[:div {:class "hidden text-4xl font-bold text-green-600 text-red-600 text-gray-900 text-xs text-gray-400 mt-2"
|
||||
:aria-hidden "true"}])))
|
||||
|
||||
(defn- ui-routes [opts]
|
||||
[["/"
|
||||
|
||||
Reference in New Issue
Block a user