Fix
This commit is contained in:
@@ -7,3 +7,4 @@ __pycache__/
|
||||
*.log
|
||||
/.env
|
||||
/token
|
||||
/=0.27
|
||||
|
||||
@@ -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
|
||||
|
||||
- `make run` — Start dev server on port 4000
|
||||
- `make tailwind` — Watch Tailwind CSS for changes
|
||||
- `make test` — Run tests
|
||||
- `make run` — Start dev server on port 4000 (connects to btcdata at BTCDATA_URL from `.env`)
|
||||
- `make run-local` — Start dev server using BTCDATA_LOCAL_URL from `.env`
|
||||
- `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
|
||||
|
||||
@@ -17,20 +23,26 @@ Docker runs alongside local dev on different ports:
|
||||
| btcprice | localhost:4000 | localhost:4041 |
|
||||
| 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
|
||||
|
||||
- **Framework:** FastAPI
|
||||
- **Server:** Uvicorn
|
||||
- **Templates:** Inline HTML strings + HTMX (WebSocket extension)
|
||||
- **Styling:** Tailwind CSS v4
|
||||
- **Framework:** FastAPI + Uvicorn
|
||||
- **Templates:** Inline HTML strings in Python + HTMX (WebSocket extension)
|
||||
- **Styling:** Tailwind CSS v4 — scans `app/**/*.py` for class names (configured in `resources/css/input.css` via `@source`)
|
||||
- **Data backend:** btcdata (separate service at BTCDATA_URL, default http://localhost:4100)
|
||||
- **No database** — all data comes from btcdata
|
||||
|
||||
## Source Layout
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
app/
|
||||
└── main.py # FastAPI app: routes, WebSocket proxy, HTML rendering
|
||||
```
|
||||
btcprice acts as a WebSocket proxy between the browser and btcdata:
|
||||
1. Browser connects via HTMX WebSocket to `/ws/price` or `/ws/strike`
|
||||
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
@@ -3,9 +3,10 @@ import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -38,6 +39,7 @@ HOME_HTML = """\
|
||||
<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-ext-ws@2/ws.js"></script>
|
||||
<script src="https://unpkg.com/lightweight-charts@4/dist/lightweight-charts.standalone.production.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mx-auto max-w-lg px-4 py-6">
|
||||
@@ -58,8 +60,62 @@ HOME_HTML = """\
|
||||
</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>
|
||||
<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>
|
||||
</html>"""
|
||||
|
||||
@@ -77,6 +133,20 @@ async def health():
|
||||
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) ---
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
fastapi>=0.115
|
||||
uvicorn[standard]>=0.34
|
||||
websockets>=14.0
|
||||
httpx>=0.27
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user