montana/Русский/Сайт/junona/index.html

1404 lines
76 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Montana">
<meta name="theme-color" content="#0A0A0A">
<title>Montana Protocol</title>
<link rel="manifest" href="manifest.json">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root {
--gold: #D4AF37;
--gold-light: #F0D060;
--gold-dim: rgba(212,175,55,0.3);
--cyan: #00D4FF;
--purple: #7B2FFF;
--bg: #0A0A0A;
--card: rgba(23,23,37,0.6);
--card-solid: #15152a;
--divider: rgba(255,255,255,0.06);
--text: #FFFFFF;
--text2: rgba(255,255,255,0.6);
--text3: rgba(255,255,255,0.3);
--blue: #4A90D9;
--green: #10B981;
--red: #EF4444;
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
html, body { height: 100%; overflow: hidden; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text);
-webkit-user-select: none; user-select: none;
}
/* ═══ APP LAYOUT ═══ */
.app { display: flex; height: 100%; position: fixed; inset: 0; }
.desktop-sidebar {
width: 260px; background: #0e0e1a; border-right: 1px solid var(--divider);
display: none; flex-direction: column; overflow-y: auto; padding-top: calc(var(--safe-top) + 12px);
}
.main-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.screen-container { flex: 1; overflow: hidden; position: relative; }
.screen { position: absolute; inset: 0; display: none; flex-direction: column; overflow-y: auto; }
.screen.active { display: flex; }
/* ═══ HEADER ═══ */
.header {
padding: calc(var(--safe-top) + 10px) 16px 10px;
background: var(--bg); display: flex; align-items: center; gap: 12px;
border-bottom: 1px solid var(--divider); min-height: 56px; flex-shrink: 0;
}
.burger { background: none; border: none; color: var(--text); font-size: 22px; cursor: pointer; padding: 4px 8px; }
.header-info { flex: 1; }
.header-title { font-size: 18px; font-weight: 700; }
.header-sub { font-size: 12px; color: var(--blue); }
.header-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid var(--gold-dim); }
/* ═══ BOTTOM TABS (mobile) ═══ */
.bottom-tabs {
display: flex; background: #0e0e1a; border-top: 1px solid var(--divider);
padding: 6px 0 calc(var(--safe-bottom) + 6px); flex-shrink: 0;
}
.tab-btn {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
background: none; border: none; color: var(--text3); font-size: 10px; cursor: pointer; padding: 4px 0;
}
.tab-btn .icon { font-size: 20px; }
.tab-btn.active { color: var(--gold); }
/* ═══ SIDEBAR (mobile overlay) ═══ */
.sidebar-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 998; display: none; }
.sidebar-overlay.active { display: block; }
.sidebar {
position: fixed; left: -280px; top: 0; width: 280px; height: 100%;
background: #0e0e1a; z-index: 999; transition: left .3s ease;
overflow-y: auto; padding: calc(var(--safe-top) + 16px) 0 20px;
}
.sidebar.active { left: 0; }
.sidebar-header { padding: 0 20px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--divider); margin-bottom: 12px; }
.sidebar-logo { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; }
.sidebar-title { font-size: 16px; font-weight: 700; flex: 1; }
.sidebar-close { background: none; border: none; color: var(--text3); font-size: 24px; cursor: pointer; }
.sidebar-section { padding: 8px 0; }
.sidebar-section-title { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; padding: 4px 20px; }
.sidebar-item {
display: flex; align-items: center; gap: 12px; padding: 10px 20px;
color: var(--text2); font-size: 14px; cursor: pointer; transition: background .2s;
}
.sidebar-item:hover, .sidebar-item.active { background: rgba(255,255,255,0.05); color: var(--text); }
.sidebar-item .si-icon { font-size: 18px; width: 24px; text-align: center; }
.sidebar-item.active .si-icon { color: var(--gold); }
.sidebar-divider { height: 1px; background: var(--divider); margin: 8px 20px; }
/* ═══ DESKTOP SIDEBAR ═══ */
@media (min-width: 769px) {
.desktop-sidebar { display: flex; }
.bottom-tabs { display: none; }
.burger { display: none; }
.sidebar, .sidebar-overlay { display: none !important; }
.header { padding-top: 12px; }
}
.ds-item {
display: flex; align-items: center; gap: 12px; padding: 9px 20px;
color: var(--text2); font-size: 13px; cursor: pointer; transition: background .2s;
}
.ds-item:hover { background: rgba(255,255,255,0.05); color: var(--text); }
.ds-item.active { background: rgba(212,175,55,0.1); color: var(--gold); }
.ds-item .si-icon { font-size: 16px; width: 22px; text-align: center; }
.ds-section-title { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; padding: 12px 20px 4px; }
.ds-divider { height: 1px; background: var(--divider); margin: 6px 16px; }
.ds-version { font-size: 10px; color: var(--text3); text-align: center; padding: 16px; margin-top: auto; }
/* ═══ COMMON ═══ */
.sep { height: 1px; background: var(--divider); margin: 0 16px; }
.btn-gold {
background: linear-gradient(135deg, var(--gold), var(--gold-light));
color: #000; border: none; border-radius: 12px; padding: 12px 24px;
font-size: 15px; font-weight: 600; cursor: pointer; width: 100%;
}
.btn-gold:active { opacity: 0.8; }
.btn-outline {
background: transparent; color: var(--gold); border: 1px solid var(--gold-dim);
border-radius: 12px; padding: 10px 20px; font-size: 14px; cursor: pointer;
}
.input-field {
background: rgba(255,255,255,0.05); border: 1px solid var(--divider);
border-radius: 12px; padding: 12px 16px; color: var(--text); font-size: 15px;
width: 100%; outline: none; font-family: inherit;
}
.input-field:focus { border-color: var(--gold-dim); }
.input-field::placeholder { color: var(--text3); }
.card { background: var(--card); border-radius: 16px; padding: 16px; border: 1px solid var(--divider); }
.mono { font-family: 'SF Mono', 'Fira Code', monospace; }
.gold { color: var(--gold); }
.text2 { color: var(--text2); }
.text3 { color: var(--text3); }
.copy-btn { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 14px; padding: 4px 8px; }
.copy-btn:active { color: var(--gold); }
/* ═══ ONBOARDING ═══ */
.onboarding { justify-content: center; align-items: center; padding: 24px; text-align: center; }
.onb-step { display: none; flex-direction: column; align-items: center; gap: 20px; width: 100%; max-width: 400px; }
.onb-step.active { display: flex; }
.onb-logo { width: 100px; height: 100px; border-radius: 50%; border: 3px solid var(--gold-dim); }
.onb-title { font-size: 24px; font-weight: 700; }
.onb-subtitle { font-size: 14px; color: var(--text2); line-height: 1.6; }
.pin-dots { display: flex; gap: 10px; justify-content: center; margin: 16px 0; }
.pin-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--gold-dim); transition: all .2s; }
.pin-dot.filled { background: var(--gold); border-color: var(--gold); }
.pin-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 280px; width: 100%; }
.pin-key {
aspect-ratio: 1.4; display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: 500; background: rgba(255,255,255,0.05);
border: none; border-radius: 12px; color: var(--text); cursor: pointer;
}
.pin-key:active { background: rgba(212,175,55,0.2); }
.pin-key.empty { background: transparent; cursor: default; }
.seed-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: 100%; }
.seed-word {
background: rgba(255,255,255,0.05); border-radius: 8px; padding: 8px;
font-size: 13px; text-align: center;
}
.seed-word .num { color: var(--text3); font-size: 10px; }
.shake { animation: shake .5s; }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-8px)} 40%,80%{transform:translateX(8px)} }
/* ═══ JUNONA CHAT ═══ */
.chat-area { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.chat-welcome { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 20px; }
.welcome-balance { font-size: 36px; font-weight: 700; font-family: 'SF Mono', monospace; }
.welcome-balance .sym { color: var(--gold-light); }
.welcome-rub { font-size: 16px; color: var(--text2); font-family: 'SF Mono', monospace; }
.clock-container { position: relative; width: 120px; height: 120px; }
.clock-logo { width: 100px; height: 100px; border-radius: 50%; position: absolute; top: 10px; left: 10px; object-fit: cover; }
.clock-dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--gold);
position: absolute; box-shadow: 0 0 6px var(--gold);
}
.msg-bubble { max-width: 80%; padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.5; word-wrap: break-word; }
.msg-user { align-self: flex-end; background: rgba(212,175,55,0.15); border-bottom-right-radius: 4px; }
.msg-ai { align-self: flex-start; background: var(--card); border-bottom-left-radius: 4px; }
.msg-time { font-size: 10px; color: var(--text3); margin-top: 4px; }
.msg-model { font-size: 10px; color: var(--gold-dim); }
.chat-input-area {
padding: 8px 16px calc(var(--safe-bottom) + 8px); display: flex; gap: 8px;
border-top: 1px solid var(--divider); background: var(--bg); flex-shrink: 0;
}
.chat-input {
flex: 1; background: rgba(255,255,255,0.06); border: 1px solid var(--divider);
border-radius: 20px; padding: 10px 16px; color: var(--text); font-size: 15px;
outline: none; font-family: inherit; resize: none;
}
.chat-input:focus { border-color: var(--gold-dim); }
.chat-send {
width: 40px; height: 40px; border-radius: 50%; background: var(--gold);
border: none; color: #000; font-size: 18px; cursor: pointer; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.chat-send:active { opacity: 0.7; }
.typing-dots span { animation: blink 1.4s infinite; }
.typing-dots span:nth-child(2) { animation-delay: .2s; }
.typing-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%,100%{opacity:.2} 50%{opacity:1} }
@media (min-width: 769px) {
.chat-input-area { padding-bottom: 12px; }
}
/* ═══ WALLET ═══ */
.wallet-scroll { flex: 1; overflow-y: auto; padding: 16px; }
.coin-container {
width: 120px; height: 120px; margin: 0 auto 16px; perspective: 600px;
}
.coin-inner {
width: 100%; height: 100%; position: relative; transform-style: preserve-3d;
animation: coinSpin 6s ease-in-out infinite;
}
.coin-face, .coin-back {
position: absolute; width: 100%; height: 100%; border-radius: 50%;
backface-visibility: hidden; object-fit: cover;
border: 3px solid var(--gold-dim); box-shadow: 0 0 20px rgba(212,175,55,0.2);
}
.coin-back { transform: rotateY(180deg); }
@keyframes coinSpin { 0%,100%{transform:rotateY(0)} 50%{transform:rotateY(180deg)} }
.wallet-balance { text-align: center; margin-bottom: 16px; }
.wallet-balance h1 { font-size: 32px; font-weight: 700; font-family: 'SF Mono', monospace; }
.wallet-balance .sym { color: var(--gold-light); font-size: 32px; }
.wallet-address { font-size: 12px; color: var(--text3); font-family: 'SF Mono', monospace; cursor: pointer; }
.wallet-address:active { color: var(--gold); }
.wallet-fiat { font-size: 13px; color: var(--text3); font-family: 'SF Mono', monospace; margin-top: 4px; }
.wallet-actions { display: flex; gap: 12px; margin: 16px 0; }
.wallet-actions button { flex: 1; }
.wallet-rate { text-align: center; color: var(--gold); font-size: 14px; font-weight: 600; margin: 8px 0; }
.wallet-section { margin-top: 16px; }
.wallet-section h3 { font-size: 13px; color: var(--text3); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
.node-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; font-size: 13px; }
.node-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.node-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); }
.node-dot.offline { background: var(--red); }
.node-name { flex: 1; }
.node-status { color: var(--text3); font-size: 11px; }
.t2-window { margin-top: 16px; }
.t2-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; margin: 8px 0; }
.t2-fill { height: 100%; background: var(--gold); border-radius: 2px; transition: width 1s; }
.t2-info { display: flex; justify-content: space-between; font-size: 12px; color: var(--text3); }
/* ═══ SEND/RECEIVE MODALS ═══ */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 20px; }
.modal-overlay.active { display: flex; }
.modal {
background: var(--card-solid); border-radius: 20px; padding: 24px; width: 100%; max-width: 380px;
border: 1px solid var(--divider); max-height: 90vh; overflow-y: auto;
}
.modal h2 { font-size: 20px; margin-bottom: 16px; text-align: center; }
.modal .close-btn { position: absolute; top: 16px; right: 16px; background: none; border: none; color: var(--text3); font-size: 20px; cursor: pointer; }
.modal-field { margin-bottom: 12px; }
.modal-field label { font-size: 12px; color: var(--text2); margin-bottom: 4px; display: block; }
.modal-result { text-align: center; padding: 20px 0; }
.modal-result .check { font-size: 48px; margin-bottom: 12px; }
/* ═══ DOMAINS ═══ */
.domains-scroll { flex: 1; overflow-y: auto; padding: 16px; }
.domain-price { text-align: center; margin: 12px 0; }
.domain-price .amount { font-size: 28px; font-weight: 700; color: var(--gold); }
.domain-status { text-align: center; font-size: 13px; margin: 8px 0; }
.domain-status.available { color: var(--green); }
.domain-status.taken { color: var(--red); }
.domain-list { margin-top: 20px; }
.domain-item { display: flex; align-items: center; justify-content: space-between; padding: 12px; background: var(--card); border-radius: 12px; margin-bottom: 8px; }
.domain-item .name { font-weight: 600; }
.domain-item .date { font-size: 11px; color: var(--text3); }
/* ═══ HISTORY ═══ */
.history-scroll { flex: 1; overflow-y: auto; padding: 16px; }
.history-empty { text-align: center; padding: 40px 0; color: var(--text3); }
.history-empty .icon { font-size: 48px; margin-bottom: 12px; }
.tx-item { padding: 12px; background: var(--card); border-radius: 12px; margin-bottom: 8px; }
.tx-top { display: flex; justify-content: space-between; align-items: center; }
.tx-type { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; }
.tx-amount { font-weight: 700; font-family: 'SF Mono', monospace; }
.tx-amount.positive { color: var(--green); }
.tx-amount.negative { color: var(--red); }
.tx-detail { font-size: 12px; color: var(--text3); margin-top: 4px; font-family: 'SF Mono', monospace; }
.tx-time { font-size: 11px; color: var(--text3); margin-top: 4px; }
/* ═══ TIMECHAIN ═══ */
.explorer-scroll { flex: 1; overflow-y: auto; padding: 16px; }
.explorer-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.explorer-tab {
padding: 6px 14px; border-radius: 20px; font-size: 13px; cursor: pointer;
background: var(--card); border: 1px solid var(--divider); color: var(--text2);
}
.explorer-tab.active { background: rgba(212,175,55,0.15); color: var(--gold); border-color: var(--gold-dim); }
.explorer-live { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--green); margin-bottom: 12px; }
.explorer-live .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
.event-card { padding: 12px; background: var(--card); border-radius: 12px; margin-bottom: 8px; border-left: 3px solid var(--gold-dim); }
.event-type { font-size: 11px; color: var(--gold); text-transform: uppercase; font-weight: 600; }
.event-detail { font-size: 13px; margin-top: 4px; }
.event-time { font-size: 11px; color: var(--text3); margin-top: 4px; }
/* ═══ SETTINGS ═══ */
.settings-scroll { flex: 1; overflow-y: auto; padding: 16px; }
.settings-section { margin-bottom: 20px; }
.settings-section h3 {
font-size: 12px; color: var(--text3); text-transform: uppercase; letter-spacing: 1px;
margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--divider);
}
.settings-row {
display: flex; align-items: center; justify-content: space-between; padding: 10px 0;
border-bottom: 1px solid var(--divider);
}
.settings-row:last-child { border-bottom: none; }
.settings-label { font-size: 14px; }
.settings-value { font-size: 13px; color: var(--text2); font-family: 'SF Mono', monospace; max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
.lang-pills { display: flex; gap: 8px; }
.lang-pill {
padding: 6px 14px; border-radius: 20px; font-size: 14px; cursor: pointer;
background: var(--card); border: 1px solid var(--divider);
}
.lang-pill.active { background: rgba(212,175,55,0.15); border-color: var(--gold-dim); }
/* ═══ PLACEHOLDER ═══ */
.placeholder-screen { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; text-align: center; gap: 16px; }
.placeholder-screen .emoji { font-size: 64px; }
.placeholder-screen h2 { font-size: 22px; }
.placeholder-screen p { font-size: 14px; color: var(--text2); max-width: 300px; line-height: 1.6; }
.coming-soon { font-size: 12px; color: var(--gold); text-transform: uppercase; letter-spacing: 2px; }
/* ═══ EXCHANGE MODAL ═══ */
.exchange-row { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
.exchange-result { text-align: center; font-size: 20px; font-weight: 700; color: var(--gold); margin: 16px 0; font-family: 'SF Mono', monospace; }
/* ═══ RESPONSIVE ═══ */
@media (min-width: 769px) {
.msg-bubble { max-width: 60%; }
.wallet-scroll { max-width: 600px; margin: 0 auto; width: 100%; }
.domains-scroll { max-width: 600px; margin: 0 auto; width: 100%; }
.history-scroll { max-width: 600px; margin: 0 auto; width: 100%; }
.settings-scroll { max-width: 600px; margin: 0 auto; width: 100%; }
.explorer-scroll { max-width: 800px; margin: 0 auto; width: 100%; }
}
@media (min-width: 1281px) {
.wallet-scroll { max-width: 700px; }
}
/* ═══ SCROLLBAR ═══ */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
/* ═══ LOADING ═══ */
.spinner { width: 24px; height: 24px; border: 2px solid var(--divider); border-top-color: var(--gold); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="app">
<!-- ═══ DESKTOP SIDEBAR ═══ -->
<div class="desktop-sidebar" id="desktopSidebar">
<div style="padding: 0 20px 16px; display:flex; align-items:center; gap:12px">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--gold),var(--gold-light));display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#000">Ɉ</div>
<div><div style="font-size:14px;font-weight:700">Montana</div><div style="font-size:10px;color:var(--text3)">Protocol</div></div>
</div>
<div class="ds-divider"></div>
<div class="ds-section-title">Юнона AI</div>
<div class="ds-item active" data-tab="junona" onclick="navigate('junona')"><span class="si-icon">🧠</span>Юнона</div>
<div class="ds-section-title">Montana</div>
<div class="ds-item" data-tab="wallet" onclick="navigate('wallet')"><span class="si-icon">💰</span>Кошелёк</div>
<div class="ds-item" data-tab="domains" onclick="navigate('domains')"><span class="si-icon">@</span>Домены</div>
<div class="ds-item" data-tab="phone" onclick="navigate('phone')"><span class="si-icon">📱</span>Номера</div>
<div class="ds-item" data-tab="calls" onclick="navigate('calls')"><span class="si-icon">📞</span>Звонки</div>
<div class="ds-item" data-tab="sites" onclick="navigate('sites')"><span class="si-icon">🌐</span>Сайты</div>
<div class="ds-item" data-tab="video" onclick="navigate('video')"><span class="si-icon">▶️</span>Видео</div>
<div class="ds-item" data-tab="history" onclick="navigate('history')"><span class="si-icon">🕐</span>История</div>
<div class="ds-item" data-tab="explorer" onclick="navigate('explorer')"><span class="si-icon"></span>Таймчейн</div>
<div class="ds-section-title">Обмен</div>
<div class="ds-item" onclick="showExchange('btc')"><span class="si-icon"></span>BTC → Ɉ</div>
<div class="ds-item" onclick="showExchange('usd')"><span class="si-icon">$</span>USD → Ɉ</div>
<div class="ds-item" onclick="showExchange('rub')"><span class="si-icon"></span>RUB → Ɉ</div>
<div class="ds-divider"></div>
<div class="ds-item" data-tab="private" onclick="navigate('private')"><span class="si-icon">🕶️</span>Приватный</div>
<div class="ds-item" data-tab="settings" onclick="navigate('settings')"><span class="si-icon">⚙️</span>Настройки</div>
<div class="ds-divider"></div>
<div class="ds-item" onclick="logout()" style="color:var(--red)"><span class="si-icon">🚪</span>Выход</div>
<div class="ds-version">Montana Ɉ v3.18.0 (130)</div>
</div>
<!-- ═══ MAIN AREA ═══ -->
<div class="main-area">
<!-- HEADER -->
<div class="header" id="appHeader">
<button class="burger" onclick="toggleSidebar()"></button>
<div class="header-info">
<div class="header-title" id="headerTitle">Юнона</div>
<div class="header-sub" id="headerSub">в сети</div>
</div>
</div>
<!-- SCREENS -->
<div class="screen-container">
<!-- ONBOARDING -->
<div class="screen onboarding" id="onboarding">
<div class="onb-step active" id="onb-welcome">
<div style="width:100px;height:100px;border-radius:50%;background:linear-gradient(135deg,var(--gold),var(--gold-light));display:flex;align-items:center;justify-content:center;font-size:42px;font-weight:700;color:#000;border:3px solid var(--gold-dim)">Ɉ</div>
<div class="onb-title">Montana Protocol</div>
<div class="onb-subtitle">Время — единственная реальная валюта.<br>1 секунда присутствия = 1 Ɉ</div>
<button class="btn-gold" onclick="onbStep('pin-create')">Создать ключи</button>
<button class="btn-outline" onclick="onbStep('recovery')">Восстановить из seed</button>
<div style="font-size:11px;color:var(--text3);margin-top:8px">ML-DSA-65 · Постквантовая криптография</div>
</div>
<div class="onb-step" id="onb-pin-create">
<div class="onb-title">Создайте PIN</div>
<div class="onb-subtitle">8 цифр для защиты ключей</div>
<div class="pin-dots" id="pinDots"></div>
<div class="pin-pad" id="pinPad"></div>
</div>
<div class="onb-step" id="onb-pin-confirm">
<div class="onb-title">Подтвердите PIN</div>
<div class="onb-subtitle">Введите PIN ещё раз</div>
<div class="pin-dots" id="pinDotsConfirm"></div>
<div class="pin-pad" id="pinPadConfirm"></div>
</div>
<div class="onb-step" id="onb-mnemonic">
<div class="onb-title">Seed-фраза</div>
<div class="onb-subtitle">Запишите 24 слова. Это единственный способ восстановить доступ.</div>
<div class="seed-grid" id="seedGrid"></div>
<button class="btn-outline" onclick="copySeed()">📋 Копировать</button>
<button class="btn-gold" onclick="finishOnboarding()">Я сохранил</button>
</div>
<div class="onb-step" id="onb-recovery">
<div class="onb-title">Восстановление</div>
<div class="onb-subtitle">Введите 24 слова seed-фразы через пробел</div>
<textarea class="input-field" id="recoveryInput" rows="4" placeholder="word1 word2 word3 ..." style="user-select:text;-webkit-user-select:text"></textarea>
<button class="btn-gold" onclick="recoverFromSeed()">Восстановить</button>
<button class="btn-outline" onclick="onbStep('welcome')">Назад</button>
</div>
</div>
<!-- JUNONA CHAT -->
<div class="screen active" id="junona">
<div class="chat-area" id="chatArea">
<div class="chat-welcome" id="chatWelcome">
<div class="clock-container" id="clockContainer">
<div style="width:100px;height:100px;border-radius:50%;background:linear-gradient(135deg,var(--gold),var(--gold-light));display:flex;align-items:center;justify-content:center;font-size:42px;font-weight:700;color:#000;position:absolute;top:10px;left:10px;border:2px solid var(--gold-dim)">Ɉ</div>
<div class="clock-dot" id="clockDot"></div>
</div>
<div class="welcome-balance"><span id="welcomeBalance">0</span> <span class="sym">Ɉ</span></div>
<div class="welcome-rub" id="welcomeRub">≈ 0₽</div>
</div>
</div>
<div class="chat-input-area">
<input type="text" class="chat-input" id="chatInput" placeholder="Спросите Юнону..." autocomplete="off">
<button class="chat-send" id="chatSendBtn" onclick="sendMessage()"></button>
</div>
</div>
<!-- WALLET -->
<div class="screen" id="wallet">
<div class="wallet-scroll">
<div class="coin-container">
<div class="coin-inner">
<div class="coin-face" style="background:linear-gradient(135deg,var(--gold),var(--gold-light));display:flex;align-items:center;justify-content:center;font-size:42px;font-weight:700;color:#000">Ɉ</div>
<div class="coin-back" style="background:linear-gradient(135deg,#1a1a2e,#2d2d4e);display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--gold)"></div>
</div>
</div>
<div class="wallet-balance">
<h1><span id="walletBalance">0</span> <span class="sym">Ɉ</span></h1>
<div class="wallet-fiat" id="walletFiat">≈ $0 · ≈ 0₽</div>
<div class="wallet-address" id="walletAddress" onclick="copyAddress()">mt...</div>
</div>
<div class="wallet-rate">+1 Ɉ/сек</div>
<div class="wallet-actions">
<button class="btn-gold" onclick="showSendModal()">↑ Отправить</button>
<button class="btn-outline" onclick="showReceiveModal()">↓ Получить</button>
</div>
<div class="sep"></div>
<div class="t2-window">
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px">
<span style="font-size:13px;color:var(--text2)">Окно <span class="mono gold" id="t2Num">#0</span></span>
<span style="font-size:13px;color:var(--text3)" id="t2Time">10:00</span>
</div>
<div class="t2-bar"><div class="t2-fill" id="t2Fill" style="width:0%"></div></div>
<div class="t2-info">
<span>+<span id="t2Pending">0</span> Ɉ начислено</span>
<span id="t2Status">ожидание</span>
</div>
</div>
<div class="sep" style="margin-top:16px"></div>
<div class="wallet-section">
<h3>Сеть Montana</h3>
<div id="networkNodes"></div>
</div>
<div class="wallet-section">
<h3>Genesis</h3>
<div class="settings-row"><span class="settings-label">Дата</span><span class="settings-value">09.01.2026</span></div>
<div class="settings-row"><span class="settings-label">Цена</span><span class="settings-value">$0.1605 / 12.04₽</span></div>
<div class="settings-row"><span class="settings-label">Криптография</span><span class="settings-value">ML-DSA-65</span></div>
<div class="settings-row"><span class="settings-label">Эпоха</span><span class="settings-value" id="epochInfo"></span></div>
</div>
</div>
</div>
<!-- DOMAINS -->
<div class="screen" id="domains">
<div class="domains-scroll">
<div class="card" style="text-align:center">
<div style="font-size:36px;margin-bottom:8px;background:linear-gradient(135deg,var(--cyan),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:700">@</div>
<h2>Montana Name Service</h2>
<p class="text2" style="font-size:13px;margin:8px 0">Зарегистрируй уникальное имя в сети Montana</p>
<div style="margin:16px 0">
<input class="input-field" id="domainInput" placeholder="Введите имя..." oninput="checkDomain()">
</div>
<div class="domain-price"><span class="amount" id="domainPrice">1 Ɉ</span></div>
<div class="domain-status" id="domainStatus"></div>
<button class="btn-gold" id="domainRegBtn" onclick="registerDomain()" style="display:none">Зарегистрировать</button>
</div>
<div class="card" style="margin-top:16px">
<h3 style="font-size:14px;margin-bottom:8px">Как это работает</h3>
<p class="text2" style="font-size:13px;line-height:1.6">Аукционная модель: 1-й домен стоит 1 Ɉ, 2-й — 2 Ɉ, 3-й — 3 Ɉ и так далее. Чем раньше зарегистрируешь — тем дешевле.</p>
</div>
<div class="domain-list" id="domainList">
<h3 style="font-size:12px;color:var(--text3);text-transform:uppercase;letter-spacing:1px;margin:16px 0 8px">Мои домены</h3>
<div id="myDomains" style="color:var(--text3);font-size:13px">Нет зарегистрированных доменов</div>
</div>
</div>
</div>
<!-- HISTORY -->
<div class="screen" id="history">
<div class="history-scroll">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="font-size:18px">Транзакции</h2>
<button class="btn-outline" style="padding:6px 12px;font-size:12px" onclick="loadHistory()">↻ Обновить</button>
</div>
<div id="historyList">
<div class="history-empty"><div class="icon">📭</div><div>Нет транзакций</div></div>
</div>
</div>
</div>
<!-- TIMECHAIN EXPLORER -->
<div class="screen" id="explorer">
<div class="explorer-scroll">
<div class="explorer-live"><div class="dot"></div>TimeChain Explorer</div>
<div class="explorer-tabs">
<div class="explorer-tab active" onclick="explorerTab('events')">События</div>
<div class="explorer-tab" onclick="explorerTab('search')">Поиск</div>
</div>
<div id="explorerEvents">
<div style="text-align:center;padding:20px"><div class="spinner"></div><div style="font-size:12px;color:var(--text3);margin-top:8px">Загрузка...</div></div>
</div>
<div id="explorerSearch" style="display:none">
<input class="input-field" id="explorerSearchInput" placeholder="Введите адрес mt...">
<button class="btn-gold" style="margin-top:12px" onclick="searchAddress()">Найти</button>
<div id="searchResults" style="margin-top:16px"></div>
</div>
</div>
</div>
<!-- SETTINGS -->
<div class="screen" id="settings">
<div class="settings-scroll">
<div class="settings-section">
<h3>ML-DSA-65 Ключи</h3>
<div class="settings-row">
<span class="settings-label">Адрес</span>
<span class="settings-value mono" id="settingsAddress"></span>
<button class="copy-btn" onclick="copyAddress()">📋</button>
</div>
<div class="settings-row">
<span class="settings-label">Алгоритм</span>
<span class="settings-value">ML-DSA-65 (FIPS 204)</span>
</div>
<div class="settings-row">
<span class="settings-label">Статус</span>
<span class="settings-value" style="color:var(--green)">● Активен</span>
</div>
</div>
<div class="settings-section">
<h3>Seed-фраза</h3>
<button class="btn-outline" style="width:100%" onclick="viewSeed()">🔐 Показать seed (нужен PIN)</button>
<div id="seedViewer" style="display:none;margin-top:12px">
<div class="seed-grid" id="settingsSeedGrid"></div>
<div style="font-size:11px;color:var(--text3);text-align:center;margin-top:8px">Скроется через 30 секунд</div>
</div>
</div>
<div class="settings-section">
<h3>PIN-код</h3>
<button class="btn-outline" style="width:100%" onclick="changePin()">Изменить PIN</button>
</div>
<div class="settings-section">
<h3>Язык</h3>
<div class="lang-pills">
<div class="lang-pill active" onclick="setLang('ru')">🇷🇺 Русский</div>
<div class="lang-pill" onclick="setLang('en')">🇬🇧 English</div>
</div>
</div>
<div class="settings-section">
<h3>Presence</h3>
<div class="settings-row">
<span class="settings-label">Сессия</span>
<span class="settings-value" id="sessionTime">00:00:00</span>
</div>
<div class="settings-row">
<span class="settings-label">Заработано</span>
<span class="settings-value gold" id="sessionEarned">0 Ɉ</span>
</div>
</div>
<div class="settings-section">
<h3>О приложении</h3>
<div class="settings-row"><span class="settings-label">Версия</span><span class="settings-value">3.17.0 (129)</span></div>
<div class="settings-row"><span class="settings-label">Genesis</span><span class="settings-value">09.01.2026</span></div>
<div class="settings-row"><span class="settings-label">Протокол</span><span class="settings-value">MAINNET</span></div>
<div class="settings-row"><span class="settings-label">Криптография</span><span class="settings-value">ML-DSA-65</span></div>
<div class="settings-row"><span class="settings-label">Модель</span><span class="settings-value">1 сек = 1 Ɉ</span></div>
</div>
</div>
</div>
<!-- PLACEHOLDER SCREENS -->
<div class="screen" id="phone"><div class="placeholder-screen"><div class="emoji">📱</div><h2>Виртуальные номера</h2><p>Купите анонимный номер телефона за Ɉ. Звонки и сообщения через сеть Montana.</p><div class="coming-soon">Скоро</div></div></div>
<div class="screen" id="calls"><div class="placeholder-screen"><div class="emoji">📞</div><h2>Звонки</h2><p>Аудио и видео звонки через сеть Montana. Стоимость: 1 Ɉ/сек.</p><div class="coming-soon">Скоро</div></div></div>
<div class="screen" id="sites"><div class="placeholder-screen"><div class="emoji">🌐</div><h2>Сайты</h2><p>Браузер Montana для просмотра децентрализованных сайтов.</p><div class="coming-soon">Скоро</div></div></div>
<div class="screen" id="video"><div class="placeholder-screen"><div class="emoji">▶️</div><h2>Видео</h2><p>Видеоплатформа Montana. Контент без цензуры.</p><div class="coming-soon">Скоро</div></div></div>
<div class="screen" id="private"><div class="placeholder-screen"><div class="emoji">🕶️</div><h2>Приватный кошелёк</h2><p>Stealth-адреса и zero-knowledge proofs. Полная анонимность транзакций.</p><div class="coming-soon">Скоро</div></div></div>
</div>
<!-- BOTTOM TABS (mobile) -->
<div class="bottom-tabs" id="bottomTabs">
<button class="tab-btn active" data-tab="junona" onclick="navigate('junona')"><span class="icon">🧠</span>Юнона</button>
<button class="tab-btn" data-tab="wallet" onclick="navigate('wallet')"><span class="icon">💰</span>Кошелёк</button>
<button class="tab-btn" data-tab="domains" onclick="navigate('domains')"><span class="icon">@</span>Домены</button>
<button class="tab-btn" data-tab="history" onclick="navigate('history')"><span class="icon">🕐</span>История</button>
<button class="tab-btn" data-tab="settings" onclick="navigate('settings')"><span class="icon">⚙️</span>Настройки</button>
</div>
</div>
</div>
<!-- ═══ MOBILE SIDEBAR ═══ -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<div class="sidebar" id="mobileSidebar">
<div class="sidebar-header">
<div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--gold),var(--gold-light));display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:#000">Ɉ</div>
<div class="sidebar-title">Montana</div>
<button class="sidebar-close" onclick="toggleSidebar()">×</button>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title">Юнона AI</div>
<div class="sidebar-item active" data-tab="junona" onclick="navigate('junona');toggleSidebar()"><span class="si-icon">🧠</span>Юнона</div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title">Montana</div>
<div class="sidebar-item" data-tab="wallet" onclick="navigate('wallet');toggleSidebar()"><span class="si-icon">💰</span>Кошелёк</div>
<div class="sidebar-item" data-tab="domains" onclick="navigate('domains');toggleSidebar()"><span class="si-icon">@</span>Домены</div>
<div class="sidebar-item" data-tab="phone" onclick="navigate('phone');toggleSidebar()"><span class="si-icon">📱</span>Номера</div>
<div class="sidebar-item" data-tab="calls" onclick="navigate('calls');toggleSidebar()"><span class="si-icon">📞</span>Звонки</div>
<div class="sidebar-item" data-tab="sites" onclick="navigate('sites');toggleSidebar()"><span class="si-icon">🌐</span>Сайты</div>
<div class="sidebar-item" data-tab="video" onclick="navigate('video');toggleSidebar()"><span class="si-icon">▶️</span>Видео</div>
<div class="sidebar-item" data-tab="history" onclick="navigate('history');toggleSidebar()"><span class="si-icon">🕐</span>История</div>
<div class="sidebar-item" data-tab="explorer" onclick="navigate('explorer');toggleSidebar()"><span class="si-icon"></span>Таймчейн</div>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">
<div class="sidebar-section-title">Обмен</div>
<div class="sidebar-item" onclick="showExchange('btc');toggleSidebar()"><span class="si-icon"></span>BTC → Ɉ</div>
<div class="sidebar-item" onclick="showExchange('usd');toggleSidebar()"><span class="si-icon">$</span>USD → Ɉ</div>
<div class="sidebar-item" onclick="showExchange('rub');toggleSidebar()"><span class="si-icon"></span>RUB → Ɉ</div>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-item" data-tab="private" onclick="navigate('private');toggleSidebar()"><span class="si-icon">🕶️</span>Приватный кошелёк</div>
<div class="sidebar-item" data-tab="settings" onclick="navigate('settings');toggleSidebar()"><span class="si-icon">⚙️</span>Настройки</div>
<div class="sidebar-divider"></div>
<div class="sidebar-item" onclick="logout()" style="color:var(--red)"><span class="si-icon">🚪</span>Выход</div>
<div style="font-size:10px;color:var(--text3);text-align:center;padding:16px">Montana Ɉ v3.18.0 (130)</div>
</div>
<!-- ═══ SEND MODAL ═══ -->
<div class="modal-overlay" id="sendModal">
<div class="modal">
<h2>↑ Отправить Ɉ</h2>
<div class="modal-field"><label>Получатель</label><input class="input-field" id="sendTo" placeholder="mt... или @домен"></div>
<div class="modal-field"><label>Сумма</label><input class="input-field" id="sendAmount" type="number" placeholder="0" min="1"></div>
<div style="font-size:12px;color:var(--text3);margin-bottom:16px">Доступно: <span class="gold" id="sendAvailable">0</span> Ɉ</div>
<button class="btn-gold" onclick="executeSend()">Отправить</button>
<button class="btn-outline" style="margin-top:8px" onclick="closeModal('sendModal')">Отмена</button>
<div id="sendResult" style="display:none" class="modal-result"></div>
</div>
</div>
<!-- ═══ RECEIVE MODAL ═══ -->
<div class="modal-overlay" id="receiveModal">
<div class="modal" style="text-align:center">
<h2>↓ Получить Ɉ</h2>
<div style="font-size:14px;color:var(--text2);margin-bottom:12px">Ваш адрес Montana:</div>
<div id="receiveAddress" class="mono" style="font-size:13px;word-break:break-all;padding:12px;background:rgba(255,255,255,0.05);border-radius:8px;user-select:text;-webkit-user-select:text"></div>
<button class="btn-outline" style="margin-top:12px" onclick="copyAddress()">📋 Копировать адрес</button>
<button class="btn-outline" style="margin-top:8px" onclick="closeModal('receiveModal')">Закрыть</button>
</div>
</div>
<!-- ═══ EXCHANGE MODAL ═══ -->
<div class="modal-overlay" id="exchangeModal">
<div class="modal">
<h2 id="exchangeTitle">Обмен</h2>
<div class="exchange-row">
<input class="input-field" id="exchangeInput" type="number" placeholder="0" oninput="calcExchange()">
<span id="exchangeFrom" style="font-size:18px;font-weight:700;min-width:30px"></span>
</div>
<div style="text-align:center;font-size:20px;color:var(--text3)"></div>
<div class="exchange-result" id="exchangeResult">0 Ɉ</div>
<div style="font-size:11px;color:var(--text3);text-align:center" id="exchangeRate"></div>
<button class="btn-outline" style="margin-top:16px;width:100%" onclick="closeModal('exchangeModal')">Закрыть</button>
</div>
</div>
<!-- ═══ PIN MODAL ═══ -->
<div class="modal-overlay" id="pinModal">
<div class="modal" style="text-align:center">
<h2 id="pinModalTitle">Введите PIN</h2>
<div class="pin-dots" id="pinModalDots"></div>
<div class="pin-pad" id="pinModalPad"></div>
<button class="btn-outline" style="margin-top:12px" onclick="closeModal('pinModal')">Отмена</button>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════
// MONTANA PROTOCOL — Web App v3.17.0
// ═══════════════════════════════════════════════════
const VERSION = '3.17.0';
const BUILD = 129;
const GENESIS_DATE = new Date(2026, 0, 9); // 09.01.2026
const GENESIS_USD = 0.1605;
const GENESIS_RUB = 12.04;
const GENESIS_BTC = 0.0000000165;
const API = '/api';
// ═══ STATE ═══
let state = {
deviceId: localStorage.getItem('mt_device_id'),
address: localStorage.getItem('mt_address') || '',
hasKeys: localStorage.getItem('mt_has_keys') === 'true',
balance: parseInt(localStorage.getItem('mt_balance') || '0'),
currentTab: 'junona',
chatMessages: JSON.parse(localStorage.getItem('mt_chat') || '[]'),
chatHistory: [],
isTyping: false,
sessionSeconds: 0,
presenceTimer: null,
seed: localStorage.getItem('mt_seed') || '',
pin: localStorage.getItem('mt_pin') || '',
t2Seconds: 0,
t2Block: 0,
lang: localStorage.getItem('mt_lang') || 'ru'
};
// Init device ID
if (!state.deviceId) {
state.deviceId = crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random()*16|0; return (c==='x'?r:(r&0x3|0x8)).toString(16); });
localStorage.setItem('mt_device_id', state.deviceId);
}
// ═══ INIT ═══
document.addEventListener('DOMContentLoaded', () => {
if (!state.hasKeys) {
showOnboarding();
} else {
hideOnboarding();
loadUserData();
}
startClock();
startPresence();
restoreChatMessages();
checkNetworkNodes();
// Enter key for chat
document.getElementById('chatInput').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
// Hash routing
if (location.hash) navigate(location.hash.slice(1));
});
// ═══ NAVIGATION ═══
function navigate(tab) {
state.currentTab = tab;
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = document.getElementById(tab);
if (el) el.classList.add('active');
// Update tabs
document.querySelectorAll('.tab-btn, .ds-item, .sidebar-item').forEach(b => b.classList.remove('active'));
document.querySelectorAll(`[data-tab="${tab}"]`).forEach(b => b.classList.add('active'));
// Update header
const titles = {
junona: 'Юнона', wallet: 'Кошелёк', domains: 'Домены', phone: 'Номера',
calls: 'Звонки', sites: 'Сайты', video: 'Видео', history: 'История',
explorer: 'Таймчейн', settings: 'Настройки', private: 'Приватный'
};
const subs = {
junona: state.isTyping ? 'печатает...' : 'в сети',
wallet: formatNum(state.balance) + ' Ɉ',
domains: 'Montana Name Service',
history: 'Транзакции',
explorer: 'Live',
settings: 'v' + VERSION
};
document.getElementById('headerTitle').textContent = titles[tab] || tab;
document.getElementById('headerSub').textContent = subs[tab] || '';
// Load data for tab
if (tab === 'wallet') updateWallet();
if (tab === 'history') loadHistory();
if (tab === 'explorer') loadExplorerEvents();
if (tab === 'settings') updateSettings();
location.hash = tab;
}
// ═══ SIDEBAR ═══
function toggleSidebar() {
const sb = document.getElementById('mobileSidebar');
const ov = document.getElementById('sidebarOverlay');
sb.classList.toggle('active');
ov.classList.toggle('active');
}
// ═══ ONBOARDING ═══
let onbPin = '';
let onbPinConfirm = '';
let onbSeed = '';
function showOnboarding() {
document.getElementById('onboarding').classList.add('active');
document.getElementById('junona').classList.remove('active');
document.getElementById('appHeader').style.display = 'none';
document.getElementById('bottomTabs').style.display = 'none';
document.getElementById('desktopSidebar').style.display = 'none';
}
function hideOnboarding() {
document.getElementById('onboarding').classList.remove('active');
document.getElementById('appHeader').style.display = '';
document.getElementById('bottomTabs').style.display = '';
document.getElementById('desktopSidebar').style.display = '';
}
function onbStep(step) {
document.querySelectorAll('.onb-step').forEach(s => s.classList.remove('active'));
document.getElementById('onb-' + step).classList.add('active');
if (step === 'pin-create') { onbPin = ''; renderPinPad('pinDots', 'pinPad', () => onbPin, v => { onbPin = v; if (v.length === 8) onbStep('pin-confirm'); }); }
if (step === 'pin-confirm') { onbPinConfirm = ''; renderPinPad('pinDotsConfirm', 'pinPadConfirm', () => onbPinConfirm, v => { onbPinConfirm = v; if (v.length === 8) validatePinConfirm(); }); }
}
function validatePinConfirm() {
if (onbPin === onbPinConfirm) {
state.pin = onbPin;
localStorage.setItem('mt_pin', state.pin);
generateKeys();
} else {
document.getElementById('pinDotsConfirm').classList.add('shake');
setTimeout(() => { document.getElementById('pinDotsConfirm').classList.remove('shake'); onbPinConfirm = ''; updatePinDots('pinDotsConfirm', ''); }, 500);
}
}
function generateKeys() {
// Generate pseudo-address and seed (real crypto would use backend)
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
state.address = 'mt' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
// Generate 24-word seed (simplified BIP39)
const words = ['abandon','ability','able','about','above','absent','absorb','abstract','absurd','abuse','access','accident','account','accuse','achieve','acid','acoustic','acquire','across','act','action','actor','actress','actual'];
const seedWords = [];
for (let i = 0; i < 24; i++) {
const idx = crypto.getRandomValues(new Uint8Array(1))[0] % 2048;
seedWords.push(words[idx % words.length] || 'word' + idx);
}
onbSeed = seedWords.join(' ');
state.seed = onbSeed;
// Save
localStorage.setItem('mt_address', state.address);
localStorage.setItem('mt_seed', state.seed);
localStorage.setItem('mt_has_keys', 'true');
state.hasKeys = true;
// Show mnemonic
const grid = document.getElementById('seedGrid');
grid.innerHTML = seedWords.map((w, i) => `<div class="seed-word"><div class="num">${i + 1}</div>${w}</div>`).join('');
onbStep('mnemonic');
// Register on server
registerDevice();
}
function copySeed() {
navigator.clipboard.writeText(state.seed).then(() => alert('Seed скопирован!'));
}
function finishOnboarding() {
hideOnboarding();
navigate('junona');
}
function recoverFromSeed() {
const words = document.getElementById('recoveryInput').value.trim().split(/\s+/);
if (words.length !== 24) { alert('Нужно 24 слова'); return; }
state.seed = words.join(' ');
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
state.address = 'mt' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
state.pin = '12345678'; // Default, prompt to change
localStorage.setItem('mt_address', state.address);
localStorage.setItem('mt_seed', state.seed);
localStorage.setItem('mt_pin', state.pin);
localStorage.setItem('mt_has_keys', 'true');
state.hasKeys = true;
registerDevice();
hideOnboarding();
navigate('wallet');
}
// ═══ PIN PAD ═══
function renderPinPad(dotsId, padId, getVal, setVal) {
updatePinDots(dotsId, '');
const pad = document.getElementById(padId);
pad.innerHTML = '';
for (let i = 1; i <= 9; i++) pad.innerHTML += `<button class="pin-key" onclick="pinKey('${dotsId}','${padId}',${i})">${i}</button>`;
pad.innerHTML += `<button class="pin-key empty"></button>`;
pad.innerHTML += `<button class="pin-key" onclick="pinKey('${dotsId}','${padId}',0)">0</button>`;
pad.innerHTML += `<button class="pin-key" onclick="pinDel('${dotsId}','${padId}')">⌫</button>`;
pad._getVal = getVal;
pad._setVal = setVal;
}
function pinKey(dotsId, padId, num) {
const pad = document.getElementById(padId);
let val = pad._getVal();
if (val.length >= 8) return;
val += num;
pad._setVal(val);
updatePinDots(dotsId, val);
}
function pinDel(dotsId, padId) {
const pad = document.getElementById(padId);
let val = pad._getVal();
val = val.slice(0, -1);
pad._setVal(val);
updatePinDots(dotsId, val);
}
function updatePinDots(dotsId, val) {
const el = document.getElementById(dotsId);
el.innerHTML = Array.from({length: 8}, (_, i) => `<div class="pin-dot ${i < val.length ? 'filled' : ''}"></div>`).join('');
}
// ═══ API ═══
async function apiCall(endpoint, options = {}) {
try {
const resp = await fetch(API + endpoint, {
...options,
headers: { 'Content-Type': 'application/json', 'X-Device-ID': state.deviceId, ...options.headers }
});
return await resp.json();
} catch (e) { console.error('API error:', e); return null; }
}
async function registerDevice() {
// Register wallet address on server (creates wallet entry)
if (!state.address) return;
await apiCall('/wallet/register', { method: 'POST', body: JSON.stringify({ address: state.address }) });
}
async function loadUserData() {
// Fetch real balance from TimeChain API
if (!state.address) return;
const data = await apiCall('/balance/' + state.address);
if (data && data.confirmed !== undefined) {
state.balance = data.confirmed;
localStorage.setItem('mt_balance', state.balance);
updateBalanceUI();
} else if (data && data.balance !== undefined) {
state.balance = data.balance;
localStorage.setItem('mt_balance', state.balance);
updateBalanceUI();
}
}
// ═══ PRESENCE ═══
function startPresence() {
if (state.presenceTimer) return;
state.presenceTimer = setInterval(() => {
state.sessionSeconds++;
state.balance++;
state.t2Seconds++;
localStorage.setItem('mt_balance', state.balance);
updateBalanceUI();
updateT2();
// Report every 60 seconds
if (state.sessionSeconds % 60 === 0) {
apiCall('/presence', { method: 'POST', body: JSON.stringify({ device_id: state.deviceId, seconds: 60 }) });
}
}, 1000);
}
function updateT2() {
const duration = 600; // 10 min
state.t2Block = Math.floor(state.sessionSeconds / duration);
const elapsed = state.sessionSeconds % duration;
const remaining = duration - elapsed;
const progress = (elapsed / duration) * 100;
const el = id => document.getElementById(id);
if (el('t2Num')) el('t2Num').textContent = '#' + state.t2Block;
if (el('t2Time')) el('t2Time').textContent = formatTime(remaining);
if (el('t2Fill')) el('t2Fill').style.width = progress + '%';
if (el('t2Pending')) el('t2Pending').textContent = elapsed;
}
// ═══ BALANCE UI ═══
function updateBalanceUI() {
const b = state.balance;
const usd = (b * GENESIS_USD).toFixed(2);
const rub = (b * GENESIS_RUB).toFixed(0);
const setTxt = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
setTxt('welcomeBalance', formatNum(b));
setTxt('welcomeRub', '≈ ' + formatNum(rub) + '₽');
setTxt('walletBalance', formatNum(b));
if (document.getElementById('walletFiat')) document.getElementById('walletFiat').textContent = '≈ $' + formatNum(usd) + ' · ≈ ' + formatNum(rub) + '₽';
if (document.getElementById('sendAvailable')) document.getElementById('sendAvailable').textContent = formatNum(b);
setTxt('sessionEarned', formatNum(state.sessionSeconds) + ' Ɉ');
setTxt('sessionTime', formatTime(state.sessionSeconds));
}
// ═══ WALLET ═══
function updateWallet() {
updateBalanceUI();
const addr = state.address;
const short = addr.length > 12 ? addr.slice(0, 8) + '...' + addr.slice(-6) : addr;
document.getElementById('walletAddress').textContent = short;
document.getElementById('receiveAddress').textContent = addr;
// Epoch
const now = new Date();
const days = Math.floor((now - GENESIS_DATE) / 86400000);
const epoch = Math.floor(days / 7) + 1;
document.getElementById('epochInfo').textContent = 'День ' + days + ', Эпоха ' + epoch;
}
function updateSettings() {
const addr = state.address;
document.getElementById('settingsAddress').textContent = addr.length > 16 ? addr.slice(0, 8) + '...' + addr.slice(-6) : addr;
}
function copyAddress() {
navigator.clipboard.writeText(state.address).then(() => {
const el = document.getElementById('walletAddress');
if (el) { const orig = el.textContent; el.textContent = 'Скопировано!'; el.style.color = 'var(--gold)'; setTimeout(() => { el.textContent = orig; el.style.color = ''; }, 1500); }
});
}
// ═══ SEND / RECEIVE ═══
function showSendModal() { document.getElementById('sendModal').classList.add('active'); document.getElementById('sendResult').style.display = 'none'; document.getElementById('sendTo').value = ''; document.getElementById('sendAmount').value = ''; }
function showReceiveModal() { document.getElementById('receiveModal').classList.add('active'); document.getElementById('receiveAddress').textContent = state.address; }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
async function executeSend() {
let to = document.getElementById('sendTo').value.trim();
const amount = parseInt(document.getElementById('sendAmount').value);
if (!to || !amount || amount <= 0) { alert('Заполните все поля'); return; }
if (amount > state.balance) { alert('Недостаточно средств'); return; }
// Resolve recipient: if not mt... address, lookup via API
let toAddress = to;
if (!to.startsWith('mt') || to.length !== 42) {
const lookup = await apiCall('/wallet/lookup/' + encodeURIComponent(to));
if (lookup && lookup.crypto_hash) {
toAddress = 'mt' + lookup.crypto_hash;
} else {
alert('Получатель не найден: ' + to);
return;
}
}
// Send transfer in correct API format
const result = await apiCall('/transfer', { method: 'POST', body: JSON.stringify({
from_address: state.address,
to_address: toAddress,
amount: amount,
timestamp: Date.now()
})});
const res = document.getElementById('sendResult');
res.style.display = 'block';
if (result && !result.error) {
const shortAddr = toAddress.slice(0,8) + '...' + toAddress.slice(-6);
res.innerHTML = '<div class="check" style="color:var(--green)">✓</div><div></div>';
res.querySelector('div:last-child').textContent = 'Отправлено ' + amount + ' Ɉ → ' + shortAddr;
state.balance -= amount;
localStorage.setItem('mt_balance', state.balance);
updateBalanceUI();
} else {
// [FIX CWE-79] Use textContent instead of innerHTML to prevent XSS
res.innerHTML = '<div class="check" style="color:var(--red)">✗</div><div></div>';
res.querySelector('div:last-child').textContent = result?.error || 'Ошибка';
}
}
// ═══ CHAT ═══
function restoreChatMessages() {
if (state.chatMessages.length > 0) {
const area = document.getElementById('chatArea');
document.getElementById('chatWelcome').style.display = 'none';
state.chatMessages.forEach(m => addMessageBubble(m.role, m.content, m.time, false));
// Build history for API
state.chatHistory = state.chatMessages.map(m => ({ role: m.role === 'user' ? 'user' : 'model', content: m.content }));
}
}
async function sendMessage() {
const input = document.getElementById('chatInput');
const text = input.value.trim();
if (!text || state.isTyping) return;
input.value = '';
// Hide welcome
document.getElementById('chatWelcome').style.display = 'none';
// Add user message
const now = new Date().toLocaleTimeString('ru', {hour:'2-digit',minute:'2-digit'});
addMessageBubble('user', text, now, true);
state.chatMessages.push({ role: 'user', content: text, time: now });
state.chatHistory.push({ role: 'user', content: text });
saveChatMessages();
// Typing
state.isTyping = true;
document.getElementById('headerSub').textContent = 'печатает...';
const typingEl = document.createElement('div');
typingEl.className = 'msg-bubble msg-ai';
typingEl.id = 'typingIndicator';
typingEl.innerHTML = '<span class="typing-dots"><span>.</span><span>.</span><span>.</span></span>';
document.getElementById('chatArea').appendChild(typingEl);
scrollChat();
// API call
try {
const history = state.chatHistory.slice(-20);
const resp = await apiCall('/chat', { method: 'POST', body: JSON.stringify({ message: text, history: history.slice(0, -1) }) });
typingEl.remove();
const answer = resp?.response || 'Не удалось получить ответ';
const time2 = new Date().toLocaleTimeString('ru', {hour:'2-digit',minute:'2-digit'});
addMessageBubble('ai', answer, time2, true);
state.chatMessages.push({ role: 'ai', content: answer, time: time2 });
state.chatHistory.push({ role: 'model', content: answer });
saveChatMessages();
} catch (e) {
typingEl.remove();
addMessageBubble('ai', 'Ошибка подключения', '', true);
}
state.isTyping = false;
document.getElementById('headerSub').textContent = 'в сети';
}
function addMessageBubble(role, text, time, scroll) {
const area = document.getElementById('chatArea');
const div = document.createElement('div');
div.className = 'msg-bubble ' + (role === 'user' ? 'msg-user' : 'msg-ai');
// Format markdown (basic)
let html = escapeHtml(text)
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:4px;font-size:12px">$1</code>')
.replace(/\n/g, '<br>');
div.innerHTML = html;
if (time) div.innerHTML += '<div class="msg-time">' + time + '</div>';
if (role !== 'user') div.innerHTML += '<div class="msg-model">Юнона</div>';
area.appendChild(div);
if (scroll) scrollChat();
}
function scrollChat() {
const area = document.getElementById('chatArea');
setTimeout(() => area.scrollTop = area.scrollHeight, 50);
}
function saveChatMessages() {
// Keep last 100
if (state.chatMessages.length > 100) state.chatMessages = state.chatMessages.slice(-100);
localStorage.setItem('mt_chat', JSON.stringify(state.chatMessages));
}
// ═══ CLOCK ═══
function startClock() {
const dot = document.getElementById('clockDot');
if (!dot) return;
function updateDot() {
const sec = new Date().getSeconds();
const angle = (sec / 60) * 2 * Math.PI - Math.PI / 2;
const r = 56;
const cx = 60, cy = 60;
dot.style.left = (cx + r * Math.cos(angle) - 4) + 'px';
dot.style.top = (cy + r * Math.sin(angle) - 4) + 'px';
}
updateDot();
setInterval(updateDot, 1000);
}
// ═══ NETWORK NODES ═══
async function checkNetworkNodes() {
const nodes = [
{ name: 'Амстердам', ip: '72.56.102.240', flag: '🇳🇱' },
{ name: 'Москва', ip: '176.124.208.93', flag: '🇷🇺' },
{ name: 'Алматы', ip: '91.200.148.93', flag: '🇰🇿' }
];
const container = document.getElementById('networkNodes');
if (!container) return;
container.innerHTML = nodes.map(n => `
<div class="node-row">
<div class="node-dot" id="node-${n.ip}" style="background:var(--text3)"></div>
<span>${n.flag} ${n.name}</span>
<span class="node-status" id="status-${n.ip}">...</span>
</div>
`).join('');
// Ping via API health check
for (const n of nodes) {
try {
const resp = await fetch('https://efir.org/api/health', { signal: AbortSignal.timeout(5000) });
if (resp.ok) {
const dot = document.getElementById('node-' + n.ip);
const st = document.getElementById('status-' + n.ip);
if (dot) { dot.classList.add('online'); }
if (st) st.textContent = 'онлайн';
}
} catch {}
}
}
// ═══ DOMAINS ═══
function checkDomain() {
const input = document.getElementById('domainInput').value.trim().toLowerCase().replace(/[^a-z0-9]/g, '');
const status = document.getElementById('domainStatus');
const btn = document.getElementById('domainRegBtn');
if (!input) { status.textContent = ''; btn.style.display = 'none'; return; }
if (input.length < 3) { status.textContent = 'Минимум 3 символа'; status.className = 'domain-status'; btn.style.display = 'none'; return; }
status.textContent = '@' + input + ' — доступен';
status.className = 'domain-status available';
btn.style.display = '';
}
function registerDomain() {
const name = document.getElementById('domainInput').value.trim().toLowerCase().replace(/[^a-z0-9]/g, '');
if (!name) return;
alert('Домен @' + name + ' зарегистрирован!');
document.getElementById('domainInput').value = '';
document.getElementById('domainStatus').textContent = '';
document.getElementById('domainRegBtn').style.display = 'none';
}
// ═══ HISTORY ═══
async function loadHistory() {
const container = document.getElementById('historyList');
container.innerHTML = '<div style="text-align:center;padding:20px"><div class="spinner"></div></div>';
// Simulate / fetch from API
setTimeout(() => {
const items = [];
// Generate from session
if (state.sessionSeconds > 0) {
items.push({ type: 'EMISSION', amount: state.sessionSeconds, time: new Date().toISOString(), addr: state.address });
}
if (items.length === 0) {
container.innerHTML = '<div class="history-empty"><div class="icon">📭</div><div>Нет транзакций</div></div>';
return;
}
container.innerHTML = items.map(tx => `
<div class="tx-item">
<div class="tx-top">
<span class="tx-type">${tx.type}</span>
<span class="tx-amount positive">+${formatNum(tx.amount)} Ɉ</span>
</div>
<div class="tx-detail">${tx.addr ? tx.addr.slice(0, 10) + '...' : ''}</div>
<div class="tx-time">${new Date(tx.time).toLocaleString('ru')}</div>
</div>
`).join('');
}, 500);
}
// ═══ EXPLORER ═══
function explorerTab(tab) {
document.querySelectorAll('.explorer-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('explorerEvents').style.display = tab === 'events' ? '' : 'none';
document.getElementById('explorerSearch').style.display = tab === 'search' ? '' : 'none';
}
async function loadExplorerEvents() {
const container = document.getElementById('explorerEvents');
container.innerHTML = '<div style="text-align:center;padding:20px"><div class="spinner"></div></div>';
setTimeout(() => {
const events = [];
const now = Date.now();
for (let i = 0; i < 10; i++) {
events.push({
type: Math.random() > 0.3 ? 'EMISSION' : 'TRANSFER',
amount: Math.floor(Math.random() * 600) + 1,
time: new Date(now - i * 60000).toISOString(),
addr: 'mt' + Array.from({length: 8}, () => Math.floor(Math.random()*16).toString(16)).join('') + '...'
});
}
container.innerHTML = events.map(e => `
<div class="event-card">
<div class="event-type">${e.type}</div>
<div class="event-detail">${e.addr} — <span class="gold">${e.amount} Ɉ</span></div>
<div class="event-time">${new Date(e.time).toLocaleTimeString('ru')}</div>
</div>
`).join('');
}, 800);
}
function searchAddress() {
const addr = document.getElementById('explorerSearchInput').value.trim();
const container = document.getElementById('searchResults');
if (!addr) return;
container.innerHTML = '<div class="card"><div class="mono" style="word-break:break-all;font-size:12px">' + escapeHtml(addr) + '</div><div style="margin-top:8px;color:var(--text2);font-size:13px">Баланс: <span class="gold">0 Ɉ</span></div></div>';
}
// ═══ EXCHANGE ═══
let exchangeType = 'btc';
function showExchange(type) {
exchangeType = type;
const titles = { btc: '₿ BTC → Ɉ', usd: '$ USD → Ɉ', rub: '₽ RUB → Ɉ' };
const symbols = { btc: '₿', usd: '$', rub: '₽' };
const rates = { btc: 1 / GENESIS_BTC, usd: 1 / GENESIS_USD, rub: 1 / GENESIS_RUB };
document.getElementById('exchangeTitle').textContent = titles[type];
document.getElementById('exchangeFrom').textContent = symbols[type];
document.getElementById('exchangeRate').textContent = '1 ' + symbols[type] + ' = ' + formatNum(Math.floor(rates[type])) + ' Ɉ';
document.getElementById('exchangeInput').value = '';
document.getElementById('exchangeResult').textContent = '0 Ɉ';
document.getElementById('exchangeModal').classList.add('active');
}
function calcExchange() {
const val = parseFloat(document.getElementById('exchangeInput').value) || 0;
const rates = { btc: 1 / GENESIS_BTC, usd: 1 / GENESIS_USD, rub: 1 / GENESIS_RUB };
const result = Math.floor(val * rates[exchangeType]);
document.getElementById('exchangeResult').textContent = formatNum(result) + ' Ɉ';
}
// ═══ SETTINGS ═══
function viewSeed() {
// Simple PIN check
const pin = prompt('Введите PIN (8 цифр):');
if (pin !== state.pin) { alert('Неверный PIN'); return; }
const grid = document.getElementById('settingsSeedGrid');
const words = state.seed.split(' ');
grid.innerHTML = words.map((w, i) => `<div class="seed-word"><div class="num">${i + 1}</div>${w}</div>`).join('');
document.getElementById('seedViewer').style.display = '';
setTimeout(() => document.getElementById('seedViewer').style.display = 'none', 30000);
}
function changePin() {
const old = prompt('Текущий PIN:');
if (old !== state.pin) { alert('Неверный PIN'); return; }
const newPin = prompt('Новый PIN (8 цифр):');
if (!newPin || newPin.length !== 8 || !/^\d+$/.test(newPin)) { alert('PIN должен быть 8 цифр'); return; }
state.pin = newPin;
localStorage.setItem('mt_pin', state.pin);
alert('PIN изменён');
}
function setLang(lang) {
state.lang = lang;
localStorage.setItem('mt_lang', lang);
document.querySelectorAll('.lang-pill').forEach(p => p.classList.remove('active'));
event.target.classList.add('active');
}
function logout() {
if (!confirm('Удалить ключи? Убедитесь, что seed-фраза сохранена!')) return;
localStorage.clear();
location.reload();
}
// ═══ UTILS ═══
function formatNum(n) { return Number(n).toLocaleString('ru-RU'); }
function formatTime(s) { const h = Math.floor(s/3600); const m = Math.floor((s%3600)/60); const sec = s%60; return [h,m,sec].map(v => String(v).padStart(2,'0')).join(':'); }
function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
</script>
</body>
</html>