1404 lines
76 KiB
HTML
1404 lines
76 KiB
HTML
<!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>
|