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:
parent
3a54d7f437
commit
5db9332157
7 changed files with 679 additions and 42 deletions
|
|
@ -207,7 +207,12 @@ pub async fn update_bot(
|
||||||
}
|
}
|
||||||
drop(db);
|
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);
|
let running = manager.is_running(&id);
|
||||||
Json(BotResponse { config, running }).into_response()
|
Json(BotResponse { config, running }).into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
middleware,
|
middleware,
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{delete, get, post, put},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
@ -36,7 +36,9 @@ pub fn build(state: AppState) -> Router {
|
||||||
.nest("/api", api)
|
.nest("/api", api)
|
||||||
.route("/", get(login_handler))
|
.route("/", get(login_handler))
|
||||||
.route("/dashboard", get(dashboard_handler))
|
.route("/dashboard", get(dashboard_handler))
|
||||||
|
.route("/bots", get(bots_handler))
|
||||||
.route("/bots/:id", get(bot_detail_handler))
|
.route("/bots/:id", get(bot_detail_handler))
|
||||||
|
.route("/positions", get(positions_handler))
|
||||||
.with_state(state)
|
.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(
|
async fn bot_detail_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ use tokio::sync::{broadcast, Mutex};
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
use tokio_tungstenite::connect_async;
|
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::bot::strategy::RedCandleStrategy;
|
||||||
|
use crate::storage::closed::ClosedPosition;
|
||||||
use crate::storage::config::BotConfig;
|
use crate::storage::config::BotConfig;
|
||||||
use crate::storage::db::Database;
|
use crate::storage::db::Database;
|
||||||
use crate::storage::history::TradeRecord;
|
use crate::storage::history::TradeRecord;
|
||||||
|
|
@ -40,13 +41,31 @@ impl BotRunner {
|
||||||
db: Arc<Mutex<Database>>,
|
db: Arc<Mutex<Database>>,
|
||||||
event_tx: broadcast::Sender<TradeEvent>,
|
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
|
// Fiyat verisi her zaman canlı Binance stream'inden gelir
|
||||||
let ws_base = BINANCE_WS_URL;
|
let ws_base = BINANCE_WS_URL;
|
||||||
let stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str());
|
let stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str());
|
||||||
|
|
||||||
info!("[{}] Bot başlatıldı. WS: {}", config.symbol, stream);
|
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 {
|
loop {
|
||||||
if *shutdown.lock().await {
|
if *shutdown.lock().await {
|
||||||
info!("[{}] Bot durduruldu.", config.symbol);
|
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
|
// Kline dönüşümü strategy için
|
||||||
impl From<&KlineWs> for Kline {
|
impl From<&KlineWs> for Kline {
|
||||||
fn from(k: &KlineWs) -> Self {
|
fn from(k: &KlineWs) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,22 @@
|
||||||
html, body { height: 100%; overflow: hidden; }
|
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; }
|
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 {
|
#topbar {
|
||||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
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 { font-size: 15px; font-weight: 700; margin-right: 12px; }
|
||||||
.tb-logo span { font-size: 15px; font-weight: 700; }
|
|
||||||
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
|
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
|
||||||
.tb-sep { width: 1px; height: 24px; background: var(--border); }
|
nav { display: flex; gap: 4px; margin-right: 12px; }
|
||||||
#tb-name { font-size: 16px; font-weight: 600; color: var(--text); }
|
.nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; }
|
||||||
#tb-symbol { font-size: 13px; color: var(--muted); }
|
.nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); }
|
||||||
#tb-price { font-size: 20px; font-weight: 700; color: var(--text); min-width: 100px; }
|
.nav-link.active { color: var(--text); background: rgba(108,99,255,.15); }
|
||||||
#tb-change { font-size: 13px; font-weight: 600; min-width: 60px; }
|
.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 { 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 .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
.tb-stat .val { font-size: 12px; color: var(--text); }
|
.tb-stat .val { font-size: 12px; color: var(--text); }
|
||||||
|
|
@ -46,8 +49,12 @@
|
||||||
.btn-delete:hover { background: rgba(231,76,60,.1); }
|
.btn-delete:hover { background: rgba(231,76,60,.1); }
|
||||||
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
.btn-primary:hover { background: #5a52d5; }
|
.btn-primary:hover { background: #5a52d5; }
|
||||||
.back-link { color: var(--muted); font-size: 12px; text-decoration: none; display: flex; align-items: center; gap: 4px; }
|
.btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; }
|
||||||
.back-link:hover { color: var(--text); }
|
.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 ─────────────────────────────── */
|
/* ── MAIN LAYOUT ─────────────────────────────── */
|
||||||
#layout {
|
#layout {
|
||||||
|
|
@ -135,12 +142,15 @@
|
||||||
|
|
||||||
<!-- TOP BAR -->
|
<!-- TOP BAR -->
|
||||||
<div id="topbar">
|
<div id="topbar">
|
||||||
<div class="tb-logo">
|
<div class="tb-logo"><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></div>
|
||||||
<span><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></span>
|
<nav>
|
||||||
</div>
|
<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 class="tb-sep"></div>
|
||||||
<div id="tb-name">—</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 class="tb-sep"></div>
|
||||||
<div id="tb-price">—</div>
|
<div id="tb-price">—</div>
|
||||||
<div id="tb-change">—</div>
|
<div id="tb-change">—</div>
|
||||||
|
|
@ -152,8 +162,11 @@
|
||||||
<span id="tb-badge"></span>
|
<span id="tb-badge"></span>
|
||||||
<div id="tb-actions" style="display:flex;gap:8px"></div>
|
<div id="tb-actions" style="display:flex;gap:8px"></div>
|
||||||
<div class="tb-sep"></div>
|
<div class="tb-sep"></div>
|
||||||
<a href="/dashboard" class="back-link">← Dashboard</a>
|
<div class="mode-toggle">
|
||||||
<button class="btn btn-delete" style="font-size:12px;padding:4px 12px" onclick="logout()">Çıkış</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- MAIN LAYOUT -->
|
<!-- 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 };
|
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
|
||||||
function buyTime(ms) {
|
function buyTime(ms) {
|
||||||
const tf = TF_SECONDS[bot.timeframe] || 300;
|
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 ──────────────────────────────────────
|
// ── Load bot ──────────────────────────────────────
|
||||||
async function loadBot() {
|
async function loadBot() {
|
||||||
const res = await api('GET', `/bots/${BOT_ID}`);
|
const [botRes, modeRes] = await Promise.all([
|
||||||
if (!res.ok) { window.location.href = '/dashboard'; return; }
|
api('GET', `/bots/${BOT_ID}`),
|
||||||
bot = await res.json();
|
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`;
|
document.title = `${bot.name} — CryptoFox Mukan Edition`;
|
||||||
renderTopBar();
|
renderTopBar();
|
||||||
renderEditForm();
|
renderEditForm();
|
||||||
|
|
@ -362,6 +388,7 @@ function startPriceWs() {
|
||||||
|
|
||||||
// ── Chart ─────────────────────────────────────────
|
// ── Chart ─────────────────────────────────────────
|
||||||
function initChart() {
|
function initChart() {
|
||||||
|
if (chart) return;
|
||||||
const container = document.getElementById('chart');
|
const container = document.getElementById('chart');
|
||||||
chart = LightweightCharts.createChart(container, {
|
chart = LightweightCharts.createChart(container, {
|
||||||
layout: { background: { color: '#0f0f13' }, textColor: '#888' },
|
layout: { background: { color: '#0f0f13' }, textColor: '#888' },
|
||||||
|
|
|
||||||
351
src/web/bots.html
Normal file
351
src/web/bots.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
|
|
||||||
|
// ── 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
141
src/web/positions.html
Normal 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">Açı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">Açı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>
|
||||||
Loading…
Add table
Reference in a new issue