337 lines
15 KiB
HTML
337 lines
15 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); }
|
||
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; }
|
||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; padding: 18px; }
|
||
.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-actions { padding: 0 18px 18px; }
|
||
.empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
||
#auth-overlay { position: fixed; inset: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||
.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; }
|
||
.ts-wrapper .ts-control { background: var(--bg); border-color: var(--border); color: var(--text); min-height: 32px; padding: 4px 10px; border-radius: 5px; }
|
||
.ts-wrapper.focus .ts-control { border-color: var(--accent); box-shadow: none; }
|
||
.ts-dropdown { background: var(--surface); border-color: var(--border); color: var(--text); }
|
||
.ts-dropdown .option { padding: 7px 12px; }
|
||
.ts-dropdown .option.active { background: rgba(108,99,255,0.2); color: var(--text); }
|
||
.ts-wrapper .ts-control .item { color: var(--text); background: rgba(108,99,255,0.15); border-radius: 3px; padding: 1px 6px; }
|
||
</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" />
|
||
<button class="btn btn-primary" onclick="login()">Giriş</button>
|
||
<span class="auth-error" id="auth-error">Geçersiz token</span>
|
||
</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>
|
||
</header>
|
||
|
||
<main>
|
||
<section>
|
||
<div class="section-header">Botlar <span id="bot-count" style="color:var(--text)">0</span></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">Yeni Bot Ekle</div>
|
||
<div class="form-grid">
|
||
<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-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="0.1" placeholder="Min. seçilecek" />
|
||
</div>
|
||
<div class="form-group"><label>Kar %</label><input id="f-profit" type="number" step="0.1" placeholder="2" /></div>
|
||
<div class="form-group"><label>Testnet</label>
|
||
<select id="f-testnet">
|
||
<option value="false">Hayır (Gerçek)</option>
|
||
<option value="true">Evet (Testnet)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-actions"><button class="btn btn-primary" onclick="createBot()">Bot Ekle</button></div>
|
||
</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;
|
||
|
||
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() {
|
||
loadBots();
|
||
loadPositions();
|
||
loadClosed();
|
||
loadSymbols();
|
||
}
|
||
|
||
async function loadSymbols() {
|
||
const res = await api('GET', '/symbols');
|
||
const symbols = await res.json();
|
||
|
||
symbolSelect = new TomSelect('#f-symbol', {
|
||
valueField: 'symbol',
|
||
labelField: 'symbol',
|
||
searchField: 'symbol',
|
||
options: symbols,
|
||
placeholder: 'Ara: DOGE, BTC...',
|
||
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 = symbols.find(s => s.symbol === value);
|
||
if (!item) return;
|
||
document.getElementById('f-usdt').value = item.min_notional;
|
||
document.getElementById('f-min-label').textContent = `(min ${item.min_notional} USDT)`;
|
||
},
|
||
});
|
||
}
|
||
|
||
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 = document.getElementById('f-symbol').value.trim().toUpperCase();
|
||
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 testnet = document.getElementById('f-testnet').value === 'true';
|
||
if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
|
||
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent, testnet });
|
||
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', {
|
||
headers: { 'Authorization': 'Bearer ' + AUTH_TOKEN }
|
||
});
|
||
// EventSource doesn't support custom headers natively; use URL param workaround
|
||
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>
|