430 lines
19 KiB
HTML
430 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="tr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Mukan Special 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: #888;
|
||
--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: 16px 24px; display: flex; align-items: center; gap: 12px; }
|
||
header h1 { font-size: 18px; font-weight: 600; color: var(--accent); }
|
||
header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
|
||
header .status-dot.connected { background: var(--green); }
|
||
header .spacer { flex: 1; }
|
||
.mode-toggle { display: flex; align-items: center; gap: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px; }
|
||
.mode-btn { padding: 4px 14px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.15s; }
|
||
.mode-btn.active-live { background: rgba(46,204,113,0.15); color: var(--green); }
|
||
.mode-btn.active-testnet { background: rgba(243,156,18,0.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: 0.05em; color: var(--muted); display: flex; align-items: center; justify-content: space-between; }
|
||
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: 0.05em; color: var(--muted); font-weight: 500; }
|
||
tr:last-child td { border-bottom: none; }
|
||
tr:hover td { background: rgba(255,255,255,0.02); }
|
||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||
.badge-running { background: rgba(46,204,113,0.15); color: var(--green); }
|
||
.badge-stopped { background: rgba(136,136,136,0.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 0.15s; }
|
||
.btn:hover { background: rgba(255,255,255,0.05); }
|
||
.btn-start { border-color: var(--green); color: var(--green); }
|
||
.btn-start:hover { background: rgba(46,204,113,0.1); }
|
||
.btn-stop { border-color: var(--yellow); color: var(--yellow); }
|
||
.btn-stop:hover { background: rgba(243,156,18,0.1); }
|
||
.btn-delete { border-color: var(--red); color: var(--red); }
|
||
.btn-delete:hover { background: rgba(231,76,60,0.1); }
|
||
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.btn-primary:hover { background: #5a52d5; }
|
||
.empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
||
|
||
/* Auth overlay */
|
||
#auth-overlay { position: fixed; inset: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; z-index: 200; }
|
||
.auth-box { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 32px; width: 320px; display: flex; flex-direction: column; gap: 16px; }
|
||
.auth-box h2 { font-size: 16px; color: var(--accent); }
|
||
.auth-box input { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 9px 12px; color: var(--text); font-size: 14px; width: 100%; }
|
||
.auth-box input:focus { outline: none; border-color: var(--accent); }
|
||
.auth-error { color: var(--red); font-size: 12px; display: none; }
|
||
|
||
/* Modal */
|
||
#modal-overlay { position: fixed; inset: 0; background: rgba(0,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; display: flex; flex-direction: column; overflow: visible; }
|
||
.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; line-height: 1; padding: 0 4px; }
|
||
.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: 0.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; }
|
||
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--accent); }
|
||
.form-group select option { background: var(--surface); }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
|
||
/* TomSelect dark theme */
|
||
.ts-wrapper .ts-control,
|
||
.ts-wrapper.multi .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,
|
||
.ts-wrapper.single.focus .ts-control { border-color: var(--accent) !important; box-shadow: none !important; }
|
||
.ts-wrapper .ts-control input,
|
||
.ts-wrapper .ts-control input:focus { background: transparent !important; color: var(--text) !important; outline: none !important; }
|
||
.ts-dropdown,
|
||
.ts-dropdown.single,
|
||
.ts-dropdown.multi { background: var(--surface) !important; border-color: var(--border) !important; color: var(--text) !important; z-index: 150; max-height: 260px !important; }
|
||
.ts-dropdown .ts-dropdown-content { max-height: 260px !important; overflow-y: auto !important; }
|
||
.ts-dropdown .option { padding: 7px 12px; color: var(--text) !important; background: transparent !important; }
|
||
.ts-dropdown .option:hover,
|
||
.ts-dropdown .option.active { background: rgba(108,99,255,0.2) !important; color: var(--text) !important; }
|
||
.ts-dropdown .option.selected { background: rgba(108,99,255,0.1) !important; }
|
||
.ts-wrapper .ts-control .item { color: var(--text) !important; background: rgba(108,99,255,0.15) !important; border-radius: 3px; padding: 1px 6px; border: none !important; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="auth-overlay">
|
||
<div class="auth-box">
|
||
<h2>Mukan Special Edition</h2>
|
||
<input type="password" id="token-input" placeholder="Auth token" onkeydown="if(event.key==='Enter')login()" />
|
||
<button class="btn btn-primary" onclick="login()">Giriş</button>
|
||
<span class="auth-error" id="auth-error">Geçersiz token</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="modal-overlay" onclick="if(event.target===this)closeModal()">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2>Yeni Bot Ekle</h2>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>İsim</label>
|
||
<input id="f-name" placeholder="DOGE Bot" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Coin (USDT çifti)</label>
|
||
<select id="f-symbol"><option value="">Yükleniyor...</option></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Timeframe</label>
|
||
<select id="f-timeframe">
|
||
<option value="1m">1m</option>
|
||
<option value="5m" selected>5m</option>
|
||
<option value="15m">15m</option>
|
||
<option value="30m">30m</option>
|
||
<option value="1h">1h</option>
|
||
<option value="4h">4h</option>
|
||
<option value="1d">1d</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>USDT Miktarı <span id="f-min-label" style="color:var(--muted);font-size:10px"></span></label>
|
||
<input id="f-usdt" type="number" step="1" inputmode="numeric" placeholder="—" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kar %</label>
|
||
<input id="f-profit" type="number" step="0.1" placeholder="2" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn" onclick="closeModal()">İptal</button>
|
||
<button class="btn btn-primary" onclick="createBot()">Bot Ekle</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<header>
|
||
<h1>MSE Dashboard</h1>
|
||
<span class="status-dot" id="sse-dot"></span>
|
||
<span id="sse-label" style="font-size:12px; color:var(--muted)">bağlanıyor...</span>
|
||
<span class="spacer"></span>
|
||
<div class="mode-toggle">
|
||
<button class="mode-btn" id="btn-testnet" onclick="switchMode('testnet')">Testnet</button>
|
||
<button class="mode-btn" id="btn-live" onclick="switchMode('live')">Canlı</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<section>
|
||
<div class="section-header">
|
||
Botlar <span id="bot-count" style="color:var(--text)">0</span>
|
||
<button class="btn btn-primary" onclick="openModal()" style="font-size:12px;padding:4px 12px">+ Yeni Bot</button>
|
||
</div>
|
||
<table id="bots-table">
|
||
<thead><tr><th>İsim</th><th>Sembol</th><th>Timeframe</th><th>USDT</th><th>Kar %</th><th>Testnet</th><th>Durum</th><th></th></tr></thead>
|
||
<tbody id="bots-body"><tr><td colspan="8" class="empty">Yükleniyor...</td></tr></tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<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="positions-body"><tr><td colspan="6" class="empty">Açık pozisyon yok</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>Zaman</th></tr></thead>
|
||
<tbody id="closed-body"><tr><td colspan="7" class="empty">Henüz işlem yok</td></tr></tbody>
|
||
</table>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
let AUTH_TOKEN = localStorage.getItem('mse_token') || '';
|
||
let sseSource = null;
|
||
let symbolSelect = null;
|
||
let allSymbols = [];
|
||
let currentMode = 'testnet';
|
||
|
||
function login() {
|
||
const t = document.getElementById('token-input').value.trim();
|
||
if (!t) return;
|
||
AUTH_TOKEN = t;
|
||
localStorage.setItem('mse_token', t);
|
||
tryConnect();
|
||
}
|
||
|
||
async function api(method, path, body) {
|
||
const res = await fetch('/api' + path, {
|
||
method,
|
||
headers: { 'Authorization': 'Bearer ' + AUTH_TOKEN, 'Content-Type': 'application/json' },
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
if (res.status === 401) { showAuthOverlay(); throw new Error('Unauthorized'); }
|
||
return res;
|
||
}
|
||
|
||
function showAuthOverlay() {
|
||
document.getElementById('auth-overlay').style.display = 'flex';
|
||
}
|
||
|
||
async function tryConnect() {
|
||
const res = await fetch('/api/bots', { headers: { 'Authorization': 'Bearer ' + AUTH_TOKEN } });
|
||
if (res.status === 401) {
|
||
document.getElementById('auth-error').style.display = 'block';
|
||
return;
|
||
}
|
||
document.getElementById('auth-overlay').style.display = 'none';
|
||
loadAll();
|
||
connectSSE();
|
||
}
|
||
|
||
async function loadAll() {
|
||
await loadMode();
|
||
loadBots();
|
||
loadPositions();
|
||
loadClosed();
|
||
loadSymbols();
|
||
}
|
||
|
||
async function loadMode() {
|
||
const res = await api('GET', '/mode');
|
||
const data = await res.json();
|
||
currentMode = data.mode;
|
||
updateModeUI();
|
||
}
|
||
|
||
function updateModeUI() {
|
||
const btnTestnet = document.getElementById('btn-testnet');
|
||
const btnLive = document.getElementById('btn-live');
|
||
btnTestnet.className = 'mode-btn' + (currentMode === 'testnet' ? ' active-testnet' : '');
|
||
btnLive.className = 'mode-btn' + (currentMode === 'live' ? ' active-live' : '');
|
||
}
|
||
|
||
async function switchMode(mode) {
|
||
if (mode === currentMode) return;
|
||
if (mode === 'live') {
|
||
if (!confirm('Canlı moda geçilecek. Çalışan tüm botlar durdurulacak. Devam edilsin mi?')) return;
|
||
}
|
||
await api('POST', '/mode', { mode });
|
||
currentMode = mode;
|
||
updateModeUI();
|
||
loadAll();
|
||
}
|
||
|
||
async function loadSymbols() {
|
||
const res = await api('GET', '/symbols');
|
||
allSymbols = await res.json();
|
||
|
||
symbolSelect = new TomSelect('#f-symbol', {
|
||
valueField: 'symbol',
|
||
labelField: 'symbol',
|
||
searchField: 'symbol',
|
||
options: allSymbols,
|
||
placeholder: 'Ara veya seç...',
|
||
minChars: 0,
|
||
maxOptions: false,
|
||
dropdownParent: 'body',
|
||
render: {
|
||
option: (data) =>
|
||
`<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<strong>${data.symbol}</strong>
|
||
<span style="color:#888;font-size:11px">min ${data.min_notional} USDT</span>
|
||
</div>`,
|
||
item: (data) => `<div>${data.symbol}</div>`,
|
||
},
|
||
onChange(value) {
|
||
const item = allSymbols.find(s => s.symbol === value);
|
||
if (!item) return;
|
||
const usdtInput = document.getElementById('f-usdt');
|
||
usdtInput.value = item.min_notional;
|
||
usdtInput.min = item.min_notional;
|
||
document.getElementById('f-min-label').textContent = `(min ${item.min_notional} USDT)`;
|
||
},
|
||
});
|
||
}
|
||
|
||
function openModal() {
|
||
const modeLabel = currentMode === 'testnet' ? '🟡 Testnet' : '🟢 Canlı';
|
||
document.querySelector('.modal-header h2').textContent = `Yeni Bot Ekle — ${modeLabel}`;
|
||
document.getElementById('modal-overlay').classList.add('open');
|
||
if (symbolSelect) symbolSelect.focus();
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal-overlay').classList.remove('open');
|
||
}
|
||
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
||
|
||
async function loadBots() {
|
||
const res = await api('GET', '/bots');
|
||
const bots = await res.json();
|
||
document.getElementById('bot-count').textContent = bots.length;
|
||
const tbody = document.getElementById('bots-body');
|
||
if (!bots.length) { tbody.innerHTML = '<tr><td colspan="8" class="empty">Henüz bot yok</td></tr>'; return; }
|
||
tbody.innerHTML = bots.map(b => `
|
||
<tr>
|
||
<td>${esc(b.name)}</td>
|
||
<td><strong>${esc(b.symbol)}</strong></td>
|
||
<td>${esc(b.timeframe)}</td>
|
||
<td>${b.usdt_amount} USDT</td>
|
||
<td>%${b.profit_percent}</td>
|
||
<td>${b.testnet ? '🟡 Test' : '🟢 Gerçek'}</td>
|
||
<td><span class="badge ${b.running ? 'badge-running' : 'badge-stopped'}">${b.running ? 'Çalışıyor' : 'Durdu'}</span></td>
|
||
<td style="display:flex;gap:6px;flex-wrap:wrap">
|
||
${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>
|
||
</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
async function loadPositions() {
|
||
const res = await api('GET', '/positions');
|
||
const positions = await res.json();
|
||
const tbody = document.getElementById('positions-body');
|
||
if (!positions.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr>'; return; }
|
||
tbody.innerHTML = positions.map(p => `
|
||
<tr>
|
||
<td>${esc(p.bot_name)}</td>
|
||
<td><strong>${esc(p.symbol)}</strong></td>
|
||
<td>${p.buy_price.toFixed(6)}</td>
|
||
<td>${p.sell_target.toFixed(6)}</td>
|
||
<td>${p.quantity}</td>
|
||
<td>${new Date(p.opened_at).toLocaleString('tr-TR')}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
async function loadClosed() {
|
||
const res = await api('GET', '/positions/closed');
|
||
const closed = await res.json();
|
||
const tbody = document.getElementById('closed-body');
|
||
if (!closed.length) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>'; return; }
|
||
tbody.innerHTML = closed.map(c => `
|
||
<tr>
|
||
<td>${esc(c.bot_name)}</td>
|
||
<td><strong>${esc(c.symbol)}</strong></td>
|
||
<td>${c.buy_price.toFixed(6)}</td>
|
||
<td>${c.sell_target.toFixed(6)}</td>
|
||
<td>${c.quantity}</td>
|
||
<td><span class="badge ${c.status==='FILLED'?'badge-running':'badge-stopped'}">${c.status}</span></td>
|
||
<td>${new Date(c.closed_at).toLocaleString('tr-TR')}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
async function createBot() {
|
||
const name = document.getElementById('f-name').value.trim();
|
||
const symbol = symbolSelect ? symbolSelect.getValue() : '';
|
||
const timeframe = document.getElementById('f-timeframe').value;
|
||
const usdt_amount = parseFloat(document.getElementById('f-usdt').value);
|
||
const profit_percent = parseFloat(document.getElementById('f-profit').value);
|
||
const usdtInput = document.getElementById('f-usdt');
|
||
const minNotional = parseFloat(usdtInput.min) || 0;
|
||
if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
|
||
if (usdt_amount < minNotional) { alert(`Minimum işlem miktarı ${minNotional} USDT`); usdtInput.focus(); return; }
|
||
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
|
||
closeModal();
|
||
document.getElementById('f-name').value = '';
|
||
if (symbolSelect) symbolSelect.clear();
|
||
document.getElementById('f-usdt').value = '';
|
||
document.getElementById('f-profit').value = '';
|
||
document.getElementById('f-min-label').textContent = '';
|
||
loadBots();
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
function connectSSE() {
|
||
if (sseSource) sseSource.close();
|
||
sseSource = new EventSource(`/api/events?token=${encodeURIComponent(AUTH_TOKEN)}`);
|
||
sseSource.addEventListener('trade', () => { loadPositions(); loadClosed(); });
|
||
sseSource.onopen = () => {
|
||
document.getElementById('sse-dot').className = 'status-dot connected';
|
||
document.getElementById('sse-label').textContent = 'canlı';
|
||
};
|
||
sseSource.onerror = () => {
|
||
document.getElementById('sse-dot').className = 'status-dot';
|
||
document.getElementById('sse-label').textContent = 'bağlantı kesildi';
|
||
setTimeout(connectSSE, 5000);
|
||
};
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
if (AUTH_TOKEN) tryConnect();
|
||
else showAuthOverlay();
|
||
</script>
|
||
</body>
|
||
</html>
|