feat(logs): color-coded log levels for buy/sell/skip events

Replace emoji prefixes with dedicated log level types: buy (orange),
sell (green), skip (purple), info (gray). CSS updated in bot.html.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-19 20:38:09 +03:00
parent 64806dd32e
commit 050cc18051
2 changed files with 319 additions and 313 deletions

View file

@ -122,12 +122,12 @@ impl BotRunner {
{
Ok(Some(result)) => {
info!(
"[{}] İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}",
"[{}] İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}",
config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent
);
{
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "trade", &format!("İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok();
db_guard.insert_log(&config.id, "buy", &format!("İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok();
}
let record = TradeRecord {
@ -175,17 +175,17 @@ impl BotRunner {
}).ok();
}
Ok(None) => {
info!("[{}] İşlem yapılmadı (yeşil mum).", config.symbol);
info!("[{}] İşlem yapılmadı (yeşil mum).", config.symbol);
{
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "info", "Yeşil mum, işlem yapılmadı.").ok();
db_guard.insert_log(&config.id, "skip", "Yeşil mum, işlem yapılmadı.").ok();
}
}
Err(e) => {
error!("[{}] Strateji hatası: {:?}", config.symbol, e);
error!("[{}] Strateji hatası: {:?}", config.symbol, e);
{
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "error", &format!("Strateji hatası: {}", e)).ok();
db_guard.insert_log(&config.id, "error", &format!("Strateji hatası: {}", e)).ok();
}
}
}
@ -295,7 +295,7 @@ async fn check_open_positions(
} 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();
db_guard.insert_log(&config.id, "sell", &format!("Pozisyon kapatıldı | Order #{} | Durum: {}", pos.order_id, status)).ok();
}
event_tx.send(TradeEvent {
bot_id: config.id.clone(),

View file

@ -16,208 +16,169 @@
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; }
/* ── HEADER / TOP BAR ───────────────────────── */
#topbar {
background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 0; flex-shrink: 0;
}
.tb-logo { font-size: 15px; font-weight: 700; margin-right: 12px; }
/* ── SHARED HEADER STYLES ────────────────────── */
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
.logo { font-size: 16px; font-weight: 700; }
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
nav { display: flex; gap: 4px; margin-right: 12px; }
.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); }
.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); }
.spacer { flex: 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); }
.btn-logout { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--red); background: transparent; color: var(--red); font-size: 12px; cursor: pointer; transition: all .15s; }
.btn-logout:hover { background: rgba(231,76,60,.1); }
/* ── BUTTONS ─────────────────────────────────── */
.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-danger { border-color: var(--red); color: var(--red); }
.btn-danger:hover { background: rgba(231,76,60,.1); }
/* ── BADGES ──────────────────────────────────── */
.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); }
.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-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-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #5a52d5; }
.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 ─────────────────────────────── */
/* ── PRICE BAR ───────────────────────────────── */
#price-bar {
background: var(--surface2); border-bottom: 1px solid var(--border);
padding: 0 16px; height: 44px; display: flex; align-items: center; gap: 0; flex-shrink: 0;
}
.pb-sep { width: 1px; height: 20px; background: var(--border); margin: 0 14px; }
#pb-name { font-size: 14px; font-weight: 600; }
#pb-symbol { font-size: 12px; color: var(--muted); margin-left: 6px; }
#pb-price { font-size: 18px; font-weight: 700; margin-left: 0; min-width: 90px; }
#pb-change { font-size: 12px; font-weight: 600; min-width: 52px; }
.pb-stat { display: flex; flex-direction: column; gap: 1px; }
.pb-stat .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.pb-stat .val { font-size: 12px; color: var(--text); }
#pb-tf { background: rgba(108,99,255,.15); color: var(--accent); padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
#chart-ohlc { font-size: 11px; color: var(--muted); display: flex; gap: 10px; }
#chart-ohlc span b { color: var(--text); }
/* ── MAIN 3-COLUMN LAYOUT ────────────────────── */
#layout {
flex: 1; display: grid; min-height: 0;
grid-template-columns: 360px 1fr;
grid-template-columns: 260px 1fr 260px;
grid-template-rows: 1fr;
}
/* ── LEFT PANE ───────────────────────────────── */
#left-pane {
border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden;
/* ── PANE COMMONS ────────────────────────────── */
.pane { border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
.pane:last-child { border-right: none; border-left: 1px solid var(--border); }
.pane-hdr {
padding: 7px 12px; border-bottom: 1px solid var(--border);
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: .06em; color: var(--muted); flex-shrink: 0;
display: flex; align-items: center; justify-content: space-between;
background: var(--surface2);
}
.pane-header {
padding: 8px 14px; border-bottom: 1px solid var(--border);
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .05em; color: var(--muted); display: flex; align-items: center; justify-content: space-between;
flex-shrink: 0;
}
.pane-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.pane-tab {
flex: 1; padding: 9px 8px; text-align: center; font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
cursor: pointer; border-bottom: 2px solid transparent; transition: all .15s;
}
.pane-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.pane-tab:hover:not(.active) { color: var(--text); }
.pane-content { overflow-y: auto; }
.pane-content::-webkit-scrollbar { width: 4px; }
.pane-content::-webkit-scrollbar-track { background: transparent; }
.pane-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* settings section */
#settings-section { border-bottom: 1px solid var(--border); flex-shrink: 0; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.form-group input {
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
padding: 6px 9px; color: var(--text); font-size: 12px; width: 100%;
}
.form-group input:focus { outline: none; border-color: var(--accent); }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#save-status { font-size: 10px; color: var(--green); display: none; }
/* ── LEFT PANE — POSITIONS ───────────────────── */
#left-pane { }
.pos-half { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.pos-half + .pos-half { border-top: 1px solid var(--border); }
.pos-scroll { flex: 1; overflow-y: auto; }
.pos-scroll::-webkit-scrollbar { width: 3px; }
.pos-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* positions */
#pos-section { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.pos-table { width: 100%; border-collapse: collapse; }
.pos-table th, .pos-table td { padding: 7px 14px; text-align: right; border-bottom: 1px solid rgba(255,255,255,.04); font-size: 12px; }
.pos-table th { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 500; text-align: right; position: sticky; top: 0; background: var(--surface2); z-index: 1; }
.pos-table td:first-child, .pos-table th:first-child { text-align: left; }
.pos-table th { font-size: 9px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 500; padding: 5px 10px; text-align: right; position: sticky; top: 0; background: var(--surface2); z-index: 1; border-bottom: 1px solid var(--border); }
.pos-table th:first-child { text-align: left; }
.pos-table td { padding: 6px 10px; text-align: right; font-size: 11px; border-bottom: 1px solid rgba(255,255,255,.03); }
.pos-table td:first-child { text-align: left; font-size: 10px; color: var(--muted); }
.pos-table tr:hover td { background: rgba(255,255,255,.02); }
.empty-row { padding: 24px 14px; text-align: center; color: var(--muted); font-size: 12px; }
.profit-up { color: var(--green); }
.profit-down { color: var(--red); }
.empty-row { padding: 16px 10px; text-align: center; color: var(--muted); font-size: 11px; }
.c-green { color: var(--green); }
.c-red { color: var(--red); }
.c-muted { color: var(--muted); }
/* ── CENTER PANE ─────────────────────────────── */
#center-pane { display: flex; flex-direction: column; overflow: hidden; }
/* chart */
#chart-header { padding: 8px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
#chart-tf-label { background: rgba(108,99,255,.15); color: var(--accent); padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
#chart-ohlc { font-size: 11px; color: var(--muted); display: flex; gap: 10px; }
#chart-ohlc span b { color: var(--text); }
#center-pane { display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--border); }
#chart-container { flex: 6; min-height: 0; position: relative; }
#chart { width: 100%; height: 100%; }
/* log terminal */
#log-section { flex: 4; display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border); }
#log-section { flex: 3; display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border); }
#log-terminal {
flex: 1; background: #0a0a0e; padding: 10px 12px; overflow-y: auto;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 11px; line-height: 1.7;
flex: 1; background: #0a0a0e; padding: 8px 10px; overflow-y: auto;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 11px; line-height: 1.6;
}
#log-terminal::-webkit-scrollbar { width: 4px; }
#log-terminal::-webkit-scrollbar { width: 3px; }
#log-terminal::-webkit-scrollbar-thumb { background: #2a2a38; border-radius: 2px; }
.log-line { display: flex; gap: 8px; }
.log-time { color: #444; min-width: 70px; flex-shrink: 0; font-size: 10px; padding-top: 1px; }
.log-level { min-width: 40px; flex-shrink: 0; font-weight: 700; font-size: 10px; padding-top: 1px; }
.log-level.info { color: #6c63ff; }
.log-level.trade { color: var(--green); }
.log-time { color: #444; min-width: 62px; flex-shrink: 0; font-size: 10px; padding-top: 1px; }
.log-level { min-width: 38px; flex-shrink: 0; font-weight: 700; font-size: 10px; padding-top: 1px; }
.log-level.info { color: #888; }
.log-level.buy { color: #f0a500; }
.log-level.sell { color: var(--green); }
.log-level.skip { color: #6c63ff; }
.log-level.error { color: var(--red); }
.log-level.trade { color: var(--green); }
.log-msg { color: #a0a0b8; word-break: break-word; }
#log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); display: inline-block; }
/* ── RIGHT PANE ──────────────────────────────── */
#right-pane { overflow-y: auto; }
#right-pane::-webkit-scrollbar { width: 3px; }
#right-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* status card */
.status-card { padding: 16px 14px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; gap: 12px; }
.status-badge-lg { padding: 4px 16px; border-radius: 20px; font-size: 12px; font-weight: 700; letter-spacing: .04em; }
.play-btn {
width: 52px; height: 52px; border-radius: 50%; border: 2px solid var(--border);
background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center;
font-size: 20px; color: var(--text); transition: all .2s;
}
.play-btn.state-running { border-color: var(--yellow); color: var(--yellow); }
.play-btn.state-running:hover { background: rgba(243,156,18,.1); }
.play-btn.state-stopped { border-color: var(--green); color: var(--green); }
.play-btn.state-stopped:hover { background: rgba(46,204,113,.1); }
.del-btn-small { font-size: 11px; color: var(--muted); background: none; border: none; cursor: pointer; text-decoration: underline; }
.del-btn-small:hover { color: var(--red); }
/* settings card */
.settings-card { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.form-group input { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 9px; color: var(--text); font-size: 12px; width: 100%; }
.form-group input:focus { outline: none; border-color: var(--accent); }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#save-status { font-size: 10px; color: var(--green); }
/* stats card */
.stats-card { padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
.stat-row { display: flex; justify-content: space-between; align-items: center; }
.stat-row .lbl { font-size: 11px; color: var(--muted); }
.stat-row .val { font-size: 12px; font-weight: 600; }
</style>
</head>
<body>
<!-- TOP BAR -->
<div id="topbar">
<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"></div>
<div class="tb-sep"></div>
<div id="tb-price"></div>
<div id="tb-change"></div>
<div class="tb-sep"></div>
<div class="tb-stat"><span class="lbl">24s Yüksek</span><span class="val" id="tb-high"></span></div>
<div class="tb-stat"><span class="lbl">24s Düşük</span><span class="val" id="tb-low"></span></div>
<div class="tb-stat"><span class="lbl">24s Hacim</span><span class="val" id="tb-vol"></span></div>
<div class="spacer"></div>
<span id="tb-badge"></span>
<div id="tb-actions" style="display:flex;gap:8px"></div>
<div class="tb-sep"></div>
<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>
{% include "_header.html" %}
<!-- MAIN LAYOUT -->
<div id="layout">
<!-- LEFT: Ayarlar + Pozisyonlar -->
<div id="left-pane">
<!-- Settings -->
<div id="settings-section">
<div class="pane-header">
Bot Ayarları
<span id="save-status">✓ Kaydedildi</span>
</div>
<div style="padding:12px;display:flex;flex-direction:column;gap:10px">
<div class="form-group">
<label>İsim</label>
<input id="e-name" type="text" />
</div>
<div class="form-grid">
<div class="form-group">
<label>USDT Miktarı</label>
<input id="e-usdt" type="number" step="1" />
</div>
<div class="form-group">
<label>Kar %</label>
<input id="e-profit" type="number" step="0.1" />
</div>
</div>
<button class="btn btn-primary" onclick="saveBot()" style="width:100%">Kaydet</button>
</div>
</div>
<!-- Pozisyonlar -->
<div id="pos-section">
<div class="pane-header">Pozisyonlar</div>
<div class="pane-tabs">
<div class="pane-tab active" id="tab-open" onclick="switchTab('open')">ık</div>
<div class="pane-tab" id="tab-closed" onclick="switchTab('closed')">Kapalı</div>
</div>
<div class="pane-content" id="left-content">
<div class="empty-row">Yükleniyor...</div>
</div>
</div>
</div>
<!-- CENTER: Chart + Log -->
<div id="center-pane">
<div id="chart-header">
<span id="chart-tf-label"></span>
<!-- FIYAT ÇUBUĞU -->
<div id="price-bar">
<span id="pb-name"></span>
<span id="pb-symbol"></span>
<div class="pb-sep"></div>
<span id="pb-price"></span>
<span id="pb-change" style="margin-left:8px"></span>
<div class="pb-sep"></div>
<div class="pb-stat"><span class="lbl">24s Yüksek</span><span class="val" id="pb-high"></span></div>
<div class="pb-sep" style="margin:0 10px"></div>
<div class="pb-stat"><span class="lbl">24s Düşük</span><span class="val" id="pb-low"></span></div>
<div class="pb-sep" style="margin:0 10px"></div>
<div class="pb-stat"><span class="lbl">24s Hacim</span><span class="val" id="pb-vol"></span></div>
<div class="pb-sep"></div>
<span id="pb-tf"></span>
<div class="pb-sep" style="margin:0 10px"></div>
<div id="chart-ohlc">
<span>A: <b id="ohlc-o"></b></span>
<span>Y: <b id="ohlc-h"></b></span>
@ -225,24 +186,105 @@
<span>K: <b id="ohlc-c"></b></span>
</div>
</div>
<!-- 3 SÜTUN LAYOUT -->
<div id="layout">
<!-- SOL: Açık + Kapalı Pozisyonlar -->
<div id="left-pane" class="pane">
<div class="pos-half">
<div class="pane-hdr">
ık Pozisyonlar
<span id="open-count" class="c-muted">0</span>
</div>
<div class="pos-scroll">
<table class="pos-table">
<thead><tr><th>Zaman</th><th>Alış</th><th>Hedef</th><th>Adet</th></tr></thead>
<tbody id="open-body"><tr><td colspan="4"><div class="empty-row"></div></td></tr></tbody>
</table>
</div>
</div>
<div class="pos-half">
<div class="pane-hdr">
Kapalı İşlemler
<span id="closed-count" class="c-muted">0</span>
</div>
<div class="pos-scroll">
<table class="pos-table">
<thead><tr><th>Zaman</th><th>Alış</th><th>Satış</th><th>Sonuç</th></tr></thead>
<tbody id="closed-body"><tr><td colspan="4"><div class="empty-row"></div></td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- ORTA: Grafik + Log -->
<div id="center-pane">
<div id="chart-container">
<div id="chart"></div>
</div>
<!-- Log -->
<div id="log-section">
<div class="pane-header">
<div class="pane-hdr">
Canlı Log
<div style="display:flex;align-items:center;gap:6px">
<span id="log-dot"></span>
<span id="log-status" style="font-size:10px;color:var(--muted)"></span>
<button class="btn" style="font-size:10px;padding:2px 7px" onclick="clearLog()">Temizle</button>
<button class="btn" style="font-size:10px;padding:2px 6px" onclick="clearLog()">Temizle</button>
</div>
</div>
<div id="log-terminal"></div>
</div>
</div>
<!-- SAĞ: Durum + Ayarlar + İstatistikler -->
<div id="right-pane" class="pane">
<!-- Durum Kartı -->
<div class="status-card">
<span id="status-badge" class="badge badge-stopped status-badge-lg">Durduruldu</span>
<button id="play-btn" class="play-btn state-stopped" onclick="toggleBot()"></button>
<button class="del-btn-small" onclick="deleteBot()">botu sil</button>
</div>
<!-- Ayarlar Kartı -->
<div class="pane-hdr">
Bot Ayarları
<span id="save-status" style="display:none">✓ kaydedildi</span>
</div>
<div class="settings-card">
<div class="form-group">
<label>İsim</label>
<input id="e-name" type="text" />
</div>
<div class="form-grid">
<div class="form-group">
<label>USDT</label>
<input id="e-usdt" type="number" step="1" />
</div>
<div class="form-group">
<label>Kar %</label>
<input id="e-profit" type="number" step="0.1" />
</div>
</div>
<button class="btn btn-primary" onclick="saveBot()" style="width:100%;font-size:12px">Kaydet</button>
</div>
<!-- İstatistikler (mock) -->
<div class="pane-hdr">İstatistikler</div>
<div class="stats-card">
<div class="stat-row"><span class="lbl">Toplam İşlem</span><span class="val" id="stat-total"></span></div>
<div class="stat-row"><span class="lbl">Kazanılan</span><span class="val c-green" id="stat-won"></span></div>
<div class="stat-row"><span class="lbl">Kaybedilen</span><span class="val c-red" id="stat-lost"></span></div>
<div class="stat-row"><span class="lbl">ık Pozisyon</span><span class="val" id="stat-open"></span></div>
<div class="stat-row"><span class="lbl">Sembol</span><span class="val" id="stat-symbol"></span></div>
<div class="stat-row"><span class="lbl">Timeframe</span><span class="val" id="stat-tf"></span></div>
</div>
</div>
</div>
<script>
@ -252,13 +294,15 @@ let logSource = null;
let lastLogId = 0;
let chart = null;
let candleSeries = null;
let currentTab = 'open';
let priceWs = null;
let priceLines = [];
let openPositionsCache = [];
let closedPositionsCache = [];
// ── API helper ────────────────────────────────────
const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h','1d':'1d','1w':'1w' };
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
// ── helpers ───────────────────────────────────────
async function api(method, path, body) {
const res = await fetch('/api' + path, {
method, credentials: 'include',
@ -270,19 +314,15 @@ async function api(method, path, body) {
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
// ── Timeframe → Binance interval ─────────────────
const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h','1d':'1d','1w':'1w' };
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
function shortDate(ms) { return new Date(ms).toLocaleString('tr-TR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); }
function buyTime(ms) {
const tf = TF_SECONDS[bot.timeframe] || 300;
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;
const cur = Math.floor(sec / tf) * tf;
return cur - tf;
}
// ── Load bot ──────────────────────────────────────
// ── Load ──────────────────────────────────────────
async function loadBot() {
const [botRes, modeRes] = await Promise.all([
api('GET', `/bots/${BOT_ID}`),
@ -290,31 +330,45 @@ async function loadBot() {
]);
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.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
}
document.title = `${bot.name} — CryptoFox Mukan Edition`;
renderTopBar();
renderPriceBar();
renderStatusCard();
renderEditForm();
document.getElementById('chart-tf-label').textContent = bot.timeframe;
renderStats();
initChart();
loadTicker();
startPriceWs();
}
function renderTopBar() {
document.getElementById('tb-name').textContent = bot.name;
document.getElementById('tb-symbol').textContent = bot.symbol;
document.getElementById('tb-badge').innerHTML = `<span class="badge ${bot.running ? 'badge-running' : 'badge-stopped'}">${bot.running ? 'Çalışıyor' : 'Durdu'}</span>`;
const actions = document.getElementById('tb-actions');
if (bot.running) {
actions.innerHTML = `<button class="btn btn-stop" onclick="stopBot()">Durdur</button><button class="btn btn-delete" onclick="deleteBot()">Sil</button>`;
function renderPriceBar() {
document.getElementById('pb-name').textContent = bot.name;
document.getElementById('pb-symbol').textContent = bot.symbol;
document.getElementById('pb-tf').textContent = bot.timeframe;
}
function renderStatusCard() {
const running = bot.running;
const badge = document.getElementById('status-badge');
badge.textContent = running ? 'Çalışıyor' : 'Durduruldu';
badge.className = 'badge status-badge-lg ' + (running ? 'badge-running' : 'badge-stopped');
const btn = document.getElementById('play-btn');
if (running) {
btn.textContent = '⏸';
btn.className = 'play-btn state-running';
btn.title = 'Durdur';
} else {
actions.innerHTML = `<button class="btn btn-start" onclick="startBot()">Başlat</button><button class="btn btn-delete" onclick="deleteBot()">Sil</button>`;
btn.textContent = '▶';
btn.className = 'play-btn state-stopped';
btn.title = 'Başlat';
}
}
@ -324,20 +378,34 @@ function renderEditForm() {
document.getElementById('e-profit').value = bot.profit_percent;
}
function renderStats() {
document.getElementById('stat-symbol').textContent = bot.symbol;
document.getElementById('stat-tf').textContent = bot.timeframe;
const total = closedPositionsCache.length;
const won = closedPositionsCache.filter(c => c.status === 'FILLED').length;
const lost = total - won;
document.getElementById('stat-total').textContent = total || '—';
document.getElementById('stat-won').textContent = won || '—';
document.getElementById('stat-lost').textContent = lost || '—';
document.getElementById('stat-open').textContent = openPositionsCache.length || '—';
}
// ── Bot actions ───────────────────────────────────
async function startBot() {
await api('POST', `/bots/${BOT_ID}/start`);
await loadBot();
}
async function stopBot() {
async function toggleBot() {
if (bot.running) {
await api('POST', `/bots/${BOT_ID}/stop`);
} else {
await api('POST', `/bots/${BOT_ID}/start`);
}
await loadBot();
}
async function deleteBot() {
if (!confirm(`"${bot.name}" botunu silmek istiyor musun?`)) return;
await api('DELETE', `/bots/${BOT_ID}`);
window.location.href = '/dashboard';
window.location.href = '/bots';
}
async function saveBot() {
const name = document.getElementById('e-name').value.trim();
const usdt_amount = parseFloat(document.getElementById('e-usdt').value);
@ -345,30 +413,31 @@ async function saveBot() {
if (!name || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
const res = await api('PUT', `/bots/${BOT_ID}`, { name, usdt_amount, profit_percent });
bot = await res.json();
renderTopBar();
renderPriceBar();
renderStatusCard();
renderStats();
const s = document.getElementById('save-status');
s.style.display = 'inline';
setTimeout(() => s.style.display = 'none', 2000);
}
// ── Ticker (24h stats) ────────────────────────────
// ── Ticker ────────────────────────────────────────
async function loadTicker() {
try {
const res = await fetch(`https://api.binance.com/api/v3/ticker/24hr?symbol=${bot.symbol}`);
const t = await res.json();
const change = parseFloat(t.priceChangePercent);
document.getElementById('tb-price').textContent = fmt(t.lastPrice, 2);
const chEl = document.getElementById('tb-change');
document.getElementById('pb-price').textContent = fmt(t.lastPrice, 2);
const chEl = document.getElementById('pb-change');
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
document.getElementById('tb-high').textContent = fmt(t.highPrice, 2);
document.getElementById('tb-low').textContent = fmt(t.lowPrice, 2);
document.getElementById('pb-high').textContent = fmt(t.highPrice, 2);
document.getElementById('pb-low').textContent = fmt(t.lowPrice, 2);
const vol = parseFloat(t.quoteVolume);
document.getElementById('tb-vol').textContent = vol >= 1e9 ? (vol/1e9).toFixed(2)+'B' : vol >= 1e6 ? (vol/1e6).toFixed(2)+'M' : fmt(vol, 0);
document.getElementById('pb-vol').textContent = vol >= 1e9 ? (vol/1e9).toFixed(2)+'B' : vol >= 1e6 ? (vol/1e6).toFixed(2)+'M' : fmt(vol, 0);
} catch(e) {}
}
// ── Live price via Binance WS ─────────────────────
function startPriceWs() {
if (priceWs) priceWs.close();
const sym = bot.symbol.toLowerCase();
@ -377,8 +446,8 @@ function startPriceWs() {
const t = JSON.parse(e.data);
const price = parseFloat(t.c);
const change = parseFloat(t.P);
document.getElementById('tb-price').textContent = fmt(price, 2);
const chEl = document.getElementById('tb-change');
document.getElementById('pb-price').textContent = fmt(price, 2);
const chEl = document.getElementById('pb-change');
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
};
@ -404,8 +473,6 @@ function initChart() {
borderUpColor: '#2ecc71', borderDownColor: '#e74c3c',
wickUpColor: '#2ecc71', wickDownColor: '#e74c3c',
});
// Crosshair OHLC güncelle
chart.subscribeCrosshairMove(p => {
if (!p.seriesData || !p.seriesData.size) return;
const d = p.seriesData.get(candleSeries);
@ -415,33 +482,22 @@ function initChart() {
document.getElementById('ohlc-l').textContent = fmt(d.low, 2);
document.getElementById('ohlc-c').textContent = fmt(d.close, 2);
});
// Resize observer
new ResizeObserver(() => {
chart.applyOptions({ width: container.clientWidth, height: container.clientHeight });
}).observe(container);
loadKlines();
startKlineWs();
}
async function loadKlines() {
const interval = TF_MAP[bot.timeframe] || '5m';
const baseUrl = 'https://api.binance.com/api/v3/klines';
try {
const res = await fetch(`${baseUrl}?symbol=${bot.symbol}&interval=${interval}&limit=200`);
const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${bot.symbol}&interval=${interval}&limit=200`);
const data = await res.json();
const candles = data.map(k => ({
time: Math.floor(k[0] / 1000),
open: parseFloat(k[1]),
high: parseFloat(k[2]),
low: parseFloat(k[3]),
close: parseFloat(k[4]),
}));
const candles = data.map(k => ({ time: Math.floor(k[0]/1000), open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]) }));
candleSeries.setData(candles);
chart.timeScale().scrollToRealTime();
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 80, to: candles.length + 5 });
if (candles.length) {
const last = candles[candles.length - 1];
document.getElementById('ohlc-o').textContent = fmt(last.open, 2);
@ -450,7 +506,7 @@ async function loadKlines() {
document.getElementById('ohlc-c').textContent = fmt(last.close, 2);
}
updateChartOverlays();
} catch(e) { console.error('loadKlines error:', e); }
} catch(e) {}
}
let klineWs = null;
@ -458,139 +514,92 @@ function startKlineWs() {
if (klineWs) klineWs.close();
const interval = TF_MAP[bot.timeframe] || '5m';
const sym = bot.symbol.toLowerCase();
const wsUrl = `wss://stream.binance.com:9443/ws/${sym}@kline_${interval}`;
klineWs = new WebSocket(wsUrl);
klineWs = new WebSocket(`wss://stream.binance.com:9443/ws/${sym}@kline_${interval}`);
klineWs.onmessage = e => {
const msg = JSON.parse(e.data);
const k = msg.k;
const candle = {
time: Math.floor(k.t / 1000),
open: parseFloat(k.o), high: parseFloat(k.h),
low: parseFloat(k.l), close: parseFloat(k.c),
};
const k = JSON.parse(e.data).k;
const candle = { time: Math.floor(k.t/1000), open: parseFloat(k.o), high: parseFloat(k.h), low: parseFloat(k.l), close: parseFloat(k.c) };
candleSeries.update(candle);
document.getElementById('ohlc-o').textContent = fmt(candle.open, 2);
document.getElementById('ohlc-h').textContent = fmt(candle.high, 2);
document.getElementById('ohlc-l').textContent = fmt(candle.low, 2);
document.getElementById('ohlc-c').textContent = fmt(candle.close, 2);
// Fiyatı da güncelle
document.getElementById('tb-price').textContent = fmt(candle.close, 2);
document.getElementById('pb-price').textContent = fmt(candle.close, 2);
};
klineWs.onerror = () => {};
klineWs.onclose = () => setTimeout(startKlineWs, 5000);
}
// ── Chart overlays ────────────────────────────────
function updateChartOverlays() {
if (!candleSeries) return;
// Price lines temizle
priceLines.forEach(pl => candleSeries.removePriceLine(pl));
priceLines = [];
// Açık pozisyonlar → hedef price line (yeşil) + alış marker
openPositionsCache.forEach(p => {
priceLines.push(candleSeries.createPriceLine({
price: p.sell_target,
color: '#2ecc71',
lineWidth: 1,
price: p.sell_target, color: '#2ecc71', lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: true,
title: fmt(p.sell_target, 2),
axisLabelVisible: true, title: fmt(p.sell_target, 2),
}));
});
// Markers: açık + kapalı pozisyonlar
const buyMap = new Map();
const sellMap = new Map();
const buyMap = new Map(), sellMap = new Map();
openPositionsCache.forEach(p => {
const t = buyTime(p.opened_at);
if (!buyMap.has(t)) buyMap.set(t, []);
buyMap.get(t).push(fmt(p.buy_price, 2));
});
closedPositionsCache.forEach(c => {
const tb = buyTime(c.opened_at);
if (!buyMap.has(tb)) buyMap.set(tb, []);
buyMap.get(tb).push(fmt(c.buy_price, 2));
if (c.status === 'FILLED') {
const ts = Math.floor(c.closed_at / 1000);
if (!sellMap.has(ts)) sellMap.set(ts, []);
sellMap.get(ts).push(fmt(c.sell_target, 2));
}
});
const markers = [];
buyMap.forEach((prices, time) => {
markers.push({
time,
position: 'belowBar',
color: '#6c63ff',
shape: 'arrowUp',
text: prices.length > 1 ? `×${prices.length}` : '',
size: 1,
});
});
sellMap.forEach((prices, time) => {
markers.push({
time,
position: 'aboveBar',
color: '#2ecc71',
shape: 'arrowDown',
text: prices.length > 1 ? `×${prices.length}` : '',
size: 1,
});
});
buyMap.forEach((_, time) => markers.push({ time, position: 'belowBar', color: '#6c63ff', shape: 'arrowUp', size: 1 }));
sellMap.forEach((_, time) => markers.push({ time, position: 'aboveBar', color: '#2ecc71', shape: 'arrowDown', size: 1 }));
markers.sort((a, b) => a.time - b.time);
candleSeries.setMarkers(markers);
}
// ── Pozisyon sekmeleri ────────────────────────────
function switchTab(tab) {
currentTab = tab;
document.getElementById('tab-open').classList.toggle('active', tab === 'open');
document.getElementById('tab-closed').classList.toggle('active', tab === 'closed');
if (tab === 'open') loadPositions();
else loadClosed();
}
// ── Positions ─────────────────────────────────────
async function loadPositions() {
const res = await api('GET', `/bots/${BOT_ID}/positions`);
const data = await res.json();
openPositionsCache = data;
openPositionsCache = await res.json();
if (candleSeries) updateChartOverlays();
const el = document.getElementById('left-content');
if (!data.length) { el.innerHTML = '<div class="empty-row">ık pozisyon yok</div>'; return; }
el.innerHTML = `<table class="pos-table">
<thead><tr><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Zaman</th></tr></thead>
<tbody>${data.map(p => `<tr>
document.getElementById('open-count').textContent = openPositionsCache.length;
const tbody = document.getElementById('open-body');
if (!openPositionsCache.length) {
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-row">ık pozisyon yok</div></td></tr>';
} else {
tbody.innerHTML = openPositionsCache.map(p => `<tr>
<td>${shortDate(p.opened_at)}</td>
<td>${fmt(p.buy_price, 4)}</td>
<td class="profit-up">${fmt(p.sell_target, 4)}</td>
<td>${p.quantity}</td>
<td style="font-size:10px;color:var(--muted)">${new Date(p.opened_at).toLocaleString('tr-TR',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'2-digit'})}</td>
</tr>`).join('')}</tbody>
</table>`;
<td class="c-green">${fmt(p.sell_target, 4)}</td>
<td class="c-muted">${parseFloat(p.quantity).toFixed(4)}</td>
</tr>`).join('');
}
renderStats();
}
async function loadClosed() {
const res = await api('GET', `/bots/${BOT_ID}/positions/closed`);
const data = await res.json();
closedPositionsCache = data;
closedPositionsCache = await res.json();
if (candleSeries) updateChartOverlays();
const el = document.getElementById('left-content');
if (!data.length) { el.innerHTML = '<div class="empty-row">Henüz kapalı işlem yok</div>'; return; }
el.innerHTML = `<table class="pos-table">
<thead><tr><th>Alış</th><th>Satış</th><th>Durum</th><th>Kapanış</th></tr></thead>
<tbody>${data.map(c => `<tr>
document.getElementById('closed-count').textContent = closedPositionsCache.length;
const tbody = document.getElementById('closed-body');
if (!closedPositionsCache.length) {
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-row">Henüz işlem yok</div></td></tr>';
} else {
tbody.innerHTML = closedPositionsCache.map(c => `<tr>
<td>${shortDate(c.opened_at)}</td>
<td>${fmt(c.buy_price, 4)}</td>
<td class="${c.status==='FILLED'?'profit-up':'profit-down'}">${fmt(c.sell_target, 4)}</td>
<td><span class="badge ${c.status==='FILLED'?'badge-running':'badge-stopped'}" style="font-size:10px">${c.status}</span></td>
<td style="font-size:10px;color:var(--muted)">${new Date(c.closed_at).toLocaleString('tr-TR',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'2-digit'})}</td>
</tr>`).join('')}</tbody>
</table>`;
<td>${fmt(c.sell_target, 4)}</td>
<td class="${c.status === 'FILLED' ? 'c-green' : 'c-muted'}" style="font-size:10px;font-weight:600">${c.status}</td>
</tr>`).join('');
}
renderStats();
}
// ── Log ───────────────────────────────────────────
@ -621,10 +630,7 @@ function connectLogSSE() {
if (log.id <= lastLogId) return;
lastLogId = log.id;
appendLog(log);
if (log.level === 'trade') {
loadPositions();
loadClosed();
}
if (log.level === 'trade') { loadPositions(); loadClosed(); }
});
logSource.onopen = () => {
document.getElementById('log-dot').style.background = 'var(--green)';