This commit is contained in:
2026-09-10 13:01:11 +02:00
parent 7c68220404
commit d5e5b78334
5 changed files with 554 additions and 17 deletions
+1
View File
@@ -7,3 +7,4 @@ __pycache__/
*.log *.log
/.env /.env
/token /token
/=0.27
+27 -15
View File
@@ -1,12 +1,18 @@
# btcprice # CLAUDE.md
Bitcoin price tracker UI — Python FastAPI web application. Serves the HTML/Tailwind UI. Connects to btcdata for live price data via WebSocket. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
Bitcoin price tracker UI — Python FastAPI web application. Serves an HTML/Tailwind page with live price updates streamed from btcdata (separate service) via WebSocket using HTMX.
## Build & Development Commands ## Build & Development Commands
- `make run` — Start dev server on port 4000 - `make run` — Start dev server on port 4000 (connects to btcdata at BTCDATA_URL from `.env`)
- `make tailwind` — Watch Tailwind CSS for changes - `make run-local` — Start dev server using BTCDATA_LOCAL_URL from `.env`
- `make test` — Run tests - `make tailwind` — Watch Tailwind CSS for changes (run in separate terminal)
- `make test` — Run tests with pytest (`tests/` directory)
- `npm run css:build` — One-off Tailwind CSS build
## Docker ## Docker
@@ -17,20 +23,26 @@ Docker runs alongside local dev on different ports:
| btcprice | localhost:4000 | localhost:4041 | | btcprice | localhost:4000 | localhost:4041 |
| btcdata | localhost:4100 | localhost:4101 | | btcdata | localhost:4100 | localhost:4101 |
btcprice's docker-compose includes btcdata via `include: ../btcdata/docker-compose.yml`. Uses external `btc-network` Docker network. btcdata must be running separately (or via its own docker-compose).
## Architecture ## Architecture
- **Framework:** FastAPI - **Framework:** FastAPI + Uvicorn
- **Server:** Uvicorn - **Templates:** Inline HTML strings in Python + HTMX (WebSocket extension)
- **Templates:** Inline HTML strings + HTMX (WebSocket extension) - **Styling:** Tailwind CSS v4 — scans `app/**/*.py` for class names (configured in `resources/css/input.css` via `@source`)
- **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)
- **No database** — all data comes from btcdata - **No database** — all data comes from btcdata
## Source Layout ### Data Flow
``` btcprice acts as a WebSocket proxy between the browser and btcdata:
app/ 1. Browser connects via HTMX WebSocket to `/ws/price` or `/ws/strike`
└── main.py # FastAPI app: routes, WebSocket proxy, HTML rendering 2. btcprice proxies to btcdata's `/api/price/ws` or `/api/strike/ws`
``` 3. JSON from btcdata is transformed into HTML fragments (in `price_json_to_html` / `strike_json_to_html`)
4. HTMX swaps the HTML into the DOM by matching element IDs (`price-display`, `strike-display`)
### Key Conventions
- Numbers use Danish locale formatting (dots for thousands, commas for decimals) via `da_fmt()`
- Price changes are color-coded: green (up), red (down), gray (unchanged)
- All HTML is built as string concatenation in Python — no template engine
+71 -1
View File
@@ -3,9 +3,10 @@ import logging
import os import os
from datetime import datetime from datetime import datetime
import httpx
import websockets import websockets
from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, HTMLResponse from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -38,6 +39,7 @@ HOME_HTML = """\
<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> <script src="https://unpkg.com/htmx.org@2/dist/htmx.min.js"></script>
<script src="https://unpkg.com/htmx-ext-ws@2/ws.js"></script> <script src="https://unpkg.com/htmx-ext-ws@2/ws.js"></script>
<script src="https://unpkg.com/lightweight-charts@4/dist/lightweight-charts.standalone.production.js"></script>
</head> </head>
<body> <body>
<div class="mx-auto max-w-lg px-4 py-6"> <div class="mx-auto max-w-lg px-4 py-6">
@@ -58,8 +60,62 @@ HOME_HTML = """\
</div> </div>
</div> </div>
</div> </div>
<div class="mt-4">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<p class="text-sm text-gray-500 mb-3">1h Candles (1m)</p>
<div id="chart-minute" style="height:300px"></div>
</div>
</div>
<div class="mt-4">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<p class="text-sm text-gray-500 mb-3">24h Candles (1h)</p>
<div id="chart-hour" style="height:300px"></div>
</div>
</div>
<div class="hidden text-4xl text-2xl font-bold text-green-600 text-red-600 text-gray-900 text-xs text-gray-400 mt-2 flex justify-center justify-between items-center gap-4 mt-1 mt-3 text-sm text-gray-600 text-gray-500 mb-3 py-1 grid grid-cols-2 gap-x-6 gap-y-1" aria-hidden="true"></div> <div class="hidden text-4xl text-2xl font-bold text-green-600 text-red-600 text-gray-900 text-xs text-gray-400 mt-2 flex justify-center justify-between items-center gap-4 mt-1 mt-3 text-sm text-gray-600 text-gray-500 mb-3 py-1 grid grid-cols-2 gap-x-6 gap-y-1" aria-hidden="true"></div>
</div> </div>
<script>
(function() {
function initChart(containerId, endpoint, intervalMs) {
const container = document.getElementById(containerId);
const chart = LightweightCharts.createChart(container, {
layout: { background: { type: 'solid', color: '#ffffff' }, textColor: '#6b7280' },
grid: { vertLines: { color: '#f3f4f6' }, horzLines: { color: '#f3f4f6' } },
rightPriceScale: { borderColor: '#e5e7eb' },
timeScale: { borderColor: '#e5e7eb', timeVisible: true, secondsVisible: false },
});
const series = chart.addCandlestickSeries({
upColor: '#16a34a', downColor: '#dc2626',
borderUpColor: '#16a34a', borderDownColor: '#dc2626',
wickUpColor: '#16a34a', wickDownColor: '#dc2626',
});
function loadData() {
fetch(endpoint)
.then(r => r.json())
.then(candles => {
const data = candles.map(c => ({
time: c.ts,
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close),
})).sort((a, b) => a.time - b.time);
series.setData(data);
chart.timeScale().fitContent();
})
.catch(e => console.error('Chart fetch error:', e));
}
loadData();
setInterval(loadData, intervalMs);
new ResizeObserver(() => {
chart.applyOptions({ width: container.clientWidth });
}).observe(container);
}
initChart('chart-minute', '/api/kraken-minute', 60 * 1000);
initChart('chart-hour', '/api/kraken-hour', 5 * 60 * 1000);
})();
</script>
</body> </body>
</html>""" </html>"""
@@ -77,6 +133,20 @@ async def health():
return {"status": "up"} return {"status": "up"}
@app.get("/api/kraken-hour")
async def kraken_hour():
async with httpx.AsyncClient() as client:
resp = await client.get(f"{BTCDATA_URL}/api/kraken-hour", timeout=10)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
@app.get("/api/kraken-minute")
async def kraken_minute():
async with httpx.AsyncClient() as client:
resp = await client.get(f"{BTCDATA_URL}/api/kraken-minute", timeout=10)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
# --- Formatting helpers (Danish locale) --- # --- Formatting helpers (Danish locale) ---
+1
View File
@@ -1,3 +1,4 @@
fastapi>=0.115 fastapi>=0.115
uvicorn[standard]>=0.34 uvicorn[standard]>=0.34
websockets>=14.0 websockets>=14.0
httpx>=0.27
File diff suppressed because one or more lines are too long