feat(mse): shared bot modal component + pagination on positions/bots
- 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>
This commit is contained in:
parent
d1085cc4cc
commit
2ff8d57c08
7 changed files with 290 additions and 294 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -1061,6 +1061,12 @@ version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memo-map"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|
@ -1077,6 +1083,16 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minijinja"
|
||||||
|
version = "2.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d"
|
||||||
|
dependencies = [
|
||||||
|
"memo-map",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -1105,6 +1121,7 @@ dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"log",
|
"log",
|
||||||
|
"minijinja",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,4 @@ axum-extra = { version = "0.9", features = ["cookie"] }
|
||||||
|
|
||||||
# Async stream generator
|
# Async stream generator
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
|
minijinja = "2.19.0"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ static ENV: OnceLock<Environment<'static>> = OnceLock::new();
|
||||||
fn env() -> &'static Environment<'static> {
|
fn env() -> &'static Environment<'static> {
|
||||||
ENV.get_or_init(|| {
|
ENV.get_or_init(|| {
|
||||||
let mut e = Environment::new();
|
let mut e = Environment::new();
|
||||||
e.add_template_owned("_header.html", include_str!("web/_header.html").to_string()).unwrap();
|
e.add_template_owned("_header.html", include_str!("web/_header.html").to_string()).unwrap();
|
||||||
|
e.add_template_owned("_bot_modal.html", include_str!("web/_bot_modal.html").to_string()).unwrap();
|
||||||
e.add_template_owned("index.html", include_str!("web/index.html").to_string()).unwrap();
|
e.add_template_owned("index.html", include_str!("web/index.html").to_string()).unwrap();
|
||||||
e.add_template_owned("bots.html", include_str!("web/bots.html").to_string()).unwrap();
|
e.add_template_owned("bots.html", include_str!("web/bots.html").to_string()).unwrap();
|
||||||
e.add_template_owned("bot.html", include_str!("web/bot.html").to_string()).unwrap();
|
e.add_template_owned("bot.html", include_str!("web/bot.html").to_string()).unwrap();
|
||||||
|
|
|
||||||
127
src/web/_bot_modal.html
Normal file
127
src/web/_bot_modal.html
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<div id="modal-overlay" onclick="if(event.target===this)closeModal()">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="modal-title">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" type="text" placeholder="DOGE Bot" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Coin (USDT çifti)</label>
|
||||||
|
<select id="f-symbol"><option value="">Ara veya seç...</option></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-label" 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>
|
||||||
|
(function() {
|
||||||
|
let _symbolSelect = null;
|
||||||
|
let _symbolMinMap = {};
|
||||||
|
let _symbolsLoaded = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadSymbols() {
|
||||||
|
if (_symbolsLoaded) return;
|
||||||
|
_symbolsLoaded = true;
|
||||||
|
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="">Ara veya seç...</option>' +
|
||||||
|
symbols.map(s => `<option value="${s.symbol}">${s.symbol}</option>`).join('');
|
||||||
|
|
||||||
|
_symbolSelect = new TomSelect('#f-symbol', {
|
||||||
|
maxOptions: 2000,
|
||||||
|
searchField: ['text'],
|
||||||
|
render: {
|
||||||
|
option: d => `<div style="display:flex;justify-content:space-between;align-items:center"><strong>${d.text}</strong><span style="color:#888;font-size:11px;margin-left:8px">min ${_symbolMinMap[d.value] || '—'} USDT</span></div>`,
|
||||||
|
item: d => `<div>${d.text}</div>`,
|
||||||
|
},
|
||||||
|
onChange: v => {
|
||||||
|
const min = _symbolMinMap[v];
|
||||||
|
document.getElementById('f-min-label').textContent = min ? `(min: ${min} USDT)` : '';
|
||||||
|
const inp = document.getElementById('f-usdt');
|
||||||
|
if (min) { inp.min = min; if (!inp.value || parseFloat(inp.value) < min) inp.value = min; }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.openModal = async function() {
|
||||||
|
const modeRes = await _api('GET', '/mode');
|
||||||
|
const { mode } = await modeRes.json();
|
||||||
|
const label = mode === 'testnet' ? 'Testnet' : 'Canlı';
|
||||||
|
document.getElementById('modal-title').textContent = `Yeni Bot Ekle — ${label}`;
|
||||||
|
document.getElementById('modal-overlay').classList.add('open');
|
||||||
|
await _loadSymbols();
|
||||||
|
if (_symbolSelect) _symbolSelect.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeModal = function() {
|
||||||
|
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-label').textContent = '';
|
||||||
|
if (_symbolSelect) _symbolSelect.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.createBot = async function() {
|
||||||
|
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`); document.getElementById('f-usdt').focus(); return; }
|
||||||
|
const res = await _api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
|
||||||
|
if (!res.ok) { alert('Bot oluşturulamadı: ' + await res.text()); return; }
|
||||||
|
closeModal();
|
||||||
|
if (typeof onBotCreated === 'function') onBotCreated();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
@ -114,6 +114,11 @@
|
||||||
.ts-dropdown .option { padding: 7px 12px; color: var(--text) !important; }
|
.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-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; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -136,58 +141,9 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Yeni Bot Modal -->
|
{% include "_bot_modal.html" %}
|
||||||
<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>
|
<script>
|
||||||
let symbolSelect = null;
|
|
||||||
let symbolMinMap = {};
|
|
||||||
let currentMode = 'live';
|
|
||||||
|
|
||||||
async function api(method, path, body) {
|
async function api(method, path, body) {
|
||||||
const res = await fetch('/api' + path, {
|
const res = await fetch('/api' + path, {
|
||||||
method, credentials: 'include',
|
method, credentials: 'include',
|
||||||
|
|
@ -199,21 +155,8 @@ async function api(method, path, body) {
|
||||||
}
|
}
|
||||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
|
|
||||||
// ── Mode ──
|
function onBotCreated() { loadBots(); }
|
||||||
async function loadMode() {
|
function onModeChange() { loadBots(); }
|
||||||
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 ──
|
// ── Bots ──
|
||||||
async function loadBots() {
|
async function loadBots() {
|
||||||
|
|
@ -283,60 +226,11 @@ async function deleteBot(id, name) {
|
||||||
loadBots();
|
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() {
|
async function logout() {
|
||||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMode();
|
|
||||||
loadBots();
|
loadBots();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -94,55 +94,19 @@
|
||||||
.ts-dropdown .option.active { background: rgba(108,99,255,0.2) !important; color: var(--text) !important; }
|
.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-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; }
|
.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; }
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination { display: flex; align-items: center; justify-content: center; gap: 4px; padding: 8px 0; border-top: 1px solid var(--border); }
|
||||||
|
.pg-btn { background: none; border: 1px solid var(--border); color: var(--muted); border-radius: 3px; padding: 2px 9px; font-size: 11px; cursor: pointer; }
|
||||||
|
.pg-btn:hover { color: var(--text); border-color: var(--accent); }
|
||||||
|
.pg-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
|
.pg-info { font-size: 11px; color: var(--muted); margin-left: 6px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<div id="modal-overlay" onclick="if(event.target===this)closeModal()">
|
{% include "_bot_modal.html" %}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% include "_header.html" %}
|
{% include "_header.html" %}
|
||||||
|
|
||||||
|
|
@ -165,6 +129,7 @@
|
||||||
<thead><tr><th>Bot</th><th>Sembol</th><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Zaman</th></tr></thead>
|
<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>
|
<tbody id="positions-body"><tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="pagination" id="positions-pagination"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -173,14 +138,15 @@
|
||||||
<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>
|
<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>
|
<tbody id="closed-body"><tr><td colspan="7" class="empty">Henüz işlem yok</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="pagination" id="closed-pagination"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let sseSource = null;
|
let sseSource = null;
|
||||||
let symbolSelect = null;
|
const PAGE_SIZE = 20;
|
||||||
let allSymbols = [];
|
let positionsCache = [], posPage = 0;
|
||||||
let currentMode = 'testnet';
|
let closedCache = [], closedPage = 0;
|
||||||
|
|
||||||
async function api(method, path, body) {
|
async function api(method, path, body) {
|
||||||
const res = await fetch('/api' + path, {
|
const res = await fetch('/api' + path, {
|
||||||
|
|
@ -199,83 +165,15 @@ async function logout() {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onBotCreated() { loadBots(); }
|
||||||
|
function onModeChange() { loadBots(); loadPositions(); loadClosed(); }
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
await loadMode();
|
|
||||||
loadBots();
|
loadBots();
|
||||||
loadPositions();
|
loadPositions();
|
||||||
loadClosed();
|
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;
|
|
||||||
const label = mode === 'live' ? 'canlı' : 'testnet';
|
|
||||||
if (!confirm(`${label.charAt(0).toUpperCase() + label.slice(1)} 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() {
|
async function loadBots() {
|
||||||
const res = await api('GET', '/bots');
|
const res = await api('GET', '/bots');
|
||||||
const bots = await res.json();
|
const bots = await res.json();
|
||||||
|
|
@ -303,57 +201,74 @@ async function loadBots() {
|
||||||
|
|
||||||
async function loadPositions() {
|
async function loadPositions() {
|
||||||
const res = await api('GET', '/positions');
|
const res = await api('GET', '/positions');
|
||||||
const positions = await res.json();
|
positionsCache = await res.json();
|
||||||
|
posPage = 0;
|
||||||
|
renderPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPositions() {
|
||||||
const tbody = document.getElementById('positions-body');
|
const tbody = document.getElementById('positions-body');
|
||||||
if (!positions.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr>'; return; }
|
if (!positionsCache.length) {
|
||||||
tbody.innerHTML = positions.map(p => `
|
tbody.innerHTML = '<tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr>';
|
||||||
<tr>
|
document.getElementById('positions-pagination').innerHTML = '';
|
||||||
<td>${esc(p.bot_name)}</td>
|
return;
|
||||||
<td><strong>${esc(p.symbol)}</strong></td>
|
}
|
||||||
<td>${p.buy_price.toFixed(6)}</td>
|
const page = positionsCache.slice(posPage * PAGE_SIZE, (posPage + 1) * PAGE_SIZE);
|
||||||
<td>${p.sell_target.toFixed(6)}</td>
|
tbody.innerHTML = page.map(p => `<tr>
|
||||||
<td>${p.quantity}</td>
|
<td>${esc(p.bot_name)}</td>
|
||||||
<td>${new Date(p.opened_at).toLocaleString('tr-TR')}</td>
|
<td><strong>${esc(p.symbol)}</strong></td>
|
||||||
</tr>`).join('');
|
<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('');
|
||||||
|
renderPagination('positions-pagination', positionsCache.length, posPage, 'goPosPage');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadClosed() {
|
async function loadClosed() {
|
||||||
const res = await api('GET', '/positions/closed');
|
const res = await api('GET', '/positions/closed');
|
||||||
const closed = await res.json();
|
closedCache = await res.json();
|
||||||
const tbody = document.getElementById('closed-body');
|
closedPage = 0;
|
||||||
if (!closed.length) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>'; return; }
|
renderClosed();
|
||||||
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() {
|
function renderClosed() {
|
||||||
const name = document.getElementById('f-name').value.trim();
|
const tbody = document.getElementById('closed-body');
|
||||||
const symbol = symbolSelect ? symbolSelect.getValue() : '';
|
if (!closedCache.length) {
|
||||||
const timeframe = document.getElementById('f-timeframe').value;
|
tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>';
|
||||||
const usdt_amount = parseFloat(document.getElementById('f-usdt').value);
|
document.getElementById('closed-pagination').innerHTML = '';
|
||||||
const profit_percent = parseFloat(document.getElementById('f-profit').value);
|
return;
|
||||||
const usdtInput = document.getElementById('f-usdt');
|
}
|
||||||
const minNotional = parseFloat(usdtInput.min) || 0;
|
const page = closedCache.slice(closedPage * PAGE_SIZE, (closedPage + 1) * PAGE_SIZE);
|
||||||
if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
|
tbody.innerHTML = page.map(c => `<tr>
|
||||||
if (usdt_amount < minNotional) { alert(`Minimum işlem miktarı ${minNotional} USDT`); usdtInput.focus(); return; }
|
<td>${esc(c.bot_name)}</td>
|
||||||
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
|
<td><strong>${esc(c.symbol)}</strong></td>
|
||||||
closeModal();
|
<td>${c.buy_price.toFixed(6)}</td>
|
||||||
document.getElementById('f-name').value = '';
|
<td>${c.sell_target.toFixed(6)}</td>
|
||||||
if (symbolSelect) symbolSelect.clear();
|
<td>${c.quantity}</td>
|
||||||
document.getElementById('f-usdt').value = '';
|
<td><span class="badge ${c.status==='FILLED'?'badge-running':'badge-stopped'}">${c.status}</span></td>
|
||||||
document.getElementById('f-profit').value = '';
|
<td>${new Date(c.closed_at).toLocaleString('tr-TR')}</td>
|
||||||
document.getElementById('f-min-label').textContent = '';
|
</tr>`).join('');
|
||||||
loadBots();
|
renderPagination('closed-pagination', closedCache.length, closedPage, 'goClosedPage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPagination(containerId, total, currentPage, fnName) {
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||||
|
if (totalPages <= 1) { el.innerHTML = ''; return; }
|
||||||
|
const lo = Math.max(0, currentPage - 2), hi = Math.min(totalPages - 1, currentPage + 2);
|
||||||
|
let html = '';
|
||||||
|
if (currentPage > 0) html += `<button class="pg-btn" onclick="${fnName}(${currentPage-1})">‹</button>`;
|
||||||
|
for (let i = lo; i <= hi; i++)
|
||||||
|
html += `<button class="pg-btn${i===currentPage?' active':''}" onclick="${fnName}(${i})">${i+1}</button>`;
|
||||||
|
if (currentPage < totalPages - 1) html += `<button class="pg-btn" onclick="${fnName}(${currentPage+1})">›</button>`;
|
||||||
|
const start = currentPage * PAGE_SIZE;
|
||||||
|
html += `<span class="pg-info">${start+1}–${Math.min(start+PAGE_SIZE,total)} / ${total}</span>`;
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
function goPosPage(p) { posPage = p; renderPositions(); }
|
||||||
|
function goClosedPage(p) { closedPage = p; renderClosed(); }
|
||||||
|
|
||||||
async function startBot(id) {
|
async function startBot(id) {
|
||||||
await api('POST', `/bots/${id}/start`);
|
await api('POST', `/bots/${id}/start`);
|
||||||
loadBots();
|
loadBots();
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@
|
||||||
.badge-new { background: rgba(108,99,255,.15); color: var(--accent); }
|
.badge-new { background: rgba(108,99,255,.15); color: var(--accent); }
|
||||||
.badge-canceled { background: rgba(136,136,136,.1); color: var(--muted); }
|
.badge-canceled { background: rgba(136,136,136,.1); color: var(--muted); }
|
||||||
.empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
.empty { padding: 24px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||||
|
.pagination { display: flex; align-items: center; gap: 4px; padding: 12px 18px; 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -55,6 +60,7 @@
|
||||||
<thead><tr><th>Bot</th><th>Sembol</th><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Zaman</th></tr></thead>
|
<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>
|
<tbody id="open-body"><tr><td colspan="6" class="empty">Yükleniyor...</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="pagination" id="open-pagination"></div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<div class="section-header">Kapalı İşlemler</div>
|
<div class="section-header">Kapalı İşlemler</div>
|
||||||
|
|
@ -62,10 +68,15 @@
|
||||||
<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>
|
<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>
|
<tbody id="closed-body"><tr><td colspan="7" class="empty">Yükleniyor...</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="pagination" id="closed-pagination"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
let openCache = [], closedCache = [];
|
||||||
|
let openPage = 1, closedPage = 1;
|
||||||
|
|
||||||
async function api(method, path) {
|
async function api(method, path) {
|
||||||
const res = await fetch('/api' + path, { method, credentials: 'include' });
|
const res = await fetch('/api' + path, { method, credentials: 'include' });
|
||||||
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
|
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
|
||||||
|
|
@ -74,21 +85,33 @@ async function api(method, path) {
|
||||||
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
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'); }
|
function ts(ms) { return new Date(ms).toLocaleString('tr-TR'); }
|
||||||
|
|
||||||
async function loadMode() {
|
function onModeChange() { loadOpen(); loadClosed(); }
|
||||||
const res = await api('GET', '/mode');
|
|
||||||
if (!res.ok) return;
|
function renderPagination(containerId, total, current, fnName) {
|
||||||
const { mode } = await res.json();
|
const el = document.getElementById(containerId);
|
||||||
document.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
|
if (total <= PAGE_SIZE) { el.innerHTML = ''; return; }
|
||||||
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
|
const pages = Math.ceil(total / PAGE_SIZE);
|
||||||
|
const from = (current - 1) * PAGE_SIZE + 1;
|
||||||
|
const to = Math.min(current * PAGE_SIZE, total);
|
||||||
|
let html = '';
|
||||||
|
if (current > 1) html += `<button class="pg-btn" onclick="${fnName}(${current - 1})">‹</button>`;
|
||||||
|
for (let i = 1; i <= pages; i++) {
|
||||||
|
if (pages > 7 && Math.abs(i - current) > 2 && i !== 1 && i !== pages) {
|
||||||
|
if (i === current - 3 || i === current + 3) html += `<span class="pg-info">…</span>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
html += `<button class="pg-btn${i === current ? ' active' : ''}" onclick="${fnName}(${i})">${i}</button>`;
|
||||||
|
}
|
||||||
|
if (current < pages) html += `<button class="pg-btn" onclick="${fnName}(${current + 1})">›</button>`;
|
||||||
|
html += `<span class="pg-info">${from}–${to} / ${total}</span>`;
|
||||||
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOpen() {
|
function renderOpen() {
|
||||||
const res = await api('GET', '/positions');
|
|
||||||
const tbody = document.getElementById('open-body');
|
const tbody = document.getElementById('open-body');
|
||||||
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Yüklenemedi</td></tr>'; return; }
|
if (!openCache.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr>'; document.getElementById('open-pagination').innerHTML = ''; return; }
|
||||||
const data = await res.json();
|
const page = openCache.slice((openPage - 1) * PAGE_SIZE, openPage * PAGE_SIZE);
|
||||||
if (!data.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">Açık pozisyon yok</td></tr>'; return; }
|
tbody.innerHTML = page.map(p => `<tr>
|
||||||
tbody.innerHTML = data.map(p => `<tr>
|
|
||||||
<td>${p.bot_name}</td>
|
<td>${p.bot_name}</td>
|
||||||
<td>${p.symbol}</td>
|
<td>${p.symbol}</td>
|
||||||
<td>${fmt(p.buy_price, 4)}</td>
|
<td>${fmt(p.buy_price, 4)}</td>
|
||||||
|
|
@ -96,15 +119,14 @@ async function loadOpen() {
|
||||||
<td>${fmt(p.quantity, 4)}</td>
|
<td>${fmt(p.quantity, 4)}</td>
|
||||||
<td>${ts(p.opened_at)}</td>
|
<td>${ts(p.opened_at)}</td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
|
renderPagination('open-pagination', openCache.length, openPage, 'goOpenPage');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadClosed() {
|
function renderClosed() {
|
||||||
const res = await api('GET', '/positions/closed');
|
|
||||||
const tbody = document.getElementById('closed-body');
|
const tbody = document.getElementById('closed-body');
|
||||||
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Yüklenemedi</td></tr>'; return; }
|
if (!closedCache.length) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>'; document.getElementById('closed-pagination').innerHTML = ''; return; }
|
||||||
const data = await res.json();
|
const page = closedCache.slice((closedPage - 1) * PAGE_SIZE, closedPage * PAGE_SIZE);
|
||||||
if (!data.length) { tbody.innerHTML = '<tr><td colspan="7" class="empty">Henüz işlem yok</td></tr>'; return; }
|
tbody.innerHTML = page.map(p => `<tr>
|
||||||
tbody.innerHTML = data.map(p => `<tr>
|
|
||||||
<td>${p.bot_name}</td>
|
<td>${p.bot_name}</td>
|
||||||
<td>${p.symbol}</td>
|
<td>${p.symbol}</td>
|
||||||
<td>${fmt(p.buy_price, 4)}</td>
|
<td>${fmt(p.buy_price, 4)}</td>
|
||||||
|
|
@ -113,6 +135,26 @@ async function loadClosed() {
|
||||||
<td><span class="badge badge-${p.status.toLowerCase()}">${p.status}</span></td>
|
<td><span class="badge badge-${p.status.toLowerCase()}">${p.status}</span></td>
|
||||||
<td>${ts(p.closed_at)}</td>
|
<td>${ts(p.closed_at)}</td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
|
renderPagination('closed-pagination', closedCache.length, closedPage, 'goClosedPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
function goOpenPage(p) { openPage = p; renderOpen(); }
|
||||||
|
function goClosedPage(p) { closedPage = p; renderClosed(); }
|
||||||
|
|
||||||
|
async function loadOpen() {
|
||||||
|
const res = await api('GET', '/positions');
|
||||||
|
if (!res.ok) { document.getElementById('open-body').innerHTML = '<tr><td colspan="6" class="empty">Yüklenemedi</td></tr>'; return; }
|
||||||
|
openCache = await res.json();
|
||||||
|
openPage = 1;
|
||||||
|
renderOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadClosed() {
|
||||||
|
const res = await api('GET', '/positions/closed');
|
||||||
|
if (!res.ok) { document.getElementById('closed-body').innerHTML = '<tr><td colspan="7" class="empty">Yüklenemedi</td></tr>'; return; }
|
||||||
|
closedCache = await res.json();
|
||||||
|
closedPage = 1;
|
||||||
|
renderClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
|
|
@ -120,7 +162,6 @@ async function logout() {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMode();
|
|
||||||
loadOpen();
|
loadOpen();
|
||||||
loadClosed();
|
loadClosed();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue