feat(mse): unified header, testnet order tracking, bot restart on update

- Ortak header yapısı: logo sol, nav orta (Dashboard/Botlar/Pozisyonlar), mode+çıkış sağ — index.html, bots.html, bot.html hepsinde aynı
- positions.html eklendi (yeni /positions sayfası)
- runner.rs: 30s periyodik order status sorgusu — FILLED/CANCELED pozisyonları closed_positions'a taşır, open_positions'dan siler
- update_bot: çalışan bot varsa durdur + yeni config ile yeniden başlat
- initChart guard: çift grafik oluşmasını engeller
- routes.rs: /positions route eklendi, unused import temizlendi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-19 20:19:56 +03:00
parent 3a54d7f437
commit 5db9332157
7 changed files with 679 additions and 42 deletions

View file

@ -207,7 +207,12 @@ pub async fn update_bot(
}
drop(db);
let manager = state.manager.lock().await;
let mut manager = state.manager.lock().await;
let was_running = manager.is_running(&id);
if was_running {
manager.stop(&id).await;
manager.start(config.clone());
}
let running = manager.is_running(&id);
Json(BotResponse { config, running }).into_response()
}

View file

@ -2,7 +2,7 @@ use axum::{
extract::State,
middleware,
response::{IntoResponse, Redirect},
routing::{delete, get, post, put},
routing::{get, post},
Router,
};
use axum_extra::extract::CookieJar;
@ -36,7 +36,9 @@ pub fn build(state: AppState) -> Router {
.nest("/api", api)
.route("/", get(login_handler))
.route("/dashboard", get(dashboard_handler))
.route("/bots", get(bots_handler))
.route("/bots/:id", get(bot_detail_handler))
.route("/positions", get(positions_handler))
.with_state(state)
}
@ -59,6 +61,32 @@ async fn login_handler(
}
}
async fn bots_handler(
State(state): State<AppState>,
jar: CookieJar,
) -> impl IntoResponse {
let token = jar.get("mse_session").map(|c| c.value().to_string());
let authed = match token {
Some(t) => { let db = state.db.lock().await; db.session_exists(&t).unwrap_or(false) }
None => false,
};
if authed { axum::response::Html(include_str!("../web/bots.html")).into_response() }
else { Redirect::to("/").into_response() }
}
async fn positions_handler(
State(state): State<AppState>,
jar: CookieJar,
) -> impl IntoResponse {
let token = jar.get("mse_session").map(|c| c.value().to_string());
let authed = match token {
Some(t) => { let db = state.db.lock().await; db.session_exists(&t).unwrap_or(false) }
None => false,
};
if authed { axum::response::Html(include_str!("../web/positions.html")).into_response() }
else { Redirect::to("/").into_response() }
}
async fn bot_detail_handler(
State(state): State<AppState>,
jar: CookieJar,

View file

@ -7,8 +7,9 @@ use tokio::sync::{broadcast, Mutex};
use tokio::time::{sleep, Duration};
use tokio_tungstenite::connect_async;
use crate::binance::{client::BinanceClient, models::{Kline, Timeframe}};
use crate::binance::{client::BinanceClient, models::Kline};
use crate::bot::strategy::RedCandleStrategy;
use crate::storage::closed::ClosedPosition;
use crate::storage::config::BotConfig;
use crate::storage::db::Database;
use crate::storage::history::TradeRecord;
@ -40,13 +41,31 @@ impl BotRunner {
db: Arc<Mutex<Database>>,
event_tx: broadcast::Sender<TradeEvent>,
) {
let client = BinanceClient::new(api_key, api_secret, config.testnet);
let client = BinanceClient::new(api_key.clone(), api_secret.clone(), config.testnet);
// Fiyat verisi her zaman canlı Binance stream'inden gelir
let ws_base = BINANCE_WS_URL;
let stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str());
info!("[{}] Bot başlatıldı. WS: {}", config.symbol, stream);
// Açık pozisyonların order durumunu periyodik sorgula
{
let db2 = Arc::clone(&db);
let ev2 = event_tx.clone();
let shutdown2 = Arc::clone(&shutdown);
let cfg2 = config.clone();
let api_key2 = api_key.clone();
let api_secret2 = api_secret.clone();
tokio::spawn(async move {
let check_client = BinanceClient::new(api_key2, api_secret2, cfg2.testnet);
loop {
sleep(Duration::from_secs(30)).await;
if *shutdown2.lock().await { break; }
check_open_positions(&check_client, &db2, &ev2, &cfg2).await;
}
});
}
loop {
if *shutdown.lock().await {
info!("[{}] Bot durduruldu.", config.symbol);
@ -232,6 +251,68 @@ impl KlineWs {
}
}
async fn check_open_positions(
client: &BinanceClient,
db: &Arc<Mutex<Database>>,
event_tx: &broadcast::Sender<TradeEvent>,
config: &BotConfig,
) {
let positions = {
let db_guard = db.lock().await;
match db_guard.get_positions_by_bot(&config.id) {
Ok(p) => p,
Err(_) => return,
}
};
for pos in positions {
let status = match client.get_order_status(&pos.symbol, pos.order_id).await {
Ok(s) => s,
Err(e) => {
warn!("[{}] Order status sorgu hatası #{}: {}", config.symbol, pos.order_id, e);
continue;
}
};
match status.as_str() {
"FILLED" | "CANCELED" | "EXPIRED" | "REJECTED" => {
let closed = ClosedPosition {
order_id: pos.order_id,
bot_id: pos.bot_id.clone(),
bot_name: pos.bot_name.clone(),
symbol: pos.symbol.clone(),
buy_price: pos.buy_price,
sell_target: pos.sell_target,
quantity: pos.quantity,
profit_percent: pos.profit_percent,
opened_at: pos.opened_at,
closed_at: chrono::Utc::now().timestamp_millis(),
status: status.clone(),
};
let db_guard = db.lock().await;
if let Err(e) = db_guard.insert_closed(&closed) {
error!("[{}] Kapalı pozisyon kayıt hatası: {}", config.symbol, e);
} else {
db_guard.remove_position(pos.order_id).ok();
info!("[{}] Pozisyon kapatıldı #{} ({})", config.symbol, pos.order_id, status);
db_guard.insert_log(&config.id, "trade", &format!("✅ Pozisyon kapatıldı | Order #{} | Durum: {}", pos.order_id, status)).ok();
}
event_tx.send(TradeEvent {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
buy_price: pos.buy_price,
sell_target: pos.sell_target,
quantity: pos.quantity,
profit_percent: pos.profit_percent,
timestamp: chrono::Utc::now().timestamp_millis(),
}).ok();
}
_ => {}
}
}
}
// Kline dönüşümü strategy için
impl From<&KlineWs> for Kline {
fn from(k: &KlineWs) -> Self {

View file

@ -16,19 +16,22 @@
html, body { height: 100%; overflow: hidden; }
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; display: flex; flex-direction: column; }
/* ── TOP BAR ─────────────────────────────────── */
/* ── HEADER / TOP BAR ───────────────────────── */
#topbar {
background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 20px; flex-shrink: 0;
padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 0; flex-shrink: 0;
}
.tb-logo { display: flex; align-items: center; gap: 8px; }
.tb-logo span { font-size: 15px; font-weight: 700; }
.tb-logo { font-size: 15px; font-weight: 700; margin-right: 12px; }
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
.tb-sep { width: 1px; height: 24px; background: var(--border); }
#tb-name { font-size: 16px; font-weight: 600; color: var(--text); }
#tb-symbol { font-size: 13px; color: var(--muted); }
#tb-price { font-size: 20px; font-weight: 700; color: var(--text); min-width: 100px; }
#tb-change { font-size: 13px; font-weight: 600; min-width: 60px; }
nav { display: flex; gap: 4px; margin-right: 12px; }
.nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; }
.nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); }
.nav-link.active { color: var(--text); background: rgba(108,99,255,.15); }
.tb-sep { width: 1px; height: 24px; background: var(--border); margin: 0 12px; }
#tb-name { font-size: 15px; font-weight: 600; color: var(--text); }
#tb-symbol { font-size: 13px; color: var(--muted); margin-left: 4px; }
#tb-price { font-size: 18px; font-weight: 700; color: var(--text); min-width: 90px; }
#tb-change { font-size: 13px; font-weight: 600; min-width: 55px; }
.tb-stat { display: flex; flex-direction: column; gap: 1px; }
.tb-stat .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.tb-stat .val { font-size: 12px; color: var(--text); }
@ -46,8 +49,12 @@
.btn-delete:hover { background: rgba(231,76,60,.1); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #5a52d5; }
.back-link { color: var(--muted); font-size: 12px; text-decoration: none; display: flex; align-items: center; gap: 4px; }
.back-link:hover { color: var(--text); }
.btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; }
.btn-logout:hover { background: rgba(231,76,60,.1); }
.mode-toggle { display: flex; align-items: center; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 3px; margin-right: 8px; }
.mode-btn { padding: 3px 10px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .15s; }
.mode-btn.active-live { background: rgba(46,204,113,.15); color: var(--green); }
.mode-btn.active-testnet { background: rgba(243,156,18,.15); color: var(--yellow); }
/* ── MAIN LAYOUT ─────────────────────────────── */
#layout {
@ -135,12 +142,15 @@
<!-- TOP BAR -->
<div id="topbar">
<div class="tb-logo">
<span><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></span>
</div>
<div class="tb-logo"><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></div>
<nav>
<a href="/dashboard" class="nav-link">Dashboard</a>
<a href="/bots" class="nav-link active">Botlar</a>
<a href="/positions" class="nav-link">Pozisyonlar</a>
</nav>
<div class="tb-sep"></div>
<div id="tb-name"></div>
<div id="tb-symbol" style="margin-left:4px"></div>
<div id="tb-symbol"></div>
<div class="tb-sep"></div>
<div id="tb-price"></div>
<div id="tb-change"></div>
@ -152,8 +162,11 @@
<span id="tb-badge"></span>
<div id="tb-actions" style="display:flex;gap:8px"></div>
<div class="tb-sep"></div>
<a href="/dashboard" class="back-link">← Dashboard</a>
<button class="btn btn-delete" style="font-size:12px;padding:4px 12px" onclick="logout()">Çıkış</button>
<div class="mode-toggle">
<button class="mode-btn" id="btn-testnet" onclick="">Testnet</button>
<button class="mode-btn" id="btn-live" onclick="">Canlı</button>
</div>
<button class="btn btn-logout" onclick="logout()">Çıkış</button>
</div>
<!-- MAIN LAYOUT -->
@ -263,14 +276,27 @@ const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h'
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
function buyTime(ms) {
const tf = TF_SECONDS[bot.timeframe] || 300;
return Math.floor(ms / 1000) - tf;
const sec = Math.floor(ms / 1000);
// Hangi mum aralığına düştüğünü bul, bir önceki mumun açılışını döndür
const currentCandleOpen = Math.floor(sec / tf) * tf;
return currentCandleOpen - tf;
}
// ── Load bot ──────────────────────────────────────
async function loadBot() {
const res = await api('GET', `/bots/${BOT_ID}`);
if (!res.ok) { window.location.href = '/dashboard'; return; }
bot = await res.json();
const [botRes, modeRes] = await Promise.all([
api('GET', `/bots/${BOT_ID}`),
api('GET', '/mode'),
]);
if (!botRes.ok) { window.location.href = '/dashboard'; return; }
bot = await botRes.json();
if (modeRes.ok) {
const { mode } = await modeRes.json();
const btnLive = document.getElementById('btn-live');
const btnTest = document.getElementById('btn-testnet');
btnLive.className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
btnTest.className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
}
document.title = `${bot.name} — CryptoFox Mukan Edition`;
renderTopBar();
renderEditForm();
@ -362,6 +388,7 @@ function startPriceWs() {
// ── Chart ─────────────────────────────────────────
function initChart() {
if (chart) return;
const container = document.getElementById('chart');
chart = LightweightCharts.createChart(container, {
layout: { background: { color: '#0f0f13' }, textColor: '#888' },

351
src/web/bots.html Normal file
View file

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Botlar — CryptoFox Mukan Edition</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tom-select@2/dist/css/tom-select.min.css">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2/dist/js/tom-select.complete.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f13; --surface: #1a1a22; --border: #2a2a38;
--text: #e0e0f0; --muted: #666; --accent: #6c63ff;
--green: #2ecc71; --red: #e74c3c; --yellow: #f39c12;
}
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 14px; min-height: 100vh; }
/* ── HEADER ── */
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 16px; }
.logo { font-size: 16px; font-weight: 700; }
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
.h-sep { width: 1px; height: 20px; background: var(--border); }
nav { display: flex; gap: 4px; }
.nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; }
.nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); }
.nav-link.active { color: var(--text); background: rgba(108,99,255,.15); }
.spacer { flex: 1; }
.btn { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 12px; cursor: pointer; transition: all .15s; white-space: nowrap; }
.btn:hover { background: rgba(255,255,255,.05); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #5a52d5; }
.btn-start { border-color: var(--green); color: var(--green); }
.btn-start:hover { background: rgba(46,204,113,.1); }
.btn-stop { border-color: var(--yellow); color: var(--yellow); }
.btn-stop:hover { background: rgba(243,156,18,.1); }
.btn-delete { border-color: var(--red); color: var(--red); }
.btn-delete:hover { background: rgba(231,76,60,.1); }
.btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; }
.btn-logout:hover { background: rgba(231,76,60,.1); }
/* ── MODE TOGGLE ── */
.mode-toggle { display: flex; align-items: center; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 3px; }
.mode-btn { padding: 3px 12px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .15s; }
.mode-btn.active-live { background: rgba(46,204,113,.15); color: var(--green); }
.mode-btn.active-testnet { background: rgba(243,156,18,.15); color: var(--yellow); }
/* ── MAIN ── */
main { padding: 24px; max-width: 1400px; margin: 0 auto; }
/* ── GROUP HEADER ── */
.group-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.group-label .count { background: rgba(255,255,255,.07); color: var(--text); padding: 1px 7px; border-radius: 10px; font-size: 11px; }
.group-running .group-label { color: var(--green); }
.group-section { margin-bottom: 32px; }
/* ── CARDS GRID ── */
.cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 14px; }
/* ── BOT CARD ── */
.bot-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
display: flex; flex-direction: column; gap: 0; overflow: hidden;
transition: border-color .15s;
}
.bot-card.running { border-color: rgba(46,204,113,.35); }
.bot-card:hover { border-color: rgba(108,99,255,.4); }
.card-header { padding: 14px 16px 10px; display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.card-title { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.card-name { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-meta { display: flex; align-items: center; gap: 6px; }
.card-symbol { font-size: 12px; color: var(--muted); font-weight: 500; }
.card-tf { font-size: 11px; color: var(--muted); background: rgba(255,255,255,.06); padding: 1px 6px; border-radius: 4px; }
.card-badges { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.badge-running { background: rgba(46,204,113,.15); color: var(--green); }
.badge-stopped { background: rgba(136,136,136,.1); color: var(--muted); }
.badge-testnet { background: rgba(243,156,18,.12); color: var(--yellow); font-size: 10px; }
.card-stats { padding: 10px 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.stat-value { font-size: 13px; font-weight: 600; }
.stat-value.green { color: var(--green); }
.card-actions { padding: 10px 16px; display: flex; align-items: center; gap: 8px; }
.card-detail-link { font-size: 12px; color: var(--muted); text-decoration: none; margin-left: auto; transition: color .15s; }
.card-detail-link:hover { color: var(--accent); }
/* ── EMPTY ── */
.empty-state { color: var(--muted); font-size: 13px; padding: 20px 0; }
/* ── MODAL ── */
#modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 100; }
#modal-overlay.open { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; width: 480px; max-width: 95vw; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.modal-header h2 { font-size: 15px; font-weight: 600; }
.modal-close { background: none; border: none; color: var(--muted); font-size: 20px; cursor: pointer; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 14px; }
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
.form-group { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.form-group input, .form-group select { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 7px 10px; color: var(--text); font-size: 13px; width: 100%; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--accent); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ts-wrapper .ts-control, .ts-wrapper.single .ts-control { background: var(--bg) !important; border-color: var(--border) !important; color: var(--text) !important; min-height: 34px; padding: 4px 10px; border-radius: 5px; box-shadow: none !important; }
.ts-wrapper.focus .ts-control { border-color: var(--accent) !important; box-shadow: none !important; }
.ts-wrapper .ts-control input { background: transparent !important; color: var(--text) !important; outline: none !important; }
.ts-dropdown, .ts-dropdown.single { background: var(--surface) !important; border-color: var(--border) !important; color: var(--text) !important; z-index: 200; }
.ts-dropdown .option { padding: 7px 12px; color: var(--text) !important; }
.ts-dropdown .option:hover, .ts-dropdown .option.active { background: rgba(108,99,255,.2) !important; }
.ts-wrapper .ts-control .item { color: var(--text) !important; background: rgba(108,99,255,.15) !important; border-radius: 3px; padding: 1px 6px; border: none !important; }
</style>
</head>
<body>
<header>
<div class="logo"><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></div>
<div class="h-sep"></div>
<nav>
<a href="/dashboard" class="nav-link">Dashboard</a>
<a href="/bots" class="nav-link active">Botlar</a>
<a href="/positions" class="nav-link">Pozisyonlar</a>
</nav>
<div class="spacer"></div>
<div class="mode-toggle">
<button class="mode-btn" id="btn-live" onclick="setMode('live')">Canlı</button>
<button class="mode-btn" id="btn-testnet" onclick="setMode('testnet')">Testnet</button>
</div>
<button class="btn btn-primary" onclick="openModal()">+ Yeni Bot</button>
<button class="btn btn-logout" onclick="logout()">Çıkış</button>
</header>
<main>
<div class="group-section group-running">
<div class="group-label">Çalışıyor <span class="count" id="count-running">0</span></div>
<div class="cards-grid" id="grid-running"><div class="empty-state">Çalışan bot yok</div></div>
</div>
<div class="group-section">
<div class="group-label">Durdurulmuş <span class="count" id="count-stopped">0</span></div>
<div class="cards-grid" id="grid-stopped"><div class="empty-state">Durdurulan bot yok</div></div>
</div>
</main>
<!-- Yeni Bot Modal -->
<div id="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal">
<div class="modal-header">
<h2>Yeni Bot Oluştur</h2>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>İsim</label>
<input id="f-name" type="text" placeholder="Bot ismi" />
</div>
<div class="form-group">
<label>Sembol</label>
<select id="f-symbol"></select>
</div>
<div class="form-row">
<div class="form-group">
<label>Timeframe</label>
<select id="f-tf">
<option value="1m">1 Dakika</option>
<option value="5m" selected>5 Dakika</option>
<option value="15m">15 Dakika</option>
<option value="30m">30 Dakika</option>
<option value="1h">1 Saat</option>
<option value="4h">4 Saat</option>
<option value="1d">1 Gün</option>
<option value="1w">1 Hafta</option>
</select>
</div>
<div class="form-group">
<label>Kar %</label>
<input id="f-profit" type="number" step="0.1" placeholder="2" min="0.1" />
</div>
</div>
<div class="form-group">
<label>USDT Miktarı <span id="f-min-note" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none"></span></label>
<input id="f-usdt" type="number" step="1" placeholder="10" min="1" />
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal()">İptal</button>
<button class="btn btn-primary" onclick="createBot()">Oluştur</button>
</div>
</div>
</div>
<script>
let symbolSelect = null;
let symbolMinMap = {};
let currentMode = 'live';
async function api(method, path, body) {
const res = await fetch('/api' + path, {
method, credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
return res;
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ── Mode ──
async function loadMode() {
const res = await api('GET', '/mode');
const { mode } = await res.json();
currentMode = mode;
document.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
}
async function setMode(mode) {
await api('POST', '/mode', { mode });
currentMode = mode;
document.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
loadBots();
}
// ── Bots ──
async function loadBots() {
const res = await api('GET', '/bots');
const bots = await res.json();
const running = bots.filter(b => b.running);
const stopped = bots.filter(b => !b.running);
document.getElementById('count-running').textContent = running.length;
document.getElementById('count-stopped').textContent = stopped.length;
renderGrid('grid-running', running, 'Çalışan bot yok');
renderGrid('grid-stopped', stopped, 'Durdurulan bot yok');
}
function renderGrid(id, bots, emptyMsg) {
const grid = document.getElementById(id);
if (!bots.length) { grid.innerHTML = `<div class="empty-state">${emptyMsg}</div>`; return; }
grid.innerHTML = bots.map(b => `
<div class="bot-card ${b.running ? 'running' : ''}" id="card-${b.id}">
<div class="card-header">
<div class="card-title">
<div class="card-name">${esc(b.name)}</div>
<div class="card-meta">
<span class="card-symbol">${esc(b.symbol)}</span>
<span class="card-tf">${esc(b.timeframe)}</span>
</div>
</div>
<div class="card-badges">
<span class="badge ${b.running ? 'badge-running' : 'badge-stopped'}">${b.running ? '● Çalışıyor' : '● Durdu'}</span>
${b.testnet ? '<span class="badge badge-testnet">TESTNET</span>' : ''}
</div>
</div>
<div class="card-stats">
<div class="stat">
<span class="stat-label">İşlem Miktarı</span>
<span class="stat-value">${b.usdt_amount} USDT</span>
</div>
<div class="stat">
<span class="stat-label">Kar Hedefi</span>
<span class="stat-value green">%${b.profit_percent}</span>
</div>
</div>
<div class="card-actions">
${b.running
? `<button class="btn btn-stop" onclick="stopBot('${b.id}')">Durdur</button>`
: `<button class="btn btn-start" onclick="startBot('${b.id}')">Başlat</button>`
}
<button class="btn btn-delete" onclick="deleteBot('${b.id}','${esc(b.name)}')">Sil</button>
<a href="/bots/${b.id}" class="card-detail-link">Detay →</a>
</div>
</div>
`).join('');
}
async function startBot(id) {
await api('POST', `/bots/${id}/start`);
loadBots();
}
async function stopBot(id) {
await api('POST', `/bots/${id}/stop`);
loadBots();
}
async function deleteBot(id, name) {
if (!confirm(`"${name}" botunu silmek istiyor musun?`)) return;
await api('DELETE', `/bots/${id}`);
loadBots();
}
// ── Modal ──
async function openModal() {
document.getElementById('modal-overlay').classList.add('open');
if (!symbolSelect) await loadSymbols();
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('open');
document.getElementById('f-name').value = '';
document.getElementById('f-usdt').value = '';
document.getElementById('f-profit').value = '';
document.getElementById('f-min-note').textContent = '';
if (symbolSelect) symbolSelect.clear();
}
async function loadSymbols() {
const res = await api('GET', '/symbols');
const symbols = await res.json();
symbolMinMap = {};
symbols.forEach(s => symbolMinMap[s.symbol] = s.min_notional);
const sel = document.getElementById('f-symbol');
sel.innerHTML = '<option value="">Sembol seç...</option>' +
symbols.map(s => `<option value="${s.symbol}">${s.symbol}</option>`).join('');
symbolSelect = new TomSelect('#f-symbol', {
maxOptions: 2000, searchField: ['text'],
onChange: v => {
const min = symbolMinMap[v];
document.getElementById('f-min-note').textContent = min ? `(min: ${min} USDT)` : '';
document.getElementById('f-usdt').min = min || 1;
}
});
}
async function createBot() {
const name = document.getElementById('f-name').value.trim();
const symbol = symbolSelect ? symbolSelect.getValue() : document.getElementById('f-symbol').value;
const timeframe = document.getElementById('f-tf').value;
const usdt_amount = parseFloat(document.getElementById('f-usdt').value);
const profit_percent = parseFloat(document.getElementById('f-profit').value);
if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
const min = symbolMinMap[symbol] || 0;
if (usdt_amount < min) { alert(`Minimum işlem miktarı ${min} USDT`); return; }
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
closeModal();
loadBots();
}
async function logout() {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
window.location.href = '/';
}
loadMode();
loadBots();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

141
src/web/positions.html Normal file
View file

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pozisyonlar — CryptoFox Mukan Edition</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f13; --surface: #1a1a22; --border: #2a2a38;
--text: #e0e0f0; --muted: #666; --accent: #6c63ff;
--green: #2ecc71; --red: #e74c3c; --yellow: #f39c12;
}
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 14px; min-height: 100vh; }
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 16px; }
.logo { font-size: 16px; font-weight: 700; }
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
.h-sep { width: 1px; height: 20px; background: var(--border); }
nav { display: flex; gap: 4px; }
.nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; }
.nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); }
.nav-link.active { color: var(--text); background: rgba(108,99,255,.15); }
.spacer { flex: 1; }
.btn { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 12px; cursor: pointer; transition: all .15s; }
.btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; }
.btn-logout:hover { background: rgba(231,76,60,.1); }
.mode-toggle { display: flex; align-items: center; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 3px; }
.mode-btn { padding: 3px 12px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .15s; }
.mode-btn.active-live { background: rgba(46,204,113,.15); color: var(--green); }
.mode-btn.active-testnet { background: rgba(243,156,18,.15); color: var(--yellow); }
main { padding: 24px; max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.section-header { padding: 14px 18px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 18px; text-align: left; border-bottom: 1px solid var(--border); }
th { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 500; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,.02); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.badge-filled { background: rgba(46,204,113,.15); color: var(--green); }
.badge-new { background: rgba(108,99,255,.15); color: var(--accent); }
.badge-canceled { background: rgba(136,136,136,.1); color: var(--muted); }
.empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; }
</style>
</head>
<body>
<header>
<div class="logo"><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></div>
<div class="h-sep"></div>
<nav>
<a href="/dashboard" class="nav-link">Dashboard</a>
<a href="/bots" class="nav-link">Botlar</a>
<a href="/positions" class="nav-link active">Pozisyonlar</a>
</nav>
<div class="spacer"></div>
<div class="mode-toggle">
<button class="mode-btn" id="btn-testnet">Testnet</button>
<button class="mode-btn" id="btn-live">Canlı</button>
</div>
<button class="btn btn-logout" onclick="logout()">Çıkış</button>
</header>
<main>
<section>
<div class="section-header">ık Pozisyonlar</div>
<table>
<thead><tr><th>Bot</th><th>Sembol</th><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Zaman</th></tr></thead>
<tbody id="open-body"><tr><td colspan="6" class="empty">Yükleniyor...</td></tr></tbody>
</table>
</section>
<section>
<div class="section-header">Kapalı İşlemler</div>
<table>
<thead><tr><th>Bot</th><th>Sembol</th><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Durum</th><th>Kapanış</th></tr></thead>
<tbody id="closed-body"><tr><td colspan="7" class="empty">Yükleniyor...</td></tr></tbody>
</table>
</section>
</main>
<script>
async function api(method, path) {
const res = await fetch('/api' + path, { method, credentials: 'include' });
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
return res;
}
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
function ts(ms) { return new Date(ms).toLocaleString('tr-TR'); }
async function loadMode() {
const res = await api('GET', '/mode');
if (!res.ok) return;
const { mode } = await res.json();
document.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
}
async function loadOpen() {
const res = await api('GET', '/positions');
const tbody = document.getElementById('open-body');
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Yüklenemedi</td></tr>'; return; }
const data = await res.json();
if (!data.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">ık pozisyon yok</td></tr>'; return; }
tbody.innerHTML = data.map(p => `<tr>
<td>${p.bot_name}</td>
<td>${p.symbol}</td>
<td>${fmt(p.buy_price, 4)}</td>
<td>${fmt(p.sell_target, 4)}</td>
<td>${fmt(p.quantity, 4)}</td>
<td>${ts(p.opened_at)}</td>
</tr>`).join('');
}
async function loadClosed() {
const res = await api('GET', '/positions/closed');
const tbody = document.getElementById('closed-body');
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Yüklenemedi</td></tr>'; return; }
const data = await res.json();
if (!data.length) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>'; return; }
tbody.innerHTML = data.map(p => `<tr>
<td>${p.bot_name}</td>
<td>${p.symbol}</td>
<td>${fmt(p.buy_price, 4)}</td>
<td>${fmt(p.sell_target, 4)}</td>
<td>${fmt(p.quantity, 4)}</td>
<td><span class="badge badge-${p.status.toLowerCase()}">${p.status}</span></td>
<td>${ts(p.closed_at)}</td>
</tr>`).join('');
}
async function logout() {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
window.location.href = '/';
}
loadMode();
loadOpen();
loadClosed();
</script>
</body>
</html>