diff --git a/.gitignore b/.gitignore index e4ffb68..9c3b474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,6 @@ -/target -/classes -/checkouts -profiles.clj -pom.xml -pom.xml.asc -*.jar -*.class -/.lein-* -/.nrepl-port -/.cpcache -/.clj-kondo +__pycache__/ +*.pyc +.venv/ /node_modules /log .DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index f1ee68b..66e9502 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,12 @@ # btcprice -Bitcoin price tracker UI — Clojure Kit web application. Serves the HTML/Tailwind UI. Connects to btcdata for live price data via WebSocket. +Bitcoin price tracker UI — Python FastAPI web application. Serves the HTML/Tailwind UI. Connects to btcdata for live price data via WebSocket. ## Build & Development Commands - `make run` — Start dev server on port 4000 -- `make repl` — Start nREPL -- `make test` — Run tests - `make tailwind` — Watch Tailwind CSS for changes -- `make uberjar` — Build production JAR -- `docker compose up -d` — Start btcprice + btcdata containers +- `make test` — Run tests ## Docker @@ -22,21 +19,11 @@ Docker runs alongside local dev on different ports: btcprice's docker-compose includes btcdata via `include: ../btcdata/docker-compose.yml`. -## REPL Commands - -```clojure -(dev-prep!) ;; Prepare dev system -(go) ;; Start system -(reset) ;; Reload code & restart -(halt) ;; Stop system -``` - ## Architecture -- **Framework:** Kit (Integrant-based) -- **Server:** Undertow -- **Routing:** Reitit -- **Templates:** Hiccup + HTMX (WebSocket extension) +- **Framework:** FastAPI +- **Server:** Uvicorn +- **Templates:** Inline HTML strings + HTMX (WebSocket extension) - **Styling:** Tailwind CSS v4 - **Data backend:** btcdata (separate service at BTCDATA_URL, default http://localhost:4100) - **No database** — all data comes from btcdata @@ -44,21 +31,6 @@ btcprice's docker-compose includes btcdata via `include: ../btcdata/docker-compo ## Source Layout ``` -src/clj/pmagnus/btcprice/ -├── core.clj # App entry point -├── config.clj # System config loader -└── web/ - ├── handler.clj # Ring handler + routes - ├── htmx.clj # page/fragment macros - ├── controllers/ - │ └── health.clj # Health check - ├── middleware/ - │ ├── core.clj # Base middleware - │ ├── exception.clj # Exception handling - │ └── formats.clj # Content negotiation - └── routes/ - ├── api.clj # /api routes (JSON) - ├── ui.clj # UI routes (HTML + HTMX WebSocket) - ├── ws_proxy.clj # WebSocket proxy to btcdata - └── utils.clj # Route utilities +app/ +└── main.py # FastAPI app: routes, WebSocket proxy, HTML rendering ``` diff --git a/Dockerfile b/Dockerfile index 36268d4..39e0f28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,25 +4,22 @@ WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci COPY resources/css/ resources/css/ -COPY src/ src/ +COPY app/ app/ COPY tailwind.config.js ./ RUN npm run css:build -FROM clojure:temurin-21-tools-deps-alpine AS build +FROM python:3.13-slim -WORKDIR /build -COPY deps.edn build.clj ./ -RUN clj -Sforce -P -COPY . . +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app/ app/ +COPY resources/public/ resources/public/ COPY --from=css /build/resources/public/css/output.css resources/public/css/output.css -RUN clj -T:build all - -FROM eclipse-temurin:21-jre-alpine - -COPY --from=build /build/target/btcprice-standalone.jar /btcprice/btcprice-standalone.jar EXPOSE 4040 ENV PORT=4040 ENV BTCDATA_URL=http://btcdata:4101 -CMD ["java", "-jar", "/btcprice/btcprice-standalone.jar"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "4040"] diff --git a/Makefile b/Makefile index c2c7b20..e7ac9e2 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,19 @@ include .env export -.PHONY: clean run run-docker repl tailwind test uberjar +.PHONY: clean run run-docker tailwind test clean: - rm -rf target + rm -rf __pycache__ app/__pycache__ run: - clj -M:dev -e "(dev-prep!) (go)" -r + .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 4000 --reload -run-docker: - BTCDATA_URL=http://localhost:4101 clj -M:dev -e "(dev-prep!) (go)" -r - -repl: - clj -M:dev:nrepl +run-local: + BTCDATA_URL=$(BTCDATA_LOCAL_URL) .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 4000 --reload tailwind: npm run css:watch test: - clj -M:test - -uberjar: - clj -T:build all + .venv/bin/python -m pytest tests/ diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..e19b9bf --- /dev/null +++ b/app/main.py @@ -0,0 +1,246 @@ +import json +import logging +import os +from datetime import datetime + +import websockets +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("btcprice") + +BTCDATA_URL = os.environ.get("BTCDATA_URL", "http://localhost:4100") + +app = FastAPI() + +app.mount("/css", StaticFiles(directory="resources/public/css"), name="css") + + +@app.get("/favicon.ico", include_in_schema=False) +async def favicon(): + return FileResponse("resources/public/favicon.svg", media_type="image/svg+xml") + + +# --- HTML page --- + +HOME_HTML = """\ + + +
+ + + + + +Connecting...
+Loading Strike rates...
+${da_fmt(",.2f", p)}
' + ) + + if data.get("price_eur") is not None: + p_eur = float(data["price_eur"]) + p_dkk = float(data["price_dkk"]) + eur_rate = p_eur / p + dkk_rate = p_dkk / p + + html += ( + 'Updated {ts}
' + html += "' + f'{da_fmt(",d", sats)} sats
' + ) + + html += '$" (da-fmt "%,.2f" p) "
" - (when price_eur - (let [p-eur (double (bigdec price_eur)) - p-dkk (double (bigdec price_dkk)) - eur (/ p-eur p) - dkk (/ p-dkk p)] - (str "Updated " ts "
" - "" - (da-fmt "%,d" sats) " sats
") - "") - "