feat: живая цена SOL по WebSocket + деплой в k3s (Dockerfile/helm)
Фронт подключается к wss://<host>/api/ws и выводит цену SOL, которую шлюз транслирует из NATS. Автопереподключение, статус соединения, счётчик сообщений. Добавлены Dockerfile/nginx.conf/helm для деплоя.
This commit is contained in:
+147
@@ -23,6 +23,22 @@
|
||||
<body>
|
||||
<h1>Binary Rocket :: техтест через HTTP-шлюз (фантики, без Phantom)</h1>
|
||||
|
||||
<fieldset>
|
||||
<legend>Цена SOL (WebSocket)</legend>
|
||||
<div class="row">
|
||||
<span style="font-size:22px;font-weight:bold">SOL: <span id="solPrice">—</span></span>
|
||||
<span class="hint" id="solPriceTs"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>Статус: <span id="wsStatus" class="hint">отключено</span></span>
|
||||
<span class="hint">сообщений: <span id="wsMsgCount">0</span></span>
|
||||
<button id="wsToggle" onclick="wsToggle()">Подключить</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>WS URL</label><input id="wsUrl" size="46" placeholder="(пусто — вычислить из gwBase)"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Конфигурация шлюза</legend>
|
||||
<div class="row">
|
||||
@@ -199,6 +215,135 @@ async function gwClose(){
|
||||
|
||||
function copyLog(){ const t=document.getElementById('log').innerText; navigator.clipboard?.writeText(t).then(()=>log.ok('лог скопирован')).catch(()=>{}); }
|
||||
|
||||
// ---------- WebSocket: живая цена SOL ----------
|
||||
let ws = null;
|
||||
let wsReconnectTimer = null;
|
||||
let wsBackoffMs = 3000;
|
||||
const WS_BACKOFF_MAX = 15000;
|
||||
let wsIntentClosed = false;
|
||||
let wsMsgCount = 0;
|
||||
let solPriceTsMs = 0;
|
||||
|
||||
function setWsStatus(text, cls){
|
||||
const e = document.getElementById('wsStatus');
|
||||
if(!e) return;
|
||||
e.textContent = text;
|
||||
e.className = cls || 'hint';
|
||||
}
|
||||
|
||||
function syncWsToggleButton(){
|
||||
const btn = document.getElementById('wsToggle');
|
||||
if(!btn) return;
|
||||
const isUp = ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING);
|
||||
btn.textContent = isUp ? 'Отключить' : 'Подключить';
|
||||
}
|
||||
|
||||
function formatSolPrice(p){
|
||||
const s = String(p);
|
||||
const dot = s.indexOf('.');
|
||||
if(dot === -1) return s;
|
||||
const intPart = s.slice(0, dot);
|
||||
const frac = s.slice(dot + 1);
|
||||
if(frac.length > 2) return (intPart || '0') + '.' + frac.slice(0, 2);
|
||||
return s;
|
||||
}
|
||||
|
||||
function updateSolPriceMeta(){
|
||||
const e = document.getElementById('solPriceTs');
|
||||
if(!e) return;
|
||||
if(!solPriceTsMs){ e.textContent = ''; return; }
|
||||
const sec = Math.max(0, Math.floor((Date.now() - solPriceTsMs) / 1000));
|
||||
const t = new Date(solPriceTsMs).toLocaleTimeString();
|
||||
e.textContent = ' ('+t+', '+sec+'с назад)';
|
||||
}
|
||||
|
||||
function wsUrl(){
|
||||
const v = document.getElementById('wsUrl').value.trim();
|
||||
if(v) return v;
|
||||
const base = gwBase();
|
||||
let u;
|
||||
if(base.startsWith('https://')) u = 'wss://' + base.slice('https://'.length);
|
||||
else if(base.startsWith('http://')) u = 'ws://' + base.slice('http://'.length);
|
||||
else u = base;
|
||||
return u.replace(/\/+$/, '') + '/ws';
|
||||
}
|
||||
|
||||
function connectWs(){
|
||||
if(ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
||||
wsIntentClosed = false;
|
||||
const url = wsUrl();
|
||||
document.getElementById('wsUrl').placeholder = url;
|
||||
setWsStatus('подключение…', 'hint');
|
||||
try{
|
||||
ws = new WebSocket(url);
|
||||
}catch(e){
|
||||
log.err('ws: не удалось открыть '+url+' — '+e.message);
|
||||
setWsStatus('ошибка', 'err');
|
||||
scheduleWsReconnect();
|
||||
syncWsToggleButton();
|
||||
return;
|
||||
}
|
||||
ws.onopen = () => {
|
||||
wsBackoffMs = 3000;
|
||||
setWsStatus('подключено', 'ok');
|
||||
syncWsToggleButton();
|
||||
log.ok('ws: connected '+url);
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
let m;
|
||||
try{ m = JSON.parse(ev.data); }catch(e){ return; }
|
||||
if(!m || m.type !== 'price') return;
|
||||
wsMsgCount++;
|
||||
document.getElementById('wsMsgCount').textContent = String(wsMsgCount);
|
||||
const fullPrice = String(m.price);
|
||||
const short = formatSolPrice(fullPrice);
|
||||
const sp = document.getElementById('solPrice');
|
||||
sp.textContent = short;
|
||||
sp.title = fullPrice;
|
||||
solPriceTsMs = Number(m.ts) || Date.now();
|
||||
updateSolPriceMeta();
|
||||
if(wsMsgCount === 1 || wsMsgCount % 50 === 0){
|
||||
log.info('ws: price='+fullPrice+' (symbol='+m.symbol+')');
|
||||
}
|
||||
};
|
||||
ws.onerror = () => {
|
||||
setWsStatus('ошибка', 'err');
|
||||
};
|
||||
ws.onclose = () => {
|
||||
ws = null;
|
||||
syncWsToggleButton();
|
||||
if(wsIntentClosed){
|
||||
setWsStatus('отключено', 'hint');
|
||||
return;
|
||||
}
|
||||
setWsStatus('переподключение…', 'hint');
|
||||
scheduleWsReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleWsReconnect(){
|
||||
if(wsReconnectTimer) return;
|
||||
const delay = wsBackoffMs;
|
||||
wsReconnectTimer = setTimeout(() => {
|
||||
wsReconnectTimer = null;
|
||||
connectWs();
|
||||
}, delay);
|
||||
wsBackoffMs = Math.min(WS_BACKOFF_MAX, wsBackoffMs * 2);
|
||||
}
|
||||
|
||||
function disconnectWs(){
|
||||
wsIntentClosed = true;
|
||||
if(wsReconnectTimer){ clearTimeout(wsReconnectTimer); wsReconnectTimer = null; }
|
||||
if(ws){
|
||||
try{ ws.close(); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function wsToggle(){
|
||||
const isUp = ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING);
|
||||
if(isUp) disconnectWs(); else connectWs();
|
||||
}
|
||||
|
||||
(function init(){
|
||||
const el = document.getElementById('gwBase');
|
||||
if(!el.value.trim()){
|
||||
@@ -207,6 +352,8 @@ function copyLog(){ const t=document.getElementById('log').innerText; navigator.
|
||||
}
|
||||
gwState();
|
||||
gwBalance();
|
||||
connectWs();
|
||||
setInterval(updateSolPriceMeta, 1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user