Add BTC candlestick chart with local OHLC storage
- Store daily BTC OHLC candles in SQLite to avoid hitting CoinGecko on every load - Seed with 30 days of daily candles on first boot (free tier gives daily granularity for days<=30) - Auto-detect and replace coarse legacy candle data on startup - Daily refresh adds new candles on each container restart - New GET /candles endpoint (supports ?days=365 and ?days=all) - Switch BTCHistoryChart to BTCCandlestickChart using lightweight-charts (TradingView) - Purchase markers with nearest-candle matching and multi-purchase merging - Fullscreen mode showing all stored candles - Fix frontend port to 3001 and pass REACT_APP_API_URL as build arg Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import AddPurchase from '../components/AddPurchase';
|
||||
import PurchaseList from '../components/PurchaseList';
|
||||
import PortfolioChart from '../components/PortfolioChart';
|
||||
import BTCHistoryChart from '../components/BTCHistoryChart';
|
||||
import BTCCandlestickChart from '../components/BTCCandlestickChart';
|
||||
|
||||
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
@@ -12,7 +12,7 @@ const styles = {
|
||||
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' },
|
||||
logo: { fontSize: '1.4rem', fontWeight: 700, color: '#f7931a' },
|
||||
logoutBtn: { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer' },
|
||||
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem', marginBottom: '1.5rem' },
|
||||
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', marginBottom: '1.5rem' },
|
||||
statCard: { background: '#1a1a1a', padding: '1rem', borderRadius: '12px', border: '1px solid #333' },
|
||||
statLabel: { color: '#888', fontSize: '0.8rem', marginBottom: '0.3rem' },
|
||||
statValue: { fontSize: '1.2rem', fontWeight: 700 },
|
||||
@@ -36,7 +36,9 @@ function StatCard({ label, value, highlight }) {
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [purchases, setPurchases] = useState([]);
|
||||
const [history, setHistory] = useState(null);
|
||||
const [candles, setCandles] = useState(null);
|
||||
const [candlesAll, setCandlesAll] = useState(null);
|
||||
const [fullscreenChart, setFullscreenChart] = useState(false);
|
||||
const [chartView, setChartView] = useState('both'); // 'both' | 'portfolio' | 'history'
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -46,10 +48,10 @@ export default function Dashboard() {
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [statsRes, purchasesRes, historyRes] = await Promise.all([
|
||||
fetch(`${API}/stats`, { headers: authHeaders() }),
|
||||
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
||||
fetch(`${API}/history`, { headers: authHeaders() }),
|
||||
const [statsRes, purchasesRes, candlesRes] = await Promise.all([
|
||||
fetch(`${API}/stats`, { headers: authHeaders() }),
|
||||
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
||||
fetch(`${API}/candles?days=365`, { headers: authHeaders() }),
|
||||
]);
|
||||
if (statsRes.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
@@ -58,12 +60,21 @@ export default function Dashboard() {
|
||||
}
|
||||
setStats(await statsRes.json());
|
||||
setPurchases(await purchasesRes.json());
|
||||
setHistory(await historyRes.json());
|
||||
setCandles(await candlesRes.json());
|
||||
} catch {
|
||||
// silently fail — network may be unavailable
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const fetchAllCandles = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/candles?days=all`, { headers: authHeaders() });
|
||||
if (res.ok) setCandlesAll(await res.json());
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
@@ -73,10 +84,18 @@ export default function Dashboard() {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleToggleFullscreen = useCallback(() => {
|
||||
if (!fullscreenChart && !candlesAll) fetchAllCandles();
|
||||
setFullscreenChart(f => !f);
|
||||
}, [fullscreenChart, candlesAll, fetchAllCandles]);
|
||||
|
||||
const plHighlight = stats
|
||||
? stats.profit_loss >= 0 ? 'positive' : 'negative'
|
||||
: 'neutral';
|
||||
|
||||
// Fullscreen uses all-candles data once loaded, otherwise falls back to 365-day set
|
||||
const activeCandles = (fullscreenChart && candlesAll) ? candlesAll : candles;
|
||||
|
||||
return (
|
||||
<div style={styles.app}>
|
||||
<div style={styles.header}>
|
||||
@@ -87,8 +106,8 @@ export default function Dashboard() {
|
||||
{stats && (
|
||||
<div style={styles.statsGrid}>
|
||||
<StatCard label="Total Invested" value={`€${stats.total_invested.toLocaleString()}`} />
|
||||
<StatCard label="Total BTC" value={`₿${stats.total_btc}`} highlight="neutral" />
|
||||
<StatCard label="Avg Buy Price" value={`€${stats.average_price.toLocaleString()}`} />
|
||||
<StatCard label="Total BTC" value={`₿${stats.total_btc}`} highlight="neutral" />
|
||||
<StatCard label="Current BTC Price" value={`€${stats.current_price.toLocaleString()}`} />
|
||||
<StatCard label="Portfolio Value" value={`€${stats.portfolio_value.toLocaleString()}`} />
|
||||
<StatCard
|
||||
@@ -100,7 +119,7 @@ export default function Dashboard() {
|
||||
)}
|
||||
|
||||
<div style={styles.tabs}>
|
||||
{[['both', 'Both'], ['portfolio', 'Portfolio'], ['history', '1-Year BTC']].map(([key, label]) => (
|
||||
{[['both', 'Both'], ['portfolio', 'Portfolio'], ['history', 'BTC Candles']].map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
style={chartView === key ? styles.tabActive : styles.tab}
|
||||
@@ -109,7 +128,15 @@ export default function Dashboard() {
|
||||
))}
|
||||
</div>
|
||||
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} stats={stats} />}
|
||||
{(chartView === 'both' || chartView === 'history') && <BTCHistoryChart history={history} stats={stats} />}
|
||||
{(chartView === 'both' || chartView === 'history') && (
|
||||
<BTCCandlestickChart
|
||||
candles={activeCandles?.candles ?? null}
|
||||
purchases={activeCandles?.purchases ?? purchases}
|
||||
stats={stats}
|
||||
fullscreen={fullscreenChart}
|
||||
onToggleFullscreen={handleToggleFullscreen}
|
||||
/>
|
||||
)}
|
||||
<AddPurchase onAdded={fetchData} />
|
||||
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user