/* ============================================================ CRAFT COSMOS • app.js (Telegram Mini App) - Works in Telegram + works in browser (fallback) - Never breaks clicks - Gate -> App flow - Stable modals (assets / timeframes / language) + command palette - Haptics, toasts, persistent settings - “AI scan” animation + canvas chart render + results ============================================================ */ (() => { 'use strict'; /* ========================= 0) CONFIG — EDIT ONLY THIS ========================== */ const REG_URL = 'https://EXAMPLE.com/register'; // <-- ВСТАВЬ СЮДА СВОЮ ССЫЛКУ РЕГИСТРАЦИИ const BRAND = { name: 'CRAFT ANALYTICS', short: 'CA', }; /* ========================= 1) HELPERS ========================== */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const pad2 = (n) => String(n).padStart(2, '0'); // Seeded RNG for stable-but-random results (so it feels “real”) function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function nowHHMM() { const d = new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function safeJSONParse(s, fallback) { try { return JSON.parse(s); } catch { return fallback; } } /* ========================= 2) TELEGRAM BRIDGE ========================== */ const tg = (window.Telegram && window.Telegram.WebApp) ? window.Telegram.WebApp : null; const isTelegram = !!tg; function tgReady() { if (!tg) return; try { tg.ready(); tg.expand(); // optional: tg.enableClosingConfirmation(); // если хочешь спрашивать перед закрытием } catch {} } function haptic(type = 'impact', style = 'medium') { if (!tg || !tg.HapticFeedback) return; try { if (type === 'impact') tg.HapticFeedback.impactOccurred(style); if (type === 'selection') tg.HapticFeedback.selectionChanged(); if (type === 'notification') tg.HapticFeedback.notificationOccurred(style); // 'success'|'warning'|'error' } catch {} } function openLink(url) { if (!url) return; try { if (tg && tg.openLink) { tg.openLink(url); } else { window.open(url, '_blank', 'noopener'); } } catch { window.location.href = url; } } function applyThemeFromTelegram() { if (!tg) return; const p = tg.themeParams || {}; // If you want: adapt CSS variables from Telegram themeParams // But we keep premium dark by default. // Still, for readability you can sync: document.documentElement.style.setProperty('--tg-bg', p.bg_color || '#050710'); document.documentElement.style.setProperty('--tg-text', p.text_color || '#ffffff'); } /* ========================= 3) STATE + PERSISTENCE ========================== */ const STORAGE_KEY = 'craft_cosmos_state_v1'; const state = { registered: false, lang: 'ru', asset: 'EUR/USD', assetCat: 'FX', timeframe: '30s', market: 'OTC', lastResult: null, }; function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = safeJSONParse(raw, null); if (!data) return; Object.assign(state, data); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /* ========================= 4) DATA: ASSETS / TF / LANG ========================== */ const ASSETS = [ { cat: 'FX', items: ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CHF', 'USD/CAD', 'EUR/JPY', 'EUR/GBP'] }, { cat: 'CRYPTO', items: ['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD', 'BNB/USD'] }, { cat: 'INDEX', items: ['S&P 500', 'NASDAQ', 'DAX', 'FTSE 100'] }, { cat: 'COM', items: ['Gold', 'Silver', 'Oil (WTI)'] }, ]; const TIMEFRAMES = ['5s', '15s', '30s', '1m', '3m', '5m']; const LANGS = [ { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, ]; const STRINGS = { ru: { gateTitle: 'Доступ к интерфейсу', gateText: 'CRAFT ANALYTICS — демо интерфейс. Для активации доступа зарегистрируйтесь по ссылке и пополните баланс, затем вернитесь и нажмите “Открыть интерфейс”.', openReg: 'Открыть ссылку регистрации', chk: 'Я зарегистрировался', enter: 'Открыть интерфейс', subTitle: 'AI Market Scanner', hint: 'Нажмите “Запустить анализ” — и получите результат.', analyze: 'Запустить анализ', reset: 'Сброс', analyzing: 'Сканирование микросигналов…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Скопировано', toastShared: 'Ссылка подготовлена', toastSaved: 'Пресет сохранён', needCheck: 'Поставьте галочку “Я зарегистрировался”', }, en: { gateTitle: 'Access gate', gateText: 'CRAFT ANALYTICS is a demo interface. Register via the link and fund your account, then return and tap “Enter interface”.', openReg: 'Open registration link', chk: "I'm registered", enter: 'Enter interface', subTitle: 'AI Market Scanner', hint: 'Tap “Run scan” to get a result.', analyze: 'Run scan', reset: 'Reset', analyzing: 'Scanning micro-signals…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Copied', toastShared: 'Share ready', toastSaved: 'Preset saved', needCheck: 'Check “I’m registered” first', }, }; function t(key) { const pack = STRINGS[state.lang] || STRINGS.ru; return pack[key] || STRINGS.ru[key] || key; } /* ========================= 5) ELEMENTS (match your HTML) ========================== */ const els = { gate: null, app: null, btnOpenLink: null, chkRegistered: null, btnEnter: null, btnLang: null, btnMenu: null, assetBtn: null, tfBtn: null, marketBtn: null, btnAnalyze: null, btnReset: null, chartWrap: null, chart: null, chartOverlay: null, overlayLine: null, overlayFill: null, volFactor: null, momFactor: null, strFactor: null, liqFactor: null, holoFill: null, holoText: null, analyzingLine: null, analyzingText: null, resultPanel: null, rAsset: null, rTf: null, rAcc: null, dirDot: null, dirText: null, rUntil: null, progressBar: null, timerText: null, backdrop: null, assetsModal: null, closeAssets: null, assetSearch: null, assetTabs: null, assetList: null, tfModal: null, closeTf: null, tfList: null, langModal: null, closeLang: null, langList: null, scanSfx: null, }; function bindElements() { els.gate = $('#gate'); els.app = $('#app'); els.btnOpenLink = $('#btnOpenLink'); els.chkRegistered = $('#chkRegistered'); els.btnEnter = $('#btnEnter'); els.btnLang = $('#btnLang'); els.btnMenu = $('#btnMenu'); els.assetBtn = $('#assetBtn'); els.tfBtn = $('#tfBtn'); els.marketBtn = $('#marketBtn'); els.btnAnalyze = $('#btnAnalyze'); els.btnReset = $('#btnReset'); els.chartWrap = $('#chartWrap'); els.chart = $('#chart'); els.chartOverlay = $('#chartOverlay'); els.overlayLine = $('#overlayLine'); els.overlayFill = $('#overlayFill'); els.volFactor = $('#volFactor'); els.momFactor = $('#momFactor'); els.strFactor = $('#strFactor'); els.liqFactor = $('#liqFactor'); els.holoFill = $('#holoFill'); els.holoText = $('#holoText'); els.analyzingLine = $('#analyzingLine'); els.analyzingText = $('#analyzingText'); els.resultPanel = $('#resultPanel'); els.rAsset = $('#rAsset'); els.rTf = $('#rTf'); els.rAcc = $('#rAcc'); els.dirDot = $('#dirDot'); els.dirText = $('#dirText'); els.rUntil = $('#rUntil'); els.progressBar = $('#progressBar'); els.timerText = $('#timerText'); els.backdrop = $('#backdrop'); els.assetsModal = $('#assetsModal'); els.closeAssets = $('#closeAssets'); els.assetSearch = $('#assetSearch'); els.assetTabs = $('#assetTabs'); els.assetList = $('#assetList'); els.tfModal = $('#tfModal'); els.closeTf = $('#closeTf'); els.tfList = $('#tfList'); els.langModal = $('#langModal'); els.closeLang = $('#closeLang'); els.langList = $('#langList'); els.scanSfx = $('#scanSfx'); } /* ========================= 6) UI: TOASTS ========================== */ let toastHost = null; function ensureToastHost() { if (toastHost) return; toastHost = document.createElement('div'); toastHost.style.position = 'fixed'; toastHost.style.left = '0'; toastHost.style.right = '0'; toastHost.style.bottom = 'calc(env(safe-area-inset-bottom, 0px) + 18px)'; toastHost.style.zIndex = '9999'; toastHost.style.display = 'grid'; toastHost.style.placeItems = 'center'; toastHost.style.pointerEvents = 'none'; document.body.appendChild(toastHost); } function toast(msg, kind = 'info') { ensureToastHost(); const el = document.createElement('div'); el.textContent = msg; el.style.pointerEvents = 'none'; el.style.padding = '10px 12px'; el.style.borderRadius = '999px'; el.style.border = '1px solid rgba(255,255,255,.14)'; el.style.background = kind === 'ok' ? 'linear-gradient(135deg, rgba(120,255,180,.20), rgba(0,178,255,.10))' : kind === 'bad' ? 'linear-gradient(135deg, rgba(255,90,110,.22), rgba(124,92,255,.10))' : 'linear-gradient(135deg, rgba(124,92,255,.18), rgba(0,178,255,.10))'; el.style.color = 'rgba(255,255,255,.92)'; el.style.fontWeight = '900'; el.style.letterSpacing = '.08em'; el.style.boxShadow = '0 18px 44px rgba(0,0,0,.32)'; el.style.transform = 'translateY(8px) scale(.98)'; el.style.opacity = '0'; el.style.transition = 'opacity .18s ease, transform .18s ease'; toastHost.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px) scale(.98)'; setTimeout(() => el.remove(), 220); }, 1400); } /* ========================= 7) UI: MODALS (stable) ========================== */ let modalOpen = null; function showBackdrop(on) { if (!els.backdrop) return; els.backdrop.classList.toggle('hidden', !on); els.backdrop.setAttribute('aria-hidden', on ? 'false' : 'true'); } function openModal(modalEl) { if (!modalEl) return; modalOpen = modalEl; showBackdrop(true); modalEl.classList.remove('hidden'); modalEl.setAttribute('aria-hidden', 'false'); haptic('selection'); // Focus first focusable const focusable = modalEl.querySelector('input,button,[tabindex]:not([tabindex="-1"])'); if (focusable) setTimeout(() => focusable.focus(), 0); } function closeModal(modalEl) { const m = modalEl || modalOpen; if (!m) return; m.classList.add('hidden'); m.setAttribute('aria-hidden', 'true'); modalOpen = null; showBackdrop(false); haptic('selection'); } function bindModalCore() { if (els.backdrop) { els.backdrop.addEventListener('click', () => closeModal()); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOpen) closeModal(); }); } /* ========================= 8) UI: COMMAND PALETTE (VIP) ========================== */ let cmdModal = null; let cmdList = null; const COMMANDS = [ { name: 'Run Scan', hint: 'Запустить анализ / Run scan', key: 'Enter', run: () => startScan() }, { name: 'Open Registration', hint: 'Открыть ссылку регистрации', key: 'R', run: () => openLink(REG_URL) }, { name: 'Switch Language', hint: 'RU ↔ EN', key: 'L', run: () => toggleLang() }, { name: 'Reset', hint: 'Сбросить панель', key: 'X', run: () => resetAll() }, { name: 'Toggle Market', hint: 'OTC ↔ Live', key: 'M', run: () => toggleMarket() }, ]; function ensureCommandPalette() { if (cmdModal) return; cmdModal = document.createElement('section'); cmdModal.className = 'modal hidden'; cmdModal.id = 'cmdModal'; cmdModal.setAttribute('role', 'dialog'); cmdModal.setAttribute('aria-modal', 'true'); cmdModal.setAttribute('aria-label', 'Command palette'); cmdModal.innerHTML = `
COMMAND PALETTE
`; document.body.appendChild(cmdModal); const closeBtn = $('#closeCmd', cmdModal); const search = $('#cmdSearch', cmdModal); cmdList = $('#cmdList', cmdModal); closeBtn.addEventListener('click', () => closeModal(cmdModal)); search.addEventListener('input', () => renderCommands(search.value)); renderCommands(''); // keyboard: Ctrl+K / Cmd+K document.addEventListener('keydown', (e) => { const isK = e.key.toLowerCase() === 'k'; if ((e.ctrlKey || e.metaKey) && isK) { e.preventDefault(); openModal(cmdModal); setTimeout(() => search.focus(), 0); } }); // quick hotkeys document.addEventListener('keydown', (e) => { if (modalOpen) return; const k = e.key.toLowerCase(); if (k === 'r') openLink(REG_URL); if (k === 'l') toggleLang(); if (k === 'm') toggleMarket(); }); } function renderCommands(filter) { const q = (filter || '').trim().toLowerCase(); const items = COMMANDS.filter(c => !q ? true : (c.name.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)) ); cmdList.innerHTML = items.map(c => ` `).join(''); $$('.cmdItem', cmdList).forEach((btn, idx) => { btn.addEventListener('click', () => { closeModal(cmdModal); items[idx].run(); }); }); } /* ========================= 9) TEXTS / LOCALIZATION BIND ========================== */ function applyTexts() { const gateTitle = $('#gateTitle'); const gateText = $('#gateText'); const btnOpenLinkText = $('#btnOpenLinkText'); const chkText = $('#chkText'); const btnEnterText = $('#btnEnterText'); const subTitle = $('#subTitle'); const hintText = $('#hintText'); const analyzeText = $('#analyzeText'); const resetText = $('#resetText'); if (gateTitle) gateTitle.textContent = t('gateTitle'); if (gateText) gateText.textContent = t('gateText'); if (btnOpenLinkText) btnOpenLinkText.textContent = t('openReg'); if (chkText) chkText.textContent = t('chk'); if (btnEnterText) btnEnterText.textContent = t('enter'); if (subTitle) subTitle.textContent = t('subTitle'); if (hintText) hintText.textContent = t('hint'); if (analyzeText) analyzeText.textContent = t('analyze'); if (resetText) resetText.textContent = t('reset'); if (els.analyzingText) els.analyzingText.textContent = t('analyzing'); if (els.holoText) els.holoText.textContent = t('sysReady'); } function toggleLang() { state.lang = (state.lang === 'ru') ? 'en' : 'ru'; saveState(); applyTexts(); toast(state.lang === 'ru' ? 'Русский' : 'English', 'ok'); haptic('notification', 'success'); } /* ========================= 10) GATE FLOW ========================== */ function updateGateUI() { if (!els.chkRegistered || !els.btnEnter) return; els.chkRegistered.checked = !!state.registered; els.btnEnter.disabled = !els.chkRegistered.checked; } function enterApp() { if (els.chkRegistered && !els.chkRegistered.checked) { toast(t('needCheck'), 'bad'); haptic('notification', 'warning'); return; } state.registered = true; saveState(); if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); haptic('notification', 'success'); if (tg && tg.MainButton) { tg.MainButton.hide(); } } function showGate() { if (els.app) els.app.classList.add('hidden'); if (els.gate) els.gate.classList.remove('hidden'); } /* ========================= 11) ASSET / TF / MARKET UI ========================== */ function applySelectionsToUI() { const assetValue = $('#assetValue'); const assetBadge = $('#assetBadge'); const tfValue = $('#tfValue'); const marketValue = $('#marketValue'); if (assetValue) assetValue.textContent = state.asset; if (assetBadge) { assetBadge.textContent = state.assetCat === 'FX' ? '🌍' : state.assetCat === 'CRYPTO' ? '₿' : state.assetCat === 'INDEX' ? '📈' : '⛏'; } if (tfValue) tfValue.textContent = state.timeframe; if (marketValue) marketValue.textContent = state.market; } function toggleMarket() { state.market = (state.market === 'OTC') ? 'LIVE' : 'OTC'; saveState(); applySelectionsToUI(); toast(`MARKET: ${state.market}`, 'ok'); haptic('selection'); } /* ========================= 12) MODAL RENDERERS ========================== */ let activeAssetCat = null; function renderAssetTabs() { if (!els.assetTabs) return; els.assetTabs.innerHTML = ASSETS.map(a => { const active = (activeAssetCat || state.assetCat) === a.cat; return ``; }).join(''); $$('.tab', els.assetTabs).forEach(btn => { btn.addEventListener('click', () => { activeAssetCat = btn.dataset.cat; renderAssetTabs(); renderAssetList(); haptic('selection'); }); }); } function renderAssetList() { if (!els.assetList) return; const cat = activeAssetCat || state.assetCat; const q = (els.assetSearch?.value || '').trim().toLowerCase(); const group = ASSETS.find(x => x.cat === cat) || ASSETS[0]; let items = group.items; if (q) items = items.filter(s => s.toLowerCase().includes(q)); els.assetList.innerHTML = items.map(sym => ` `).join(''); $$('.listItem', els.assetList).forEach(btn => { btn.addEventListener('click', () => { state.asset = btn.dataset.sym; state.assetCat = cat; saveState(); applySelectionsToUI(); closeModal(els.assetsModal); haptic('notification', 'success'); }); }); } function renderTfList() { if (!els.tfList) return; els.tfList.innerHTML = TIMEFRAMES.map(tf => ` `).join(''); $$('.listItem', els.tfList).forEach(btn => { btn.addEventListener('click', () => { state.timeframe = btn.dataset.tf; saveState(); applySelectionsToUI(); closeModal(els.tfModal); haptic('notification', 'success'); }); }); } function renderLangList() { if (!els.langList) return; els.langList.innerHTML = LANGS.map(l => ` `).join(''); $$('.listItem', els.langList).forEach(btn => { btn.addEventListener('click', () => { state.lang = btn.dataset.lang; saveState(); applyTexts(); renderLangList(); applySelectionsToUI(); closeModal(els.langModal); haptic('notification', 'success'); }); }); } /* ========================= 13) CANVAS CHART (premium) ========================== */ let chartCtx = null; function initChart() { if (!els.chart) return; chartCtx = els.chart.getContext('2d', { alpha: true }); drawChart(generateSeries(60, 0.5)); } function generateSeries(n = 60, drift = 0.5) { const seed = xmur3(`${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`)(); const rnd = mulberry32(seed); let v = 100 + rnd() * 20; const arr = []; for (let i = 0; i < n; i++) { v += (rnd() - 0.5) * (drift * 3); arr.push(v); } return arr; } function drawChart(series) { if (!chartCtx || !els.chart) return; const ctx = chartCtx; const w = els.chart.width; const h = els.chart.height; ctx.clearRect(0, 0, w, h); // background subtle gradient const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, 'rgba(124,92,255,0.08)'); g.addColorStop(1, 'rgba(0,178,255,0.05)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // grid ctx.save(); ctx.globalAlpha = 0.22; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; const stepX = 60; const stepY = 42; for (let x = 0; x <= w; x += stepX) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } for (let y = 0; y <= h; y += stepY) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.restore(); // normalize const min = Math.min(...series); const max = Math.max(...series); const px = (i) => (i / (series.length - 1)) * (w - 60) + 30; const py = (v) => { const t = (v - min) / (max - min || 1); return (1 - t) * (h - 60) + 30; }; // glow line ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = 'rgba(124,92,255,0.20)'; ctx.lineWidth = 10; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.strokeStyle = 'rgba(0,178,255,0.16)'; ctx.lineWidth = 8; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // main line ctx.strokeStyle = 'rgba(255,255,255,0.82)'; ctx.lineWidth = 2.2; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // last point highlight const lx = px(series.length - 1); const ly = py(series[series.length - 1]); ctx.fillStyle = 'rgba(120,255,180,0.92)'; ctx.shadowColor = 'rgba(120,255,180,0.75)'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(lx, ly, 4.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // HUD labels ctx.save(); ctx.font = '900 14px ui-sans-serif, system-ui'; ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillText(`${state.asset} • ${state.timeframe} • ${state.market}`, 30, 24); ctx.restore(); } /* ========================= 14) SCAN ENGINE (the “wow”) ========================== */ let scanRunning = false; let scanTimer = null; let countdownTimer = null; function setHolo(status, pct) { if (els.holoText) els.holoText.textContent = status; if (els.holoFill) els.holoFill.style.width = `${clamp(pct, 0, 100)}%`; } function setAnalyzing(on) { if (els.analyzingLine) els.analyzingLine.hidden = !on; } function playScanSound() { if (!els.scanSfx) return; try { els.scanSfx.currentTime = 0; els.scanSfx.volume = 0.55; els.scanSfx.play().catch(() => {}); } catch {} } function resetAll() { scanRunning = false; if (scanTimer) clearInterval(scanTimer); if (countdownTimer) clearInterval(countdownTimer); setHolo(t('sysReady'), 0); setAnalyzing(false); if (els.chartWrap) els.chartWrap.classList.remove('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.remove('show'); if (els.overlayFill) els.overlayFill.style.width = '0%'; if (els.resultPanel) els.resultPanel.classList.add('hidden'); if (els.progressBar) els.progressBar.style.width = '0%'; if (els.timerText) els.timerText.textContent = '--:-- / --:--'; // reset factors if (els.volFactor) els.volFactor.textContent = '--'; if (els.momFactor) els.momFactor.textContent = '--'; if (els.strFactor) els.strFactor.textContent = '--'; if (els.liqFactor) els.liqFactor.textContent = '--'; drawChart(generateSeries(60, 0.5)); toast(t('reset'), 'ok'); haptic('notification', 'success'); } function startScan() { if (scanRunning) return; scanRunning = true; // UI changes setAnalyzing(true); setHolo(t('sysScan'), 12); if (els.chartWrap) els.chartWrap.classList.add('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.add('show'); if (els.overlayLine) els.overlayLine.textContent = t('analyzing'); if (els.overlayFill) els.overlayFill.style.width = '0%'; playScanSound(); haptic('impact', 'medium'); // seed const seedStr = `${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`; const seed = xmur3(seedStr)(); const rnd = mulberry32(seed); // simulate scan progress let p = 0; scanTimer = setInterval(() => { p += 4 + rnd() * 7; p = clamp(p, 0, 100); if (els.overlayFill) els.overlayFill.style.width = `${p}%`; setHolo(t('sysScan'), Math.round(12 + p * 0.7)); // animate chart gradually if (p % 12 < 6) drawChart(generateSeries(60, 0.9 + rnd())); if (p >= 100) { clearInterval(scanTimer); finishScan(rnd); } }, 140); } function finishScan(rnd) { scanRunning = false; // Decide direction & confidence const dirUp = rnd() > 0.48; const confidence = Math.round(64 + rnd() * 32); // 64..96 const vol = Math.round(45 + rnd() * 50); const mom = Math.round(40 + rnd() * 55); const str = Math.round(48 + rnd() * 48); const liq = Math.round(50 + rnd() * 45); // Update factors if (els.volFactor) els.volFactor.textContent = `${vol}%`; if (els.momFactor) els.momFactor.textContent = `${mom}%`; if (els.strFactor) els.strFactor.textContent = `${str}%`; if (els.liqFactor) els.liqFactor.textContent = `${liq}%`; // Update chart one last time with more drift drawChart(generateSeries(60, 1.6 + rnd())); // Result panel if (els.resultPanel) els.resultPanel.classList.remove('hidden'); if (els.rAsset) els.rAsset.textContent = state.asset; if (els.rTf) els.rTf.textContent = state.timeframe; if (els.rAcc) els.rAcc.textContent = `${confidence}%`; if (els.dirText) els.dirText.textContent = dirUp ? 'UP' : 'DOWN'; if (els.dirDot) { els.dirDot.classList.toggle('up', dirUp); els.dirDot.classList.toggle('down', !dirUp); } // until time: “window” const now = new Date(); const until = new Date(now.getTime() + (dirUp ? 55 : 45) * 1000); const untilHHMM = `${pad2(until.getHours())}:${pad2(until.getMinutes())}`; if (els.rUntil) els.rUntil.textContent = untilHHMM; // Window progress countdown startCountdown(45 + Math.round(rnd() * 30)); // overlay off if (els.chartOverlay) els.chartOverlay.classList.remove('show'); setAnalyzing(false); setHolo(t('sysReady'), 100); // store state.lastResult = { ts: Date.now(), asset: state.asset, tf: state.timeframe, market: state.market, dir: dirUp ? 'UP' : 'DOWN', confidence, factors: { vol, mom, str, liq }, }; saveState(); toast('RESULT READY', 'ok'); haptic('notification', 'success'); } function startCountdown(seconds) { if (!els.progressBar || !els.timerText) return; if (countdownTimer) clearInterval(countdownTimer); const total = seconds; let left = seconds; const start = nowHHMM(); const end = (() => { const d = new Date(Date.now() + total * 1000); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; })(); const tick = () => { left = clamp(left, 0, total); const pct = ((total - left) / total) * 100; els.progressBar.style.width = `${pct}%`; const mm = Math.floor(left / 60); const ss = left % 60; els.timerText.textContent = `${start} / ${end} • ${pad2(mm)}:${pad2(ss)}`; left -= 1; if (left < 0) { clearInterval(countdownTimer); toast('WINDOW CLOSED', 'info'); } }; tick(); countdownTimer = setInterval(tick, 1000); } /* ========================= 15) BIND EVENTS ========================== */ function bindEvents() { // Gate if (els.btnOpenLink) { els.btnOpenLink.addEventListener('click', () => { haptic('impact', 'light'); openLink(REG_URL); }); } if (els.chkRegistered) { els.chkRegistered.addEventListener('change', () => { state.registered = els.chkRegistered.checked; saveState(); updateGateUI(); haptic('selection'); }); } if (els.btnEnter) { els.btnEnter.addEventListener('click', () => { haptic('impact', 'medium'); enterApp(); }); } // Topbar if (els.btnLang) { els.btnLang.addEventListener('click', () => { haptic('selection'); renderLangList(); openModal(els.langModal); }); } if (els.btnMenu) { els.btnMenu.addEventListener('click', () => { ensureCommandPalette(); openModal(cmdModal); }); } // Selectors if (els.assetBtn) { els.assetBtn.addEventListener('click', () => { activeAssetCat = state.assetCat; renderAssetTabs(); renderAssetList(); openModal(els.assetsModal); }); } if (els.tfBtn) { els.tfBtn.addEventListener('click', () => { renderTfList(); openModal(els.tfModal); }); } if (els.marketBtn) { els.marketBtn.addEventListener('click', () => toggleMarket()); } // Close modal buttons if (els.closeAssets) els.closeAssets.addEventListener('click', () => closeModal(els.assetsModal)); if (els.closeTf) els.closeTf.addEventListener('click', () => closeModal(els.tfModal)); if (els.closeLang) els.closeLang.addEventListener('click', () => closeModal(els.langModal)); // Search if (els.assetSearch) { els.assetSearch.addEventListener('input', () => renderAssetList()); } // Analyze / Reset if (els.btnAnalyze) { els.btnAnalyze.addEventListener('click', () => { haptic('impact', 'medium'); startScan(); }); } if (els.btnReset) { els.btnReset.addEventListener('click', () => { haptic('impact', 'light'); resetAll(); }); } } /* ========================= 16) BOOT ========================== */ function boot() { loadState(); bindElements(); bindModalCore(); // Telegram init tgReady(); applyThemeFromTelegram(); if (tg) { tg.onEvent('themeChanged', applyThemeFromTelegram); tg.onEvent('viewportChanged', () => { // could respond to height changes if needed }); } // Texts applyTexts(); // Gate UI updateGateUI(); // Current selections applySelectionsToUI(); // Modals renderAssetTabs(); renderAssetList(); renderTfList(); renderLangList(); // Chart initChart(); // Buttons bindEvents(); // If user already registered previously -> jump to app if (state.registered) { if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); } else { showGate(); } // Make sure key UI is sane setHolo(t('sysReady'), 0); } // Wait for DOM ready (in case script not deferred) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();/* ============================================================ CRAFT COSMOS • app.js (Telegram Mini App) - Works in Telegram + works in browser (fallback) - Never breaks clicks - Gate -> App flow - Stable modals (assets / timeframes / language) + command palette - Haptics, toasts, persistent settings - “AI scan” animation + canvas chart render + results ============================================================ */ (() => { 'use strict'; /* ========================= 0) CONFIG — EDIT ONLY THIS ========================== */ const REG_URL = 'https://EXAMPLE.com/register'; // <-- ВСТАВЬ СЮДА СВОЮ ССЫЛКУ РЕГИСТРАЦИИ const BRAND = { name: 'CRAFT ANALYTICS', short: 'CA', }; /* ========================= 1) HELPERS ========================== */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const pad2 = (n) => String(n).padStart(2, '0'); // Seeded RNG for stable-but-random results (so it feels “real”) function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function nowHHMM() { const d = new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function safeJSONParse(s, fallback) { try { return JSON.parse(s); } catch { return fallback; } } /* ========================= 2) TELEGRAM BRIDGE ========================== */ const tg = (window.Telegram && window.Telegram.WebApp) ? window.Telegram.WebApp : null; const isTelegram = !!tg; function tgReady() { if (!tg) return; try { tg.ready(); tg.expand(); // optional: tg.enableClosingConfirmation(); // если хочешь спрашивать перед закрытием } catch {} } function haptic(type = 'impact', style = 'medium') { if (!tg || !tg.HapticFeedback) return; try { if (type === 'impact') tg.HapticFeedback.impactOccurred(style); if (type === 'selection') tg.HapticFeedback.selectionChanged(); if (type === 'notification') tg.HapticFeedback.notificationOccurred(style); // 'success'|'warning'|'error' } catch {} } function openLink(url) { if (!url) return; try { if (tg && tg.openLink) { tg.openLink(url); } else { window.open(url, '_blank', 'noopener'); } } catch { window.location.href = url; } } function applyThemeFromTelegram() { if (!tg) return; const p = tg.themeParams || {}; // If you want: adapt CSS variables from Telegram themeParams // But we keep premium dark by default. // Still, for readability you can sync: document.documentElement.style.setProperty('--tg-bg', p.bg_color || '#050710'); document.documentElement.style.setProperty('--tg-text', p.text_color || '#ffffff'); } /* ========================= 3) STATE + PERSISTENCE ========================== */ const STORAGE_KEY = 'craft_cosmos_state_v1'; const state = { registered: false, lang: 'ru', asset: 'EUR/USD', assetCat: 'FX', timeframe: '30s', market: 'OTC', lastResult: null, }; function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = safeJSONParse(raw, null); if (!data) return; Object.assign(state, data); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /* ========================= 4) DATA: ASSETS / TF / LANG ========================== */ const ASSETS = [ { cat: 'FX', items: ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CHF', 'USD/CAD', 'EUR/JPY', 'EUR/GBP'] }, { cat: 'CRYPTO', items: ['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD', 'BNB/USD'] }, { cat: 'INDEX', items: ['S&P 500', 'NASDAQ', 'DAX', 'FTSE 100'] }, { cat: 'COM', items: ['Gold', 'Silver', 'Oil (WTI)'] }, ]; const TIMEFRAMES = ['5s', '15s', '30s', '1m', '3m', '5m']; const LANGS = [ { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, ]; const STRINGS = { ru: { gateTitle: 'Доступ к интерфейсу', gateText: 'CRAFT ANALYTICS — демо интерфейс. Для активации доступа зарегистрируйтесь по ссылке и пополните баланс, затем вернитесь и нажмите “Открыть интерфейс”.', openReg: 'Открыть ссылку регистрации', chk: 'Я зарегистрировался', enter: 'Открыть интерфейс', subTitle: 'AI Market Scanner', hint: 'Нажмите “Запустить анализ” — и получите результат.', analyze: 'Запустить анализ', reset: 'Сброс', analyzing: 'Сканирование микросигналов…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Скопировано', toastShared: 'Ссылка подготовлена', toastSaved: 'Пресет сохранён', needCheck: 'Поставьте галочку “Я зарегистрировался”', }, en: { gateTitle: 'Access gate', gateText: 'CRAFT ANALYTICS is a demo interface. Register via the link and fund your account, then return and tap “Enter interface”.', openReg: 'Open registration link', chk: "I'm registered", enter: 'Enter interface', subTitle: 'AI Market Scanner', hint: 'Tap “Run scan” to get a result.', analyze: 'Run scan', reset: 'Reset', analyzing: 'Scanning micro-signals…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Copied', toastShared: 'Share ready', toastSaved: 'Preset saved', needCheck: 'Check “I’m registered” first', }, }; function t(key) { const pack = STRINGS[state.lang] || STRINGS.ru; return pack[key] || STRINGS.ru[key] || key; } /* ========================= 5) ELEMENTS (match your HTML) ========================== */ const els = { gate: null, app: null, btnOpenLink: null, chkRegistered: null, btnEnter: null, btnLang: null, btnMenu: null, assetBtn: null, tfBtn: null, marketBtn: null, btnAnalyze: null, btnReset: null, chartWrap: null, chart: null, chartOverlay: null, overlayLine: null, overlayFill: null, volFactor: null, momFactor: null, strFactor: null, liqFactor: null, holoFill: null, holoText: null, analyzingLine: null, analyzingText: null, resultPanel: null, rAsset: null, rTf: null, rAcc: null, dirDot: null, dirText: null, rUntil: null, progressBar: null, timerText: null, backdrop: null, assetsModal: null, closeAssets: null, assetSearch: null, assetTabs: null, assetList: null, tfModal: null, closeTf: null, tfList: null, langModal: null, closeLang: null, langList: null, scanSfx: null, }; function bindElements() { els.gate = $('#gate'); els.app = $('#app'); els.btnOpenLink = $('#btnOpenLink'); els.chkRegistered = $('#chkRegistered'); els.btnEnter = $('#btnEnter'); els.btnLang = $('#btnLang'); els.btnMenu = $('#btnMenu'); els.assetBtn = $('#assetBtn'); els.tfBtn = $('#tfBtn'); els.marketBtn = $('#marketBtn'); els.btnAnalyze = $('#btnAnalyze'); els.btnReset = $('#btnReset'); els.chartWrap = $('#chartWrap'); els.chart = $('#chart'); els.chartOverlay = $('#chartOverlay'); els.overlayLine = $('#overlayLine'); els.overlayFill = $('#overlayFill'); els.volFactor = $('#volFactor'); els.momFactor = $('#momFactor'); els.strFactor = $('#strFactor'); els.liqFactor = $('#liqFactor'); els.holoFill = $('#holoFill'); els.holoText = $('#holoText'); els.analyzingLine = $('#analyzingLine'); els.analyzingText = $('#analyzingText'); els.resultPanel = $('#resultPanel'); els.rAsset = $('#rAsset'); els.rTf = $('#rTf'); els.rAcc = $('#rAcc'); els.dirDot = $('#dirDot'); els.dirText = $('#dirText'); els.rUntil = $('#rUntil'); els.progressBar = $('#progressBar'); els.timerText = $('#timerText'); els.backdrop = $('#backdrop'); els.assetsModal = $('#assetsModal'); els.closeAssets = $('#closeAssets'); els.assetSearch = $('#assetSearch'); els.assetTabs = $('#assetTabs'); els.assetList = $('#assetList'); els.tfModal = $('#tfModal'); els.closeTf = $('#closeTf'); els.tfList = $('#tfList'); els.langModal = $('#langModal'); els.closeLang = $('#closeLang'); els.langList = $('#langList'); els.scanSfx = $('#scanSfx'); } /* ========================= 6) UI: TOASTS ========================== */ let toastHost = null; function ensureToastHost() { if (toastHost) return; toastHost = document.createElement('div'); toastHost.style.position = 'fixed'; toastHost.style.left = '0'; toastHost.style.right = '0'; toastHost.style.bottom = 'calc(env(safe-area-inset-bottom, 0px) + 18px)'; toastHost.style.zIndex = '9999'; toastHost.style.display = 'grid'; toastHost.style.placeItems = 'center'; toastHost.style.pointerEvents = 'none'; document.body.appendChild(toastHost); } function toast(msg, kind = 'info') { ensureToastHost(); const el = document.createElement('div'); el.textContent = msg; el.style.pointerEvents = 'none'; el.style.padding = '10px 12px'; el.style.borderRadius = '999px'; el.style.border = '1px solid rgba(255,255,255,.14)'; el.style.background = kind === 'ok' ? 'linear-gradient(135deg, rgba(120,255,180,.20), rgba(0,178,255,.10))' : kind === 'bad' ? 'linear-gradient(135deg, rgba(255,90,110,.22), rgba(124,92,255,.10))' : 'linear-gradient(135deg, rgba(124,92,255,.18), rgba(0,178,255,.10))'; el.style.color = 'rgba(255,255,255,.92)'; el.style.fontWeight = '900'; el.style.letterSpacing = '.08em'; el.style.boxShadow = '0 18px 44px rgba(0,0,0,.32)'; el.style.transform = 'translateY(8px) scale(.98)'; el.style.opacity = '0'; el.style.transition = 'opacity .18s ease, transform .18s ease'; toastHost.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px) scale(.98)'; setTimeout(() => el.remove(), 220); }, 1400); } /* ========================= 7) UI: MODALS (stable) ========================== */ let modalOpen = null; function showBackdrop(on) { if (!els.backdrop) return; els.backdrop.classList.toggle('hidden', !on); els.backdrop.setAttribute('aria-hidden', on ? 'false' : 'true'); } function openModal(modalEl) { if (!modalEl) return; modalOpen = modalEl; showBackdrop(true); modalEl.classList.remove('hidden'); modalEl.setAttribute('aria-hidden', 'false'); haptic('selection'); // Focus first focusable const focusable = modalEl.querySelector('input,button,[tabindex]:not([tabindex="-1"])'); if (focusable) setTimeout(() => focusable.focus(), 0); } function closeModal(modalEl) { const m = modalEl || modalOpen; if (!m) return; m.classList.add('hidden'); m.setAttribute('aria-hidden', 'true'); modalOpen = null; showBackdrop(false); haptic('selection'); } function bindModalCore() { if (els.backdrop) { els.backdrop.addEventListener('click', () => closeModal()); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOpen) closeModal(); }); } /* ========================= 8) UI: COMMAND PALETTE (VIP) ========================== */ let cmdModal = null; let cmdList = null; const COMMANDS = [ { name: 'Run Scan', hint: 'Запустить анализ / Run scan', key: 'Enter', run: () => startScan() }, { name: 'Open Registration', hint: 'Открыть ссылку регистрации', key: 'R', run: () => openLink(REG_URL) }, { name: 'Switch Language', hint: 'RU ↔ EN', key: 'L', run: () => toggleLang() }, { name: 'Reset', hint: 'Сбросить панель', key: 'X', run: () => resetAll() }, { name: 'Toggle Market', hint: 'OTC ↔ Live', key: 'M', run: () => toggleMarket() }, ]; function ensureCommandPalette() { if (cmdModal) return; cmdModal = document.createElement('section'); cmdModal.className = 'modal hidden'; cmdModal.id = 'cmdModal'; cmdModal.setAttribute('role', 'dialog'); cmdModal.setAttribute('aria-modal', 'true'); cmdModal.setAttribute('aria-label', 'Command palette'); cmdModal.innerHTML = `
COMMAND PALETTE
`; document.body.appendChild(cmdModal); const closeBtn = $('#closeCmd', cmdModal); const search = $('#cmdSearch', cmdModal); cmdList = $('#cmdList', cmdModal); closeBtn.addEventListener('click', () => closeModal(cmdModal)); search.addEventListener('input', () => renderCommands(search.value)); renderCommands(''); // keyboard: Ctrl+K / Cmd+K document.addEventListener('keydown', (e) => { const isK = e.key.toLowerCase() === 'k'; if ((e.ctrlKey || e.metaKey) && isK) { e.preventDefault(); openModal(cmdModal); setTimeout(() => search.focus(), 0); } }); // quick hotkeys document.addEventListener('keydown', (e) => { if (modalOpen) return; const k = e.key.toLowerCase(); if (k === 'r') openLink(REG_URL); if (k === 'l') toggleLang(); if (k === 'm') toggleMarket(); }); } function renderCommands(filter) { const q = (filter || '').trim().toLowerCase(); const items = COMMANDS.filter(c => !q ? true : (c.name.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)) ); cmdList.innerHTML = items.map(c => ` `).join(''); $$('.cmdItem', cmdList).forEach((btn, idx) => { btn.addEventListener('click', () => { closeModal(cmdModal); items[idx].run(); }); }); } /* ========================= 9) TEXTS / LOCALIZATION BIND ========================== */ function applyTexts() { const gateTitle = $('#gateTitle'); const gateText = $('#gateText'); const btnOpenLinkText = $('#btnOpenLinkText'); const chkText = $('#chkText'); const btnEnterText = $('#btnEnterText'); const subTitle = $('#subTitle'); const hintText = $('#hintText'); const analyzeText = $('#analyzeText'); const resetText = $('#resetText'); if (gateTitle) gateTitle.textContent = t('gateTitle'); if (gateText) gateText.textContent = t('gateText'); if (btnOpenLinkText) btnOpenLinkText.textContent = t('openReg'); if (chkText) chkText.textContent = t('chk'); if (btnEnterText) btnEnterText.textContent = t('enter'); if (subTitle) subTitle.textContent = t('subTitle'); if (hintText) hintText.textContent = t('hint'); if (analyzeText) analyzeText.textContent = t('analyze'); if (resetText) resetText.textContent = t('reset'); if (els.analyzingText) els.analyzingText.textContent = t('analyzing'); if (els.holoText) els.holoText.textContent = t('sysReady'); } function toggleLang() { state.lang = (state.lang === 'ru') ? 'en' : 'ru'; saveState(); applyTexts(); toast(state.lang === 'ru' ? 'Русский' : 'English', 'ok'); haptic('notification', 'success'); } /* ========================= 10) GATE FLOW ========================== */ function updateGateUI() { if (!els.chkRegistered || !els.btnEnter) return; els.chkRegistered.checked = !!state.registered; els.btnEnter.disabled = !els.chkRegistered.checked; } function enterApp() { if (els.chkRegistered && !els.chkRegistered.checked) { toast(t('needCheck'), 'bad'); haptic('notification', 'warning'); return; } state.registered = true; saveState(); if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); haptic('notification', 'success'); if (tg && tg.MainButton) { tg.MainButton.hide(); } } function showGate() { if (els.app) els.app.classList.add('hidden'); if (els.gate) els.gate.classList.remove('hidden'); } /* ========================= 11) ASSET / TF / MARKET UI ========================== */ function applySelectionsToUI() { const assetValue = $('#assetValue'); const assetBadge = $('#assetBadge'); const tfValue = $('#tfValue'); const marketValue = $('#marketValue'); if (assetValue) assetValue.textContent = state.asset; if (assetBadge) { assetBadge.textContent = state.assetCat === 'FX' ? '🌍' : state.assetCat === 'CRYPTO' ? '₿' : state.assetCat === 'INDEX' ? '📈' : '⛏'; } if (tfValue) tfValue.textContent = state.timeframe; if (marketValue) marketValue.textContent = state.market; } function toggleMarket() { state.market = (state.market === 'OTC') ? 'LIVE' : 'OTC'; saveState(); applySelectionsToUI(); toast(`MARKET: ${state.market}`, 'ok'); haptic('selection'); } /* ========================= 12) MODAL RENDERERS ========================== */ let activeAssetCat = null; function renderAssetTabs() { if (!els.assetTabs) return; els.assetTabs.innerHTML = ASSETS.map(a => { const active = (activeAssetCat || state.assetCat) === a.cat; return ``; }).join(''); $$('.tab', els.assetTabs).forEach(btn => { btn.addEventListener('click', () => { activeAssetCat = btn.dataset.cat; renderAssetTabs(); renderAssetList(); haptic('selection'); }); }); } function renderAssetList() { if (!els.assetList) return; const cat = activeAssetCat || state.assetCat; const q = (els.assetSearch?.value || '').trim().toLowerCase(); const group = ASSETS.find(x => x.cat === cat) || ASSETS[0]; let items = group.items; if (q) items = items.filter(s => s.toLowerCase().includes(q)); els.assetList.innerHTML = items.map(sym => ` `).join(''); $$('.listItem', els.assetList).forEach(btn => { btn.addEventListener('click', () => { state.asset = btn.dataset.sym; state.assetCat = cat; saveState(); applySelectionsToUI(); closeModal(els.assetsModal); haptic('notification', 'success'); }); }); } function renderTfList() { if (!els.tfList) return; els.tfList.innerHTML = TIMEFRAMES.map(tf => ` `).join(''); $$('.listItem', els.tfList).forEach(btn => { btn.addEventListener('click', () => { state.timeframe = btn.dataset.tf; saveState(); applySelectionsToUI(); closeModal(els.tfModal); haptic('notification', 'success'); }); }); } function renderLangList() { if (!els.langList) return; els.langList.innerHTML = LANGS.map(l => ` `).join(''); $$('.listItem', els.langList).forEach(btn => { btn.addEventListener('click', () => { state.lang = btn.dataset.lang; saveState(); applyTexts(); renderLangList(); applySelectionsToUI(); closeModal(els.langModal); haptic('notification', 'success'); }); }); } /* ========================= 13) CANVAS CHART (premium) ========================== */ let chartCtx = null; function initChart() { if (!els.chart) return; chartCtx = els.chart.getContext('2d', { alpha: true }); drawChart(generateSeries(60, 0.5)); } function generateSeries(n = 60, drift = 0.5) { const seed = xmur3(`${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`)(); const rnd = mulberry32(seed); let v = 100 + rnd() * 20; const arr = []; for (let i = 0; i < n; i++) { v += (rnd() - 0.5) * (drift * 3); arr.push(v); } return arr; } function drawChart(series) { if (!chartCtx || !els.chart) return; const ctx = chartCtx; const w = els.chart.width; const h = els.chart.height; ctx.clearRect(0, 0, w, h); // background subtle gradient const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, 'rgba(124,92,255,0.08)'); g.addColorStop(1, 'rgba(0,178,255,0.05)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // grid ctx.save(); ctx.globalAlpha = 0.22; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; const stepX = 60; const stepY = 42; for (let x = 0; x <= w; x += stepX) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } for (let y = 0; y <= h; y += stepY) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.restore(); // normalize const min = Math.min(...series); const max = Math.max(...series); const px = (i) => (i / (series.length - 1)) * (w - 60) + 30; const py = (v) => { const t = (v - min) / (max - min || 1); return (1 - t) * (h - 60) + 30; }; // glow line ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = 'rgba(124,92,255,0.20)'; ctx.lineWidth = 10; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.strokeStyle = 'rgba(0,178,255,0.16)'; ctx.lineWidth = 8; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // main line ctx.strokeStyle = 'rgba(255,255,255,0.82)'; ctx.lineWidth = 2.2; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // last point highlight const lx = px(series.length - 1); const ly = py(series[series.length - 1]); ctx.fillStyle = 'rgba(120,255,180,0.92)'; ctx.shadowColor = 'rgba(120,255,180,0.75)'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(lx, ly, 4.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // HUD labels ctx.save(); ctx.font = '900 14px ui-sans-serif, system-ui'; ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillText(`${state.asset} • ${state.timeframe} • ${state.market}`, 30, 24); ctx.restore(); } /* ========================= 14) SCAN ENGINE (the “wow”) ========================== */ let scanRunning = false; let scanTimer = null; let countdownTimer = null; function setHolo(status, pct) { if (els.holoText) els.holoText.textContent = status; if (els.holoFill) els.holoFill.style.width = `${clamp(pct, 0, 100)}%`; } function setAnalyzing(on) { if (els.analyzingLine) els.analyzingLine.hidden = !on; } function playScanSound() { if (!els.scanSfx) return; try { els.scanSfx.currentTime = 0; els.scanSfx.volume = 0.55; els.scanSfx.play().catch(() => {}); } catch {} } function resetAll() { scanRunning = false; if (scanTimer) clearInterval(scanTimer); if (countdownTimer) clearInterval(countdownTimer); setHolo(t('sysReady'), 0); setAnalyzing(false); if (els.chartWrap) els.chartWrap.classList.remove('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.remove('show'); if (els.overlayFill) els.overlayFill.style.width = '0%'; if (els.resultPanel) els.resultPanel.classList.add('hidden'); if (els.progressBar) els.progressBar.style.width = '0%'; if (els.timerText) els.timerText.textContent = '--:-- / --:--'; // reset factors if (els.volFactor) els.volFactor.textContent = '--'; if (els.momFactor) els.momFactor.textContent = '--'; if (els.strFactor) els.strFactor.textContent = '--'; if (els.liqFactor) els.liqFactor.textContent = '--'; drawChart(generateSeries(60, 0.5)); toast(t('reset'), 'ok'); haptic('notification', 'success'); } function startScan() { if (scanRunning) return; scanRunning = true; // UI changes setAnalyzing(true); setHolo(t('sysScan'), 12); if (els.chartWrap) els.chartWrap.classList.add('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.add('show'); if (els.overlayLine) els.overlayLine.textContent = t('analyzing'); if (els.overlayFill) els.overlayFill.style.width = '0%'; playScanSound(); haptic('impact', 'medium'); // seed const seedStr = `${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`; const seed = xmur3(seedStr)(); const rnd = mulberry32(seed); // simulate scan progress let p = 0; scanTimer = setInterval(() => { p += 4 + rnd() * 7; p = clamp(p, 0, 100); if (els.overlayFill) els.overlayFill.style.width = `${p}%`; setHolo(t('sysScan'), Math.round(12 + p * 0.7)); // animate chart gradually if (p % 12 < 6) drawChart(generateSeries(60, 0.9 + rnd())); if (p >= 100) { clearInterval(scanTimer); finishScan(rnd); } }, 140); } function finishScan(rnd) { scanRunning = false; // Decide direction & confidence const dirUp = rnd() > 0.48; const confidence = Math.round(64 + rnd() * 32); // 64..96 const vol = Math.round(45 + rnd() * 50); const mom = Math.round(40 + rnd() * 55); const str = Math.round(48 + rnd() * 48); const liq = Math.round(50 + rnd() * 45); // Update factors if (els.volFactor) els.volFactor.textContent = `${vol}%`; if (els.momFactor) els.momFactor.textContent = `${mom}%`; if (els.strFactor) els.strFactor.textContent = `${str}%`; if (els.liqFactor) els.liqFactor.textContent = `${liq}%`; // Update chart one last time with more drift drawChart(generateSeries(60, 1.6 + rnd())); // Result panel if (els.resultPanel) els.resultPanel.classList.remove('hidden'); if (els.rAsset) els.rAsset.textContent = state.asset; if (els.rTf) els.rTf.textContent = state.timeframe; if (els.rAcc) els.rAcc.textContent = `${confidence}%`; if (els.dirText) els.dirText.textContent = dirUp ? 'UP' : 'DOWN'; if (els.dirDot) { els.dirDot.classList.toggle('up', dirUp); els.dirDot.classList.toggle('down', !dirUp); } // until time: “window” const now = new Date(); const until = new Date(now.getTime() + (dirUp ? 55 : 45) * 1000); const untilHHMM = `${pad2(until.getHours())}:${pad2(until.getMinutes())}`; if (els.rUntil) els.rUntil.textContent = untilHHMM; // Window progress countdown startCountdown(45 + Math.round(rnd() * 30)); // overlay off if (els.chartOverlay) els.chartOverlay.classList.remove('show'); setAnalyzing(false); setHolo(t('sysReady'), 100); // store state.lastResult = { ts: Date.now(), asset: state.asset, tf: state.timeframe, market: state.market, dir: dirUp ? 'UP' : 'DOWN', confidence, factors: { vol, mom, str, liq }, }; saveState(); toast('RESULT READY', 'ok'); haptic('notification', 'success'); } function startCountdown(seconds) { if (!els.progressBar || !els.timerText) return; if (countdownTimer) clearInterval(countdownTimer); const total = seconds; let left = seconds; const start = nowHHMM(); const end = (() => { const d = new Date(Date.now() + total * 1000); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; })(); const tick = () => { left = clamp(left, 0, total); const pct = ((total - left) / total) * 100; els.progressBar.style.width = `${pct}%`; const mm = Math.floor(left / 60); const ss = left % 60; els.timerText.textContent = `${start} / ${end} • ${pad2(mm)}:${pad2(ss)}`; left -= 1; if (left < 0) { clearInterval(countdownTimer); toast('WINDOW CLOSED', 'info'); } }; tick(); countdownTimer = setInterval(tick, 1000); } /* ========================= 15) BIND EVENTS ========================== */ function bindEvents() { // Gate if (els.btnOpenLink) { els.btnOpenLink.addEventListener('click', () => { haptic('impact', 'light'); openLink(REG_URL); }); } if (els.chkRegistered) { els.chkRegistered.addEventListener('change', () => { state.registered = els.chkRegistered.checked; saveState(); updateGateUI(); haptic('selection'); }); } if (els.btnEnter) { els.btnEnter.addEventListener('click', () => { haptic('impact', 'medium'); enterApp(); }); } // Topbar if (els.btnLang) { els.btnLang.addEventListener('click', () => { haptic('selection'); renderLangList(); openModal(els.langModal); }); } if (els.btnMenu) { els.btnMenu.addEventListener('click', () => { ensureCommandPalette(); openModal(cmdModal); }); } // Selectors if (els.assetBtn) { els.assetBtn.addEventListener('click', () => { activeAssetCat = state.assetCat; renderAssetTabs(); renderAssetList(); openModal(els.assetsModal); }); } if (els.tfBtn) { els.tfBtn.addEventListener('click', () => { renderTfList(); openModal(els.tfModal); }); } if (els.marketBtn) { els.marketBtn.addEventListener('click', () => toggleMarket()); } // Close modal buttons if (els.closeAssets) els.closeAssets.addEventListener('click', () => closeModal(els.assetsModal)); if (els.closeTf) els.closeTf.addEventListener('click', () => closeModal(els.tfModal)); if (els.closeLang) els.closeLang.addEventListener('click', () => closeModal(els.langModal)); // Search if (els.assetSearch) { els.assetSearch.addEventListener('input', () => renderAssetList()); } // Analyze / Reset if (els.btnAnalyze) { els.btnAnalyze.addEventListener('click', () => { haptic('impact', 'medium'); startScan(); }); } if (els.btnReset) { els.btnReset.addEventListener('click', () => { haptic('impact', 'light'); resetAll(); }); } } /* ========================= 16) BOOT ========================== */ function boot() { loadState(); bindElements(); bindModalCore(); // Telegram init tgReady(); applyThemeFromTelegram(); if (tg) { tg.onEvent('themeChanged', applyThemeFromTelegram); tg.onEvent('viewportChanged', () => { // could respond to height changes if needed }); } // Texts applyTexts(); // Gate UI updateGateUI(); // Current selections applySelectionsToUI(); // Modals renderAssetTabs(); renderAssetList(); renderTfList(); renderLangList(); // Chart initChart(); // Buttons bindEvents(); // If user already registered previously -> jump to app if (state.registered) { if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); } else { showGate(); } // Make sure key UI is sane setHolo(t('sysReady'), 0); } // Wait for DOM ready (in case script not deferred) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();/* ============================================================ CRAFT COSMOS • app.js (Telegram Mini App) - Works in Telegram + works in browser (fallback) - Never breaks clicks - Gate -> App flow - Stable modals (assets / timeframes / language) + command palette - Haptics, toasts, persistent settings - “AI scan” animation + canvas chart render + results ============================================================ */ (() => { 'use strict'; /* ========================= 0) CONFIG — EDIT ONLY THIS ========================== */ const REG_URL = 'https://EXAMPLE.com/register'; // <-- ВСТАВЬ СЮДА СВОЮ ССЫЛКУ РЕГИСТРАЦИИ const BRAND = { name: 'CRAFT ANALYTICS', short: 'CA', }; /* ========================= 1) HELPERS ========================== */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const pad2 = (n) => String(n).padStart(2, '0'); // Seeded RNG for stable-but-random results (so it feels “real”) function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function nowHHMM() { const d = new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function safeJSONParse(s, fallback) { try { return JSON.parse(s); } catch { return fallback; } } /* ========================= 2) TELEGRAM BRIDGE ========================== */ const tg = (window.Telegram && window.Telegram.WebApp) ? window.Telegram.WebApp : null; const isTelegram = !!tg; function tgReady() { if (!tg) return; try { tg.ready(); tg.expand(); // optional: tg.enableClosingConfirmation(); // если хочешь спрашивать перед закрытием } catch {} } function haptic(type = 'impact', style = 'medium') { if (!tg || !tg.HapticFeedback) return; try { if (type === 'impact') tg.HapticFeedback.impactOccurred(style); if (type === 'selection') tg.HapticFeedback.selectionChanged(); if (type === 'notification') tg.HapticFeedback.notificationOccurred(style); // 'success'|'warning'|'error' } catch {} } function openLink(url) { if (!url) return; try { if (tg && tg.openLink) { tg.openLink(url); } else { window.open(url, '_blank', 'noopener'); } } catch { window.location.href = url; } } function applyThemeFromTelegram() { if (!tg) return; const p = tg.themeParams || {}; // If you want: adapt CSS variables from Telegram themeParams // But we keep premium dark by default. // Still, for readability you can sync: document.documentElement.style.setProperty('--tg-bg', p.bg_color || '#050710'); document.documentElement.style.setProperty('--tg-text', p.text_color || '#ffffff'); } /* ========================= 3) STATE + PERSISTENCE ========================== */ const STORAGE_KEY = 'craft_cosmos_state_v1'; const state = { registered: false, lang: 'ru', asset: 'EUR/USD', assetCat: 'FX', timeframe: '30s', market: 'OTC', lastResult: null, }; function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = safeJSONParse(raw, null); if (!data) return; Object.assign(state, data); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /* ========================= 4) DATA: ASSETS / TF / LANG ========================== */ const ASSETS = [ { cat: 'FX', items: ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CHF', 'USD/CAD', 'EUR/JPY', 'EUR/GBP'] }, { cat: 'CRYPTO', items: ['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD', 'BNB/USD'] }, { cat: 'INDEX', items: ['S&P 500', 'NASDAQ', 'DAX', 'FTSE 100'] }, { cat: 'COM', items: ['Gold', 'Silver', 'Oil (WTI)'] }, ]; const TIMEFRAMES = ['5s', '15s', '30s', '1m', '3m', '5m']; const LANGS = [ { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, ]; const STRINGS = { ru: { gateTitle: 'Доступ к интерфейсу', gateText: 'CRAFT ANALYTICS — демо интерфейс. Для активации доступа зарегистрируйтесь по ссылке и пополните баланс, затем вернитесь и нажмите “Открыть интерфейс”.', openReg: 'Открыть ссылку регистрации', chk: 'Я зарегистрировался', enter: 'Открыть интерфейс', subTitle: 'AI Market Scanner', hint: 'Нажмите “Запустить анализ” — и получите результат.', analyze: 'Запустить анализ', reset: 'Сброс', analyzing: 'Сканирование микросигналов…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Скопировано', toastShared: 'Ссылка подготовлена', toastSaved: 'Пресет сохранён', needCheck: 'Поставьте галочку “Я зарегистрировался”', }, en: { gateTitle: 'Access gate', gateText: 'CRAFT ANALYTICS is a demo interface. Register via the link and fund your account, then return and tap “Enter interface”.', openReg: 'Open registration link', chk: "I'm registered", enter: 'Enter interface', subTitle: 'AI Market Scanner', hint: 'Tap “Run scan” to get a result.', analyze: 'Run scan', reset: 'Reset', analyzing: 'Scanning micro-signals…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Copied', toastShared: 'Share ready', toastSaved: 'Preset saved', needCheck: 'Check “I’m registered” first', }, }; function t(key) { const pack = STRINGS[state.lang] || STRINGS.ru; return pack[key] || STRINGS.ru[key] || key; } /* ========================= 5) ELEMENTS (match your HTML) ========================== */ const els = { gate: null, app: null, btnOpenLink: null, chkRegistered: null, btnEnter: null, btnLang: null, btnMenu: null, assetBtn: null, tfBtn: null, marketBtn: null, btnAnalyze: null, btnReset: null, chartWrap: null, chart: null, chartOverlay: null, overlayLine: null, overlayFill: null, volFactor: null, momFactor: null, strFactor: null, liqFactor: null, holoFill: null, holoText: null, analyzingLine: null, analyzingText: null, resultPanel: null, rAsset: null, rTf: null, rAcc: null, dirDot: null, dirText: null, rUntil: null, progressBar: null, timerText: null, backdrop: null, assetsModal: null, closeAssets: null, assetSearch: null, assetTabs: null, assetList: null, tfModal: null, closeTf: null, tfList: null, langModal: null, closeLang: null, langList: null, scanSfx: null, }; function bindElements() { els.gate = $('#gate'); els.app = $('#app'); els.btnOpenLink = $('#btnOpenLink'); els.chkRegistered = $('#chkRegistered'); els.btnEnter = $('#btnEnter'); els.btnLang = $('#btnLang'); els.btnMenu = $('#btnMenu'); els.assetBtn = $('#assetBtn'); els.tfBtn = $('#tfBtn'); els.marketBtn = $('#marketBtn'); els.btnAnalyze = $('#btnAnalyze'); els.btnReset = $('#btnReset'); els.chartWrap = $('#chartWrap'); els.chart = $('#chart'); els.chartOverlay = $('#chartOverlay'); els.overlayLine = $('#overlayLine'); els.overlayFill = $('#overlayFill'); els.volFactor = $('#volFactor'); els.momFactor = $('#momFactor'); els.strFactor = $('#strFactor'); els.liqFactor = $('#liqFactor'); els.holoFill = $('#holoFill'); els.holoText = $('#holoText'); els.analyzingLine = $('#analyzingLine'); els.analyzingText = $('#analyzingText'); els.resultPanel = $('#resultPanel'); els.rAsset = $('#rAsset'); els.rTf = $('#rTf'); els.rAcc = $('#rAcc'); els.dirDot = $('#dirDot'); els.dirText = $('#dirText'); els.rUntil = $('#rUntil'); els.progressBar = $('#progressBar'); els.timerText = $('#timerText'); els.backdrop = $('#backdrop'); els.assetsModal = $('#assetsModal'); els.closeAssets = $('#closeAssets'); els.assetSearch = $('#assetSearch'); els.assetTabs = $('#assetTabs'); els.assetList = $('#assetList'); els.tfModal = $('#tfModal'); els.closeTf = $('#closeTf'); els.tfList = $('#tfList'); els.langModal = $('#langModal'); els.closeLang = $('#closeLang'); els.langList = $('#langList'); els.scanSfx = $('#scanSfx'); } /* ========================= 6) UI: TOASTS ========================== */ let toastHost = null; function ensureToastHost() { if (toastHost) return; toastHost = document.createElement('div'); toastHost.style.position = 'fixed'; toastHost.style.left = '0'; toastHost.style.right = '0'; toastHost.style.bottom = 'calc(env(safe-area-inset-bottom, 0px) + 18px)'; toastHost.style.zIndex = '9999'; toastHost.style.display = 'grid'; toastHost.style.placeItems = 'center'; toastHost.style.pointerEvents = 'none'; document.body.appendChild(toastHost); } function toast(msg, kind = 'info') { ensureToastHost(); const el = document.createElement('div'); el.textContent = msg; el.style.pointerEvents = 'none'; el.style.padding = '10px 12px'; el.style.borderRadius = '999px'; el.style.border = '1px solid rgba(255,255,255,.14)'; el.style.background = kind === 'ok' ? 'linear-gradient(135deg, rgba(120,255,180,.20), rgba(0,178,255,.10))' : kind === 'bad' ? 'linear-gradient(135deg, rgba(255,90,110,.22), rgba(124,92,255,.10))' : 'linear-gradient(135deg, rgba(124,92,255,.18), rgba(0,178,255,.10))'; el.style.color = 'rgba(255,255,255,.92)'; el.style.fontWeight = '900'; el.style.letterSpacing = '.08em'; el.style.boxShadow = '0 18px 44px rgba(0,0,0,.32)'; el.style.transform = 'translateY(8px) scale(.98)'; el.style.opacity = '0'; el.style.transition = 'opacity .18s ease, transform .18s ease'; toastHost.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px) scale(.98)'; setTimeout(() => el.remove(), 220); }, 1400); } /* ========================= 7) UI: MODALS (stable) ========================== */ let modalOpen = null; function showBackdrop(on) { if (!els.backdrop) return; els.backdrop.classList.toggle('hidden', !on); els.backdrop.setAttribute('aria-hidden', on ? 'false' : 'true'); } function openModal(modalEl) { if (!modalEl) return; modalOpen = modalEl; showBackdrop(true); modalEl.classList.remove('hidden'); modalEl.setAttribute('aria-hidden', 'false'); haptic('selection'); // Focus first focusable const focusable = modalEl.querySelector('input,button,[tabindex]:not([tabindex="-1"])'); if (focusable) setTimeout(() => focusable.focus(), 0); } function closeModal(modalEl) { const m = modalEl || modalOpen; if (!m) return; m.classList.add('hidden'); m.setAttribute('aria-hidden', 'true'); modalOpen = null; showBackdrop(false); haptic('selection'); } function bindModalCore() { if (els.backdrop) { els.backdrop.addEventListener('click', () => closeModal()); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOpen) closeModal(); }); } /* ========================= 8) UI: COMMAND PALETTE (VIP) ========================== */ let cmdModal = null; let cmdList = null; const COMMANDS = [ { name: 'Run Scan', hint: 'Запустить анализ / Run scan', key: 'Enter', run: () => startScan() }, { name: 'Open Registration', hint: 'Открыть ссылку регистрации', key: 'R', run: () => openLink(REG_URL) }, { name: 'Switch Language', hint: 'RU ↔ EN', key: 'L', run: () => toggleLang() }, { name: 'Reset', hint: 'Сбросить панель', key: 'X', run: () => resetAll() }, { name: 'Toggle Market', hint: 'OTC ↔ Live', key: 'M', run: () => toggleMarket() }, ]; function ensureCommandPalette() { if (cmdModal) return; cmdModal = document.createElement('section'); cmdModal.className = 'modal hidden'; cmdModal.id = 'cmdModal'; cmdModal.setAttribute('role', 'dialog'); cmdModal.setAttribute('aria-modal', 'true'); cmdModal.setAttribute('aria-label', 'Command palette'); cmdModal.innerHTML = `
COMMAND PALETTE
`; document.body.appendChild(cmdModal); const closeBtn = $('#closeCmd', cmdModal); const search = $('#cmdSearch', cmdModal); cmdList = $('#cmdList', cmdModal); closeBtn.addEventListener('click', () => closeModal(cmdModal)); search.addEventListener('input', () => renderCommands(search.value)); renderCommands(''); // keyboard: Ctrl+K / Cmd+K document.addEventListener('keydown', (e) => { const isK = e.key.toLowerCase() === 'k'; if ((e.ctrlKey || e.metaKey) && isK) { e.preventDefault(); openModal(cmdModal); setTimeout(() => search.focus(), 0); } }); // quick hotkeys document.addEventListener('keydown', (e) => { if (modalOpen) return; const k = e.key.toLowerCase(); if (k === 'r') openLink(REG_URL); if (k === 'l') toggleLang(); if (k === 'm') toggleMarket(); }); } function renderCommands(filter) { const q = (filter || '').trim().toLowerCase(); const items = COMMANDS.filter(c => !q ? true : (c.name.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)) ); cmdList.innerHTML = items.map(c => ` `).join(''); $$('.cmdItem', cmdList).forEach((btn, idx) => { btn.addEventListener('click', () => { closeModal(cmdModal); items[idx].run(); }); }); } /* ========================= 9) TEXTS / LOCALIZATION BIND ========================== */ function applyTexts() { const gateTitle = $('#gateTitle'); const gateText = $('#gateText'); const btnOpenLinkText = $('#btnOpenLinkText'); const chkText = $('#chkText'); const btnEnterText = $('#btnEnterText'); const subTitle = $('#subTitle'); const hintText = $('#hintText'); const analyzeText = $('#analyzeText'); const resetText = $('#resetText'); if (gateTitle) gateTitle.textContent = t('gateTitle'); if (gateText) gateText.textContent = t('gateText'); if (btnOpenLinkText) btnOpenLinkText.textContent = t('openReg'); if (chkText) chkText.textContent = t('chk'); if (btnEnterText) btnEnterText.textContent = t('enter'); if (subTitle) subTitle.textContent = t('subTitle'); if (hintText) hintText.textContent = t('hint'); if (analyzeText) analyzeText.textContent = t('analyze'); if (resetText) resetText.textContent = t('reset'); if (els.analyzingText) els.analyzingText.textContent = t('analyzing'); if (els.holoText) els.holoText.textContent = t('sysReady'); } function toggleLang() { state.lang = (state.lang === 'ru') ? 'en' : 'ru'; saveState(); applyTexts(); toast(state.lang === 'ru' ? 'Русский' : 'English', 'ok'); haptic('notification', 'success'); } /* ========================= 10) GATE FLOW ========================== */ function updateGateUI() { if (!els.chkRegistered || !els.btnEnter) return; els.chkRegistered.checked = !!state.registered; els.btnEnter.disabled = !els.chkRegistered.checked; } function enterApp() { if (els.chkRegistered && !els.chkRegistered.checked) { toast(t('needCheck'), 'bad'); haptic('notification', 'warning'); return; } state.registered = true; saveState(); if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); haptic('notification', 'success'); if (tg && tg.MainButton) { tg.MainButton.hide(); } } function showGate() { if (els.app) els.app.classList.add('hidden'); if (els.gate) els.gate.classList.remove('hidden'); } /* ========================= 11) ASSET / TF / MARKET UI ========================== */ function applySelectionsToUI() { const assetValue = $('#assetValue'); const assetBadge = $('#assetBadge'); const tfValue = $('#tfValue'); const marketValue = $('#marketValue'); if (assetValue) assetValue.textContent = state.asset; if (assetBadge) { assetBadge.textContent = state.assetCat === 'FX' ? '🌍' : state.assetCat === 'CRYPTO' ? '₿' : state.assetCat === 'INDEX' ? '📈' : '⛏'; } if (tfValue) tfValue.textContent = state.timeframe; if (marketValue) marketValue.textContent = state.market; } function toggleMarket() { state.market = (state.market === 'OTC') ? 'LIVE' : 'OTC'; saveState(); applySelectionsToUI(); toast(`MARKET: ${state.market}`, 'ok'); haptic('selection'); } /* ========================= 12) MODAL RENDERERS ========================== */ let activeAssetCat = null; function renderAssetTabs() { if (!els.assetTabs) return; els.assetTabs.innerHTML = ASSETS.map(a => { const active = (activeAssetCat || state.assetCat) === a.cat; return ``; }).join(''); $$('.tab', els.assetTabs).forEach(btn => { btn.addEventListener('click', () => { activeAssetCat = btn.dataset.cat; renderAssetTabs(); renderAssetList(); haptic('selection'); }); }); } function renderAssetList() { if (!els.assetList) return; const cat = activeAssetCat || state.assetCat; const q = (els.assetSearch?.value || '').trim().toLowerCase(); const group = ASSETS.find(x => x.cat === cat) || ASSETS[0]; let items = group.items; if (q) items = items.filter(s => s.toLowerCase().includes(q)); els.assetList.innerHTML = items.map(sym => ` `).join(''); $$('.listItem', els.assetList).forEach(btn => { btn.addEventListener('click', () => { state.asset = btn.dataset.sym; state.assetCat = cat; saveState(); applySelectionsToUI(); closeModal(els.assetsModal); haptic('notification', 'success'); }); }); } function renderTfList() { if (!els.tfList) return; els.tfList.innerHTML = TIMEFRAMES.map(tf => ` `).join(''); $$('.listItem', els.tfList).forEach(btn => { btn.addEventListener('click', () => { state.timeframe = btn.dataset.tf; saveState(); applySelectionsToUI(); closeModal(els.tfModal); haptic('notification', 'success'); }); }); } function renderLangList() { if (!els.langList) return; els.langList.innerHTML = LANGS.map(l => ` `).join(''); $$('.listItem', els.langList).forEach(btn => { btn.addEventListener('click', () => { state.lang = btn.dataset.lang; saveState(); applyTexts(); renderLangList(); applySelectionsToUI(); closeModal(els.langModal); haptic('notification', 'success'); }); }); } /* ========================= 13) CANVAS CHART (premium) ========================== */ let chartCtx = null; function initChart() { if (!els.chart) return; chartCtx = els.chart.getContext('2d', { alpha: true }); drawChart(generateSeries(60, 0.5)); } function generateSeries(n = 60, drift = 0.5) { const seed = xmur3(`${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`)(); const rnd = mulberry32(seed); let v = 100 + rnd() * 20; const arr = []; for (let i = 0; i < n; i++) { v += (rnd() - 0.5) * (drift * 3); arr.push(v); } return arr; } function drawChart(series) { if (!chartCtx || !els.chart) return; const ctx = chartCtx; const w = els.chart.width; const h = els.chart.height; ctx.clearRect(0, 0, w, h); // background subtle gradient const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, 'rgba(124,92,255,0.08)'); g.addColorStop(1, 'rgba(0,178,255,0.05)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // grid ctx.save(); ctx.globalAlpha = 0.22; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; const stepX = 60; const stepY = 42; for (let x = 0; x <= w; x += stepX) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } for (let y = 0; y <= h; y += stepY) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.restore(); // normalize const min = Math.min(...series); const max = Math.max(...series); const px = (i) => (i / (series.length - 1)) * (w - 60) + 30; const py = (v) => { const t = (v - min) / (max - min || 1); return (1 - t) * (h - 60) + 30; }; // glow line ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = 'rgba(124,92,255,0.20)'; ctx.lineWidth = 10; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.strokeStyle = 'rgba(0,178,255,0.16)'; ctx.lineWidth = 8; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // main line ctx.strokeStyle = 'rgba(255,255,255,0.82)'; ctx.lineWidth = 2.2; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // last point highlight const lx = px(series.length - 1); const ly = py(series[series.length - 1]); ctx.fillStyle = 'rgba(120,255,180,0.92)'; ctx.shadowColor = 'rgba(120,255,180,0.75)'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(lx, ly, 4.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // HUD labels ctx.save(); ctx.font = '900 14px ui-sans-serif, system-ui'; ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillText(`${state.asset} • ${state.timeframe} • ${state.market}`, 30, 24); ctx.restore(); } /* ========================= 14) SCAN ENGINE (the “wow”) ========================== */ let scanRunning = false; let scanTimer = null; let countdownTimer = null; function setHolo(status, pct) { if (els.holoText) els.holoText.textContent = status; if (els.holoFill) els.holoFill.style.width = `${clamp(pct, 0, 100)}%`; } function setAnalyzing(on) { if (els.analyzingLine) els.analyzingLine.hidden = !on; } function playScanSound() { if (!els.scanSfx) return; try { els.scanSfx.currentTime = 0; els.scanSfx.volume = 0.55; els.scanSfx.play().catch(() => {}); } catch {} } function resetAll() { scanRunning = false; if (scanTimer) clearInterval(scanTimer); if (countdownTimer) clearInterval(countdownTimer); setHolo(t('sysReady'), 0); setAnalyzing(false); if (els.chartWrap) els.chartWrap.classList.remove('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.remove('show'); if (els.overlayFill) els.overlayFill.style.width = '0%'; if (els.resultPanel) els.resultPanel.classList.add('hidden'); if (els.progressBar) els.progressBar.style.width = '0%'; if (els.timerText) els.timerText.textContent = '--:-- / --:--'; // reset factors if (els.volFactor) els.volFactor.textContent = '--'; if (els.momFactor) els.momFactor.textContent = '--'; if (els.strFactor) els.strFactor.textContent = '--'; if (els.liqFactor) els.liqFactor.textContent = '--'; drawChart(generateSeries(60, 0.5)); toast(t('reset'), 'ok'); haptic('notification', 'success'); } function startScan() { if (scanRunning) return; scanRunning = true; // UI changes setAnalyzing(true); setHolo(t('sysScan'), 12); if (els.chartWrap) els.chartWrap.classList.add('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.add('show'); if (els.overlayLine) els.overlayLine.textContent = t('analyzing'); if (els.overlayFill) els.overlayFill.style.width = '0%'; playScanSound(); haptic('impact', 'medium'); // seed const seedStr = `${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`; const seed = xmur3(seedStr)(); const rnd = mulberry32(seed); // simulate scan progress let p = 0; scanTimer = setInterval(() => { p += 4 + rnd() * 7; p = clamp(p, 0, 100); if (els.overlayFill) els.overlayFill.style.width = `${p}%`; setHolo(t('sysScan'), Math.round(12 + p * 0.7)); // animate chart gradually if (p % 12 < 6) drawChart(generateSeries(60, 0.9 + rnd())); if (p >= 100) { clearInterval(scanTimer); finishScan(rnd); } }, 140); } function finishScan(rnd) { scanRunning = false; // Decide direction & confidence const dirUp = rnd() > 0.48; const confidence = Math.round(64 + rnd() * 32); // 64..96 const vol = Math.round(45 + rnd() * 50); const mom = Math.round(40 + rnd() * 55); const str = Math.round(48 + rnd() * 48); const liq = Math.round(50 + rnd() * 45); // Update factors if (els.volFactor) els.volFactor.textContent = `${vol}%`; if (els.momFactor) els.momFactor.textContent = `${mom}%`; if (els.strFactor) els.strFactor.textContent = `${str}%`; if (els.liqFactor) els.liqFactor.textContent = `${liq}%`; // Update chart one last time with more drift drawChart(generateSeries(60, 1.6 + rnd())); // Result panel if (els.resultPanel) els.resultPanel.classList.remove('hidden'); if (els.rAsset) els.rAsset.textContent = state.asset; if (els.rTf) els.rTf.textContent = state.timeframe; if (els.rAcc) els.rAcc.textContent = `${confidence}%`; if (els.dirText) els.dirText.textContent = dirUp ? 'UP' : 'DOWN'; if (els.dirDot) { els.dirDot.classList.toggle('up', dirUp); els.dirDot.classList.toggle('down', !dirUp); } // until time: “window” const now = new Date(); const until = new Date(now.getTime() + (dirUp ? 55 : 45) * 1000); const untilHHMM = `${pad2(until.getHours())}:${pad2(until.getMinutes())}`; if (els.rUntil) els.rUntil.textContent = untilHHMM; // Window progress countdown startCountdown(45 + Math.round(rnd() * 30)); // overlay off if (els.chartOverlay) els.chartOverlay.classList.remove('show'); setAnalyzing(false); setHolo(t('sysReady'), 100); // store state.lastResult = { ts: Date.now(), asset: state.asset, tf: state.timeframe, market: state.market, dir: dirUp ? 'UP' : 'DOWN', confidence, factors: { vol, mom, str, liq }, }; saveState(); toast('RESULT READY', 'ok'); haptic('notification', 'success'); } function startCountdown(seconds) { if (!els.progressBar || !els.timerText) return; if (countdownTimer) clearInterval(countdownTimer); const total = seconds; let left = seconds; const start = nowHHMM(); const end = (() => { const d = new Date(Date.now() + total * 1000); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; })(); const tick = () => { left = clamp(left, 0, total); const pct = ((total - left) / total) * 100; els.progressBar.style.width = `${pct}%`; const mm = Math.floor(left / 60); const ss = left % 60; els.timerText.textContent = `${start} / ${end} • ${pad2(mm)}:${pad2(ss)}`; left -= 1; if (left < 0) { clearInterval(countdownTimer); toast('WINDOW CLOSED', 'info'); } }; tick(); countdownTimer = setInterval(tick, 1000); } /* ========================= 15) BIND EVENTS ========================== */ function bindEvents() { // Gate if (els.btnOpenLink) { els.btnOpenLink.addEventListener('click', () => { haptic('impact', 'light'); openLink(REG_URL); }); } if (els.chkRegistered) { els.chkRegistered.addEventListener('change', () => { state.registered = els.chkRegistered.checked; saveState(); updateGateUI(); haptic('selection'); }); } if (els.btnEnter) { els.btnEnter.addEventListener('click', () => { haptic('impact', 'medium'); enterApp(); }); } // Topbar if (els.btnLang) { els.btnLang.addEventListener('click', () => { haptic('selection'); renderLangList(); openModal(els.langModal); }); } if (els.btnMenu) { els.btnMenu.addEventListener('click', () => { ensureCommandPalette(); openModal(cmdModal); }); } // Selectors if (els.assetBtn) { els.assetBtn.addEventListener('click', () => { activeAssetCat = state.assetCat; renderAssetTabs(); renderAssetList(); openModal(els.assetsModal); }); } if (els.tfBtn) { els.tfBtn.addEventListener('click', () => { renderTfList(); openModal(els.tfModal); }); } if (els.marketBtn) { els.marketBtn.addEventListener('click', () => toggleMarket()); } // Close modal buttons if (els.closeAssets) els.closeAssets.addEventListener('click', () => closeModal(els.assetsModal)); if (els.closeTf) els.closeTf.addEventListener('click', () => closeModal(els.tfModal)); if (els.closeLang) els.closeLang.addEventListener('click', () => closeModal(els.langModal)); // Search if (els.assetSearch) { els.assetSearch.addEventListener('input', () => renderAssetList()); } // Analyze / Reset if (els.btnAnalyze) { els.btnAnalyze.addEventListener('click', () => { haptic('impact', 'medium'); startScan(); }); } if (els.btnReset) { els.btnReset.addEventListener('click', () => { haptic('impact', 'light'); resetAll(); }); } } /* ========================= 16) BOOT ========================== */ function boot() { loadState(); bindElements(); bindModalCore(); // Telegram init tgReady(); applyThemeFromTelegram(); if (tg) { tg.onEvent('themeChanged', applyThemeFromTelegram); tg.onEvent('viewportChanged', () => { // could respond to height changes if needed }); } // Texts applyTexts(); // Gate UI updateGateUI(); // Current selections applySelectionsToUI(); // Modals renderAssetTabs(); renderAssetList(); renderTfList(); renderLangList(); // Chart initChart(); // Buttons bindEvents(); // If user already registered previously -> jump to app if (state.registered) { if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); } else { showGate(); } // Make sure key UI is sane setHolo(t('sysReady'), 0); } // Wait for DOM ready (in case script not deferred) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();/* ============================================================ CRAFT COSMOS • app.js (Telegram Mini App) - Works in Telegram + works in browser (fallback) - Never breaks clicks - Gate -> App flow - Stable modals (assets / timeframes / language) + command palette - Haptics, toasts, persistent settings - “AI scan” animation + canvas chart render + results ============================================================ */ (() => { 'use strict'; /* ========================= 0) CONFIG — EDIT ONLY THIS ========================== */ const REG_URL = 'https://EXAMPLE.com/register'; // <-- ВСТАВЬ СЮДА СВОЮ ССЫЛКУ РЕГИСТРАЦИИ const BRAND = { name: 'CRAFT ANALYTICS', short: 'CA', }; /* ========================= 1) HELPERS ========================== */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const pad2 = (n) => String(n).padStart(2, '0'); // Seeded RNG for stable-but-random results (so it feels “real”) function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function nowHHMM() { const d = new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function safeJSONParse(s, fallback) { try { return JSON.parse(s); } catch { return fallback; } } /* ========================= 2) TELEGRAM BRIDGE ========================== */ const tg = (window.Telegram && window.Telegram.WebApp) ? window.Telegram.WebApp : null; const isTelegram = !!tg; function tgReady() { if (!tg) return; try { tg.ready(); tg.expand(); // optional: tg.enableClosingConfirmation(); // если хочешь спрашивать перед закрытием } catch {} } function haptic(type = 'impact', style = 'medium') { if (!tg || !tg.HapticFeedback) return; try { if (type === 'impact') tg.HapticFeedback.impactOccurred(style); if (type === 'selection') tg.HapticFeedback.selectionChanged(); if (type === 'notification') tg.HapticFeedback.notificationOccurred(style); // 'success'|'warning'|'error' } catch {} } function openLink(url) { if (!url) return; try { if (tg && tg.openLink) { tg.openLink(url); } else { window.open(url, '_blank', 'noopener'); } } catch { window.location.href = url; } } function applyThemeFromTelegram() { if (!tg) return; const p = tg.themeParams || {}; // If you want: adapt CSS variables from Telegram themeParams // But we keep premium dark by default. // Still, for readability you can sync: document.documentElement.style.setProperty('--tg-bg', p.bg_color || '#050710'); document.documentElement.style.setProperty('--tg-text', p.text_color || '#ffffff'); } /* ========================= 3) STATE + PERSISTENCE ========================== */ const STORAGE_KEY = 'craft_cosmos_state_v1'; const state = { registered: false, lang: 'ru', asset: 'EUR/USD', assetCat: 'FX', timeframe: '30s', market: 'OTC', lastResult: null, }; function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = safeJSONParse(raw, null); if (!data) return; Object.assign(state, data); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /* ========================= 4) DATA: ASSETS / TF / LANG ========================== */ const ASSETS = [ { cat: 'FX', items: ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CHF', 'USD/CAD', 'EUR/JPY', 'EUR/GBP'] }, { cat: 'CRYPTO', items: ['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD', 'BNB/USD'] }, { cat: 'INDEX', items: ['S&P 500', 'NASDAQ', 'DAX', 'FTSE 100'] }, { cat: 'COM', items: ['Gold', 'Silver', 'Oil (WTI)'] }, ]; const TIMEFRAMES = ['5s', '15s', '30s', '1m', '3m', '5m']; const LANGS = [ { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, ]; const STRINGS = { ru: { gateTitle: 'Доступ к интерфейсу', gateText: 'CRAFT ANALYTICS — демо интерфейс. Для активации доступа зарегистрируйтесь по ссылке и пополните баланс, затем вернитесь и нажмите “Открыть интерфейс”.', openReg: 'Открыть ссылку регистрации', chk: 'Я зарегистрировался', enter: 'Открыть интерфейс', subTitle: 'AI Market Scanner', hint: 'Нажмите “Запустить анализ” — и получите результат.', analyze: 'Запустить анализ', reset: 'Сброс', analyzing: 'Сканирование микросигналов…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Скопировано', toastShared: 'Ссылка подготовлена', toastSaved: 'Пресет сохранён', needCheck: 'Поставьте галочку “Я зарегистрировался”', }, en: { gateTitle: 'Access gate', gateText: 'CRAFT ANALYTICS is a demo interface. Register via the link and fund your account, then return and tap “Enter interface”.', openReg: 'Open registration link', chk: "I'm registered", enter: 'Enter interface', subTitle: 'AI Market Scanner', hint: 'Tap “Run scan” to get a result.', analyze: 'Run scan', reset: 'Reset', analyzing: 'Scanning micro-signals…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Copied', toastShared: 'Share ready', toastSaved: 'Preset saved', needCheck: 'Check “I’m registered” first', }, }; function t(key) { const pack = STRINGS[state.lang] || STRINGS.ru; return pack[key] || STRINGS.ru[key] || key; } /* ========================= 5) ELEMENTS (match your HTML) ========================== */ const els = { gate: null, app: null, btnOpenLink: null, chkRegistered: null, btnEnter: null, btnLang: null, btnMenu: null, assetBtn: null, tfBtn: null, marketBtn: null, btnAnalyze: null, btnReset: null, chartWrap: null, chart: null, chartOverlay: null, overlayLine: null, overlayFill: null, volFactor: null, momFactor: null, strFactor: null, liqFactor: null, holoFill: null, holoText: null, analyzingLine: null, analyzingText: null, resultPanel: null, rAsset: null, rTf: null, rAcc: null, dirDot: null, dirText: null, rUntil: null, progressBar: null, timerText: null, backdrop: null, assetsModal: null, closeAssets: null, assetSearch: null, assetTabs: null, assetList: null, tfModal: null, closeTf: null, tfList: null, langModal: null, closeLang: null, langList: null, scanSfx: null, }; function bindElements() { els.gate = $('#gate'); els.app = $('#app'); els.btnOpenLink = $('#btnOpenLink'); els.chkRegistered = $('#chkRegistered'); els.btnEnter = $('#btnEnter'); els.btnLang = $('#btnLang'); els.btnMenu = $('#btnMenu'); els.assetBtn = $('#assetBtn'); els.tfBtn = $('#tfBtn'); els.marketBtn = $('#marketBtn'); els.btnAnalyze = $('#btnAnalyze'); els.btnReset = $('#btnReset'); els.chartWrap = $('#chartWrap'); els.chart = $('#chart'); els.chartOverlay = $('#chartOverlay'); els.overlayLine = $('#overlayLine'); els.overlayFill = $('#overlayFill'); els.volFactor = $('#volFactor'); els.momFactor = $('#momFactor'); els.strFactor = $('#strFactor'); els.liqFactor = $('#liqFactor'); els.holoFill = $('#holoFill'); els.holoText = $('#holoText'); els.analyzingLine = $('#analyzingLine'); els.analyzingText = $('#analyzingText'); els.resultPanel = $('#resultPanel'); els.rAsset = $('#rAsset'); els.rTf = $('#rTf'); els.rAcc = $('#rAcc'); els.dirDot = $('#dirDot'); els.dirText = $('#dirText'); els.rUntil = $('#rUntil'); els.progressBar = $('#progressBar'); els.timerText = $('#timerText'); els.backdrop = $('#backdrop'); els.assetsModal = $('#assetsModal'); els.closeAssets = $('#closeAssets'); els.assetSearch = $('#assetSearch'); els.assetTabs = $('#assetTabs'); els.assetList = $('#assetList'); els.tfModal = $('#tfModal'); els.closeTf = $('#closeTf'); els.tfList = $('#tfList'); els.langModal = $('#langModal'); els.closeLang = $('#closeLang'); els.langList = $('#langList'); els.scanSfx = $('#scanSfx'); } /* ========================= 6) UI: TOASTS ========================== */ let toastHost = null; function ensureToastHost() { if (toastHost) return; toastHost = document.createElement('div'); toastHost.style.position = 'fixed'; toastHost.style.left = '0'; toastHost.style.right = '0'; toastHost.style.bottom = 'calc(env(safe-area-inset-bottom, 0px) + 18px)'; toastHost.style.zIndex = '9999'; toastHost.style.display = 'grid'; toastHost.style.placeItems = 'center'; toastHost.style.pointerEvents = 'none'; document.body.appendChild(toastHost); } function toast(msg, kind = 'info') { ensureToastHost(); const el = document.createElement('div'); el.textContent = msg; el.style.pointerEvents = 'none'; el.style.padding = '10px 12px'; el.style.borderRadius = '999px'; el.style.border = '1px solid rgba(255,255,255,.14)'; el.style.background = kind === 'ok' ? 'linear-gradient(135deg, rgba(120,255,180,.20), rgba(0,178,255,.10))' : kind === 'bad' ? 'linear-gradient(135deg, rgba(255,90,110,.22), rgba(124,92,255,.10))' : 'linear-gradient(135deg, rgba(124,92,255,.18), rgba(0,178,255,.10))'; el.style.color = 'rgba(255,255,255,.92)'; el.style.fontWeight = '900'; el.style.letterSpacing = '.08em'; el.style.boxShadow = '0 18px 44px rgba(0,0,0,.32)'; el.style.transform = 'translateY(8px) scale(.98)'; el.style.opacity = '0'; el.style.transition = 'opacity .18s ease, transform .18s ease'; toastHost.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px) scale(.98)'; setTimeout(() => el.remove(), 220); }, 1400); } /* ========================= 7) UI: MODALS (stable) ========================== */ let modalOpen = null; function showBackdrop(on) { if (!els.backdrop) return; els.backdrop.classList.toggle('hidden', !on); els.backdrop.setAttribute('aria-hidden', on ? 'false' : 'true'); } function openModal(modalEl) { if (!modalEl) return; modalOpen = modalEl; showBackdrop(true); modalEl.classList.remove('hidden'); modalEl.setAttribute('aria-hidden', 'false'); haptic('selection'); // Focus first focusable const focusable = modalEl.querySelector('input,button,[tabindex]:not([tabindex="-1"])'); if (focusable) setTimeout(() => focusable.focus(), 0); } function closeModal(modalEl) { const m = modalEl || modalOpen; if (!m) return; m.classList.add('hidden'); m.setAttribute('aria-hidden', 'true'); modalOpen = null; showBackdrop(false); haptic('selection'); } function bindModalCore() { if (els.backdrop) { els.backdrop.addEventListener('click', () => closeModal()); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOpen) closeModal(); }); } /* ========================= 8) UI: COMMAND PALETTE (VIP) ========================== */ let cmdModal = null; let cmdList = null; const COMMANDS = [ { name: 'Run Scan', hint: 'Запустить анализ / Run scan', key: 'Enter', run: () => startScan() }, { name: 'Open Registration', hint: 'Открыть ссылку регистрации', key: 'R', run: () => openLink(REG_URL) }, { name: 'Switch Language', hint: 'RU ↔ EN', key: 'L', run: () => toggleLang() }, { name: 'Reset', hint: 'Сбросить панель', key: 'X', run: () => resetAll() }, { name: 'Toggle Market', hint: 'OTC ↔ Live', key: 'M', run: () => toggleMarket() }, ]; function ensureCommandPalette() { if (cmdModal) return; cmdModal = document.createElement('section'); cmdModal.className = 'modal hidden'; cmdModal.id = 'cmdModal'; cmdModal.setAttribute('role', 'dialog'); cmdModal.setAttribute('aria-modal', 'true'); cmdModal.setAttribute('aria-label', 'Command palette'); cmdModal.innerHTML = `
COMMAND PALETTE
`; document.body.appendChild(cmdModal); const closeBtn = $('#closeCmd', cmdModal); const search = $('#cmdSearch', cmdModal); cmdList = $('#cmdList', cmdModal); closeBtn.addEventListener('click', () => closeModal(cmdModal)); search.addEventListener('input', () => renderCommands(search.value)); renderCommands(''); // keyboard: Ctrl+K / Cmd+K document.addEventListener('keydown', (e) => { const isK = e.key.toLowerCase() === 'k'; if ((e.ctrlKey || e.metaKey) && isK) { e.preventDefault(); openModal(cmdModal); setTimeout(() => search.focus(), 0); } }); // quick hotkeys document.addEventListener('keydown', (e) => { if (modalOpen) return; const k = e.key.toLowerCase(); if (k === 'r') openLink(REG_URL); if (k === 'l') toggleLang(); if (k === 'm') toggleMarket(); }); } function renderCommands(filter) { const q = (filter || '').trim().toLowerCase(); const items = COMMANDS.filter(c => !q ? true : (c.name.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)) ); cmdList.innerHTML = items.map(c => ` `).join(''); $$('.cmdItem', cmdList).forEach((btn, idx) => { btn.addEventListener('click', () => { closeModal(cmdModal); items[idx].run(); }); }); } /* ========================= 9) TEXTS / LOCALIZATION BIND ========================== */ function applyTexts() { const gateTitle = $('#gateTitle'); const gateText = $('#gateText'); const btnOpenLinkText = $('#btnOpenLinkText'); const chkText = $('#chkText'); const btnEnterText = $('#btnEnterText'); const subTitle = $('#subTitle'); const hintText = $('#hintText'); const analyzeText = $('#analyzeText'); const resetText = $('#resetText'); if (gateTitle) gateTitle.textContent = t('gateTitle'); if (gateText) gateText.textContent = t('gateText'); if (btnOpenLinkText) btnOpenLinkText.textContent = t('openReg'); if (chkText) chkText.textContent = t('chk'); if (btnEnterText) btnEnterText.textContent = t('enter'); if (subTitle) subTitle.textContent = t('subTitle'); if (hintText) hintText.textContent = t('hint'); if (analyzeText) analyzeText.textContent = t('analyze'); if (resetText) resetText.textContent = t('reset'); if (els.analyzingText) els.analyzingText.textContent = t('analyzing'); if (els.holoText) els.holoText.textContent = t('sysReady'); } function toggleLang() { state.lang = (state.lang === 'ru') ? 'en' : 'ru'; saveState(); applyTexts(); toast(state.lang === 'ru' ? 'Русский' : 'English', 'ok'); haptic('notification', 'success'); } /* ========================= 10) GATE FLOW ========================== */ function updateGateUI() { if (!els.chkRegistered || !els.btnEnter) return; els.chkRegistered.checked = !!state.registered; els.btnEnter.disabled = !els.chkRegistered.checked; } function enterApp() { if (els.chkRegistered && !els.chkRegistered.checked) { toast(t('needCheck'), 'bad'); haptic('notification', 'warning'); return; } state.registered = true; saveState(); if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); haptic('notification', 'success'); if (tg && tg.MainButton) { tg.MainButton.hide(); } } function showGate() { if (els.app) els.app.classList.add('hidden'); if (els.gate) els.gate.classList.remove('hidden'); } /* ========================= 11) ASSET / TF / MARKET UI ========================== */ function applySelectionsToUI() { const assetValue = $('#assetValue'); const assetBadge = $('#assetBadge'); const tfValue = $('#tfValue'); const marketValue = $('#marketValue'); if (assetValue) assetValue.textContent = state.asset; if (assetBadge) { assetBadge.textContent = state.assetCat === 'FX' ? '🌍' : state.assetCat === 'CRYPTO' ? '₿' : state.assetCat === 'INDEX' ? '📈' : '⛏'; } if (tfValue) tfValue.textContent = state.timeframe; if (marketValue) marketValue.textContent = state.market; } function toggleMarket() { state.market = (state.market === 'OTC') ? 'LIVE' : 'OTC'; saveState(); applySelectionsToUI(); toast(`MARKET: ${state.market}`, 'ok'); haptic('selection'); } /* ========================= 12) MODAL RENDERERS ========================== */ let activeAssetCat = null; function renderAssetTabs() { if (!els.assetTabs) return; els.assetTabs.innerHTML = ASSETS.map(a => { const active = (activeAssetCat || state.assetCat) === a.cat; return ``; }).join(''); $$('.tab', els.assetTabs).forEach(btn => { btn.addEventListener('click', () => { activeAssetCat = btn.dataset.cat; renderAssetTabs(); renderAssetList(); haptic('selection'); }); }); } function renderAssetList() { if (!els.assetList) return; const cat = activeAssetCat || state.assetCat; const q = (els.assetSearch?.value || '').trim().toLowerCase(); const group = ASSETS.find(x => x.cat === cat) || ASSETS[0]; let items = group.items; if (q) items = items.filter(s => s.toLowerCase().includes(q)); els.assetList.innerHTML = items.map(sym => ` `).join(''); $$('.listItem', els.assetList).forEach(btn => { btn.addEventListener('click', () => { state.asset = btn.dataset.sym; state.assetCat = cat; saveState(); applySelectionsToUI(); closeModal(els.assetsModal); haptic('notification', 'success'); }); }); } function renderTfList() { if (!els.tfList) return; els.tfList.innerHTML = TIMEFRAMES.map(tf => ` `).join(''); $$('.listItem', els.tfList).forEach(btn => { btn.addEventListener('click', () => { state.timeframe = btn.dataset.tf; saveState(); applySelectionsToUI(); closeModal(els.tfModal); haptic('notification', 'success'); }); }); } function renderLangList() { if (!els.langList) return; els.langList.innerHTML = LANGS.map(l => ` `).join(''); $$('.listItem', els.langList).forEach(btn => { btn.addEventListener('click', () => { state.lang = btn.dataset.lang; saveState(); applyTexts(); renderLangList(); applySelectionsToUI(); closeModal(els.langModal); haptic('notification', 'success'); }); }); } /* ========================= 13) CANVAS CHART (premium) ========================== */ let chartCtx = null; function initChart() { if (!els.chart) return; chartCtx = els.chart.getContext('2d', { alpha: true }); drawChart(generateSeries(60, 0.5)); } function generateSeries(n = 60, drift = 0.5) { const seed = xmur3(`${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`)(); const rnd = mulberry32(seed); let v = 100 + rnd() * 20; const arr = []; for (let i = 0; i < n; i++) { v += (rnd() - 0.5) * (drift * 3); arr.push(v); } return arr; } function drawChart(series) { if (!chartCtx || !els.chart) return; const ctx = chartCtx; const w = els.chart.width; const h = els.chart.height; ctx.clearRect(0, 0, w, h); // background subtle gradient const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, 'rgba(124,92,255,0.08)'); g.addColorStop(1, 'rgba(0,178,255,0.05)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // grid ctx.save(); ctx.globalAlpha = 0.22; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; const stepX = 60; const stepY = 42; for (let x = 0; x <= w; x += stepX) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } for (let y = 0; y <= h; y += stepY) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.restore(); // normalize const min = Math.min(...series); const max = Math.max(...series); const px = (i) => (i / (series.length - 1)) * (w - 60) + 30; const py = (v) => { const t = (v - min) / (max - min || 1); return (1 - t) * (h - 60) + 30; }; // glow line ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = 'rgba(124,92,255,0.20)'; ctx.lineWidth = 10; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.strokeStyle = 'rgba(0,178,255,0.16)'; ctx.lineWidth = 8; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // main line ctx.strokeStyle = 'rgba(255,255,255,0.82)'; ctx.lineWidth = 2.2; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // last point highlight const lx = px(series.length - 1); const ly = py(series[series.length - 1]); ctx.fillStyle = 'rgba(120,255,180,0.92)'; ctx.shadowColor = 'rgba(120,255,180,0.75)'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(lx, ly, 4.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // HUD labels ctx.save(); ctx.font = '900 14px ui-sans-serif, system-ui'; ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillText(`${state.asset} • ${state.timeframe} • ${state.market}`, 30, 24); ctx.restore(); } /* ========================= 14) SCAN ENGINE (the “wow”) ========================== */ let scanRunning = false; let scanTimer = null; let countdownTimer = null; function setHolo(status, pct) { if (els.holoText) els.holoText.textContent = status; if (els.holoFill) els.holoFill.style.width = `${clamp(pct, 0, 100)}%`; } function setAnalyzing(on) { if (els.analyzingLine) els.analyzingLine.hidden = !on; } function playScanSound() { if (!els.scanSfx) return; try { els.scanSfx.currentTime = 0; els.scanSfx.volume = 0.55; els.scanSfx.play().catch(() => {}); } catch {} } function resetAll() { scanRunning = false; if (scanTimer) clearInterval(scanTimer); if (countdownTimer) clearInterval(countdownTimer); setHolo(t('sysReady'), 0); setAnalyzing(false); if (els.chartWrap) els.chartWrap.classList.remove('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.remove('show'); if (els.overlayFill) els.overlayFill.style.width = '0%'; if (els.resultPanel) els.resultPanel.classList.add('hidden'); if (els.progressBar) els.progressBar.style.width = '0%'; if (els.timerText) els.timerText.textContent = '--:-- / --:--'; // reset factors if (els.volFactor) els.volFactor.textContent = '--'; if (els.momFactor) els.momFactor.textContent = '--'; if (els.strFactor) els.strFactor.textContent = '--'; if (els.liqFactor) els.liqFactor.textContent = '--'; drawChart(generateSeries(60, 0.5)); toast(t('reset'), 'ok'); haptic('notification', 'success'); } function startScan() { if (scanRunning) return; scanRunning = true; // UI changes setAnalyzing(true); setHolo(t('sysScan'), 12); if (els.chartWrap) els.chartWrap.classList.add('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.add('show'); if (els.overlayLine) els.overlayLine.textContent = t('analyzing'); if (els.overlayFill) els.overlayFill.style.width = '0%'; playScanSound(); haptic('impact', 'medium'); // seed const seedStr = `${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`; const seed = xmur3(seedStr)(); const rnd = mulberry32(seed); // simulate scan progress let p = 0; scanTimer = setInterval(() => { p += 4 + rnd() * 7; p = clamp(p, 0, 100); if (els.overlayFill) els.overlayFill.style.width = `${p}%`; setHolo(t('sysScan'), Math.round(12 + p * 0.7)); // animate chart gradually if (p % 12 < 6) drawChart(generateSeries(60, 0.9 + rnd())); if (p >= 100) { clearInterval(scanTimer); finishScan(rnd); } }, 140); } function finishScan(rnd) { scanRunning = false; // Decide direction & confidence const dirUp = rnd() > 0.48; const confidence = Math.round(64 + rnd() * 32); // 64..96 const vol = Math.round(45 + rnd() * 50); const mom = Math.round(40 + rnd() * 55); const str = Math.round(48 + rnd() * 48); const liq = Math.round(50 + rnd() * 45); // Update factors if (els.volFactor) els.volFactor.textContent = `${vol}%`; if (els.momFactor) els.momFactor.textContent = `${mom}%`; if (els.strFactor) els.strFactor.textContent = `${str}%`; if (els.liqFactor) els.liqFactor.textContent = `${liq}%`; // Update chart one last time with more drift drawChart(generateSeries(60, 1.6 + rnd())); // Result panel if (els.resultPanel) els.resultPanel.classList.remove('hidden'); if (els.rAsset) els.rAsset.textContent = state.asset; if (els.rTf) els.rTf.textContent = state.timeframe; if (els.rAcc) els.rAcc.textContent = `${confidence}%`; if (els.dirText) els.dirText.textContent = dirUp ? 'UP' : 'DOWN'; if (els.dirDot) { els.dirDot.classList.toggle('up', dirUp); els.dirDot.classList.toggle('down', !dirUp); } // until time: “window” const now = new Date(); const until = new Date(now.getTime() + (dirUp ? 55 : 45) * 1000); const untilHHMM = `${pad2(until.getHours())}:${pad2(until.getMinutes())}`; if (els.rUntil) els.rUntil.textContent = untilHHMM; // Window progress countdown startCountdown(45 + Math.round(rnd() * 30)); // overlay off if (els.chartOverlay) els.chartOverlay.classList.remove('show'); setAnalyzing(false); setHolo(t('sysReady'), 100); // store state.lastResult = { ts: Date.now(), asset: state.asset, tf: state.timeframe, market: state.market, dir: dirUp ? 'UP' : 'DOWN', confidence, factors: { vol, mom, str, liq }, }; saveState(); toast('RESULT READY', 'ok'); haptic('notification', 'success'); } function startCountdown(seconds) { if (!els.progressBar || !els.timerText) return; if (countdownTimer) clearInterval(countdownTimer); const total = seconds; let left = seconds; const start = nowHHMM(); const end = (() => { const d = new Date(Date.now() + total * 1000); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; })(); const tick = () => { left = clamp(left, 0, total); const pct = ((total - left) / total) * 100; els.progressBar.style.width = `${pct}%`; const mm = Math.floor(left / 60); const ss = left % 60; els.timerText.textContent = `${start} / ${end} • ${pad2(mm)}:${pad2(ss)}`; left -= 1; if (left < 0) { clearInterval(countdownTimer); toast('WINDOW CLOSED', 'info'); } }; tick(); countdownTimer = setInterval(tick, 1000); } /* ========================= 15) BIND EVENTS ========================== */ function bindEvents() { // Gate if (els.btnOpenLink) { els.btnOpenLink.addEventListener('click', () => { haptic('impact', 'light'); openLink(REG_URL); }); } if (els.chkRegistered) { els.chkRegistered.addEventListener('change', () => { state.registered = els.chkRegistered.checked; saveState(); updateGateUI(); haptic('selection'); }); } if (els.btnEnter) { els.btnEnter.addEventListener('click', () => { haptic('impact', 'medium'); enterApp(); }); } // Topbar if (els.btnLang) { els.btnLang.addEventListener('click', () => { haptic('selection'); renderLangList(); openModal(els.langModal); }); } if (els.btnMenu) { els.btnMenu.addEventListener('click', () => { ensureCommandPalette(); openModal(cmdModal); }); } // Selectors if (els.assetBtn) { els.assetBtn.addEventListener('click', () => { activeAssetCat = state.assetCat; renderAssetTabs(); renderAssetList(); openModal(els.assetsModal); }); } if (els.tfBtn) { els.tfBtn.addEventListener('click', () => { renderTfList(); openModal(els.tfModal); }); } if (els.marketBtn) { els.marketBtn.addEventListener('click', () => toggleMarket()); } // Close modal buttons if (els.closeAssets) els.closeAssets.addEventListener('click', () => closeModal(els.assetsModal)); if (els.closeTf) els.closeTf.addEventListener('click', () => closeModal(els.tfModal)); if (els.closeLang) els.closeLang.addEventListener('click', () => closeModal(els.langModal)); // Search if (els.assetSearch) { els.assetSearch.addEventListener('input', () => renderAssetList()); } // Analyze / Reset if (els.btnAnalyze) { els.btnAnalyze.addEventListener('click', () => { haptic('impact', 'medium'); startScan(); }); } if (els.btnReset) { els.btnReset.addEventListener('click', () => { haptic('impact', 'light'); resetAll(); }); } } /* ========================= 16) BOOT ========================== */ function boot() { loadState(); bindElements(); bindModalCore(); // Telegram init tgReady(); applyThemeFromTelegram(); if (tg) { tg.onEvent('themeChanged', applyThemeFromTelegram); tg.onEvent('viewportChanged', () => { // could respond to height changes if needed }); } // Texts applyTexts(); // Gate UI updateGateUI(); // Current selections applySelectionsToUI(); // Modals renderAssetTabs(); renderAssetList(); renderTfList(); renderLangList(); // Chart initChart(); // Buttons bindEvents(); // If user already registered previously -> jump to app if (state.registered) { if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); } else { showGate(); } // Make sure key UI is sane setHolo(t('sysReady'), 0); } // Wait for DOM ready (in case script not deferred) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();/* ============================================================ CRAFT COSMOS • app.js (Telegram Mini App) - Works in Telegram + works in browser (fallback) - Never breaks clicks - Gate -> App flow - Stable modals (assets / timeframes / language) + command palette - Haptics, toasts, persistent settings - “AI scan” animation + canvas chart render + results ============================================================ */ (() => { 'use strict'; /* ========================= 0) CONFIG — EDIT ONLY THIS ========================== */ const REG_URL = 'https://EXAMPLE.com/register'; // <-- ВСТАВЬ СЮДА СВОЮ ССЫЛКУ РЕГИСТРАЦИИ const BRAND = { name: 'CRAFT ANALYTICS', short: 'CA', }; /* ========================= 1) HELPERS ========================== */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const pad2 = (n) => String(n).padStart(2, '0'); // Seeded RNG for stable-but-random results (so it feels “real”) function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function nowHHMM() { const d = new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function safeJSONParse(s, fallback) { try { return JSON.parse(s); } catch { return fallback; } } /* ========================= 2) TELEGRAM BRIDGE ========================== */ const tg = (window.Telegram && window.Telegram.WebApp) ? window.Telegram.WebApp : null; const isTelegram = !!tg; function tgReady() { if (!tg) return; try { tg.ready(); tg.expand(); // optional: tg.enableClosingConfirmation(); // если хочешь спрашивать перед закрытием } catch {} } function haptic(type = 'impact', style = 'medium') { if (!tg || !tg.HapticFeedback) return; try { if (type === 'impact') tg.HapticFeedback.impactOccurred(style); if (type === 'selection') tg.HapticFeedback.selectionChanged(); if (type === 'notification') tg.HapticFeedback.notificationOccurred(style); // 'success'|'warning'|'error' } catch {} } function openLink(url) { if (!url) return; try { if (tg && tg.openLink) { tg.openLink(url); } else { window.open(url, '_blank', 'noopener'); } } catch { window.location.href = url; } } function applyThemeFromTelegram() { if (!tg) return; const p = tg.themeParams || {}; // If you want: adapt CSS variables from Telegram themeParams // But we keep premium dark by default. // Still, for readability you can sync: document.documentElement.style.setProperty('--tg-bg', p.bg_color || '#050710'); document.documentElement.style.setProperty('--tg-text', p.text_color || '#ffffff'); } /* ========================= 3) STATE + PERSISTENCE ========================== */ const STORAGE_KEY = 'craft_cosmos_state_v1'; const state = { registered: false, lang: 'ru', asset: 'EUR/USD', assetCat: 'FX', timeframe: '30s', market: 'OTC', lastResult: null, }; function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = safeJSONParse(raw, null); if (!data) return; Object.assign(state, data); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /* ========================= 4) DATA: ASSETS / TF / LANG ========================== */ const ASSETS = [ { cat: 'FX', items: ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CHF', 'USD/CAD', 'EUR/JPY', 'EUR/GBP'] }, { cat: 'CRYPTO', items: ['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD', 'BNB/USD'] }, { cat: 'INDEX', items: ['S&P 500', 'NASDAQ', 'DAX', 'FTSE 100'] }, { cat: 'COM', items: ['Gold', 'Silver', 'Oil (WTI)'] }, ]; const TIMEFRAMES = ['5s', '15s', '30s', '1m', '3m', '5m']; const LANGS = [ { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, ]; const STRINGS = { ru: { gateTitle: 'Доступ к интерфейсу', gateText: 'CRAFT ANALYTICS — демо интерфейс. Для активации доступа зарегистрируйтесь по ссылке и пополните баланс, затем вернитесь и нажмите “Открыть интерфейс”.', openReg: 'Открыть ссылку регистрации', chk: 'Я зарегистрировался', enter: 'Открыть интерфейс', subTitle: 'AI Market Scanner', hint: 'Нажмите “Запустить анализ” — и получите результат.', analyze: 'Запустить анализ', reset: 'Сброс', analyzing: 'Сканирование микросигналов…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Скопировано', toastShared: 'Ссылка подготовлена', toastSaved: 'Пресет сохранён', needCheck: 'Поставьте галочку “Я зарегистрировался”', }, en: { gateTitle: 'Access gate', gateText: 'CRAFT ANALYTICS is a demo interface. Register via the link and fund your account, then return and tap “Enter interface”.', openReg: 'Open registration link', chk: "I'm registered", enter: 'Enter interface', subTitle: 'AI Market Scanner', hint: 'Tap “Run scan” to get a result.', analyze: 'Run scan', reset: 'Reset', analyzing: 'Scanning micro-signals…', sysReady: 'SYSTEM READY', sysScan: 'SCANNING…', toastCopied: 'Copied', toastShared: 'Share ready', toastSaved: 'Preset saved', needCheck: 'Check “I’m registered” first', }, }; function t(key) { const pack = STRINGS[state.lang] || STRINGS.ru; return pack[key] || STRINGS.ru[key] || key; } /* ========================= 5) ELEMENTS (match your HTML) ========================== */ const els = { gate: null, app: null, btnOpenLink: null, chkRegistered: null, btnEnter: null, btnLang: null, btnMenu: null, assetBtn: null, tfBtn: null, marketBtn: null, btnAnalyze: null, btnReset: null, chartWrap: null, chart: null, chartOverlay: null, overlayLine: null, overlayFill: null, volFactor: null, momFactor: null, strFactor: null, liqFactor: null, holoFill: null, holoText: null, analyzingLine: null, analyzingText: null, resultPanel: null, rAsset: null, rTf: null, rAcc: null, dirDot: null, dirText: null, rUntil: null, progressBar: null, timerText: null, backdrop: null, assetsModal: null, closeAssets: null, assetSearch: null, assetTabs: null, assetList: null, tfModal: null, closeTf: null, tfList: null, langModal: null, closeLang: null, langList: null, scanSfx: null, }; function bindElements() { els.gate = $('#gate'); els.app = $('#app'); els.btnOpenLink = $('#btnOpenLink'); els.chkRegistered = $('#chkRegistered'); els.btnEnter = $('#btnEnter'); els.btnLang = $('#btnLang'); els.btnMenu = $('#btnMenu'); els.assetBtn = $('#assetBtn'); els.tfBtn = $('#tfBtn'); els.marketBtn = $('#marketBtn'); els.btnAnalyze = $('#btnAnalyze'); els.btnReset = $('#btnReset'); els.chartWrap = $('#chartWrap'); els.chart = $('#chart'); els.chartOverlay = $('#chartOverlay'); els.overlayLine = $('#overlayLine'); els.overlayFill = $('#overlayFill'); els.volFactor = $('#volFactor'); els.momFactor = $('#momFactor'); els.strFactor = $('#strFactor'); els.liqFactor = $('#liqFactor'); els.holoFill = $('#holoFill'); els.holoText = $('#holoText'); els.analyzingLine = $('#analyzingLine'); els.analyzingText = $('#analyzingText'); els.resultPanel = $('#resultPanel'); els.rAsset = $('#rAsset'); els.rTf = $('#rTf'); els.rAcc = $('#rAcc'); els.dirDot = $('#dirDot'); els.dirText = $('#dirText'); els.rUntil = $('#rUntil'); els.progressBar = $('#progressBar'); els.timerText = $('#timerText'); els.backdrop = $('#backdrop'); els.assetsModal = $('#assetsModal'); els.closeAssets = $('#closeAssets'); els.assetSearch = $('#assetSearch'); els.assetTabs = $('#assetTabs'); els.assetList = $('#assetList'); els.tfModal = $('#tfModal'); els.closeTf = $('#closeTf'); els.tfList = $('#tfList'); els.langModal = $('#langModal'); els.closeLang = $('#closeLang'); els.langList = $('#langList'); els.scanSfx = $('#scanSfx'); } /* ========================= 6) UI: TOASTS ========================== */ let toastHost = null; function ensureToastHost() { if (toastHost) return; toastHost = document.createElement('div'); toastHost.style.position = 'fixed'; toastHost.style.left = '0'; toastHost.style.right = '0'; toastHost.style.bottom = 'calc(env(safe-area-inset-bottom, 0px) + 18px)'; toastHost.style.zIndex = '9999'; toastHost.style.display = 'grid'; toastHost.style.placeItems = 'center'; toastHost.style.pointerEvents = 'none'; document.body.appendChild(toastHost); } function toast(msg, kind = 'info') { ensureToastHost(); const el = document.createElement('div'); el.textContent = msg; el.style.pointerEvents = 'none'; el.style.padding = '10px 12px'; el.style.borderRadius = '999px'; el.style.border = '1px solid rgba(255,255,255,.14)'; el.style.background = kind === 'ok' ? 'linear-gradient(135deg, rgba(120,255,180,.20), rgba(0,178,255,.10))' : kind === 'bad' ? 'linear-gradient(135deg, rgba(255,90,110,.22), rgba(124,92,255,.10))' : 'linear-gradient(135deg, rgba(124,92,255,.18), rgba(0,178,255,.10))'; el.style.color = 'rgba(255,255,255,.92)'; el.style.fontWeight = '900'; el.style.letterSpacing = '.08em'; el.style.boxShadow = '0 18px 44px rgba(0,0,0,.32)'; el.style.transform = 'translateY(8px) scale(.98)'; el.style.opacity = '0'; el.style.transition = 'opacity .18s ease, transform .18s ease'; toastHost.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px) scale(.98)'; setTimeout(() => el.remove(), 220); }, 1400); } /* ========================= 7) UI: MODALS (stable) ========================== */ let modalOpen = null; function showBackdrop(on) { if (!els.backdrop) return; els.backdrop.classList.toggle('hidden', !on); els.backdrop.setAttribute('aria-hidden', on ? 'false' : 'true'); } function openModal(modalEl) { if (!modalEl) return; modalOpen = modalEl; showBackdrop(true); modalEl.classList.remove('hidden'); modalEl.setAttribute('aria-hidden', 'false'); haptic('selection'); // Focus first focusable const focusable = modalEl.querySelector('input,button,[tabindex]:not([tabindex="-1"])'); if (focusable) setTimeout(() => focusable.focus(), 0); } function closeModal(modalEl) { const m = modalEl || modalOpen; if (!m) return; m.classList.add('hidden'); m.setAttribute('aria-hidden', 'true'); modalOpen = null; showBackdrop(false); haptic('selection'); } function bindModalCore() { if (els.backdrop) { els.backdrop.addEventListener('click', () => closeModal()); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOpen) closeModal(); }); } /* ========================= 8) UI: COMMAND PALETTE (VIP) ========================== */ let cmdModal = null; let cmdList = null; const COMMANDS = [ { name: 'Run Scan', hint: 'Запустить анализ / Run scan', key: 'Enter', run: () => startScan() }, { name: 'Open Registration', hint: 'Открыть ссылку регистрации', key: 'R', run: () => openLink(REG_URL) }, { name: 'Switch Language', hint: 'RU ↔ EN', key: 'L', run: () => toggleLang() }, { name: 'Reset', hint: 'Сбросить панель', key: 'X', run: () => resetAll() }, { name: 'Toggle Market', hint: 'OTC ↔ Live', key: 'M', run: () => toggleMarket() }, ]; function ensureCommandPalette() { if (cmdModal) return; cmdModal = document.createElement('section'); cmdModal.className = 'modal hidden'; cmdModal.id = 'cmdModal'; cmdModal.setAttribute('role', 'dialog'); cmdModal.setAttribute('aria-modal', 'true'); cmdModal.setAttribute('aria-label', 'Command palette'); cmdModal.innerHTML = `
COMMAND PALETTE
`; document.body.appendChild(cmdModal); const closeBtn = $('#closeCmd', cmdModal); const search = $('#cmdSearch', cmdModal); cmdList = $('#cmdList', cmdModal); closeBtn.addEventListener('click', () => closeModal(cmdModal)); search.addEventListener('input', () => renderCommands(search.value)); renderCommands(''); // keyboard: Ctrl+K / Cmd+K document.addEventListener('keydown', (e) => { const isK = e.key.toLowerCase() === 'k'; if ((e.ctrlKey || e.metaKey) && isK) { e.preventDefault(); openModal(cmdModal); setTimeout(() => search.focus(), 0); } }); // quick hotkeys document.addEventListener('keydown', (e) => { if (modalOpen) return; const k = e.key.toLowerCase(); if (k === 'r') openLink(REG_URL); if (k === 'l') toggleLang(); if (k === 'm') toggleMarket(); }); } function renderCommands(filter) { const q = (filter || '').trim().toLowerCase(); const items = COMMANDS.filter(c => !q ? true : (c.name.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)) ); cmdList.innerHTML = items.map(c => ` `).join(''); $$('.cmdItem', cmdList).forEach((btn, idx) => { btn.addEventListener('click', () => { closeModal(cmdModal); items[idx].run(); }); }); } /* ========================= 9) TEXTS / LOCALIZATION BIND ========================== */ function applyTexts() { const gateTitle = $('#gateTitle'); const gateText = $('#gateText'); const btnOpenLinkText = $('#btnOpenLinkText'); const chkText = $('#chkText'); const btnEnterText = $('#btnEnterText'); const subTitle = $('#subTitle'); const hintText = $('#hintText'); const analyzeText = $('#analyzeText'); const resetText = $('#resetText'); if (gateTitle) gateTitle.textContent = t('gateTitle'); if (gateText) gateText.textContent = t('gateText'); if (btnOpenLinkText) btnOpenLinkText.textContent = t('openReg'); if (chkText) chkText.textContent = t('chk'); if (btnEnterText) btnEnterText.textContent = t('enter'); if (subTitle) subTitle.textContent = t('subTitle'); if (hintText) hintText.textContent = t('hint'); if (analyzeText) analyzeText.textContent = t('analyze'); if (resetText) resetText.textContent = t('reset'); if (els.analyzingText) els.analyzingText.textContent = t('analyzing'); if (els.holoText) els.holoText.textContent = t('sysReady'); } function toggleLang() { state.lang = (state.lang === 'ru') ? 'en' : 'ru'; saveState(); applyTexts(); toast(state.lang === 'ru' ? 'Русский' : 'English', 'ok'); haptic('notification', 'success'); } /* ========================= 10) GATE FLOW ========================== */ function updateGateUI() { if (!els.chkRegistered || !els.btnEnter) return; els.chkRegistered.checked = !!state.registered; els.btnEnter.disabled = !els.chkRegistered.checked; } function enterApp() { if (els.chkRegistered && !els.chkRegistered.checked) { toast(t('needCheck'), 'bad'); haptic('notification', 'warning'); return; } state.registered = true; saveState(); if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); haptic('notification', 'success'); if (tg && tg.MainButton) { tg.MainButton.hide(); } } function showGate() { if (els.app) els.app.classList.add('hidden'); if (els.gate) els.gate.classList.remove('hidden'); } /* ========================= 11) ASSET / TF / MARKET UI ========================== */ function applySelectionsToUI() { const assetValue = $('#assetValue'); const assetBadge = $('#assetBadge'); const tfValue = $('#tfValue'); const marketValue = $('#marketValue'); if (assetValue) assetValue.textContent = state.asset; if (assetBadge) { assetBadge.textContent = state.assetCat === 'FX' ? '🌍' : state.assetCat === 'CRYPTO' ? '₿' : state.assetCat === 'INDEX' ? '📈' : '⛏'; } if (tfValue) tfValue.textContent = state.timeframe; if (marketValue) marketValue.textContent = state.market; } function toggleMarket() { state.market = (state.market === 'OTC') ? 'LIVE' : 'OTC'; saveState(); applySelectionsToUI(); toast(`MARKET: ${state.market}`, 'ok'); haptic('selection'); } /* ========================= 12) MODAL RENDERERS ========================== */ let activeAssetCat = null; function renderAssetTabs() { if (!els.assetTabs) return; els.assetTabs.innerHTML = ASSETS.map(a => { const active = (activeAssetCat || state.assetCat) === a.cat; return ``; }).join(''); $$('.tab', els.assetTabs).forEach(btn => { btn.addEventListener('click', () => { activeAssetCat = btn.dataset.cat; renderAssetTabs(); renderAssetList(); haptic('selection'); }); }); } function renderAssetList() { if (!els.assetList) return; const cat = activeAssetCat || state.assetCat; const q = (els.assetSearch?.value || '').trim().toLowerCase(); const group = ASSETS.find(x => x.cat === cat) || ASSETS[0]; let items = group.items; if (q) items = items.filter(s => s.toLowerCase().includes(q)); els.assetList.innerHTML = items.map(sym => ` `).join(''); $$('.listItem', els.assetList).forEach(btn => { btn.addEventListener('click', () => { state.asset = btn.dataset.sym; state.assetCat = cat; saveState(); applySelectionsToUI(); closeModal(els.assetsModal); haptic('notification', 'success'); }); }); } function renderTfList() { if (!els.tfList) return; els.tfList.innerHTML = TIMEFRAMES.map(tf => ` `).join(''); $$('.listItem', els.tfList).forEach(btn => { btn.addEventListener('click', () => { state.timeframe = btn.dataset.tf; saveState(); applySelectionsToUI(); closeModal(els.tfModal); haptic('notification', 'success'); }); }); } function renderLangList() { if (!els.langList) return; els.langList.innerHTML = LANGS.map(l => ` `).join(''); $$('.listItem', els.langList).forEach(btn => { btn.addEventListener('click', () => { state.lang = btn.dataset.lang; saveState(); applyTexts(); renderLangList(); applySelectionsToUI(); closeModal(els.langModal); haptic('notification', 'success'); }); }); } /* ========================= 13) CANVAS CHART (premium) ========================== */ let chartCtx = null; function initChart() { if (!els.chart) return; chartCtx = els.chart.getContext('2d', { alpha: true }); drawChart(generateSeries(60, 0.5)); } function generateSeries(n = 60, drift = 0.5) { const seed = xmur3(`${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`)(); const rnd = mulberry32(seed); let v = 100 + rnd() * 20; const arr = []; for (let i = 0; i < n; i++) { v += (rnd() - 0.5) * (drift * 3); arr.push(v); } return arr; } function drawChart(series) { if (!chartCtx || !els.chart) return; const ctx = chartCtx; const w = els.chart.width; const h = els.chart.height; ctx.clearRect(0, 0, w, h); // background subtle gradient const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, 'rgba(124,92,255,0.08)'); g.addColorStop(1, 'rgba(0,178,255,0.05)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // grid ctx.save(); ctx.globalAlpha = 0.22; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; const stepX = 60; const stepY = 42; for (let x = 0; x <= w; x += stepX) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } for (let y = 0; y <= h; y += stepY) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.restore(); // normalize const min = Math.min(...series); const max = Math.max(...series); const px = (i) => (i / (series.length - 1)) * (w - 60) + 30; const py = (v) => { const t = (v - min) / (max - min || 1); return (1 - t) * (h - 60) + 30; }; // glow line ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = 'rgba(124,92,255,0.20)'; ctx.lineWidth = 10; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.strokeStyle = 'rgba(0,178,255,0.16)'; ctx.lineWidth = 8; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // main line ctx.strokeStyle = 'rgba(255,255,255,0.82)'; ctx.lineWidth = 2.2; ctx.beginPath(); series.forEach((v, i) => { const x = px(i); const y = py(v); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // last point highlight const lx = px(series.length - 1); const ly = py(series[series.length - 1]); ctx.fillStyle = 'rgba(120,255,180,0.92)'; ctx.shadowColor = 'rgba(120,255,180,0.75)'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(lx, ly, 4.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // HUD labels ctx.save(); ctx.font = '900 14px ui-sans-serif, system-ui'; ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillText(`${state.asset} • ${state.timeframe} • ${state.market}`, 30, 24); ctx.restore(); } /* ========================= 14) SCAN ENGINE (the “wow”) ========================== */ let scanRunning = false; let scanTimer = null; let countdownTimer = null; function setHolo(status, pct) { if (els.holoText) els.holoText.textContent = status; if (els.holoFill) els.holoFill.style.width = `${clamp(pct, 0, 100)}%`; } function setAnalyzing(on) { if (els.analyzingLine) els.analyzingLine.hidden = !on; } function playScanSound() { if (!els.scanSfx) return; try { els.scanSfx.currentTime = 0; els.scanSfx.volume = 0.55; els.scanSfx.play().catch(() => {}); } catch {} } function resetAll() { scanRunning = false; if (scanTimer) clearInterval(scanTimer); if (countdownTimer) clearInterval(countdownTimer); setHolo(t('sysReady'), 0); setAnalyzing(false); if (els.chartWrap) els.chartWrap.classList.remove('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.remove('show'); if (els.overlayFill) els.overlayFill.style.width = '0%'; if (els.resultPanel) els.resultPanel.classList.add('hidden'); if (els.progressBar) els.progressBar.style.width = '0%'; if (els.timerText) els.timerText.textContent = '--:-- / --:--'; // reset factors if (els.volFactor) els.volFactor.textContent = '--'; if (els.momFactor) els.momFactor.textContent = '--'; if (els.strFactor) els.strFactor.textContent = '--'; if (els.liqFactor) els.liqFactor.textContent = '--'; drawChart(generateSeries(60, 0.5)); toast(t('reset'), 'ok'); haptic('notification', 'success'); } function startScan() { if (scanRunning) return; scanRunning = true; // UI changes setAnalyzing(true); setHolo(t('sysScan'), 12); if (els.chartWrap) els.chartWrap.classList.add('gridOn'); if (els.chartOverlay) els.chartOverlay.classList.add('show'); if (els.overlayLine) els.overlayLine.textContent = t('analyzing'); if (els.overlayFill) els.overlayFill.style.width = '0%'; playScanSound(); haptic('impact', 'medium'); // seed const seedStr = `${state.asset}|${state.timeframe}|${state.market}|${new Date().toDateString()}`; const seed = xmur3(seedStr)(); const rnd = mulberry32(seed); // simulate scan progress let p = 0; scanTimer = setInterval(() => { p += 4 + rnd() * 7; p = clamp(p, 0, 100); if (els.overlayFill) els.overlayFill.style.width = `${p}%`; setHolo(t('sysScan'), Math.round(12 + p * 0.7)); // animate chart gradually if (p % 12 < 6) drawChart(generateSeries(60, 0.9 + rnd())); if (p >= 100) { clearInterval(scanTimer); finishScan(rnd); } }, 140); } function finishScan(rnd) { scanRunning = false; // Decide direction & confidence const dirUp = rnd() > 0.48; const confidence = Math.round(64 + rnd() * 32); // 64..96 const vol = Math.round(45 + rnd() * 50); const mom = Math.round(40 + rnd() * 55); const str = Math.round(48 + rnd() * 48); const liq = Math.round(50 + rnd() * 45); // Update factors if (els.volFactor) els.volFactor.textContent = `${vol}%`; if (els.momFactor) els.momFactor.textContent = `${mom}%`; if (els.strFactor) els.strFactor.textContent = `${str}%`; if (els.liqFactor) els.liqFactor.textContent = `${liq}%`; // Update chart one last time with more drift drawChart(generateSeries(60, 1.6 + rnd())); // Result panel if (els.resultPanel) els.resultPanel.classList.remove('hidden'); if (els.rAsset) els.rAsset.textContent = state.asset; if (els.rTf) els.rTf.textContent = state.timeframe; if (els.rAcc) els.rAcc.textContent = `${confidence}%`; if (els.dirText) els.dirText.textContent = dirUp ? 'UP' : 'DOWN'; if (els.dirDot) { els.dirDot.classList.toggle('up', dirUp); els.dirDot.classList.toggle('down', !dirUp); } // until time: “window” const now = new Date(); const until = new Date(now.getTime() + (dirUp ? 55 : 45) * 1000); const untilHHMM = `${pad2(until.getHours())}:${pad2(until.getMinutes())}`; if (els.rUntil) els.rUntil.textContent = untilHHMM; // Window progress countdown startCountdown(45 + Math.round(rnd() * 30)); // overlay off if (els.chartOverlay) els.chartOverlay.classList.remove('show'); setAnalyzing(false); setHolo(t('sysReady'), 100); // store state.lastResult = { ts: Date.now(), asset: state.asset, tf: state.timeframe, market: state.market, dir: dirUp ? 'UP' : 'DOWN', confidence, factors: { vol, mom, str, liq }, }; saveState(); toast('RESULT READY', 'ok'); haptic('notification', 'success'); } function startCountdown(seconds) { if (!els.progressBar || !els.timerText) return; if (countdownTimer) clearInterval(countdownTimer); const total = seconds; let left = seconds; const start = nowHHMM(); const end = (() => { const d = new Date(Date.now() + total * 1000); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; })(); const tick = () => { left = clamp(left, 0, total); const pct = ((total - left) / total) * 100; els.progressBar.style.width = `${pct}%`; const mm = Math.floor(left / 60); const ss = left % 60; els.timerText.textContent = `${start} / ${end} • ${pad2(mm)}:${pad2(ss)}`; left -= 1; if (left < 0) { clearInterval(countdownTimer); toast('WINDOW CLOSED', 'info'); } }; tick(); countdownTimer = setInterval(tick, 1000); } /* ========================= 15) BIND EVENTS ========================== */ function bindEvents() { // Gate if (els.btnOpenLink) { els.btnOpenLink.addEventListener('click', () => { haptic('impact', 'light'); openLink(REG_URL); }); } if (els.chkRegistered) { els.chkRegistered.addEventListener('change', () => { state.registered = els.chkRegistered.checked; saveState(); updateGateUI(); haptic('selection'); }); } if (els.btnEnter) { els.btnEnter.addEventListener('click', () => { haptic('impact', 'medium'); enterApp(); }); } // Topbar if (els.btnLang) { els.btnLang.addEventListener('click', () => { haptic('selection'); renderLangList(); openModal(els.langModal); }); } if (els.btnMenu) { els.btnMenu.addEventListener('click', () => { ensureCommandPalette(); openModal(cmdModal); }); } // Selectors if (els.assetBtn) { els.assetBtn.addEventListener('click', () => { activeAssetCat = state.assetCat; renderAssetTabs(); renderAssetList(); openModal(els.assetsModal); }); } if (els.tfBtn) { els.tfBtn.addEventListener('click', () => { renderTfList(); openModal(els.tfModal); }); } if (els.marketBtn) { els.marketBtn.addEventListener('click', () => toggleMarket()); } // Close modal buttons if (els.closeAssets) els.closeAssets.addEventListener('click', () => closeModal(els.assetsModal)); if (els.closeTf) els.closeTf.addEventListener('click', () => closeModal(els.tfModal)); if (els.closeLang) els.closeLang.addEventListener('click', () => closeModal(els.langModal)); // Search if (els.assetSearch) { els.assetSearch.addEventListener('input', () => renderAssetList()); } // Analyze / Reset if (els.btnAnalyze) { els.btnAnalyze.addEventListener('click', () => { haptic('impact', 'medium'); startScan(); }); } if (els.btnReset) { els.btnReset.addEventListener('click', () => { haptic('impact', 'light'); resetAll(); }); } } /* ========================= 16) BOOT ========================== */ function boot() { loadState(); bindElements(); bindModalCore(); // Telegram init tgReady(); applyThemeFromTelegram(); if (tg) { tg.onEvent('themeChanged', applyThemeFromTelegram); tg.onEvent('viewportChanged', () => { // could respond to height changes if needed }); } // Texts applyTexts(); // Gate UI updateGateUI(); // Current selections applySelectionsToUI(); // Modals renderAssetTabs(); renderAssetList(); renderTfList(); renderLangList(); // Chart initChart(); // Buttons bindEvents(); // If user already registered previously -> jump to app if (state.registered) { if (els.gate) els.gate.classList.add('hidden'); if (els.app) els.app.classList.remove('hidden'); } else { showGate(); } // Make sure key UI is sane setHolo(t('sysReady'), 0); } // Wait for DOM ready (in case script not deferred) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();