PostgreSQL + async refactoring, WebSocket server, Redis caching, minified assets

This commit is contained in:
2026-07-25 13:31:40 +00:00
parent 47c7f17c90
commit 17da0b6d79
28 changed files with 1840 additions and 3244 deletions
+1
View File
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
async function logout(){try{await fetch('/web/api/auth/logout',{method:'POST'});}catch(e){}
window.location.href='/';}
async function refreshBalance(){try{const res=await fetch('/web/api/user/balance');const data=await res.json();if(data.success){const el=document.getElementById('siteBalanceDisplay');if(el){el.innerHTML=Math.floor(data.balance).toLocaleString('ru')+' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';}}}catch(e){}}
function getRarityColor(rarity){const colors={'Consumer Grade':'#b0b0b0','Industrial Grade':'#5e98d9','Mil-Spec':'#4b69ff','Mil-Spec Grade':'#4b69ff','Restricted':'#8847ff','Classified':'#d32ce6','Covert':'#eb4b4b','Rare Special Item':'#ffd700','Extraordinary':'#ffd700',};return colors[rarity]||'#b0b0b0';}
function formatNumber(n){return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,' ');}
function debounce(fn,ms=300){let timer;return function(...args){clearTimeout(timer);timer=setTimeout(()=>fn.apply(this,args),ms);};}
function escapeHtml(str){const div=document.createElement('div');div.textContent=str;return div.innerHTML;}
function showNotification(message,type='info',duration=3000){const existing=document.querySelector('.notification-toast');if(existing)existing.remove();const colors={success:'#22c55e',error:'#ef4444',info:'#3b82f6',warning:'#f59e0b',};const toast=document.createElement('div');toast.className='notification-toast';toast.style.cssText=`position:fixed;top:20px;right:20px;z-index:99999;background:#1a1a2e;border:1px solid ${colors[type]||colors.info};color:white;padding:12px 20px;border-radius:8px;font-size:14px;box-shadow:0 10px 40px rgba(0,0,0,0.5);animation:slideInRight 0.3s ease;max-width:400px;`;toast.textContent=message;document.body.appendChild(toast);setTimeout(()=>{toast.style.animation='slideOutRight 0.3s ease forwards';setTimeout(()=>toast.remove(),300);},duration);}
if(!document.getElementById('notificationStyles')){const style=document.createElement('style');style.id='notificationStyles';style.textContent=`@keyframes slideInRight{from{transform:translateX(120%);opacity:0;}
to{transform:translateX(0);opacity:1;}}@keyframes slideOutRight{from{transform:translateX(0);opacity:1;}
to{transform:translateX(120%);opacity:0;}}`;document.head.appendChild(style);}
function showAchievementPopup(title,reward){let popup=document.getElementById('achievementGlobalPopup');if(!popup){popup=document.createElement('div');popup.id='achievementGlobalPopup';popup.style.cssText=`position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:99999;background:linear-gradient(135deg,#1a3a1a,#0d260d);border:1px solid#22c55e;border-radius:12px;padding:1rem 1.5rem;box-shadow:0 10px 40px rgba(0,0,0,0.5);animation:slideInDown 0.5s ease;max-width:400px;width:90%;text-align:center;`;popup.innerHTML=`<div style="font-size:0.85rem;color:#94a3b8;margin-bottom:0.25rem;">🏆 Новое достижение!</div><div style="font-size:1.1rem;font-weight:600;"id="globalPopupTitle"></div><div style="color:#22c55e;font-size:0.9rem;margin-top:0.25rem;"id="globalPopupReward"></div>`;document.body.appendChild(popup);if(!document.getElementById('slideInDownStyle')){const s=document.createElement('style');s.id='slideInDownStyle';s.textContent=`@keyframes slideInDown{from{transform:translateX(-50%)translateY(-120%);opacity:0;}
to{transform:translateX(-50%)translateY(0);opacity:1;}}`;document.head.appendChild(s);}}
document.getElementById('globalPopupTitle').textContent=title;document.getElementById('globalPopupReward').textContent='';popup.style.display='block';if(window.SoundManager)SoundManager.achievement();setTimeout(()=>{popup.style.animation='slideInDown 0.5s ease reverse forwards';setTimeout(()=>{popup.style.display='none';popup.style.animation='slideInDown 0.5s ease';},500);},4000);}
+19
View File
@@ -0,0 +1,19 @@
(function(){'use strict';let toastId=0;const container=document.createElement('div');container.className='toast-container';container.id='toastContainer';document.body.appendChild(container);function escapeHtml(text){const d=document.createElement('div');d.textContent=text||'';return d.innerHTML;}
window.Notify={toast:function(message,type,title){if(!message)return;type=type||'info';const icons={success:'✓',error:'✕',info:'',warning:'⚠'};const titles={success:title||'Успешно',error:title||'Ошибка',info:title||'Информация',warning:title||'Внимание'};const icon=icons[type]||'';const t=titles[type];const id=++toastId;const el=document.createElement('div');el.className='toast toast-'+type;el.id='toast-'+id;el.innerHTML='<div class="toast-icon">'+icon+'</div>'+
'<div class="toast-content">'+
'<div class="toast-title">'+escapeHtml(t)+'</div>'+
'<div class="toast-message">'+escapeHtml(message)+'</div>'+
'</div>'+
'<button class="toast-close" onclick="Notify.dismiss('+id+')">✕</button>';container.appendChild(el);const timeout=setTimeout(function(){Notify.dismiss(id);},4000);el._timeout=timeout;el._id=id;return id;},success:function(message){return this.toast(message,'success');},error:function(message){return this.toast(message,'error');},info:function(message){return this.toast(message,'info');},warning:function(message){return this.toast(message,'warning');},dismiss:function(id){var el=document.getElementById('toast-'+id);if(!el)return;if(el._timeout)clearTimeout(el._timeout);if(el.classList.contains('toast-removing'))return;el.classList.add('toast-removing');setTimeout(function(){if(el.parentNode)el.parentNode.removeChild(el);},250);},confirm:function(message,title,callback){if(typeof title==='function'){callback=title;title='Подтверждение';}
title=title||'Подтверждение';var overlay=document.createElement('div');overlay.className='confirm-overlay';overlay.innerHTML='<div class="confirm-dialog">'+
'<div class="confirm-dialog-header">'+
'<div class="confirm-dialog-icon confirm-dialog-icon-warning">⚠</div>'+
'<div class="confirm-dialog-title">'+escapeHtml(title)+'</div>'+
'</div>'+
'<div class="confirm-dialog-message">'+escapeHtml(message)+'</div>'+
'<div class="confirm-dialog-actions">'+
'<button class="btn btn-outline confirm-cancel">Отмена</button>'+
'<button class="btn btn-danger confirm-ok">Подтвердить</button>'+
'</div>'+
'</div>';document.body.appendChild(overlay);var result=false;function close(){if(overlay.parentNode)overlay.parentNode.removeChild(overlay);if(callback)callback(result);}
overlay.querySelector('.confirm-cancel').addEventListener('click',function(){result=false;close();});overlay.querySelector('.confirm-ok').addEventListener('click',function(){result=true;close();});overlay.addEventListener('click',function(e){if(e.target===overlay){result=false;close();}});document.addEventListener('keydown',function handler(e){if(e.key==='Escape'){result=false;close();document.removeEventListener('keydown',handler);}});return overlay;}};})();
+4
View File
@@ -0,0 +1,4 @@
const SoundManager={_ctx:null,_enabled:true,_volume:0.3,_activeNodes:new Set(),_timeouts:[],init(){try{this._ctx=new(window.AudioContext||window.webkitAudioContext)();}catch(e){console.warn('Web Audio API not supported');this._enabled=false;}
const saved=localStorage.getItem('sound_enabled');if(saved!==null)this._enabled=saved==='true';},setEnabled(on){this._enabled=on;localStorage.setItem('sound_enabled',on);},toggle(){this.setEnabled(!this._enabled);return this._enabled;},stopAll(){this._timeouts.forEach(clearTimeout);this._timeouts=[];this._activeNodes.forEach(node=>{try{node.stop();}catch(e){}
try{node.disconnect();}catch(e){}});this._activeNodes.clear();},_trackNode(node,duration){this._activeNodes.add(node);node.addEventListener('ended',()=>this._activeNodes.delete(node));setTimeout(()=>{this._activeNodes.delete(node);try{node.disconnect();}catch(e){}},(duration+0.1)*1000);},_play(freq,duration,type='sine',volume=1){if(!this._enabled||!this._ctx)return;if(this._ctx.state==='suspended')this._ctx.resume();const osc=this._ctx.createOscillator();const gain=this._ctx.createGain();osc.type=type;osc.frequency.setValueAtTime(freq,this._ctx.currentTime);gain.gain.setValueAtTime(this._volume*volume,this._ctx.currentTime);gain.gain.exponentialRampToValueAtTime(0.001,this._ctx.currentTime+duration);osc.connect(gain);gain.connect(this._ctx.destination);osc.start();osc.stop(this._ctx.currentTime+duration);this._trackNode(osc,duration);this._trackNode(gain,duration);},_noise(duration,volume=1){if(!this._enabled||!this._ctx)return;if(this._ctx.state==='suspended')this._ctx.resume();const bufferSize=this._ctx.sampleRate*duration;const buffer=this._ctx.createBuffer(1,bufferSize,this._ctx.sampleRate);const data=buffer.getChannelData(0);for(let i=0;i<bufferSize;i++){data[i]=Math.random()*2-1;}
const source=this._ctx.createBufferSource();source.buffer=buffer;const gain=this._ctx.createGain();gain.gain.setValueAtTime(this._volume*volume,this._ctx.currentTime);gain.gain.exponentialRampToValueAtTime(0.001,this._ctx.currentTime+duration);source.connect(gain);gain.connect(this._ctx.destination);source.start();this._trackNode(source,duration);this._trackNode(gain,duration);},_delay(fn,ms){const id=setTimeout(()=>{this._timeouts=this._timeouts.filter(t=>t!==id);fn();},ms);this._timeouts.push(id);return id;},caseOpen(){this._play(800,0.1,'square',0.3);this._delay(()=>this._play(400,0.15,'sawtooth',0.2),80);this._delay(()=>this._play(200,0.2,'sine',0.15),180);this._noise(0.05,0.4);},caseRare(){this._play(523,0.15,'sine',0.3);this._delay(()=>this._play(659,0.15,'sine',0.3),100);this._delay(()=>this._play(784,0.15,'sine',0.3),200);this._delay(()=>this._play(1047,0.3,'sine',0.4),300);},caseCovert(){this._play(392,0.2,'sawtooth',0.3);this._delay(()=>this._play(523,0.2,'sawtooth',0.3),150);this._delay(()=>this._play(659,0.2,'sawtooth',0.3),300);this._delay(()=>this._play(784,0.4,'sine',0.5),450);this._delay(()=>this._play(1047,0.6,'sine',0.6),600);},slotSpin(){for(let i=0;i<8;i++){this._delay(()=>{this._play(300+Math.random()*400,0.05,'square',0.15);},i*80);}},slotMatch(){this._play(523,0.1,'sine',0.3);this._delay(()=>this._play(659,0.1,'sine',0.3),100);this._delay(()=>this._play(784,0.1,'sine',0.3),200);this._delay(()=>this._play(1047,0.4,'sine',0.5),300);},contractSubmit(){this._play(150,0.3,'sawtooth',0.2);this._delay(()=>this._play(200,0.2,'square',0.2),200);this._delay(()=>this._play(250,0.1,'square',0.15),350);},contractResult(){this._play(400,0.15,'sine',0.3);this._delay(()=>this._play(600,0.15,'sine',0.3),120);this._delay(()=>this._play(800,0.3,'sine',0.4),240);},wheelSpin(){this._play(600+Math.random()*400,0.04,'triangle',0.12);this._play(200,0.02,'square',0.06);},wheelWin(){const t=this._ctx.currentTime;if(!this._enabled||!this._ctx)return;if(this._ctx.state==='suspended')this._ctx.resume();[523,659,784,1047].forEach((freq,i)=>{const osc=this._ctx.createOscillator();const gain=this._ctx.createGain();osc.type='sine';osc.frequency.setValueAtTime(freq,t+i*0.1);gain.gain.setValueAtTime(0,t+i*0.1);gain.gain.linearRampToValueAtTime(this._volume*0.35,t+i*0.1+0.05);gain.gain.exponentialRampToValueAtTime(0.001,t+i*0.1+0.5);osc.connect(gain);gain.connect(this._ctx.destination);osc.start(t+i*0.1);osc.stop(t+i*0.1+0.5);this._trackNode(osc,0.6);this._trackNode(gain,0.6);});this._delay(()=>this._play(1568,0.4,'sine',0.25),350);this._delay(()=>this._play(2093,0.6,'sine',0.2),500);},wheelLose(){this._play(300,0.15,'sawtooth',0.25);this._delay(()=>this._play(200,0.2,'sawtooth',0.2),100);this._delay(()=>this._play(120,0.3,'sawtooth',0.15),220);this._noise(0.15,0.3);this._delay(()=>{this._play(60,0.5,'sine',0.2);this._play(45,0.6,'sine',0.15);},350);},achievement(){this._play(659,0.1,'sine',0.3);this._delay(()=>this._play(523,0.1,'sine',0.3),100);this._delay(()=>this._play(784,0.1,'sine',0.3),200);this._delay(()=>this._play(659,0.1,'sine',0.3),300);this._delay(()=>this._play(1047,0.4,'sine',0.5),400);},crashBet(){this._play(300,0.1,'square',0.15);},crashTick(){this._play(500+Math.random()*500,0.03,'sine',0.08);},crashCrashed(){this._noise(0.3,0.6);this._play(80,0.5,'sawtooth',0.4);this._delay(()=>this._play(50,0.8,'sine',0.3),100);},crashCashout(){this._play(600,0.1,'sine',0.3);this._delay(()=>this._play(800,0.1,'sine',0.3),80);this._delay(()=>this._play(1000,0.2,'sine',0.4),160);},click(){this._play(800,0.03,'sine',0.1);},error(){this._play(200,0.15,'square',0.2);},balanceUpdate(){this._play(1000,0.05,'sine',0.1);}};document.addEventListener('DOMContentLoaded',()=>SoundManager.init());
+4
View File
@@ -0,0 +1,4 @@
const WS={_ws:null,_reconnectTimer:null,_listeners:{},_connected:false,connect(){if(this._ws&&this._ws.readyState===WebSocket.OPEN)return;const protocol=window.location.protocol==='https:'?'wss:':'ws:';const url=`${protocol}
try{this._ws=new WebSocket(url);}catch(e){console.warn('WS connection failed, retrying in 5s');this._scheduleReconnect();return;}
this._ws.onopen=()=>{this._connected=true;this._emit('connected');};this._ws.onclose=()=>{this._connected=false;this._emit('disconnected');this._scheduleReconnect();};this._ws.onerror=()=>{this._emit('error');};this._ws.onmessage=(event)=>{try{const data=JSON.parse(event.data);this._emit(data.type,data);}catch(e){}};this._pingInterval=setInterval(()=>{if(this._ws&&this._ws.readyState===WebSocket.OPEN){this._ws.send(JSON.stringify({type:'ping'}));}},30000);},disconnect(){if(this._pingInterval)clearInterval(this._pingInterval);if(this._reconnectTimer)clearTimeout(this._reconnectTimer);if(this._ws){this._ws.onclose=null;this._ws.close();this._ws=null;}
this._connected=false;},_scheduleReconnect(){if(this._reconnectTimer)return;this._reconnectTimer=setTimeout(()=>{this._reconnectTimer=null;this.connect();},5000);},on(event,callback){if(!this._listeners[event])this._listeners[event]=[];this._listeners[event].push(callback);return()=>{this._listeners[event]=this._listeners[event].filter(cb=>cb!==callback);};},_emit(event,data){(this._listeners[event]||[]).forEach(cb=>cb(data));(this._listeners['*']||[]).forEach(cb=>cb(event,data));},isConnected(){return this._connected;}};document.addEventListener('DOMContentLoaded',()=>WS.connect());