/* ============================================================
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 = `
`;
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 = `
`;
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 = `
`;
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 = `
`;
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 = `
`;
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();
}
})();