Fix
This commit is contained in:
@@ -7,3 +7,4 @@ __pycache__/
|
|||||||
*.log
|
*.log
|
||||||
/.env
|
/.env
|
||||||
/token
|
/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
|
## 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
@@ -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,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
Reference in New Issue
Block a user