Merge feature/btc-history-chart: BTC candlestick chart with local OHLC storage

Resolves conflicts: combined admin + candles routers in main.py,
fixed docker-compose port to 3001:3001, restored admin styles in Dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 20:24:50 +01:00
10 changed files with 412 additions and 16 deletions
+36 -9
View File
@@ -3,7 +3,7 @@ import { useNavigate, Link } 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';
@@ -38,7 +38,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();
@@ -48,10 +50,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');
@@ -60,12 +62,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]);
@@ -78,10 +89,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}>
@@ -108,7 +127,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}
@@ -117,7 +136,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>