- Extract bot creation modal to _bot_modal.html partial (TomSelect, min_notional display)
- bots.html and index.html now include the partial via {% include %}
- Remove duplicate modal JS from bots.html; add onBotCreated/onModeChange hooks
- Add 20-item pagination to positions.html (open + closed tables)
- positions.html: remove redundant loadMode (handled by _header.html partial)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
237 lines
13 KiB
HTML
237 lines
13 KiB
HTML
<!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; }
|
||
.page-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
||
.page-title { font-size: 16px; font-weight: 600; }
|
||
|
||
/* ── 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; }
|
||
.pagination { display: flex; align-items: center; gap: 4px; padding: 12px 0 4px; flex-wrap: wrap; }
|
||
.pg-btn { padding: 4px 10px; border-radius: 5px; border: 1px solid var(--border); background: transparent; color: var(--muted); font-size: 12px; cursor: pointer; transition: all .15s; }
|
||
.pg-btn:hover { background: rgba(255,255,255,.05); color: var(--text); }
|
||
.pg-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.pg-info { font-size: 11px; color: var(--muted); margin-left: 8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
{% include "_header.html" %}
|
||
|
||
|
||
<main>
|
||
<div class="page-toolbar">
|
||
<span class="page-title">Botlar</span>
|
||
<button class="btn btn-primary" onclick="openModal()">+ Yeni Bot</button>
|
||
</div>
|
||
<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>
|
||
|
||
{% include "_bot_modal.html" %}
|
||
|
||
<script>
|
||
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,'>'); }
|
||
|
||
function onBotCreated() { loadBots(); }
|
||
function onModeChange() { 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();
|
||
}
|
||
|
||
async function logout() {
|
||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
||
window.location.href = '/';
|
||
}
|
||
|
||
loadBots();
|
||
</script>
|
||
</body>
|
||
</html>
|